Solved a bug on the assignments because of the multiple exams

This commit is contained in:
Tiago Ribeiro
2024-01-29 15:31:41 +00:00
parent a862e59574
commit 8bff64dd13
2 changed files with 425 additions and 315 deletions

View File

@@ -5,15 +5,33 @@ import {Assignment} from "@/interfaces/results";
import { calculateBandScore } from "@/utils/score"; import { calculateBandScore } from "@/utils/score";
import clsx from "clsx"; import clsx from "clsx";
import moment from "moment"; import moment from "moment";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; import {
BsBook,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
import { usePDFDownload } from "@/hooks/usePDFDownload"; import { usePDFDownload } from "@/hooks/usePDFDownload";
import { uniqBy } from "lodash";
interface Props { interface Props {
onClick?: () => void; onClick?: () => void;
allowDownload?: boolean; allowDownload?: boolean;
} }
export default function AssignmentCard({id, name, assigner, startDate, endDate, assignees, results, exams, onClick, allowDownload}: Assignment & Props) { export default function AssignmentCard({
id,
name,
assigner,
startDate,
endDate,
assignees,
results,
exams,
onClick,
allowDownload,
}: Assignment & Props) {
const { users } = useUsers(); const { users } = useUsers();
const renderPdfIcon = usePDFDownload("assignments"); const renderPdfIcon = usePDFDownload("assignments");
@@ -21,55 +39,73 @@ export default function AssignmentCard({id, name, assigner, startDate, endDate,
const resultModuleBandScores = results.map((r) => { const resultModuleBandScores = results.map((r) => {
const moduleStats = r.stats.filter((s) => s.module === module); const moduleStats = r.stats.filter((s) => s.module === module);
const correct = moduleStats.reduce((acc, curr) => acc + curr.score.correct, 0); const correct = moduleStats.reduce(
const total = moduleStats.reduce((acc, curr) => acc + curr.score.total, 0); (acc, curr) => acc + curr.score.correct,
0,
);
const total = moduleStats.reduce(
(acc, curr) => acc + curr.score.total,
0,
);
return calculateBandScore(correct, total, module, r.type); return calculateBandScore(correct, total, module, r.type);
}); });
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / results.length; return resultModuleBandScores.length === 0
? -1
: resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) /
results.length;
}; };
return ( return (
<div <div
onClick={onClick} onClick={onClick}
className="w-[350px] h-fit flex flex-col gap-6 bg-white border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"> className="border-mti-gray-platinum flex h-fit w-[350px] cursor-pointer flex-col gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow"
>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<h3 className="font-semibold text-xl">{name}</h3> <h3 className="text-xl font-semibold">{name}</h3>
{allowDownload && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} {allowDownload &&
renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
</div> </div>
<ProgressBar <ProgressBar
color={results.length / assignees.length < 0.5 ? "red" : "purple"} color={results.length / assignees.length < 0.5 ? "red" : "purple"}
percentage={(results.length / assignees.length) * 100} percentage={(results.length / assignees.length) * 100}
label={`${results.length}/${assignees.length}`} label={`${results.length}/${assignees.length}`}
className="h-5" className="h-5"
textClassName={results.length / assignees.length < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"} textClassName={
results.length / assignees.length < 0.5
? "!text-mti-gray-dim font-light"
: "text-white"
}
/> />
</div> </div>
<span className="flex gap-1 justify-between"> <span className="flex justify-between gap-1">
<span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span> <span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span>
<span>-</span> <span>-</span>
<span>{moment(endDate).format("DD/MM/YY, HH:mm")}</span> <span>{moment(endDate).format("DD/MM/YY, HH:mm")}</span>
</span> </span>
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2"> <div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
{exams.map(({module}) => ( {uniqBy(exams, (x) => x.module).map(({ module }) => (
<div <div
key={module} key={module}
className={clsx( className={clsx(
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl", "-md:px-4 flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
module === "reading" && "bg-ielts-reading", module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening", module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing", module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking", module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level", module === "level" && "bg-ielts-level",
)}> )}
{module === "reading" && <BsBook className="w-4 h-4" />} >
{module === "listening" && <BsHeadphones className="w-4 h-4" />} {module === "reading" && <BsBook className="h-4 w-4" />}
{module === "writing" && <BsPen className="w-4 h-4" />} {module === "listening" && <BsHeadphones className="h-4 w-4" />}
{module === "speaking" && <BsMegaphone className="w-4 h-4" />} {module === "writing" && <BsPen className="h-4 w-4" />}
{module === "level" && <BsClipboard className="w-4 h-4" />} {module === "speaking" && <BsMegaphone className="h-4 w-4" />}
{module === "level" && <BsClipboard className="h-4 w-4" />}
{calculateAverageModuleScore(module) > -1 && ( {calculateAverageModuleScore(module) > -1 && (
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span> <span className="text-sm">
{calculateAverageModuleScore(module).toFixed(1)}
</span>
)} )}
</div> </div>
))} ))}

View File

@@ -13,7 +13,13 @@ import clsx from "clsx";
import { capitalize, uniqBy } from "lodash"; import { capitalize, uniqBy } from "lodash";
import moment from "moment"; import moment from "moment";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; import {
BsBook,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
@@ -43,16 +49,29 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
const resultModuleBandScores = assignment.results.map((r) => { const resultModuleBandScores = assignment.results.map((r) => {
const moduleStats = r.stats.filter((s) => s.module === module); const moduleStats = r.stats.filter((s) => s.module === module);
const correct = moduleStats.reduce((acc, curr) => acc + curr.score.correct, 0); const correct = moduleStats.reduce(
const total = moduleStats.reduce((acc, curr) => acc + curr.score.total, 0); (acc, curr) => acc + curr.score.correct,
0,
);
const total = moduleStats.reduce(
(acc, curr) => acc + curr.score.total,
0,
);
return calculateBandScore(correct, total, module, r.type); return calculateBandScore(correct, total, module, r.type);
}); });
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / assignment.results.length; return resultModuleBandScores.length === 0
? -1
: resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) /
assignment.results.length;
}; };
const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => { const aggregateScoresByModule = (
const scores: {[key in Module]: {total: number; missing: number; correct: number}} = { stats: Stat[],
): { module: Module; total: number; missing: number; correct: number }[] => {
const scores: {
[key in Module]: { total: number; missing: number; correct: number };
} = {
reading: { reading: {
total: 0, total: 0,
correct: 0, correct: 0,
@@ -93,10 +112,22 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
.map((x) => ({ module: x as Module, ...scores[x as Module] })); .map((x) => ({ module: x as Module, ...scores[x as Module] }));
}; };
const customContent = (stats: Stat[], user: string, focus: "academic" | "general") => { const customContent = (
const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0); stats: Stat[],
const total = stats.reduce((accumulator, current) => accumulator + current.score.total, 0); user: string,
const aggregatedScores = aggregateScoresByModule(stats).filter((x) => x.total > 0); focus: "academic" | "general",
) => {
const correct = stats.reduce(
(accumulator, current) => accumulator + current.score.correct,
0,
);
const total = stats.reduce(
(accumulator, current) => accumulator + current.score.total,
0,
);
const aggregatedScores = aggregateScoresByModule(stats).filter(
(x) => x.total > 0,
);
const aggregatedLevels = aggregatedScores.map((x) => ({ const aggregatedLevels = aggregatedScores.map((x) => ({
module: x.module, module: x.module,
@@ -106,7 +137,9 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
const timeSpent = stats[0].timeSpent; const timeSpent = stats[0].timeSpent;
const selectExam = () => { const selectExam = () => {
const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam)); const examPromises = uniqBy(stats, "exam").map((stat) =>
getExamById(stat.module, stat.exam),
);
Promise.all(examPromises).then((exams) => { Promise.all(examPromises).then((exams) => {
if (exams.every((x) => !!x)) { if (exams.every((x) => !!x)) {
@@ -126,13 +159,17 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
const content = ( const content = (
<> <>
<div className="w-full flex justify-between -md:items-center 2xl:items-center"> <div className="-md:items-center flex w-full justify-between 2xl:items-center">
<div className="flex md:flex-col 2xl:flex-row md:gap-1 -md:gap-2 2xl:gap-2 -md:items-center 2xl:items-center"> <div className="-md:gap-2 -md:items-center flex md:flex-col md:gap-1 2xl:flex-row 2xl:items-center 2xl:gap-2">
<span className="font-medium">{formatTimestamp(stats[0].date.toString())}</span> <span className="font-medium">
{formatTimestamp(stats[0].date.toString())}
</span>
{timeSpent && ( {timeSpent && (
<> <>
<span className="md:hidden 2xl:flex"> </span> <span className="md:hidden 2xl:flex"> </span>
<span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span> <span className="text-sm">
{Math.floor(timeSpent / 60)} minutes
</span>
</> </>
)} )}
</div> </div>
@@ -141,30 +178,37 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
correct / total >= 0.7 && "text-mti-purple", correct / total >= 0.7 && "text-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
correct / total < 0.3 && "text-mti-rose", correct / total < 0.3 && "text-mti-rose",
)}> )}
>
Level{" "} Level{" "}
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} {(
aggregatedLevels.reduce(
(accumulator, current) => accumulator + current.level,
0,
) / aggregatedLevels.length
).toFixed(1)}
</span> </span>
</div> </div>
<div className="w-full flex flex-col gap-1"> <div className="flex w-full flex-col gap-1">
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2"> <div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
{aggregatedLevels.map(({ module, level }) => ( {aggregatedLevels.map(({ module, level }) => (
<div <div
key={module} key={module}
className={clsx( className={clsx(
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl", "-md:px-4 flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
module === "reading" && "bg-ielts-reading", module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening", module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing", module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking", module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level", module === "level" && "bg-ielts-level",
)}> )}
{module === "reading" && <BsBook className="w-4 h-4" />} >
{module === "listening" && <BsHeadphones className="w-4 h-4" />} {module === "reading" && <BsBook className="h-4 w-4" />}
{module === "writing" && <BsPen className="w-4 h-4" />} {module === "listening" && <BsHeadphones className="h-4 w-4" />}
{module === "speaking" && <BsMegaphone className="w-4 h-4" />} {module === "writing" && <BsPen className="h-4 w-4" />}
{module === "level" && <BsClipboard className="w-4 h-4" />} {module === "speaking" && <BsMegaphone className="h-4 w-4" />}
{module === "level" && <BsClipboard className="h-4 w-4" />}
<span className="text-sm">{level.toFixed(1)}</span> <span className="text-sm">{level.toFixed(1)}</span>
</div> </div>
))} ))}
@@ -184,25 +228,31 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
<div <div
key={user} key={user}
className={clsx( className={clsx(
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden", "border-mti-gray-platinum -md:hidden flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out",
correct / total >= 0.7 && "hover:border-mti-purple", correct / total >= 0.7 && "hover:border-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total >= 0.3 &&
correct / total < 0.7 &&
"hover:border-mti-red",
correct / total < 0.3 && "hover:border-mti-rose", correct / total < 0.3 && "hover:border-mti-rose",
)} )}
onClick={selectExam} onClick={selectExam}
role="button"> role="button"
>
{content} {content}
</div> </div>
<div <div
key={user} key={user}
className={clsx( className={clsx(
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:tooltip md:hidden", "border-mti-gray-platinum -md:tooltip flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out md:hidden",
correct / total >= 0.7 && "hover:border-mti-purple", correct / total >= 0.7 && "hover:border-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total >= 0.3 &&
correct / total < 0.7 &&
"hover:border-mti-red",
correct / total < 0.3 && "hover:border-mti-rose", correct / total < 0.3 && "hover:border-mti-rose",
)} )}
data-tip="Your screen size is too small to view previous exams." data-tip="Your screen size is too small to view previous exams."
role="button"> role="button"
>
{content} {content}
</div> </div>
</div> </div>
@@ -211,20 +261,33 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose} title={assignment?.name}> <Modal isOpen={isOpen} onClose={onClose} title={assignment?.name}>
<div className="mt-4 flex flex-col w-full gap-4"> <div className="mt-4 flex w-full flex-col gap-4">
<ProgressBar <ProgressBar
color="purple" color="purple"
label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`} label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`}
className="h-6" className="h-6"
textClassName={ textClassName={
(assignment?.results.length || 0) / (assignment?.assignees.length || 1) < 0.5 ? "!text-mti-gray-dim font-light" : "text-white" (assignment?.results.length || 0) /
(assignment?.assignees.length || 1) <
0.5
? "!text-mti-gray-dim font-light"
: "text-white"
}
percentage={
((assignment?.results.length || 0) /
(assignment?.assignees.length || 1)) *
100
} }
percentage={((assignment?.results.length || 0) / (assignment?.assignees.length || 1)) * 100}
/> />
<div className="flex gap-8 items-start"> <div className="flex items-start gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span> <span>
<span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span> Start Date:{" "}
{moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}
</span>
<span>
End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}
</span>
</div> </div>
<span> <span>
Assignees:{" "} Assignees:{" "}
@@ -236,26 +299,32 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-xl font-bold">Average Scores</span> <span className="text-xl font-bold">Average Scores</span>
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2"> <div className="-md:mt-2 flex w-full items-center gap-4">
{assignment?.exams.map(({module}) => ( {assignment &&
uniqBy(assignment.exams, (x) => x.module).map(({ module }) => (
<div <div
data-tip={capitalize(module)} data-tip={capitalize(module)}
key={module} key={module}
className={clsx( className={clsx(
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl tooltip", "-md:px-4 tooltip flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
module === "reading" && "bg-ielts-reading", module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening", module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing", module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking", module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level", module === "level" && "bg-ielts-level",
)}> )}
{module === "reading" && <BsBook className="w-4 h-4" />} >
{module === "listening" && <BsHeadphones className="w-4 h-4" />} {module === "reading" && <BsBook className="h-4 w-4" />}
{module === "writing" && <BsPen className="w-4 h-4" />} {module === "listening" && (
{module === "speaking" && <BsMegaphone className="w-4 h-4" />} <BsHeadphones className="h-4 w-4" />
{module === "level" && <BsClipboard className="w-4 h-4" />} )}
{module === "writing" && <BsPen className="h-4 w-4" />}
{module === "speaking" && <BsMegaphone className="h-4 w-4" />}
{module === "level" && <BsClipboard className="h-4 w-4" />}
{calculateAverageModuleScore(module) > -1 && ( {calculateAverageModuleScore(module) > -1 && (
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span> <span className="text-sm">
{calculateAverageModuleScore(module).toFixed(1)}
</span>
)} )}
</div> </div>
))} ))}
@@ -263,15 +332,20 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-xl font-bold"> <span className="text-xl font-bold">
Results ({assignment?.results.length}/{assignment?.assignees.length}) Results ({assignment?.results.length}/{assignment?.assignees.length}
)
</span> </span>
<div> <div>
{assignment && assignment?.results.length > 0 && ( {assignment && assignment?.results.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6"> <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 xl:gap-6">
{assignment.results.map((r) => customContent(r.stats, r.user, r.type))} {assignment.results.map((r) =>
customContent(r.stats, r.user, r.type),
)}
</div> </div>
)} )}
{assignment && assignment?.results.length === 0 && <span className="font-semibold ml-1">No results yet...</span>} {assignment && assignment?.results.length === 0 && (
<span className="ml-1 font-semibold">No results yet...</span>
)}
</div> </div>
</div> </div>
</div> </div>