From 19624e97bd3714204092c050255b7109328570ab Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 9 Nov 2023 12:34:56 +0000 Subject: [PATCH] Improved the way a teacher views the assignments --- src/dashboards/AssignmentView.tsx | 270 ++++++++++++++++++++++++++++++ src/dashboards/Teacher.tsx | 25 +-- src/pages/record.tsx | 36 +++- 3 files changed, 309 insertions(+), 22 deletions(-) create mode 100644 src/dashboards/AssignmentView.tsx diff --git a/src/dashboards/AssignmentView.tsx b/src/dashboards/AssignmentView.tsx new file mode 100644 index 00000000..27b2a5a1 --- /dev/null +++ b/src/dashboards/AssignmentView.tsx @@ -0,0 +1,270 @@ +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 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 clsx from "clsx"; +import {uniqBy} from "lodash"; +import moment from "moment"; +import {useRouter} from "next/router"; +import {BsBook, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; + +interface Props { + isOpen: boolean; + assignment?: Assignment; + onClose: () => void; +} + +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 formatTimestamp = (timestamp: string) => { + const date = moment(parseInt(timestamp)); + const formatter = "YYYY/MM/DD - HH:mm"; + + return date.format(formatter); + }; + + const calculateAverageModuleScore = (module: Module) => { + if (!assignment) return -1; + + const resultModuleBandScores = assignment.results.map((r) => { + const moduleStats = r.stats.filter((s) => s.module === module); + + 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); + }); + + 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 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, + }, + }; + + 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 Object.keys(scores) + .filter((x) => scores[x as Module].total > 0) + .map((x) => ({module: x as Module, ...scores[x as Module]})); + }; + + 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 aggregatedLevels = aggregatedScores.map((x) => ({ + module: x.module, + level: calculateBandScore(x.correct, x.total, x.module, focus), + })); + + const timeSpent = stats[0].timeSpent; + + const selectExam = () => { + const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam)); + + 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 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)} + +
+ +
+
+ {aggregatedLevels.map(({module, level}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {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?.exams.map(({module}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {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...} +
+
+
+
+ ); +} diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index 562cf771..53468023 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -44,6 +44,7 @@ import Button from "@/components/Low/Button"; import clsx from "clsx"; import ProgressBar from "@/components/Low/ProgressBar"; import AssignmentCreator from "./AssignmentCreator"; +import AssignmentView from "./AssignmentView"; interface Props { user: User; @@ -150,24 +151,14 @@ export default function TeacherDashboard({user}: Props) { return ( <> - setSelectedAssignment(undefined)} - title={selectedAssignment?.name}> -
- -
-
+ onClose={() => { + setSelectedAssignment(undefined); + setIsCreatingAssignment(false); + }} + assignment={selectedAssignment} + /> x.admin === user.id || x.participants.includes(user.id))} diff --git a/src/pages/record.tsx b/src/pages/record.tsx index 85d40294..d0ed31ab 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -23,6 +23,7 @@ import Select from "react-select"; import useGroups from "@/hooks/useGroups"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import useAssignments from "@/hooks/useAssignments"; +import {uuidv4} from "@firebase/util"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -57,7 +58,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { export default function History({user}: {user: User}) { const [statsUserId, setStatsUserId] = useState(user.id); const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>(); - const [filter, setFilter] = useState<"months" | "weeks" | "days">(); + const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">(); const {assignments} = useAssignments({}); const {users} = useUsers(); @@ -73,16 +74,19 @@ export default function History({user}: {user: User}) { useEffect(() => { if (stats && !isStatsLoading) { + console.log(stats); setGroupedStats(groupByDate(stats)); } }, [stats, isStatsLoading]); - const toggleFilter = (value: "months" | "weeks" | "days") => { + const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => { setFilter((prev) => (prev === value ? undefined : value)); }; const filterStatsByDate = (stats: {[key: string]: Stat[]}) => { - if (filter) { + console.log(filter); + + if (filter && filter !== "assignments") { const filterDate = moment() .subtract({[filter as string]: 1}) .format("x"); @@ -95,6 +99,19 @@ export default function History({user}: {user: User}) { return filteredStats; } + if (filter && filter === "assignments") { + const filteredStats: {[key: string]: Stat[]} = {}; + + Object.keys(stats).forEach((timestamp) => { + if (stats[timestamp].map((s) => s.assignment === undefined).includes(false)) + filteredStats[timestamp] = [...stats[timestamp].filter((s) => !!s.assignment)]; + }); + + console.log(filteredStats); + + return filteredStats; + } + return stats; }; @@ -234,7 +251,7 @@ export default function History({user}: {user: User}) { return ( <>
= 0.7 && "hover:border-mti-purple", @@ -246,7 +263,7 @@ export default function History({user}: {user: User}) { {content}
= 0.7 && "hover:border-mti-purple", @@ -309,6 +326,15 @@ export default function History({user}: {user: User}) { )}
+