Merged in feature-archive-assignments (pull request #38)
Added approach to archive past assignments Approved-by: Alvaro Doria
This commit is contained in:
@@ -13,11 +13,14 @@ import {
|
|||||||
BsPen,
|
BsPen,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||||
|
import { useAssignmentArchive } from "@/hooks/useAssignmentArchive";
|
||||||
import { uniqBy } from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
allowDownload?: boolean;
|
allowDownload?: boolean;
|
||||||
|
reload?: Function;
|
||||||
|
allowArchive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssignmentCard({
|
export default function AssignmentCard({
|
||||||
@@ -29,11 +32,14 @@ export default function AssignmentCard({
|
|||||||
assignees,
|
assignees,
|
||||||
results,
|
results,
|
||||||
exams,
|
exams,
|
||||||
|
archived,
|
||||||
onClick,
|
onClick,
|
||||||
allowDownload,
|
allowDownload,
|
||||||
|
reload,
|
||||||
|
allowArchive,
|
||||||
}: Assignment & Props) {
|
}: Assignment & Props) {
|
||||||
const { users } = useUsers();
|
|
||||||
const renderPdfIcon = usePDFDownload("assignments");
|
const renderPdfIcon = usePDFDownload("assignments");
|
||||||
|
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
||||||
|
|
||||||
const calculateAverageModuleScore = (module: Module) => {
|
const calculateAverageModuleScore = (module: Module) => {
|
||||||
const resultModuleBandScores = results.map((r) => {
|
const resultModuleBandScores = results.map((r) => {
|
||||||
@@ -41,11 +47,11 @@ export default function AssignmentCard({
|
|||||||
|
|
||||||
const correct = moduleStats.reduce(
|
const correct = moduleStats.reduce(
|
||||||
(acc, curr) => acc + curr.score.correct,
|
(acc, curr) => acc + curr.score.correct,
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const total = moduleStats.reduce(
|
const total = moduleStats.reduce(
|
||||||
(acc, curr) => acc + curr.score.total,
|
(acc, curr) => acc + curr.score.total,
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
return calculateBandScore(correct, total, module, r.type);
|
return calculateBandScore(correct, total, module, r.type);
|
||||||
});
|
});
|
||||||
@@ -64,8 +70,13 @@ export default function AssignmentCard({
|
|||||||
<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="text-xl font-semibold">{name}</h3>
|
<h3 className="text-xl font-semibold">{name}</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
{allowDownload &&
|
{allowDownload &&
|
||||||
renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
|
{allowArchive &&
|
||||||
|
!archived &&
|
||||||
|
renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
color={results.length / assignees.length < 0.5 ? "red" : "purple"}
|
color={results.length / assignees.length < 0.5 ? "red" : "purple"}
|
||||||
@@ -94,7 +105,7 @@ export default function AssignmentCard({
|
|||||||
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="h-4 w-4" />}
|
{module === "reading" && <BsBook className="h-4 w-4" />}
|
||||||
|
|||||||
@@ -151,9 +151,8 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) =>
|
const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||||
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 pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length;
|
|
||||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -235,7 +234,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} allowDownload />
|
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} allowDownload reload={reloadAssignments} allowArchive/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -281,7 +280,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
||||||
<span className="flex flex-col gap-1 items-center text-xl">
|
<span className="flex flex-col gap-1 items-center text-xl">
|
||||||
<span className="text-lg">Assignments</span>
|
<span className="text-lg">Assignments</span>
|
||||||
<span className="font-semibold text-mti-purple-light">{assignments.length}</span>
|
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
45
src/hooks/useAssignmentArchive.tsx
Normal file
45
src/hooks/useAssignmentArchive.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIcon = (downloadClasses: string, loadingClasses: string) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<span className={`${loadingClasses} loading loading-infinity w-6`} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BsArchive
|
||||||
|
className={`${downloadClasses} text-2xl cursor-pointer`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
archive();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderIcon;
|
||||||
|
};
|
||||||
@@ -24,4 +24,5 @@ export interface Assignment {
|
|||||||
instructorGender?: InstructorGender;
|
instructorGender?: InstructorGender;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
archived?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/pages/api/assignments/[id]/archive.tsx
Normal file
33
src/pages/api/assignments/[id]/archive.tsx
Normal file
@@ -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: true }, { 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 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user