From 0e53b4a454b61a104fd91498d39df18d10b8b6f9 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 5 May 2024 12:02:53 +0100 Subject: [PATCH] Added the ability to view archived assignments and unarchive them --- src/dashboards/AssignmentCard.tsx | 201 +++++++++---------- src/dashboards/Teacher.tsx | 28 ++- src/hooks/useAssignmentArchive.tsx | 75 ++++--- src/hooks/useAssignmentUnarchive.tsx | 42 ++++ src/pages/api/assignments/[id]/unarchive.tsx | 33 +++ 5 files changed, 227 insertions(+), 152 deletions(-) create mode 100644 src/hooks/useAssignmentUnarchive.tsx create mode 100644 src/pages/api/assignments/[id]/unarchive.tsx diff --git a/src/dashboards/AssignmentCard.tsx b/src/dashboards/AssignmentCard.tsx index f08ae396..7316c379 100644 --- a/src/dashboards/AssignmentCard.tsx +++ b/src/dashboards/AssignmentCard.tsx @@ -1,126 +1,105 @@ import ProgressBar from "@/components/Low/ProgressBar"; import useUsers from "@/hooks/useUsers"; -import { Module } from "@/interfaces"; -import { Assignment } from "@/interfaces/results"; -import { calculateBandScore } from "@/utils/score"; +import {Module} from "@/interfaces"; +import {Assignment} from "@/interfaces/results"; +import {calculateBandScore} from "@/utils/score"; import clsx from "clsx"; import moment from "moment"; -import { - BsBook, - BsClipboard, - BsHeadphones, - BsMegaphone, - BsPen, -} from "react-icons/bs"; -import { usePDFDownload } from "@/hooks/usePDFDownload"; -import { useAssignmentArchive } from "@/hooks/useAssignmentArchive"; -import { uniqBy } from "lodash"; +import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; +import {usePDFDownload} from "@/hooks/usePDFDownload"; +import {useAssignmentArchive} from "@/hooks/useAssignmentArchive"; +import {uniqBy} from "lodash"; +import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive"; interface Props { - onClick?: () => void; - allowDownload?: boolean; - reload?: Function; - allowArchive?: boolean; + onClick?: () => void; + allowDownload?: boolean; + reload?: Function; + allowArchive?: boolean; + allowUnarchive?: boolean; } export default function AssignmentCard({ - id, - name, - assigner, - startDate, - endDate, - assignees, - results, - exams, - archived, - onClick, - allowDownload, - reload, - allowArchive, + id, + name, + assigner, + startDate, + endDate, + assignees, + results, + exams, + archived, + onClick, + allowDownload, + reload, + allowArchive, + allowUnarchive, }: Assignment & Props) { - const renderPdfIcon = usePDFDownload("assignments"); - const renderArchiveIcon = useAssignmentArchive(id, reload); + const renderPdfIcon = usePDFDownload("assignments"); + const renderArchiveIcon = useAssignmentArchive(id, reload); + const renderUnarchiveIcon = useAssignmentUnarchive(id, reload); - const calculateAverageModuleScore = (module: Module) => { - const resultModuleBandScores = results.map((r) => { - const moduleStats = r.stats.filter((s) => s.module === module); + const calculateAverageModuleScore = (module: Module) => { + const resultModuleBandScores = 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); - }); + 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) / - results.length; - }; + return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / results.length; + }; - return ( -
-
-
-

{name}

-
- {allowDownload && - renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} - {allowArchive && - !archived && - renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} -
-
- -
- - {moment(startDate).format("DD/MM/YY, HH:mm")} - - - {moment(endDate).format("DD/MM/YY, HH:mm")} - -
- {uniqBy(exams, (x) => x.module).map(({ module }) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {calculateAverageModuleScore(module) > -1 && ( - - {calculateAverageModuleScore(module).toFixed(1)} - - )} -
- ))} -
-
- ); + return ( +
+
+
+

{name}

+
+ {allowDownload && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} + {allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} + {allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} +
+
+ +
+ + {moment(startDate).format("DD/MM/YY, HH:mm")} + - + {moment(endDate).format("DD/MM/YY, HH:mm")} + +
+ {uniqBy(exams, (x) => x.module).map(({module}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {calculateAverageModuleScore(module) > -1 && ( + {calculateAverageModuleScore(module).toFixed(1)} + )} +
+ ))} +
+
+ ); } diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index e7f3036d..f5c64125 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -151,8 +151,10 @@ export default function TeacherDashboard({user}: Props) { }; const AssignmentsPage = () => { - const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; + const activeFilter = (a: Assignment) => + moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived; + const archivedFilter = (a: Assignment) => a.archived; const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); return ( @@ -234,7 +236,29 @@ export default function TeacherDashboard({user}: Props) {

Past Assignments ({assignments.filter(pastFilter).length})

{assignments.filter(pastFilter).map((a) => ( - setSelectedAssignment(a)} key={a.id} allowDownload reload={reloadAssignments} allowArchive/> + setSelectedAssignment(a)} + key={a.id} + allowDownload + reload={reloadAssignments} + allowArchive + /> + ))} +
+ +
+

