diff --git a/src/dashboards/AssignmentView.tsx b/src/dashboards/AssignmentView.tsx index 42a68a7f..daa330de 100644 --- a/src/dashboards/AssignmentView.tsx +++ b/src/dashboards/AssignmentView.tsx @@ -1,354 +1,307 @@ +import Button from "@/components/Low/Button"; import ProgressBar from "@/components/Low/ProgressBar"; import Modal from "@/components/Modal"; import useUsers from "@/hooks/useUsers"; -import { Module } from "@/interfaces"; -import { Assignment } from "@/interfaces/results"; -import { Stat, User } from "@/interfaces/user"; +import {Module} from "@/interfaces"; +import {Assignment} from "@/interfaces/results"; +import {Stat, User} from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; -import { getExamById } from "@/utils/exams"; -import { sortByModule } from "@/utils/moduleUtils"; -import { calculateBandScore } from "@/utils/score"; -import { convertToUserSolutions } from "@/utils/stats"; +import {getExamById} from "@/utils/exams"; +import {sortByModule} from "@/utils/moduleUtils"; +import {calculateBandScore} from "@/utils/score"; +import {convertToUserSolutions} from "@/utils/stats"; +import axios from "axios"; import clsx from "clsx"; -import { capitalize, uniqBy } from "lodash"; +import {capitalize, uniqBy} from "lodash"; import moment from "moment"; -import { useRouter } from "next/router"; -import { - BsBook, - BsClipboard, - BsHeadphones, - BsMegaphone, - BsPen, -} from "react-icons/bs"; +import {useRouter} from "next/router"; +import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; +import {toast} from "react-toastify"; interface Props { - isOpen: boolean; - assignment?: Assignment; - onClose: () => void; + isOpen: boolean; + assignment?: Assignment; + onClose: () => void; } -export default function AssignmentView({ isOpen, assignment, onClose }: Props) { - const { users } = useUsers(); - const router = useRouter(); +export default function AssignmentView({isOpen, assignment, onClose}: Props) { + const {users} = useUsers(); + const router = useRouter(); - const setExams = useExamStore((state) => state.setExams); - const setShowSolutions = useExamStore((state) => state.setShowSolutions); - const setUserSolutions = useExamStore((state) => state.setUserSolutions); - const setSelectedModules = useExamStore((state) => state.setSelectedModules); + const setExams = useExamStore((state) => state.setExams); + const setShowSolutions = useExamStore((state) => state.setShowSolutions); + const setUserSolutions = useExamStore((state) => state.setUserSolutions); + const setSelectedModules = useExamStore((state) => state.setSelectedModules); - const formatTimestamp = (timestamp: string) => { - const date = moment(parseInt(timestamp)); - const formatter = "YYYY/MM/DD - HH:mm"; + const deleteAssignment = async () => { + if (!confirm("Are you sure you want to delete this assignment?")) return; - 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) => { - if (!assignment) return -1; + const formatTimestamp = (timestamp: string) => { + const date = moment(parseInt(timestamp)); + const formatter = "YYYY/MM/DD - HH:mm"; - const resultModuleBandScores = assignment.results.map((r) => { - const moduleStats = r.stats.filter((s) => s.module === module); + return date.format(formatter); + }; - const correct = moduleStats.reduce( - (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); - }); + const calculateAverageModuleScore = (module: Module) => { + if (!assignment) return -1; - return resultModuleBandScores.length === 0 - ? -1 - : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / - assignment.results.length; - }; + const resultModuleBandScores = assignment.results.map((r) => { + const moduleStats = r.stats.filter((s) => s.module === module); - const aggregateScoresByModule = ( - stats: Stat[], - ): { module: Module; total: number; missing: number; correct: number }[] => { - 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, - }, - }; + const correct = moduleStats.reduce((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); + }); - stats.forEach((x) => { - 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 resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / assignment.results.length; + }; - return Object.keys(scores) - .filter((x) => scores[x as Module].total > 0) - .map((x) => ({ module: x as Module, ...scores[x as Module] })); - }; + const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => { + 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, + }, + }; - 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, - ); + stats.forEach((x) => { + 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, + }; + }); - const aggregatedLevels = aggregatedScores.map((x) => ({ - module: x.module, - level: calculateBandScore(x.correct, x.total, x.module, focus), - })); + return Object.keys(scores) + .filter((x) => scores[x as Module].total > 0) + .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 examPromises = uniqBy(stats, "exam").map((stat) => - getExamById(stat.module, stat.exam), - ); + const aggregatedLevels = aggregatedScores.map((x) => ({ + module: x.module, + level: calculateBandScore(x.correct, x.total, x.module, focus), + })); - Promise.all(examPromises).then((exams) => { - 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 timeSpent = stats[0].timeSpent; - const content = ( - <> -
-
- - {formatTimestamp(stats[0].date.toString())} - - {timeSpent && ( - <> - - - {Math.floor(timeSpent / 60)} minutes - - - )} -
- = 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)} - -
+ const selectExam = () => { + const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam)); -
-
- {aggregatedLevels.map(({ module, level }) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {level.toFixed(1)} -
- ))} -
-
- - ); + Promise.all(examPromises).then((exams) => { + 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"); + } + }); + }; - return ( -
- - {(() => { - const student = users.find((u) => u.id === user); - return `${student?.name} (${student?.email})`; - })()} - -
= 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", - )} - onClick={selectExam} - role="button" - > - {content} -
-
= 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} -
-
- ); - }; + const content = ( + <> +
+
+ {formatTimestamp(stats[0].date.toString())} + {timeSpent && ( + <> + + {Math.floor(timeSpent / 60)} minutes + + )} +
+ = 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)} + +
- return ( - -
- -
-
- - Start Date:{" "} - {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")} - - - End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")} - -
- - Assignees:{" "} - {users - .filter((u) => assignment?.assignees.includes(u.id)) - .map((u) => `${u.name} (${u.email})`) - .join(", ")} - -
-
- Average Scores -
- {assignment && - uniqBy(assignment.exams, (x) => x.module).map(({ module }) => ( -
- {module === "reading" && } - {module === "listening" && ( - - )} - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {calculateAverageModuleScore(module) > -1 && ( - - {calculateAverageModuleScore(module).toFixed(1)} - - )} -
- ))} -
-
-
- - Results ({assignment?.results.length}/{assignment?.assignees.length} - ) - -
- {assignment && assignment?.results.length > 0 && ( -
- {assignment.results.map((r) => - customContent(r.stats, r.user, r.type), - )} -
- )} - {assignment && assignment?.results.length === 0 && ( - No results yet... - )} -
-
-
-
- ); +
+
+ {aggregatedLevels.map(({module, level}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {level.toFixed(1)} +
+ ))} +
+
+ + ); + + return ( +
+ + {(() => { + const student = users.find((u) => u.id === user); + return `${student?.name} (${student?.email})`; + })()} + +
= 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", + )} + onClick={selectExam} + role="button"> + {content} +
+
= 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} +
+
+ ); + }; + + return ( + +
+ +
+
+ Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")} + End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")} +
+ + Assignees:{" "} + {users + .filter((u) => assignment?.assignees.includes(u.id)) + .map((u) => `${u.name} (${u.email})`) + .join(", ")} + +
+
+ Average Scores +
+ {assignment && + uniqBy(assignment.exams, (x) => x.module).map(({module}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {calculateAverageModuleScore(module) > -1 && ( + {calculateAverageModuleScore(module).toFixed(1)} + )} +
+ ))} +
+
+
+ + Results ({assignment?.results.length}/{assignment?.assignees.length}) + +
+ {assignment && assignment?.results.length > 0 && ( +
+ {assignment.results.map((r) => customContent(r.stats, r.user, r.type))} +
+ )} + {assignment && assignment?.results.length === 0 && No results yet...} +
+
+ +
+ {assignment && (assignment.results.length === assignment.assignees.length || moment().isAfter(moment(assignment.endDate))) && ( + + )} + +
+
+
+ ); } diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index 2d181089..4535d724 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -163,6 +163,7 @@ export default function TeacherDashboard({user}: Props) { onClose={() => { setSelectedAssignment(undefined); setIsCreatingAssignment(false); + reloadAssignments(); }} assignment={selectedAssignment} />