Added the ability to delete already finished assignments

This commit is contained in:
Tiago Ribeiro
2024-02-12 11:15:04 +00:00
parent dc3373be6a
commit 4802310474
2 changed files with 278 additions and 324 deletions

View File

@@ -1,354 +1,307 @@
import Button from "@/components/Low/Button";
import ProgressBar from "@/components/Low/ProgressBar"; import ProgressBar from "@/components/Low/ProgressBar";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import { Module } from "@/interfaces"; import {Module} from "@/interfaces";
import { Assignment } from "@/interfaces/results"; import {Assignment} from "@/interfaces/results";
import { Stat, User } from "@/interfaces/user"; import {Stat, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import { getExamById } from "@/utils/exams"; import {getExamById} from "@/utils/exams";
import { sortByModule } from "@/utils/moduleUtils"; import {sortByModule} from "@/utils/moduleUtils";
import { calculateBandScore } from "@/utils/score"; import {calculateBandScore} from "@/utils/score";
import { convertToUserSolutions } from "@/utils/stats"; import {convertToUserSolutions} from "@/utils/stats";
import axios from "axios";
import clsx from "clsx"; 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 { import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
BsBook, import {toast} from "react-toastify";
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
assignment?: Assignment; assignment?: Assignment;
onClose: () => void; onClose: () => void;
} }
export default function AssignmentView({ isOpen, assignment, onClose }: Props) { export default function AssignmentView({isOpen, assignment, onClose}: Props) {
const { users } = useUsers(); const {users} = useUsers();
const router = useRouter(); const router = useRouter();
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions); const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions); const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const formatTimestamp = (timestamp: string) => { const deleteAssignment = async () => {
const date = moment(parseInt(timestamp)); if (!confirm("Are you sure you want to delete this assignment?")) return;
const formatter = "YYYY/MM/DD - HH:mm";
return date.format(formatter); axios
}; .delete(`/api/assignments/${assignment?.id}`)
.then(() => toast.success(`Successfully deleted the assignment "${assignment?.name}".`))
.catch(() => toast.error("Something went wrong, please try again later."))
.finally(onClose);
};
const calculateAverageModuleScore = (module: Module) => { const formatTimestamp = (timestamp: string) => {
if (!assignment) return -1; const date = moment(parseInt(timestamp));
const formatter = "YYYY/MM/DD - HH:mm";
const resultModuleBandScores = assignment.results.map((r) => { return date.format(formatter);
const moduleStats = r.stats.filter((s) => s.module === module); };
const correct = moduleStats.reduce( const calculateAverageModuleScore = (module: Module) => {
(acc, curr) => acc + curr.score.correct, if (!assignment) return -1;
0,
);
const total = moduleStats.reduce(
(acc, curr) => acc + curr.score.total,
0,
);
return calculateBandScore(correct, total, module, r.type);
});
return resultModuleBandScores.length === 0 const resultModuleBandScores = assignment.results.map((r) => {
? -1 const moduleStats = r.stats.filter((s) => s.module === module);
: resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) /
assignment.results.length;
};
const aggregateScoresByModule = ( const correct = moduleStats.reduce((acc, curr) => acc + curr.score.correct, 0);
stats: Stat[], const total = moduleStats.reduce((acc, curr) => acc + curr.score.total, 0);
): { module: Module; total: number; missing: number; correct: number }[] => { return calculateBandScore(correct, total, module, r.type);
const scores: { });
[key in Module]: { total: number; missing: number; correct: number };
} = {
reading: {
total: 0,
correct: 0,
missing: 0,
},
listening: {
total: 0,
correct: 0,
missing: 0,
},
writing: {
total: 0,
correct: 0,
missing: 0,
},
speaking: {
total: 0,
correct: 0,
missing: 0,
},
level: {
total: 0,
correct: 0,
missing: 0,
},
};
stats.forEach((x) => { return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / assignment.results.length;
scores[x.module!] = { };
total: scores[x.module!].total + x.score.total,
correct: scores[x.module!].correct + x.score.correct,
missing: scores[x.module!].missing + x.score.missing,
};
});
return Object.keys(scores) const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => {
.filter((x) => scores[x as Module].total > 0) const scores: {
.map((x) => ({ module: x as Module, ...scores[x as Module] })); [key in Module]: {total: number; missing: number; correct: number};
}; } = {
reading: {
total: 0,
correct: 0,
missing: 0,
},
listening: {
total: 0,
correct: 0,
missing: 0,
},
writing: {
total: 0,
correct: 0,
missing: 0,
},
speaking: {
total: 0,
correct: 0,
missing: 0,
},
level: {
total: 0,
correct: 0,
missing: 0,
},
};
const customContent = ( stats.forEach((x) => {
stats: Stat[], scores[x.module!] = {
user: string, total: scores[x.module!].total + x.score.total,
focus: "academic" | "general", correct: scores[x.module!].correct + x.score.correct,
) => { missing: scores[x.module!].missing + x.score.missing,
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) => ({ return Object.keys(scores)
module: x.module, .filter((x) => scores[x as Module].total > 0)
level: calculateBandScore(x.correct, x.total, x.module, focus), .map((x) => ({module: x as Module, ...scores[x as Module]}));
})); };
const timeSpent = stats[0].timeSpent; const customContent = (stats: Stat[], user: string, 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 selectExam = () => { const aggregatedLevels = aggregatedScores.map((x) => ({
const examPromises = uniqBy(stats, "exam").map((stat) => module: x.module,
getExamById(stat.module, stat.exam), level: calculateBandScore(x.correct, x.total, x.module, focus),
); }));
Promise.all(examPromises).then((exams) => { const timeSpent = stats[0].timeSpent;
if (exams.every((x) => !!x)) {
setUserSolutions(convertToUserSolutions(stats));
setShowSolutions(true);
setExams(exams.map((x) => x!).sort(sortByModule));
setSelectedModules(
exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
);
router.push("/exercises");
}
});
};
const content = ( const selectExam = () => {
<> const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam));
<div className="-md:items-center flex w-full justify-between 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>
{timeSpent && (
<>
<span className="md:hidden 2xl:flex"> </span>
<span className="text-sm">
{Math.floor(timeSpent / 60)} minutes
</span>
</>
)}
</div>
<span
className={clsx(
correct / total >= 0.7 && "text-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
correct / total < 0.3 && "text-mti-rose",
)}
>
Level{" "}
{(
aggregatedLevels.reduce(
(accumulator, current) => accumulator + current.level,
0,
) / aggregatedLevels.length
).toFixed(1)}
</span>
</div>
<div className="flex w-full flex-col gap-1"> Promise.all(examPromises).then((exams) => {
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2"> if (exams.every((x) => !!x)) {
{aggregatedLevels.map(({ module, level }) => ( setUserSolutions(convertToUserSolutions(stats));
<div setShowSolutions(true);
key={module} setExams(exams.map((x) => x!).sort(sortByModule));
className={clsx( setSelectedModules(
"-md:px-4 flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4", exams
module === "reading" && "bg-ielts-reading", .map((x) => x!)
module === "listening" && "bg-ielts-listening", .sort(sortByModule)
module === "writing" && "bg-ielts-writing", .map((x) => x!.module),
module === "speaking" && "bg-ielts-speaking", );
module === "level" && "bg-ielts-level", router.push("/exercises");
)} }
> });
{module === "reading" && <BsBook className="h-4 w-4" />} };
{module === "listening" && <BsHeadphones className="h-4 w-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" />}
<span className="text-sm">{level.toFixed(1)}</span>
</div>
))}
</div>
</div>
</>
);
return ( const content = (
<div className="flex flex-col gap-2"> <>
<span> <div className="-md:items-center flex w-full justify-between 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">
const student = users.find((u) => u.id === user); <span className="font-medium">{formatTimestamp(stats[0].date.toString())}</span>
return `${student?.name} (${student?.email})`; {timeSpent && (
})()} <>
</span> <span className="md:hidden 2xl:flex"> </span>
<div <span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span>
key={user} </>
className={clsx( )}
"border-mti-gray-platinum -md:hidden flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out", </div>
correct / total >= 0.7 && "hover:border-mti-purple", <span
correct / total >= 0.3 && className={clsx(
correct / total < 0.7 && correct / total >= 0.7 && "text-mti-purple",
"hover:border-mti-red", correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
correct / total < 0.3 && "hover:border-mti-rose", correct / total < 0.3 && "text-mti-rose",
)} )}>
onClick={selectExam} Level{" "}
role="button" {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
> </span>
{content} </div>
</div>
<div
key={user}
className={clsx(
"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.3 &&
correct / total < 0.7 &&
"hover:border-mti-red",
correct / total < 0.3 && "hover:border-mti-rose",
)}
data-tip="Your screen size is too small to view previous exams."
role="button"
>
{content}
</div>
</div>
);
};
return ( <div className="flex w-full flex-col gap-1">
<Modal isOpen={isOpen} onClose={onClose} title={assignment?.name}> <div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
<div className="mt-4 flex w-full flex-col gap-4"> {aggregatedLevels.map(({module, level}) => (
<ProgressBar <div
color="purple" key={module}
label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`} className={clsx(
className="h-6" "-md:px-4 flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
textClassName={ module === "reading" && "bg-ielts-reading",
(assignment?.results.length || 0) / module === "listening" && "bg-ielts-listening",
(assignment?.assignees.length || 1) < module === "writing" && "bg-ielts-writing",
0.5 module === "speaking" && "bg-ielts-speaking",
? "!text-mti-gray-dim font-light" module === "level" && "bg-ielts-level",
: "text-white" )}>
} {module === "reading" && <BsBook className="h-4 w-4" />}
percentage={ {module === "listening" && <BsHeadphones className="h-4 w-4" />}
((assignment?.results.length || 0) / {module === "writing" && <BsPen className="h-4 w-4" />}
(assignment?.assignees.length || 1)) * {module === "speaking" && <BsMegaphone className="h-4 w-4" />}
100 {module === "level" && <BsClipboard className="h-4 w-4" />}
} <span className="text-sm">{level.toFixed(1)}</span>
/> </div>
<div className="flex items-start gap-8"> ))}
<div className="flex flex-col gap-2"> </div>
<span> </div>
Start Date:{" "} </>
{moment(assignment?.startDate).format("DD/MM/YY, HH:mm")} );
</span>
<span> return (
End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")} <div className="flex flex-col gap-2">
</span> <span>
</div> {(() => {
<span> const student = users.find((u) => u.id === user);
Assignees:{" "} return `${student?.name} (${student?.email})`;
{users })()}
.filter((u) => assignment?.assignees.includes(u.id)) </span>
.map((u) => `${u.name} (${u.email})`) <div
.join(", ")} key={user}
</span> className={clsx(
</div> "border-mti-gray-platinum -md:hidden flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out",
<div className="flex flex-col gap-2"> correct / total >= 0.7 && "hover:border-mti-purple",
<span className="text-xl font-bold">Average Scores</span> correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
<div className="-md:mt-2 flex w-full items-center gap-4"> correct / total < 0.3 && "hover:border-mti-rose",
{assignment && )}
uniqBy(assignment.exams, (x) => x.module).map(({ module }) => ( onClick={selectExam}
<div role="button">
data-tip={capitalize(module)} {content}
key={module} </div>
className={clsx( <div
"-md:px-4 tooltip flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4", key={user}
module === "reading" && "bg-ielts-reading", className={clsx(
module === "listening" && "bg-ielts-listening", "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",
module === "writing" && "bg-ielts-writing", correct / total >= 0.7 && "hover:border-mti-purple",
module === "speaking" && "bg-ielts-speaking", correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
module === "level" && "bg-ielts-level", correct / total < 0.3 && "hover:border-mti-rose",
)} )}
> data-tip="Your screen size is too small to view previous exams."
{module === "reading" && <BsBook className="h-4 w-4" />} role="button">
{module === "listening" && ( {content}
<BsHeadphones className="h-4 w-4" /> </div>
)} </div>
{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 && ( return (
<span className="text-sm"> <Modal isOpen={isOpen} onClose={onClose} title={assignment?.name}>
{calculateAverageModuleScore(module).toFixed(1)} <div className="mt-4 flex w-full flex-col gap-4">
</span> <ProgressBar
)} color="purple"
</div> label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`}
))} className="h-6"
</div> textClassName={
</div> (assignment?.results.length || 0) / (assignment?.assignees.length || 1) < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"
<div className="flex flex-col gap-2"> }
<span className="text-xl font-bold"> percentage={((assignment?.results.length || 0) / (assignment?.assignees.length || 1)) * 100}
Results ({assignment?.results.length}/{assignment?.assignees.length} />
) <div className="flex items-start gap-8">
</span> <div className="flex flex-col gap-2">
<div> <span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span>
{assignment && assignment?.results.length > 0 && ( <span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span>
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 xl:gap-6"> </div>
{assignment.results.map((r) => <span>
customContent(r.stats, r.user, r.type), Assignees:{" "}
)} {users
</div> .filter((u) => assignment?.assignees.includes(u.id))
)} .map((u) => `${u.name} (${u.email})`)
{assignment && assignment?.results.length === 0 && ( .join(", ")}
<span className="ml-1 font-semibold">No results yet...</span> </span>
)} </div>
</div> <div className="flex flex-col gap-2">
</div> <span className="text-xl font-bold">Average Scores</span>
</div> <div className="-md:mt-2 flex w-full items-center gap-4">
</Modal> {assignment &&
); uniqBy(assignment.exams, (x) => x.module).map(({module}) => (
<div
data-tip={capitalize(module)}
key={module}
className={clsx(
"-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 === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level",
)}>
{module === "reading" && <BsBook className="h-4 w-4" />}
{module === "listening" && <BsHeadphones className="h-4 w-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 && (
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span>
)}
</div>
))}
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xl font-bold">
Results ({assignment?.results.length}/{assignment?.assignees.length})
</span>
<div>
{assignment && assignment?.results.length > 0 && (
<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))}
</div>
)}
{assignment && assignment?.results.length === 0 && <span className="ml-1 font-semibold">No results yet...</span>}
</div>
</div>
<div className="flex gap-4 w-full items-center justify-end">
{assignment && (assignment.results.length === assignment.assignees.length || moment().isAfter(moment(assignment.endDate))) && (
<Button variant="outline" color="red" className="w-full max-w-[200px]" onClick={deleteAssignment}>
Delete
</Button>
)}
<Button onClick={onClose} className="w-full max-w-[200px]">
Close
</Button>
</div>
</div>
</Modal>
);
} }

View File

@@ -163,6 +163,7 @@ export default function TeacherDashboard({user}: Props) {
onClose={() => { onClose={() => {
setSelectedAssignment(undefined); setSelectedAssignment(undefined);
setIsCreatingAssignment(false); setIsCreatingAssignment(false);
reloadAssignments();
}} }}
assignment={selectedAssignment} assignment={selectedAssignment}
/> />