Archived Assignments ({assignments.filter(archivedFilter).length})

+
+ {assignments.filter(archivedFilter).map((a) => ( + setSelectedAssignment(a)} + key={a.id} + allowDownload + reload={reloadAssignments} + allowUnarchive + /> ))}
diff --git a/src/hooks/useAssignmentArchive.tsx b/src/hooks/useAssignmentArchive.tsx index 67879189..397ee2a2 100644 --- a/src/hooks/useAssignmentArchive.tsx +++ b/src/hooks/useAssignmentArchive.tsx @@ -1,45 +1,42 @@ import React from "react"; import axios from "axios"; -import { toast } from "react-toastify"; -import { BsArchive } from "react-icons/bs"; +import {toast} from "react-toastify"; +import {BsArchive} from "react-icons/bs"; -export const useAssignmentArchive = ( - assignmentId: string, - reload?: Function -) => { - const [loading, setLoading] = React.useState(false); - const archive = () => { - // archive assignment - setLoading(true); - axios - .post(`/api/assignments/${assignmentId}/archive`) - .then((res) => { - toast.success("Assignment archived!"); - if(reload) reload(); - setLoading(false); - }) - .catch((err) => { - toast.error("Failed to archive the assignment!"); - setLoading(false); - }); - }; +export const useAssignmentArchive = (assignmentId: string, reload?: Function) => { + const [loading, setLoading] = React.useState(false); + const archive = () => { + // archive assignment + setLoading(true); + axios + .post(`/api/assignments/${assignmentId}/archive`) + .then((res) => { + toast.success("Assignment archived!"); + if (reload) reload(); + setLoading(false); + }) + .catch((err) => { + toast.error("Failed to archive the assignment!"); + setLoading(false); + }); + }; - const renderIcon = (downloadClasses: string, loadingClasses: string) => { - if (loading) { - return ( - - ); - } - return ( - { - e.stopPropagation(); - archive(); - }} - /> - ); - }; + const renderIcon = (downloadClasses: string, loadingClasses: string) => { + if (loading) { + return ; + } + return ( +
{ + e.stopPropagation(); + archive(); + }}> + +
+ ); + }; - return renderIcon; + return renderIcon; }; diff --git a/src/hooks/useAssignmentUnarchive.tsx b/src/hooks/useAssignmentUnarchive.tsx new file mode 100644 index 00000000..7612b504 --- /dev/null +++ b/src/hooks/useAssignmentUnarchive.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import axios from "axios"; +import {toast} from "react-toastify"; +import {BsArchive, BsFileEarmarkCheck, BsFileEarmarkCheckFill} from "react-icons/bs"; + +export const useAssignmentUnarchive = (assignmentId: string, reload?: Function) => { + const [loading, setLoading] = React.useState(false); + const archive = () => { + // archive assignment + setLoading(true); + axios + .post(`/api/assignments/${assignmentId}/unarchive`) + .then((res) => { + toast.success("Assignment unarchived!"); + if (reload) reload(); + setLoading(false); + }) + .catch((err) => { + toast.error("Failed to unarchive the assignment!"); + setLoading(false); + }); + }; + + const renderIcon = (downloadClasses: string, loadingClasses: string) => { + if (loading) { + return ; + } + return ( +
{ + e.stopPropagation(); + archive(); + }}> + +
+ ); + }; + + return renderIcon; +}; diff --git a/src/pages/api/assignments/[id]/unarchive.tsx b/src/pages/api/assignments/[id]/unarchive.tsx new file mode 100644 index 00000000..cda7498f --- /dev/null +++ b/src/pages/api/assignments/[id]/unarchive.tsx @@ -0,0 +1,33 @@ +import type {NextApiRequest, NextApiResponse} from "next"; +import {app} from "@/firebase"; +import {getFirestore, doc, getDoc, setDoc} from "firebase/firestore"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function post(req: NextApiRequest, res: NextApiResponse) { + // verify if it's a logged user that is trying to archive + if (req.session.user) { + const {id} = req.query as {id: string}; + const docSnap = await getDoc(doc(db, "assignments", id)); + + if (!docSnap.exists()) { + res.status(404).json({ok: false}); + return; + } + + await setDoc(docSnap.ref, {archived: false}, {merge: true}); + res.status(200).json({ok: true}); + return; + } + + res.status(401).json({ok: false}); +} + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "POST") return post(req, res); + res.status(404).json({ok: false}); +}