Imporvements on started management of assignment

This commit is contained in:
Joao Ramos
2024-09-03 22:56:43 +01:00
parent 8aed075553
commit a7c1ea0409
3 changed files with 630 additions and 434 deletions

View File

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

View File

@@ -1,150 +1,229 @@
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import {CorporateUser, Group, User} from "@/interfaces/user"; import { CorporateUser, Group, User } from "@/interfaces/user";
import {getUserCompanyName} from "@/resources/user"; import { getUserCompanyName } from "@/resources/user";
import {activeAssignmentFilter, archivedAssignmentFilter, futureAssignmentFilter, pastAssignmentFilter} from "@/utils/assignments"; import {
activeAssignmentFilter,
archivedAssignmentFilter,
futureAssignmentFilter,
pastAssignmentFilter,
unstartedAssignmentFilter,
} from "@/utils/assignments";
import clsx from "clsx"; import clsx from "clsx";
import {groupBy} from "lodash"; import { groupBy } from "lodash";
import {useState} from "react"; import { useState } from "react";
import {BsArrowLeft, BsArrowRepeat, BsPlus} from "react-icons/bs"; import { BsArrowLeft, BsArrowRepeat, BsPlus } from "react-icons/bs";
import AssignmentCard from "../AssignmentCard"; import AssignmentCard from "../AssignmentCard";
import AssignmentCreator from "../AssignmentCreator"; import AssignmentCreator from "../AssignmentCreator";
import AssignmentView from "../AssignmentView"; import AssignmentView from "../AssignmentView";
interface Props { interface Props {
assignments: Assignment[]; assignments: Assignment[];
corporateAssignments?: ({corporate?: CorporateUser} & Assignment)[]; corporateAssignments?: ({ corporate?: CorporateUser } & Assignment)[];
groups: Group[]; groups: Group[];
users: User[]; users: User[];
isLoading: boolean; isLoading: boolean;
user: User; user: User;
onBack: () => void; onBack: () => void;
reloadAssignments: () => void; reloadAssignments: () => void;
} }
export default function AssignmentsPage({assignments, corporateAssignments, user, groups, users, isLoading, onBack, reloadAssignments}: Props) { export default function AssignmentsPage({
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>(); assignments,
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); corporateAssignments,
user,
groups,
users,
isLoading,
onBack,
reloadAssignments,
}: Props) {
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
return ( const unstartedAssignments = assignments.filter(unstartedAssignmentFilter);
<>
<AssignmentView const displayAssignmentView = !!selectedAssignment && !isCreatingAssignment;
isOpen={!!selectedAssignment && !isCreatingAssignment}
onClose={() => { return (
setSelectedAssignment(undefined); <>
setIsCreatingAssignment(false); {displayAssignmentView && (
reloadAssignments(); <AssignmentView
}} isOpen={displayAssignmentView}
assignment={selectedAssignment} onClose={() => {
/> setSelectedAssignment(undefined);
{/** I'll be using this is creating assingment as a workaround for a key to trigger a new rendering */} setIsCreatingAssignment(false);
{isCreatingAssignment && <AssignmentCreator reloadAssignments();
assignment={selectedAssignment} }}
groups={groups} assignment={selectedAssignment}
users={users} />
user={user} )}
isCreating={isCreatingAssignment} {/** I'll be using this is creating assingment as a workaround for a key to trigger a new rendering */}
cancelCreation={() => { {isCreatingAssignment && (
setIsCreatingAssignment(false); <AssignmentCreator
setSelectedAssignment(undefined); assignment={selectedAssignment}
reloadAssignments(); groups={groups}
}} users={users}
/>} user={user}
<div className="w-full flex justify-between items-center"> isCreating={isCreatingAssignment}
<div cancelCreation={() => {
onClick={onBack} setIsCreatingAssignment(false);
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> setSelectedAssignment(undefined);
<BsArrowLeft className="text-xl" /> reloadAssignments();
<span>Back</span> }}
</div> />
<div )}
onClick={reloadAssignments} <div className="w-full flex justify-between items-center">
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> <div
<span>Reload</span> onClick={onBack}
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} /> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
</div> >
</div> <BsArrowLeft className="text-xl" />
<div className="flex flex-col gap-2"> <span>Back</span>
<span className="text-lg font-bold">Active Assignments Status</span> </div>
<div className="flex items-center gap-4"> <div
<span> onClick={reloadAssignments}
<b>Total:</b> {assignments.filter(activeAssignmentFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/ className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
{assignments.filter(activeAssignmentFilter).reduce((acc, curr) => curr.exams.length + acc, 0)} >
</span> <span>Reload</span>
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => ( <BsArrowRepeat
<div key={x}> className={clsx("text-xl", isLoading && "animate-spin")}
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span> />
<span> </div>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/ </div>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)} <div className="flex flex-col gap-2">
</span> <span className="text-lg font-bold">Active Assignments Status</span>
</div> <div className="flex items-center gap-4">
))} <span>
</div> <b>Total:</b>{" "}
</div> {assignments
<section className="flex flex-col gap-4"> .filter(activeAssignmentFilter)
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeAssignmentFilter).length})</h2> .reduce((acc, curr) => acc + curr.results.length, 0)}
<div className="flex flex-wrap gap-2"> /
{assignments.filter(activeAssignmentFilter).map((a) => ( {assignments
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} /> .filter(activeAssignmentFilter)
))} .reduce((acc, curr) => curr.exams.length + acc, 0)}
</div> </span>
</section> {Object.keys(
<section className="flex flex-col gap-4"> groupBy(corporateAssignments, (x) => x.corporate?.id)
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureAssignmentFilter).length})</h2> ).map((x) => (
<div className="flex flex-wrap gap-2"> <div key={x}>
<div <span className="font-semibold">
onClick={() => setIsCreatingAssignment(true)} {getUserCompanyName(
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"> users.find((u) => u.id === x)!,
<BsPlus className="text-6xl" /> users,
<span className="text-lg">New Assignment</span> groups
</div> )}
{assignments.filter(futureAssignmentFilter).map((a) => ( :{" "}
<AssignmentCard </span>
{...a} <span>
users={users} {groupBy(corporateAssignments, (x) => x.corporate?.id)[
onClick={() => { x
setSelectedAssignment(a); ].reduce((acc, curr) => curr.results.length + acc, 0)}
setIsCreatingAssignment(true); /
}} {groupBy(corporateAssignments, (x) => x.corporate?.id)[
key={a.id} x
/> ].reduce((acc, curr) => curr.exams.length + acc, 0)}
))} </span>
</div> </div>
</section> ))}
<section className="flex flex-col gap-4"> </div>
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastAssignmentFilter).length})</h2> </div>
<div className="flex flex-wrap gap-2"> <section className="flex flex-col gap-4">
{assignments.filter(pastAssignmentFilter).map((a) => ( <h2 className="text-2xl font-semibold">
<AssignmentCard Active Assignments (
{...a} {assignments.filter(activeAssignmentFilter).length})
users={users} </h2>
onClick={() => setSelectedAssignment(a)} <div className="flex flex-wrap gap-2">
key={a.id} {assignments.filter(activeAssignmentFilter).map((a) => (
allowDownload <AssignmentCard
reload={reloadAssignments} {...a}
allowArchive users={users}
allowExcelDownload onClick={() => setSelectedAssignment(a)}
/> key={a.id}
))} />
</div> ))}
</section> </div>
<section className="flex flex-col gap-4"> </section>
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})</h2> <section className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2"> <h2 className="text-2xl font-semibold">
{assignments.filter(archivedAssignmentFilter).map((a) => ( Active Assignments Pending Start ({unstartedAssignments.length})
<AssignmentCard </h2>
{...a} <div className="flex flex-wrap gap-2">
users={users} {unstartedAssignments.map((a) => (
onClick={() => setSelectedAssignment(a)} <AssignmentCard
key={a.id} {...a}
allowDownload users={users}
reload={reloadAssignments} onClick={() => setSelectedAssignment(a)}
allowUnarchive key={a.id}
allowExcelDownload />
/> ))}
))} </div>
</div> </section>
</section> <section className="flex flex-col gap-4">
</> <h2 className="text-2xl font-semibold">
); Planned Assignments (
{assignments.filter(futureAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
<div
onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"
>
<BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span>
</div>
{assignments.filter(futureAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
}}
key={a.id}
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(pastAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowArchive
allowExcelDownload
/>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Archived Assignments (
{assignments.filter(archivedAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(archivedAssignmentFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
reload={reloadAssignments}
allowUnarchive
allowExcelDownload
/>
))}
</div>
</section>
</>
);
} }

View File

@@ -38,3 +38,14 @@ export const activeAssignmentFilter = (a: Assignment) => {
return false; return false;
}; };
export const unstartedAssignmentFilter = (a: Assignment) => {
const currentDate = moment();
if(moment(a.endDate).isBefore(currentDate)) return false;
if(a.archived) return false;
if(a.autoStart && a.autoStartDate && moment(a.autoStartDate).isBefore(currentDate)) return false;
if(!a.start) return true;
return false;
}