Merged in features-03-Sep-24 (pull request #89)

ENCOA-146 + Bug fixing

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-09-04 08:10:12 +00:00
committed by Tiago Ribeiro
6 changed files with 731 additions and 445 deletions

View File

@@ -47,7 +47,8 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
}), }),
); );
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : new Date()); const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, 'hour').toDate());
const [endDate, setEndDate] = useState<Date | null>( const [endDate, setEndDate] = useState<Date | null>(
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(), assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
); );
@@ -55,7 +56,10 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
const [instructorGender, setInstructorGender] = useState<InstructorGender>(assignment?.instructorGender || "varied"); const [instructorGender, setInstructorGender] = useState<InstructorGender>(assignment?.instructorGender || "varied");
// creates a new exam for each assignee or just one exam for all assignees // creates a new exam for each assignee or just one exam for all assignees
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false); const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
const [released, setReleased] = useState<boolean>(false); const [released, setReleased] = useState<boolean>(assignment?.released || false);
const [autoStart, setAutostart] = useState<boolean>(assignment?.autoStart || false);
const [autoStartDate, setAutoStartDate] = useState<Date | null>(assignment ? moment(assignment.autoStartDate).toDate() : new Date());
const [useRandomExams, setUseRandomExams] = useState(true); const [useRandomExams, setUseRandomExams] = useState(true);
const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]); const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]);
@@ -90,6 +94,8 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
variant, variant,
instructorGender, instructorGender,
released, released,
autoStart,
autoStartDate,
}) })
.then(() => { .then(() => {
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`); toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
@@ -233,7 +239,7 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8"> <div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">Start Date *</label> <label className="font-normal text-base text-mti-gray-dim">Limit Start Date *</label>
<ReactDatePicker <ReactDatePicker
className={clsx( className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer", "p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
@@ -264,6 +270,23 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
onChange={(date) => setEndDate(date)} onChange={(date) => setEndDate(date)}
/> />
</div> </div>
{autoStart && (<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">Automatic Start Date *</label>
<ReactDatePicker
className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip z-10",
"transition duration-300 ease-in-out",
)}
popperClassName="!z-20"
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
dateFormat="dd/MM/yyyy HH:mm"
selected={autoStartDate}
showTimeSelect
onChange={(date) => setAutoStartDate(date)}
/>
</div>
)}
</div> </div>
{selectedModules.includes("speaking") && ( {selectedModules.includes("speaking") && (
@@ -380,7 +403,10 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
Generate different exams Generate different exams
</Checkbox> </Checkbox>
<Checkbox isChecked={released} onChange={() => setReleased((d) => !d)}> <Checkbox isChecked={released} onChange={() => setReleased((d) => !d)}>
Release automatically Auto release results
</Checkbox>
<Checkbox isChecked={autoStart} onChange={() => setAutostart((d) => !d)}>
Auto start exam
</Checkbox> </Checkbox>
</div> </div>
<div className="flex gap-4 w-full justify-end"> <div className="flex gap-4 w-full justify-end">

View File

@@ -16,8 +16,15 @@ 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 {
BsBook,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { futureAssignmentFilter } from "@/utils/assignments";
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
@@ -39,7 +46,11 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
axios axios
.delete(`/api/assignments/${assignment?.id}`) .delete(`/api/assignments/${assignment?.id}`)
.then(() => toast.success(`Successfully deleted the assignment "${assignment?.name}".`)) .then(() =>
toast.success(
`Successfully deleted the assignment "${assignment?.name}".`
)
)
.catch(() => toast.error("Something went wrong, please try again later.")) .catch(() => toast.error("Something went wrong, please try again later."))
.finally(onClose); .finally(onClose);
}; };
@@ -49,7 +60,9 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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) => { .catch((e) => {
console.log(e); console.log(e);
@@ -71,15 +84,26 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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,
0
);
const total = moduleStats.reduce(
(acc, curr) => acc + curr.score.total,
0
);
return calculateBandScore(correct, total, module, r.type); 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 = (
stats: Stat[]
): { module: Module; total: number; missing: number; correct: number }[] => {
const scores: { const scores: {
[key in Module]: { total: number; missing: number; correct: number }; [key in Module]: { total: number; missing: number; correct: number };
} = { } = {
@@ -123,10 +147,22 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
.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,
@@ -136,7 +172,9 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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)) {
@@ -147,7 +185,7 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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");
} }
@@ -158,11 +196,15 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
<> <>
<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">
{formatTimestamp(stats[0].date.toString())}
</span>
{timeSpent && ( {timeSpent && (
<> <>
<span className="md:hidden 2xl:flex"> </span> <span className="md:hidden 2xl:flex"> </span>
<span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span> <span className="text-sm">
{Math.floor(timeSpent / 60)} minutes
</span>
</> </>
)} )}
</div> </div>
@@ -170,10 +212,16 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
className={clsx( className={clsx(
correct / total >= 0.7 && "text-mti-purple", correct / total >= 0.7 && "text-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
correct / total < 0.3 && "text-mti-rose", correct / total < 0.3 && "text-mti-rose"
)}> )}
>
Level{" "} Level{" "}
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} {(
aggregatedLevels.reduce(
(accumulator, current) => accumulator + current.level,
0
) / aggregatedLevels.length
).toFixed(1)}
</span> </span>
</div> </div>
@@ -188,8 +236,9 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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" />}
{module === "listening" && <BsHeadphones className="h-4 w-4" />} {module === "listening" && <BsHeadphones className="h-4 w-4" />}
{module === "writing" && <BsPen className="h-4 w-4" />} {module === "writing" && <BsPen className="h-4 w-4" />}
@@ -216,11 +265,14 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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",
correct / total < 0.3 && "hover:border-mti-rose"
)} )}
onClick={selectExam} onClick={selectExam}
role="button"> role="button"
>
{content} {content}
</div> </div>
<div <div
@@ -228,17 +280,30 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
className={clsx( className={clsx(
"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", "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", 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",
correct / total < 0.3 && "hover:border-mti-rose"
)} )}
data-tip="Your screen size is too small to view previous exams." data-tip="Your screen size is too small to view previous exams."
role="button"> role="button"
>
{content} {content}
</div> </div>
</div> </div>
); );
}; };
const shouldRenderStart = () => {
if (assignment) {
if (futureAssignmentFilter(assignment)) {
return true;
}
}
return false;
};
return ( return (
<Modal isOpen={isOpen} onClose={onClose} title={assignment?.name}> <Modal isOpen={isOpen} onClose={onClose} title={assignment?.name}>
<div className="mt-4 flex w-full flex-col gap-4"> <div className="mt-4 flex w-full flex-col gap-4">
@@ -247,14 +312,27 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`} label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`}
className="h-6" className="h-6"
textClassName={ textClassName={
(assignment?.results.length || 0) / (assignment?.assignees.length || 1) < 0.5 ? "!text-mti-gray-dim font-light" : "text-white" (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
} }
percentage={((assignment?.results.length || 0) / (assignment?.assignees.length || 1)) * 100}
/> />
<div className="flex items-start gap-8"> <div className="flex items-start gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span> <span>
<span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</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>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span> <span>
@@ -264,7 +342,10 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
.map((u) => `${u.name} (${u.email})`) .map((u) => `${u.name} (${u.email})`)
.join(", ")} .join(", ")}
</span> </span>
<span>Assigner: {getUserName(users.find((x) => x.id === assignment?.assigner))}</span> <span>
Assigner:{" "}
{getUserName(users.find((x) => x.id === assignment?.assigner))}
</span>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -281,15 +362,20 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
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" />}
{module === "listening" && <BsHeadphones className="h-4 w-4" />} {module === "listening" && (
<BsHeadphones className="h-4 w-4" />
)}
{module === "writing" && <BsPen className="h-4 w-4" />} {module === "writing" && <BsPen className="h-4 w-4" />}
{module === "speaking" && <BsMegaphone className="h-4 w-4" />} {module === "speaking" && <BsMegaphone className="h-4 w-4" />}
{module === "level" && <BsClipboard className="h-4 w-4" />} {module === "level" && <BsClipboard className="h-4 w-4" />}
{calculateAverageModuleScore(module) > -1 && ( {calculateAverageModuleScore(module) > -1 && (
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span> <span className="text-sm">
{calculateAverageModuleScore(module).toFixed(1)}
</span>
)} )}
</div> </div>
))} ))}
@@ -297,26 +383,44 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-xl font-bold"> <span className="text-xl font-bold">
Results ({assignment?.results.length}/{assignment?.assignees.length}) Results ({assignment?.results.length}/{assignment?.assignees.length}
)
</span> </span>
<div> <div>
{assignment && assignment?.results.length > 0 && ( {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"> <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))} {assignment.results.map((r) =>
customContent(r.stats, r.user, r.type)
)}
</div> </div>
)} )}
{assignment && assignment?.results.length === 0 && <span className="ml-1 font-semibold">No results yet...</span>} {assignment && assignment?.results.length === 0 && (
<span className="ml-1 font-semibold">No results yet...</span>
)}
</div> </div>
</div> </div>
<div className="flex gap-4 w-full items-center justify-end"> <div className="flex gap-4 w-full items-center justify-end">
{assignment && (assignment.results.length === assignment.assignees.length || moment().isAfter(moment(assignment.endDate))) && ( {assignment &&
<Button variant="outline" color="red" className="w-full max-w-[200px]" onClick={deleteAssignment}> (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 Delete
</Button> </Button>
)} )}
{assignment && (assignment.results.length === 0 || moment().isAfter(moment(assignment.startDate))) && ( {/** if the assignment is not deemed as active yet, display start */}
<Button variant="outline" color="green" className="w-full max-w-[200px]" onClick={startAssignment}> {shouldRenderStart() && (
<Button
variant="outline"
color="green"
className="w-full max-w-[200px]"
onClick={startAssignment}
>
Start Start
</Button> </Button>
)} )}

View File

@@ -27,6 +27,7 @@ import {useRouter} from "next/router";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs"; import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import { activeAssignmentFilter } from "@/utils/assignments";
interface Props { interface Props {
user: User; user: User;
@@ -69,6 +70,9 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props)
}); });
}; };
const studentAssignments = assignments
.filter(activeAssignmentFilter);
return ( return (
<> <>
{linkedCorporate && ( {linkedCorporate && (
@@ -119,10 +123,9 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props)
</div> </div>
</div> </div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll"> <span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{assignments.filter((a) => moment(a.endDate).isSameOrAfter(moment())).length === 0 && {studentAssignments.length === 0 &&
"Assignments will appear here. It seems that for now there are no assignments for you."} "Assignments will appear here. It seems that for now there are no assignments for you."}
{assignments {studentAssignments
.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
.sort((a, b) => moment(a.startDate).diff(b.startDate)) .sort((a, b) => moment(a.startDate).diff(b.startDate))
.map((assignment) => ( .map((assignment) => (
<div <div
@@ -170,12 +173,12 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props)
<div <div
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden" className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
data-tip="Your screen size is too small to perform an assignment"> data-tip="Your screen size is too small to perform an assignment">
<Button disabled={!assignment.start} className="h-full w-full !rounded-xl" variant="outline"> <Button className="h-full w-full !rounded-xl" variant="outline">
Start Start
</Button> </Button>
</div> </div>
<Button <Button
disabled={!assignment.start}
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl" className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
onClick={() => startAssignment(assignment)} onClick={() => startAssignment(assignment)}
variant="outline"> variant="outline">

View File

@@ -1,7 +1,13 @@
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,
startHasExpiredAssignmentFilter,
} 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";
@@ -21,14 +27,28 @@ interface Props {
reloadAssignments: () => void; reloadAssignments: () => void;
} }
export default function AssignmentsPage({assignments, corporateAssignments, user, groups, users, isLoading, onBack, reloadAssignments}: Props) { export default function AssignmentsPage({
assignments,
corporateAssignments,
user,
groups,
users,
isLoading,
onBack,
reloadAssignments,
}: Props) {
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>(); const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const displayAssignmentView = !!selectedAssignment && !isCreatingAssignment;
const assignmentsPastExpiredStart = assignments.filter(startHasExpiredAssignmentFilter);
return ( return (
<> <>
{displayAssignmentView && (
<AssignmentView <AssignmentView
isOpen={!!selectedAssignment && !isCreatingAssignment} isOpen={displayAssignmentView}
onClose={() => { onClose={() => {
setSelectedAssignment(undefined); setSelectedAssignment(undefined);
setIsCreatingAssignment(false); setIsCreatingAssignment(false);
@@ -36,6 +56,9 @@ export default function AssignmentsPage({assignments, corporateAssignments, user
}} }}
assignment={selectedAssignment} assignment={selectedAssignment}
/> />
)}
{/** I'll be using this is creating assingment as a workaround for a key to trigger a new rendering */}
{isCreatingAssignment && (
<AssignmentCreator <AssignmentCreator
assignment={selectedAssignment} assignment={selectedAssignment}
groups={groups} groups={groups}
@@ -48,52 +71,89 @@ export default function AssignmentsPage({assignments, corporateAssignments, user
reloadAssignments(); reloadAssignments();
}} }}
/> />
)}
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={onBack} onClick={onBack}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} /> <BsArrowRepeat
className={clsx("text-xl", isLoading && "animate-spin")}
/>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-lg font-bold">Active Assignments Status</span> <span className="text-lg font-bold">Active Assignments Status</span>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span> <span>
<b>Total:</b> {assignments.filter(activeAssignmentFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/ <b>Total:</b>{" "}
{assignments.filter(activeAssignmentFilter).reduce((acc, curr) => curr.exams.length + acc, 0)} {assignments
.filter(activeAssignmentFilter)
.reduce((acc, curr) => acc + curr.results.length, 0)}
/
{assignments
.filter(activeAssignmentFilter)
.reduce((acc, curr) => curr.exams.length + acc, 0)}
</span> </span>
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => ( {Object.keys(
groupBy(corporateAssignments, (x) => x.corporate?.id)
).map((x) => (
<div key={x}> <div key={x}>
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span> <span className="font-semibold">
{getUserCompanyName(
users.find((u) => u.id === x)!,
users,
groups
)}
:{" "}
</span>
<span> <span>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/ {groupBy(corporateAssignments, (x) => x.corporate?.id)[
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)} x
].reduce((acc, curr) => curr.results.length + acc, 0)}
/
{groupBy(corporateAssignments, (x) => x.corporate?.id)[
x
].reduce((acc, curr) => curr.exams.length + acc, 0)}
</span> </span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeAssignmentFilter).length})</h2> <h2 className="text-2xl font-semibold">
Active Assignments (
{assignments.filter(activeAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(activeAssignmentFilter).map((a) => ( {assignments.filter(activeAssignmentFilter).map((a) => (
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} /> <AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
))} ))}
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureAssignmentFilter).length})</h2> <h2 className="text-2xl font-semibold">
Planned Assignments (
{assignments.filter(futureAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<div <div
onClick={() => setIsCreatingAssignment(true)} 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"> 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" /> <BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span> <span className="text-lg">New Assignment</span>
</div> </div>
@@ -111,7 +171,9 @@ export default function AssignmentsPage({assignments, corporateAssignments, user
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastAssignmentFilter).length})</h2> <h2 className="text-2xl font-semibold">
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(pastAssignmentFilter).map((a) => ( {assignments.filter(pastAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -128,7 +190,29 @@ export default function AssignmentsPage({assignments, corporateAssignments, user
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})</h2> <h2 className="text-2xl font-semibold">
Assignments start expired ({assignmentsPastExpiredStart.length})
</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(startHasExpiredAssignmentFilter).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"> <div className="flex flex-wrap gap-2">
{assignments.filter(archivedAssignmentFilter).map((a) => ( {assignments.filter(archivedAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard

View File

@@ -32,6 +32,8 @@ export interface Assignment {
// unless start is active, the assignment is not visible to the assignees // unless start is active, the assignment is not visible to the assignees
// start date now works as a limit time to start the exam // start date now works as a limit time to start the exam
start?: boolean; start?: boolean;
autoStartDate?: Date;
autoStart?: boolean;
} }
export type AssignmentWithCorporateId = Assignment & {corporateId: string}; export type AssignmentWithCorporateId = Assignment & {corporateId: string};

View File

@@ -1,10 +1,77 @@
import moment from "moment"; import moment from "moment";
import {Assignment} from "@/interfaces/results"; import {Assignment} from "@/interfaces/results";
export const futureAssignmentFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()) && !a.archived; // export const futureAssignmentFilter = (a: Assignment) => {
// if(a.archived) return false;
// if(a.start) return false;
export const pastAssignmentFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) && !a.archived; // const currentDate = moment();
// const startDate = moment(a.startDate);
// if(currentDate.isAfter(startDate)) return false;
// if(a.autoStart && a.autoStartDate) {
// return moment(a.autoStartDate).isAfter(currentDate);
// }
// return false;
// }
export const futureAssignmentFilter = (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) {
if(moment(a.startDate).isBefore(currentDate)) return false;
return true;
}
return false;
}
export const pastAssignmentFilter = (a: Assignment) => {
const currentDate = moment();
if(a.archived) {
return false;
}
return moment(a.endDate).isBefore(currentDate);
}
export const archivedAssignmentFilter = (a: Assignment) => a.archived; export const archivedAssignmentFilter = (a: Assignment) => a.archived;
export const activeAssignmentFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()); export const activeAssignmentFilter = (a: Assignment) => {
const currentDate = moment();
if(moment(a.endDate).isBefore(currentDate)) return false;
if(a.archived) return false;
if(a.start) return true;
if(a.autoStart && a.autoStartDate) {
return moment(a.autoStartDate).isBefore(currentDate);
}
// if(currentDate.isAfter(moment(a.startDate))) return true;
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) {
// if(moment(a.startDate).isBefore(currentDate)) return false;
// return true;
// }
// return false;
// }
export const startHasExpiredAssignmentFilter = (a: Assignment) => {
const currentDate = moment();
if(a.archived) return false;
if(a.start) return false;
if(currentDate.isAfter(moment(a.startDate)) && currentDate.isBefore(moment(a.endDate))) return true;
return false;
}