Merge remote-tracking branch 'origin/develop' into feature/ExamGenRework

This commit is contained in:
Carlos-Mesquita
2024-11-10 07:09:25 +00:00
17 changed files with 909 additions and 915 deletions

View File

@@ -73,7 +73,7 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
"h-5 w-5", "h-5 w-5",
`text-ielts-${currentModule}` `text-ielts-${currentModule}`
)} /> )} />
<span className="font-medium text-gray-900">Conversation</span> <span className="font-medium text-gray-900">{(sectionId === 1 || sectionId === 3) ? "Conversation" : "Monologue"}</span>
</div> </div>
} }
> >

View File

@@ -1,6 +1,7 @@
import { Session } from "@/hooks/useSessions"; import { Session } from "@/hooks/useSessions";
import { Assignment } from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments";
import { sortByModuleName } from "@/utils/moduleUtils"; import { sortByModuleName } from "@/utils/moduleUtils";
import clsx from "clsx"; import clsx from "clsx";
import moment from "moment"; import moment from "moment";
@@ -44,7 +45,16 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
<ModuleBadge className="scale-110 w-full" key={module} module={module} /> <ModuleBadge className="scale-110 w-full" key={module} module={module} />
))} ))}
</div> </div>
{!assignment.results.map((r) => r.user).includes(user.id) && ( {futureAssignmentFilter(assignment) && (
<Button
color="rose"
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
disabled
variant="outline">
Not yet started
</Button>
)}
{activeAssignmentFilter(assignment) && !assignment.results.map((r) => r.user).includes(user.id) && (
<> <>
<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"
@@ -73,22 +83,22 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
className={clsx( className={clsx(
"-md:hidden h-full w-full max-w-[50%] cursor-pointer" "-md:hidden h-full w-full max-w-[50%] cursor-pointer"
)}> )}>
<Button <Button
className={clsx("w-full h-full !rounded-xl")} className={clsx("w-full h-full !rounded-xl")}
onClick={() => resumeAssignment(session)} onClick={() => resumeAssignment(session)}
color="green" color="green"
variant="outline"> variant="outline">
Resume Resume
</Button> </Button>
</div> </div>
)} )}
</> </>
)} )}
{assignment.results.map((r) => r.user).includes(user.id) && ( {assignment.results.map((r) => r.user).includes(user.id) && (
<Button <Button
onClick={() => router.push("/record")}
color="green" color="green"
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl" className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
disabled
variant="outline"> variant="outline">
Submitted Submitted
</Button> </Button>

View File

@@ -3,105 +3,96 @@ import Modal from "@/components/Modal";
import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam"; import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import clsx from "clsx"; import clsx from "clsx";
import { useState } from "react"; import { useMemo, useState } from "react";
import { BsFillGrid3X3GapFill } from "react-icons/bs"; import { BsFillGrid3X3GapFill } from "react-icons/bs";
interface Props { interface Props {
showSolutions: boolean; exam: LevelExam
runOnClick: ((index: number) => void) | undefined; showSolutions: boolean;
runOnClick: ((index: number) => void) | undefined;
} }
const MCQuestionGrid: React.FC<Props> = ({showSolutions, runOnClick}) => { const MCQuestionGrid: React.FC<Props> = ({ exam, showSolutions, runOnClick }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { const {
userSolutions, userSolutions,
partIndex: sectionIndex, partIndex: sectionIndex,
exerciseIndex, exerciseIndex,
exam
} = useExamStore((state) => state); } = useExamStore((state) => state);
const isMultipleChoiceLevelExercise = () => { const currentExercise = useMemo(() => (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise, [exam, exerciseIndex, sectionIndex])
if (exam?.module === 'level' && typeof sectionIndex === "number" && sectionIndex > -1) { const userSolution = useMemo(() => userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!, [currentExercise.id, userSolutions])
const currentExercise = (exam as LevelExam).parts[sectionIndex].exercises[exerciseIndex]; const answeredQuestions = useMemo(() => new Set(userSolution.solutions.map(sol => sol.question.toString())), [userSolution.solutions])
return currentExercise && currentExercise.type === 'multipleChoice'; const exerciseOffset = useMemo(() => Number(currentExercise.questions[0].id), [currentExercise.questions])
} const lastExercise = useMemo(() => exerciseOffset + (currentExercise.questions.length - 1),
return false; [currentExercise.questions.length, exerciseOffset]);
};
if (!isMultipleChoiceLevelExercise() && !userSolutions) return null; const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => {
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
if (foundMap) return foundMap;
return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null;
}, null as ShuffleMap | null);
const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution;
const currentExercise = (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise; if (!userSolutions) return "";
const userSolution = userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!;
const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question.toString()));
const exerciseOffset = Number(currentExercise.questions[0].id);
const lastExercise = exerciseOffset + (currentExercise.questions.length - 1);
const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => { if (!userQuestionSolution) {
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700";
if (foundMap) return foundMap; }
return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null;
}, null as ShuffleMap | null);
const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution;
if (!userSolutions) return ""; return userQuestionSolution === newSolution ?
"!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" :
"!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark";
}
if (!userQuestionSolution) { return (
return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700"; <>
} <Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg">
<BsFillGrid3X3GapFill size={24} />
</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all"
>
<>
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3>
<div className="grid grid-cols-5 gap-3 px-4 py-2">
{currentExercise.questions.map((_, index) => {
const questionNumber = exerciseOffset + index;
const isAnswered = answeredQuestions.has(questionNumber.toString());
const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution;
return userQuestionSolution === newSolution ? const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option;
"!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" : return (
"!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark"; <Button
} variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
key={index}
return ( className={clsx(
<> "w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
<Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg"> (showSolutions ?
<BsFillGrid3X3GapFill size={24} /> getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
</Button> (isAnswered ?
<Modal "bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
isOpen={isOpen} "bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
onClose={() => setIsOpen(false)} )
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all" )
> )}
<> onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3> >
<div className="grid grid-cols-5 gap-3 px-4 py-2"> {questionNumber}
{currentExercise.questions.map((_, index) => { </Button>
const questionNumber = exerciseOffset + index; );
const isAnswered = answeredQuestions.has(questionNumber.toString()); })}
const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution; </div>
<p className="mt-4 text-sm text-gray-600 text-center">
const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option; Click a question number to jump to that question
return ( </p>
<Button </>
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")} </Modal>
key={index} </>
className={clsx( );
"w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
(showSolutions ?
getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
(isAnswered ?
"bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
"bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
)
)
)}
onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
>
{questionNumber}
</Button>
);
})}
</div>
<p className="mt-4 text-sm text-gray-600 text-center">
Click a question number to jump to that question
</p>
</>
</Modal>
</>
);
} }
export default MCQuestionGrid; export default MCQuestionGrid;

View File

@@ -1,11 +1,11 @@
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import { moduleLabels } from "@/utils/moduleUtils"; import { moduleLabels } from "@/utils/moduleUtils";
import clsx from "clsx"; import clsx from "clsx";
import { ReactNode, useState } from "react"; import { ReactNode, useMemo, useState } from "react";
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs"; import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
import ProgressBar from "../../Low/ProgressBar"; import ProgressBar from "../../Low/ProgressBar";
import Timer from "../Timer"; import Timer from "../Timer";
import { Exercise } from "@/interfaces/exam"; import { Exercise, LevelExam } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import React from "react"; import React from "react";
import MCQuestionGrid from "./MCQuestionGrid"; import MCQuestionGrid from "./MCQuestionGrid";
@@ -38,9 +38,7 @@ export default function ModuleTitle({
showSolutions = false, showSolutions = false,
runOnClick = undefined runOnClick = undefined
}: Props) { }: Props) {
const { const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state);
exam
} = useExamStore((state) => state);
const moduleIcon: { [key in Module]: ReactNode } = { const moduleIcon: { [key in Module]: ReactNode } = {
reading: <BsBook className="text-ielts-reading w-6 h-6" />, reading: <BsBook className="text-ielts-reading w-6 h-6" />,
@@ -50,6 +48,14 @@ export default function ModuleTitle({
level: <BsClipboard className="text-ielts-level w-6 h-6" />, level: <BsClipboard className="text-ielts-level w-6 h-6" />,
}; };
const showGrid = useMemo(() =>
exam?.module === "level"
&& partIndex > -1
&& exam.parts[partIndex].exercises[examExerciseIndex].type === "multipleChoice"
&& !!userSolutions,
[exam, examExerciseIndex, partIndex, userSolutions]
)
return ( return (
<> <>
{showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />} {showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />}
@@ -67,7 +73,7 @@ export default function ModuleTitle({
return ( return (
<div key={index} className="text-2xl font-semibold flex flex-col gap-2"> <div key={index} className="text-2xl font-semibold flex flex-col gap-2">
{partInstructions.split("\\n").map((line, lineIndex) => ( {partInstructions.split("\\n").map((line, lineIndex) => (
<span key={lineIndex} dangerouslySetInnerHTML={{__html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></span> <span key={lineIndex} dangerouslySetInnerHTML={{ __html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>') }}></span>
))} ))}
</div> </div>
); );
@@ -87,7 +93,7 @@ export default function ModuleTitle({
</div> </div>
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" /> <ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
</div> </div>
{exam?.module === "level" && <MCQuestionGrid showSolutions={showSolutions} runOnClick={runOnClick}/>} {showGrid && <MCQuestionGrid exam={exam as LevelExam} showSolutions={showSolutions} runOnClick={runOnClick} />}
</div> </div>
</div> </div>
</> </>

View File

@@ -1,27 +1,27 @@
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import clsx from "clsx"; import clsx from "clsx";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import {BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; import { BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
import {generate} from "random-words"; import { generate } from "random-words";
import {capitalize} from "lodash"; import { capitalize } from "lodash";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {Group, User} from "@/interfaces/user"; import { Group, User } from "@/interfaces/user";
import ProgressBar from "@/components/Low/ProgressBar"; import ProgressBar from "@/components/Low/ProgressBar";
import {calculateAverageLevel} from "@/utils/score"; import { calculateAverageLevel } from "@/utils/score";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
import moment from "moment"; import moment from "moment";
import axios from "axios"; import axios from "axios";
import {getExam} from "@/utils/exams"; import { getExam } from "@/utils/exams";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import Checkbox from "@/components/Low/Checkbox"; import Checkbox from "@/components/Low/Checkbox";
import {InstructorGender, Variant} from "@/interfaces/exam"; import { InstructorGender, Variant } from "@/interfaces/exam";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import useExams from "@/hooks/useExams"; import useExams from "@/hooks/useExams";
import {useListSearch} from "@/hooks/useListSearch"; import { useListSearch } from "@/hooks/useListSearch";
interface Props { interface Props {
isCreating: boolean; isCreating: boolean;
@@ -34,7 +34,7 @@ interface Props {
const SIZE = 12; const SIZE = 12;
export default function AssignmentCreator({isCreating, assignment, user, groups, users, cancelCreation}: Props) { export default function AssignmentCreator({ isCreating, assignment, user, groups, users, cancelCreation }: Props) {
const [studentsPage, setStudentsPage] = useState(0); const [studentsPage, setStudentsPage] = useState(0);
const [teachersPage, setTeachersPage] = useState(0); const [teachersPage, setTeachersPage] = useState(0);
@@ -43,14 +43,14 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
const [teachers, setTeachers] = useState<string[]>(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]); const [teachers, setTeachers] = useState<string[]>(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]);
const [name, setName] = useState( const [name, setName] = useState(
assignment?.name || assignment?.name ||
generate({ generate({
minLength: 6, minLength: 6,
maxLength: 8, maxLength: 8,
min: 2, min: 2,
max: 3, max: 3,
join: " ", join: " ",
formatter: capitalize, formatter: capitalize,
}), }),
); );
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "hour").toDate()); const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "hour").toDate());
@@ -65,18 +65,17 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
const [released, setReleased] = useState<boolean>(assignment?.released || false); const [released, setReleased] = useState<boolean>(assignment?.released || false);
const [autoStart, setAutostart] = useState<boolean>(assignment?.autoStart || 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 }[]>([]);
const {exams} = useExams(); const { exams } = useExams();
const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]); const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]);
const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]); const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]);
const {rows: filteredStudentsRows, renderSearch: renderStudentSearch, text: studentText} = useListSearch([["name"], ["email"]], userStudents); const { rows: filteredStudentsRows, renderSearch: renderStudentSearch, text: studentText } = useListSearch([["name"], ["email"]], userStudents);
const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch, text: teacherText} = useListSearch([["name"], ["email"]], userTeachers); const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch, text: teacherText } = useListSearch([["name"], ["email"]], userTeachers);
useEffect(() => setStudentsPage(0), [studentText]); useEffect(() => setStudentsPage(0), [studentText]);
const studentRows = useMemo( const studentRows = useMemo(
@@ -131,7 +130,6 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
instructorGender, instructorGender,
released, released,
autoStart, 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!`);
@@ -306,24 +304,6 @@ 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") && (
@@ -337,9 +317,9 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)} onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
disabled={!selectedModules.includes("speaking") || !!assignment} disabled={!selectedModules.includes("speaking") || !!assignment}
options={[ options={[
{value: "male", label: "Male"}, { value: "male", label: "Male" },
{value: "female", label: "Female"}, { value: "female", label: "Female" },
{value: "varied", label: "Varied"}, { value: "varied", label: "Varied" },
]} ]}
/> />
</div> </div>
@@ -362,12 +342,12 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
}} }}
onChange={(value) => onChange={(value) =>
value value
? setExamIDs((prev) => [...prev.filter((x) => x.module !== module), {id: value.value!, module}]) ? setExamIDs((prev) => [...prev.filter((x) => x.module !== module), { id: value.value!, module }])
: setExamIDs((prev) => prev.filter((x) => x.module !== module)) : setExamIDs((prev) => prev.filter((x) => x.module !== module))
} }
options={exams options={exams
.filter((x) => !x.isDiagnostic && x.module === module) .filter((x) => !x.isDiagnostic && x.module === module)
.map((x) => ({value: x.id, label: x.id}))} .map((x) => ({ value: x.id, label: x.id }))}
/> />
</div> </div>
))} ))}
@@ -394,7 +374,7 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) && users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
"!bg-mti-purple-light !text-white", "!bg-mti-purple-light !text-white",
)}> )}>
{g.name} {g.name}
</button> </button>
@@ -475,7 +455,7 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) && users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
"!bg-mti-purple-light !text-white", "!bg-mti-purple-light !text-white",
)}> )}>
{g.name} {g.name}
</button> </button>

View File

@@ -1,14 +1,14 @@
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle"; import ModuleTitle from "@/components/Medium/ModuleTitle";
import {moduleResultText} from "@/constants/ielts"; import { moduleResultText } from "@/constants/ielts";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {User} from "@/interfaces/user"; import { User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {calculateBandScore, getGradingLabel} from "@/utils/score"; import { calculateBandScore, getGradingLabel } from "@/utils/score";
import clsx from "clsx"; import clsx from "clsx";
import Link from "next/link"; import Link from "next/link";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {Fragment, useEffect, useState} from "react"; import { Fragment, useEffect, useState } from "react";
import { import {
BsArrowCounterclockwise, BsArrowCounterclockwise,
BsBan, BsBan,
@@ -21,14 +21,14 @@ import {
BsPen, BsPen,
BsShareFill, BsShareFill,
} from "react-icons/bs"; } from "react-icons/bs";
import {LevelScore} from "@/constants/ielts"; import { LevelScore } from "@/constants/ielts";
import {getLevelScore} from "@/utils/score"; import { getLevelScore } from "@/utils/score";
import {capitalize} from "lodash"; import { capitalize } from "lodash";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import {UserSolution} from "@/interfaces/exam"; import { UserSolution } from "@/interfaces/exam";
import ai_usage from "@/utils/ai.detection"; import ai_usage from "@/utils/ai.detection";
import useGradingSystem from "@/hooks/useGrading"; import useGradingSystem from "@/hooks/useGrading";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
interface Score { interface Score {
module: Module; module: Module;
@@ -49,20 +49,23 @@ interface Props {
isLoading: boolean; isLoading: boolean;
assignment?: Assignment; assignment?: Assignment;
onViewResults: (moduleIndex?: number) => void; onViewResults: (moduleIndex?: number) => void;
destination?: string
} }
export default function Finish({user, scores, modules, information, solutions, isLoading, assignment, onViewResults}: Props) { export default function Finish({ user, scores, modules, information, solutions, isLoading, assignment, onViewResults, destination }: Props) {
const [selectedModule, setSelectedModule] = useState(modules[0]); const [selectedModule, setSelectedModule] = useState(modules[0]);
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!); const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false); const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
const aiUsage = Math.round(ai_usage(solutions) * 100); const aiUsage = Math.round(ai_usage(solutions) * 100);
const exams = useExamStore((state) => state.exams); const exams = useExamStore((state) => state.exams);
const {gradingSystem} = useGradingSystem(); const { gradingSystem } = useGradingSystem();
const router = useRouter()
useEffect(() => setSelectedScore(scores.find((x) => x.module === selectedModule)!), [scores, selectedModule]); useEffect(() => setSelectedScore(scores.find((x) => x.module === selectedModule)!), [scores, selectedModule]);
const moduleColors: {[key in Module]: {progress: string; inner: string}} = { const moduleColors: { [key in Module]: { progress: string; inner: string } } = {
reading: { reading: {
progress: "text-ielts-reading", progress: "text-ielts-reading",
inner: "bg-ielts-reading-light", inner: "bg-ielts-reading-light",
@@ -286,10 +289,8 @@ export default function Finish({user, scores, modules, information, solutions, i
<div className="flex gap-8"> <div className="flex gap-8">
<div className="flex w-fit cursor-pointer flex-col items-center gap-1"> <div className="flex w-fit cursor-pointer flex-col items-center gap-1">
<button <button
onClick={() => window.location.reload()} onClick={() => router.push(destination || "/exam")}
// disabled={user.type === "admin"} disabled={!!assignment}
// TODO: temporarily disabled
disabled
className="bg-mti-purple-light hover:bg-mti-purple flex h-11 w-11 items-center justify-center rounded-full transition duration-300 ease-in-out"> className="bg-mti-purple-light hover:bg-mti-purple flex h-11 w-11 items-center justify-center rounded-full transition duration-300 ease-in-out">
<BsArrowCounterclockwise className="h-7 w-7 text-white" /> <BsArrowCounterclockwise className="h-7 w-7 text-white" />
</button> </button>
@@ -325,7 +326,7 @@ export default function Finish({user, scores, modules, information, solutions, i
)} )}
</div> </div>
<Link href="/" className="w-full max-w-[200px] self-end"> <Link href={destination || "/"} className="w-full max-w-[200px] self-end">
<Button color="purple" className="w-full max-w-[200px] self-end"> <Button color="purple" className="w-full max-w-[200px] self-end">
Dashboard Dashboard
</Button> </Button>

View File

@@ -18,62 +18,64 @@ import { Tab } from "@headlessui/react";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import { typeCheckWordsMC } from "@/utils/type.check"; import { typeCheckWordsMC } from "@/utils/type.check";
import SectionNavbar from "../Navigation/SectionNavbar"; import SectionNavbar from "../Navigation/SectionNavbar";
import AudioPlayer from "@/components/Low/AudioPlayer";
interface Props { interface Props {
exam: LevelExam; exam: LevelExam;
showSolutions?: boolean; showSolutions?: boolean;
onFinish: (userSolutions: UserSolution[]) => void; onFinish: (userSolutions: UserSolution[]) => void;
preview?: boolean; preview?: boolean;
partDividers?: boolean; partDividers?: boolean;
} }
export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) { export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) {
const levelBgColor = "bg-ielts-level-light"; const levelBgColor = "bg-ielts-level-light";
const examState = useExamStore((state) => state); const examState = useExamStore((state) => state);
const persistentExamState = usePersistentExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state);
const { const {
userSolutions, userSolutions,
hasExamEnded, hasExamEnded,
partIndex, partIndex,
exerciseIndex, exerciseIndex,
questionIndex, questionIndex,
shuffles, shuffles,
currentSolution, currentSolution,
setBgColor, setBgColor,
setUserSolutions, setUserSolutions,
setHasExamEnded, setHasExamEnded,
setPartIndex, setPartIndex,
setExerciseIndex, setExerciseIndex,
setQuestionIndex, setQuestionIndex,
setShuffles, setShuffles,
setCurrentSolution setCurrentSolution
} = !preview ? examState : persistentExamState; } = !preview ? examState : persistentExamState;
// In case client want to switch back // In case client want to switch back
const textRenderDisabled = true; const textRenderDisabled = true;
const [showSubmissionModal, setShowSubmissionModal] = useState(false); const [timesListened, setTimesListened] = useState(0);
const [showQuestionsModal, setShowQuestionsModal] = useState(false); const [showSubmissionModal, setShowSubmissionModal] = useState(false);
const [continueAnyways, setContinueAnyways] = useState(false); const [showQuestionsModal, setShowQuestionsModal] = useState(false);
const [textRender, setTextRender] = useState(false); const [continueAnyways, setContinueAnyways] = useState(false);
const [changedPrompt, setChangedPrompt] = useState(false); const [textRender, setTextRender] = useState(false);
const [nextExerciseCalled, setNextExerciseCalled] = useState(false); const [changedPrompt, setChangedPrompt] = useState(false);
const [currentSolutionSet, setCurrentSolutionSet] = useState(false); const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0])); const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0]));
const [questionModalKwargs, setQuestionModalKwargs] = useState<{ const [questionModalKwargs, setQuestionModalKwargs] = useState<{
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
}>({ }>({
type: "blankQuestions", type: "blankQuestions",
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
}); });
const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined); const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined);
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions); const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
const [startNow, setStartNow] = useState<boolean>(!showSolutions); const [startNow, setStartNow] = useState<boolean>(!showSolutions);
useEffect(() => { useEffect(() => {
@@ -84,445 +86,483 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [exerciseIndex]); }, [exerciseIndex]);
useEffect(() => { useEffect(() => {
if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) { if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) {
setCurrentExercise(exam.parts[0].exercises[0]); setCurrentExercise(exam.parts[0].exercises[0]);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentExercise, partIndex, exerciseIndex]); }, [currentExercise, partIndex, exerciseIndex]);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined); const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined); const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
const [totalLines, setTotalLines] = useState<number>(0); const [totalLines, setTotalLines] = useState<number>(0);
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined) const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
useEffect(() => { useEffect(() => {
if (typeof currentSolution !== "undefined") { if (typeof currentSolution !== "undefined") {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
setCurrentSolutionSet(true); setCurrentSolutionSet(true);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise]) }, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise])
useEffect(() => { useEffect(() => {
if (typeof currentSolution !== "undefined") { if (typeof currentSolution !== "undefined") {
setCurrentSolution(undefined); setCurrentSolution(undefined);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentSolution]); }, [currentSolution]);
useEffect(() => { useEffect(() => {
if (showSolutions) { if (showSolutions) {
const solutionShuffles = userSolutions.map(solution => ({ const solutionShuffles = userSolutions.map(solution => ({
exerciseID: solution.exercise, exerciseID: solution.exercise,
shuffles: solution.shuffleMaps || [] shuffles: solution.shuffleMaps || []
})); }));
setShuffles(solutionShuffles); setShuffles(solutionShuffles);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const getExercise = () => { const getExercise = () => {
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
exercise = { exercise = {
...exercise, ...exercise,
userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [], userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [],
}; };
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
return exercise; return exercise;
}; };
useEffect(() => { useEffect(() => {
setCurrentExercise(getExercise()); setCurrentExercise(getExercise());
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [partIndex, exerciseIndex, questionIndex]); }, [partIndex, exerciseIndex, questionIndex]);
const next = () => { const next = () => {
setNextExerciseCalled(true); setNextExerciseCalled(true);
} }
const nextExercise = () => { const nextExercise = () => {
scrollToTop(); scrollToTop();
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
setExerciseIndex(exerciseIndex + 1); setExerciseIndex(exerciseIndex + 1);
setCurrentSolutionSet(false); setCurrentSolutionSet(false);
return; return;
} }
if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) {
modalKwargs(); modalKwargs();
setShowQuestionsModal(true); setShowQuestionsModal(true);
return; return;
} }
if (partIndex + 1 < exam.parts.length && !hasExamEnded) { if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) { if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
modalKwargs(); modalKwargs();
setShowQuestionsModal(true); setShowQuestionsModal(true);
return; return;
} }
if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) { if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) {
setShowPartDivider(true); setShowPartDivider(true);
setBgColor(levelBgColor); setBgColor(levelBgColor);
} }
setSeenParts(prev => new Set(prev).add(partIndex + 1)); setSeenParts(prev => new Set(prev).add(partIndex + 1));
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) { if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) {
setTextRender(true); setTextRender(true);
} }
setPartIndex(partIndex + 1);
setExerciseIndex(0);
setQuestionIndex(0);
setCurrentSolutionSet(false);
return;
}
if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) { setTimesListened(0);
modalKwargs(); setPartIndex(partIndex + 1);
setShowQuestionsModal(true); setExerciseIndex(0);
} setQuestionIndex(0);
setCurrentSolutionSet(false);
return;
}
setHasExamEnded(false); if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) {
setCurrentSolutionSet(false); modalKwargs();
if (typeof showSolutionsSave !== "undefined") { setShowQuestionsModal(true);
onFinish(showSolutionsSave); }
} else {
onFinish(userSolutions);
}
}
useEffect(() => { setHasExamEnded(false);
if (nextExerciseCalled && currentSolutionSet) { setCurrentSolutionSet(false);
nextExercise(); if (typeof showSolutionsSave !== "undefined") {
setNextExerciseCalled(false); onFinish(showSolutionsSave);
} } else {
// eslint-disable-next-line react-hooks/exhaustive-deps onFinish(userSolutions);
}, [nextExerciseCalled, currentSolutionSet]) }
}
const previousExercise = (solution?: UserSolution) => { useEffect(() => {
scrollToTop(); if (nextExerciseCalled && currentSolutionSet) {
nextExercise();
setNextExerciseCalled(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nextExerciseCalled, currentSolutionSet])
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) { const previousExercise = (solution?: UserSolution) => {
setTextRender(true); scrollToTop();
return;
}
if (questionIndex == 0) { if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
setPartIndex(partIndex - 1); setTextRender(true);
if (!seenParts.has(partIndex - 1)) { return;
setBgColor(levelBgColor); }
setShowPartDivider(true);
setQuestionIndex(0);
setSeenParts(prev => new Set(prev).add(partIndex - 1));
return;
}
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; if (questionIndex == 0) {
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex]; setPartIndex(partIndex - 1);
setExerciseIndex(lastExerciseIndex); if (!seenParts.has(partIndex - 1)) {
setBgColor(levelBgColor);
setShowPartDivider(true);
setQuestionIndex(0);
setSeenParts(prev => new Set(prev).add(partIndex - 1));
return;
}
if (lastExercise.type === "multipleChoice") { const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
setQuestionIndex(lastExercise.questions.length - 1) const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
} else { setExerciseIndex(lastExerciseIndex);
setQuestionIndex(0)
}
return;
}
setExerciseIndex(exerciseIndex - 1); if (lastExercise.type === "multipleChoice") {
if (exerciseIndex - 1 === -1) { setQuestionIndex(lastExercise.questions.length - 1)
setPartIndex(partIndex - 1); } else {
const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; setQuestionIndex(0)
const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; }
if (previousExercise.type === "multipleChoice") { return;
setQuestionIndex(previousExercise.questions.length - 1) }
}
}
}; setExerciseIndex(exerciseIndex - 1);
if (exerciseIndex - 1 === -1) {
setPartIndex(partIndex - 1);
const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex];
if (previousExercise.type === "multipleChoice") {
setQuestionIndex(previousExercise.questions.length - 1)
}
}
const calculateExerciseIndex = () => { };
return exam.parts.reduce((acc, curr, index) => {
if (index < partIndex) {
return acc + countExercises(curr.exercises)
}
return acc;
}, 0) + (questionIndex + 1);
};
const renderText = () => ( const calculateExerciseIndex = () => {
<> return exam.parts.reduce((acc, curr, index) => {
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}> if (index < partIndex) {
<> return acc + countExercises(curr.exercises)
<div className="flex flex-col w-full gap-2"> }
{textRender && !textRenderDisabled ? ( return acc;
<> }, 0) + (questionIndex + 1);
<h4 className="text-xl font-semibold"> };
Please read the following excerpt attentively, you will then be asked questions about the text you&apos;ve read.
</h4>
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
</>
) : (
<h4 className="text-xl font-semibold">
Answer the questions on the right based on what you&apos;ve read.
</h4>
)}
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
{exam.parts[partIndex].context &&
<TextComponent
part={exam.parts[partIndex]}
contextWords={contextWords}
setContextWordLines={setContextWordLines}
setTotalLines={setTotalLines}
/>}
</div>
</>
</div>
{textRender && !textRenderDisabled && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
className="max-w-[200px] w-full"
onClick={() => { setTextRender(false); previousExercise(); }}
>
Back
</Button>
<Button color="purple" onClick={() => setTextRender(false)} className="max-w-[200px] self-end w-full"> const renderAudioPlayer = () => (
Next <div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
</Button> {exam?.parts[partIndex]?.audio?.source ? (
</div> <>
)} <div className="flex flex-col w-full gap-2">
</> <h4 className="text-xl font-semibold">Please listen to the following audio attentively.</h4>
); <span className="text-base">
{(() => {
const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes;
return audioRepeatTimes && audioRepeatTimes > 0
? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).`
: "You may listen to the audio as many times as you would like.";
})()}
</span>
</div>
<div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
<AudioPlayer
key={partIndex}
src={exam?.parts[partIndex]?.audio?.source ?? ''}
color="listening"
onEnd={() => setTimesListened((prev) => prev + 1)}
disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null &&
timesListened === exam.parts[partIndex]?.audio?.repeatableTimes}
disablePause
/>
</div>
</>
) : (
<span>This section will be displayed the audio once it has been generated.</span>
)}
const partLabel = () => { </div>
const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : ''; );
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
if (currentExercise?.type === "multipleChoice") { const renderText = () => (
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` <>
} <div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
<>
<div className="flex flex-col w-full gap-2">
{textRender && !textRenderDisabled ? (
<>
<h4 className="text-xl font-semibold">
Please read the following excerpt attentively, you will then be asked questions about the text you&apos;ve read.
</h4>
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
</>
) : (
<h4 className="text-xl font-semibold">
Answer the questions on the right based on what you&apos;ve read.
</h4>
)}
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
{(exam.parts[partIndex].context || exam.parts[partIndex].text) &&
<TextComponent
part={exam.parts[partIndex]}
contextWords={contextWords}
setContextWordLines={setContextWordLines}
setTotalLines={setTotalLines}
/>}
</div>
</>
</div>
{textRender && !textRenderDisabled && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
className="max-w-[200px] w-full"
onClick={() => { setTextRender(false); previousExercise(); }}
>
Back
</Button>
if (typeof exam.parts[partIndex].context === "string") { <Button color="purple" onClick={() => setTextRender(false)} className="max-w-[200px] self-end w-full">
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise; Next
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}` </Button>
} </div>
} )}
</>
);
const answeredEveryQuestion = (partIndex: number) => { const partLabel = () => {
return exam.parts[partIndex].exercises.every((exercise) => { const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : '';
const userSolution = userSolutions.find(x => x.exercise === exercise.id); if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
switch (exercise.type) { return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
case 'multipleChoice':
return userSolution?.solutions.length === exercise.questions.length;
case 'fillBlanks':
return userSolution?.solutions.length === exercise.words.length;
case 'writeBlanks':
return userSolution?.solutions.length === exercise.solutions.length;
case 'matchSentences':
return userSolution?.solutions.length === exercise.sentences.length;
case 'trueFalse':
return userSolution?.solutions.length === exercise.questions.length;
}
return false;
});
}
useEffect(() => { if (currentExercise?.type === "multipleChoice") {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
}
const findMatch = (index: number) => { if (typeof exam.parts[partIndex].context === "string") {
if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) { const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
const match = currentExercise!.questions[index].prompt.match(regex); return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}`
if (match) { }
return { match: match[1], originalLine: match[2] } }
}
}
return;
}
// if the client for some whatever random reason decides const answeredEveryQuestion = (partIndex: number) => {
// to add more questions update this return exam.parts[partIndex].exercises.every((exercise) => {
const numberOfQuestions = 2; const userSolution = userSolutions.find(x => x.exercise === exercise.id);
switch (exercise.type) {
case 'multipleChoice':
return userSolution?.solutions.length === exercise.questions.length;
case 'fillBlanks':
return userSolution?.solutions.length === exercise.words.length;
case 'writeBlanks':
return userSolution?.solutions.length === exercise.solutions.length;
case 'matchSentences':
return userSolution?.solutions.length === exercise.sentences.length;
case 'trueFalse':
return userSolution?.solutions.length === exercise.questions.length;
}
return false;
});
}
if (exam.parts[partIndex].context) { useEffect(() => {
const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => { const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
const result = findMatch(questionIndex + i);
if (!!result) {
acc.push(result);
}
return acc;
}, []);
if (hits.length > 0) { const findMatch = (index: number) => {
setContextWords(hits) if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) {
} const match = currentExercise!.questions[index].prompt.match(regex);
} if (match) {
return { match: match[1], originalLine: match[2] }
}
}
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps // if the client for some whatever random reason decides
}, [currentExercise, questionIndex, totalLines]); // to add more questions update this
const numberOfQuestions = 2;
useEffect(() => { if (exam.parts[partIndex].context) {
if ( const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => {
exerciseIndex !== -1 && currentExercise && const result = findMatch(questionIndex + i);
currentExercise.type === "multipleChoice" && if (!!result) {
exam.parts[partIndex].context && contextWordLines acc.push(result);
) { }
if (contextWordLines.length > 0) { return acc;
contextWordLines.forEach((n, i) => { }, []);
if (contextWords && contextWords[i] && n !== -1) {
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace( if (hits.length > 0) {
`in line ${contextWords[i].originalLine}`, setContextWords(hits)
`in line ${n}` }
); }
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
} // eslint-disable-next-line react-hooks/exhaustive-deps
}) }, [currentExercise, questionIndex, totalLines]);
setChangedPrompt(true);
} useEffect(() => {
} if (
// eslint-disable-next-line react-hooks/exhaustive-deps exerciseIndex !== -1 && currentExercise &&
}, [contextWordLines]); currentExercise.type === "multipleChoice" &&
exam.parts[partIndex].context && contextWordLines
) {
if (contextWordLines.length > 0) {
contextWordLines.forEach((n, i) => {
if (contextWords && contextWords[i] && n !== -1) {
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace(
`in line ${contextWords[i].originalLine}`,
`in line ${n}`
);
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
}
})
setChangedPrompt(true);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contextWordLines]);
useEffect(() => { useEffect(() => {
if (continueAnyways) { if (continueAnyways) {
setContinueAnyways(false); setContinueAnyways(false);
nextExercise(); nextExercise();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [continueAnyways]); }, [continueAnyways]);
const modalKwargs = () => { const modalKwargs = () => {
const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = {
type: "blankQuestions", type: "blankQuestions",
unanswered: false, unanswered: false,
onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }
}; };
if (partIndex === exam.parts.length - 1) { if (partIndex === exam.parts.length - 1) {
kwargs.type = "submit" kwargs.type = "submit"
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex)); kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
} }
setQuestionModalKwargs(kwargs); setQuestionModalKwargs(kwargs);
} }
const mcNavKwargs = { const mcNavKwargs = {
userSolutions: userSolutions, userSolutions: userSolutions,
exam: exam, exam: exam,
partIndex: partIndex, partIndex: partIndex,
showSolutions: showSolutions, showSolutions: showSolutions,
setExerciseIndex: setExerciseIndex, setExerciseIndex: setExerciseIndex,
setPartIndex: setPartIndex, setPartIndex: setPartIndex,
runOnClick: setQuestionIndex runOnClick: setQuestionIndex
} }
const memoizedRender = useMemo(() => { const memoizedRender = useMemo(() => {
setChangedPrompt(false); setChangedPrompt(false);
return ( return (
<> <>
{textRender && !textRenderDisabled ? {textRender && !textRenderDisabled ?
renderText() : renderText() :
<> <>
{exam.parts[partIndex]?.context && renderText()} {exam.parts[partIndex]?.context && renderText()}
{(showSolutions) ? {exam.parts[partIndex]?.audio && renderAudioPlayer()}
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) : {(showSolutions) ?
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise) currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
} currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
</> }
} </>
</>) }
// eslint-disable-next-line react-hooks/exhaustive-deps </>
}, [textRender, currentExercise, changedPrompt]); )
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textRender, currentExercise, changedPrompt]);
return ( return (
<> <>
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}> <div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
<Modal <Modal
className={"!w-2/6 !p-8"} className={"!w-2/6 !p-8"}
titleClassName={"font-bold text-3xl text-mti-rose-light"} titleClassName={"font-bold text-3xl text-mti-rose-light"}
isOpen={showSubmissionModal} isOpen={showSubmissionModal}
onClose={() => { }} onClose={() => { }}
title={"Confirm Submission"} title={"Confirm Submission"}
> >
<> <>
<p className="text-xl mt-8 mb-12">Are you sure you want to proceed with the submission?</p> <p className="text-xl mt-8 mb-12">Are you sure you want to proceed with the submission?</p>
<div className="w-full flex justify-between"> <div className="w-full flex justify-between">
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl"> <Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
Cancel Cancel
</Button> </Button>
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl"> <Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl">
Confirm Confirm
</Button> </Button>
</div> </div>
</> </>
</Modal> </Modal>
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} /> <QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
{ {
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) && !(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
<Timer minTimer={exam.minTimer} disableTimer={showSolutions || preview} standalone={true} /> <Timer minTimer={exam.minTimer} disableTimer={showSolutions || preview} standalone={true} />
} }
{(showPartDivider || startNow) ? {(showPartDivider || startNow) ?
<PartDivider <PartDivider
module="level" module="level"
sectionLabel="Part" sectionLabel="Part"
defaultTitle="Placement Test" defaultTitle="Placement Test"
section={exam.parts[partIndex]} section={exam.parts[partIndex]}
sectionIndex={partIndex} sectionIndex={partIndex}
onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }} onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }}
/> : ( /> : (
<> <>
{exam.parts[0].intro && exam.parts.length !== 1 && ( {exam.parts[0].intro && (
<SectionNavbar <SectionNavbar
module="level" module="level"
sections={exam.parts} sections={exam.parts}
sectionLabel="Part" sectionLabel="Part"
sectionIndex={partIndex} sectionIndex={partIndex}
setSectionIndex={setPartIndex} setSectionIndex={setPartIndex}
onClick={ onClick={
(index: number) => { (index: number) => {
setExerciseIndex(0); setExerciseIndex(0);
setQuestionIndex(0); setQuestionIndex(0);
if (!seenParts.has(index)) { if (!seenParts.has(index)) {
setShowPartDivider(true); setShowPartDivider(true);
setBgColor(levelBgColor); setBgColor(levelBgColor);
setSeenParts(prev => new Set(prev).add(index)); setSeenParts(prev => new Set(prev).add(index));
} }
} }
} /> } />
)} )}
<ModuleTitle <ModuleTitle
examLabel={exam.label} examLabel={exam.label}
partLabel={partLabel()} partLabel={partLabel()}
minTimer={exam.minTimer} minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()} exerciseIndex={calculateExerciseIndex()}
module="level" module="level"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))} totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
disableTimer={showSolutions} disableTimer={showSolutions}
showTimer={false} showTimer={false}
{...mcNavKwargs} {...mcNavKwargs}
/> />
<div <div
className={clsx( className={clsx(
"mb-20 w-full", "mb-20 w-full",
!!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4", !!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4",
)}> )}>
{memoizedRender} {memoizedRender}
</div> </div>
</> </>
)} )}
</div> </div>
</> </>
); );
} }

View File

@@ -1,8 +1,8 @@
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {InstructorGender} from "./exam"; import { InstructorGender } from "./exam";
import {Stat} from "./user"; import { Stat } from "./user";
export type UserResults = {[key in Module]: ModuleResult}; export type UserResults = { [key in Module]: ModuleResult };
interface ModuleResult { interface ModuleResult {
exams: string[]; exams: string[];
@@ -22,7 +22,7 @@ export interface Assignment {
assigner: string; assigner: string;
assignees: string[]; assignees: string[];
results: AssignmentResult[]; results: AssignmentResult[];
exams: {id: string; module: Module; assignee: string}[]; exams: { id: string; module: Module; assignee: string }[];
instructorGender?: InstructorGender; instructorGender?: InstructorGender;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
@@ -32,9 +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; autoStart?: boolean;
entity?: string; entity?: string;
} }
export type AssignmentWithCorporateId = Assignment & {corporateId: string}; export type AssignmentWithCorporateId = Assignment & { corporateId: string };

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import AbandonPopup from "@/components/AbandonPopup"; import AbandonPopup from "@/components/AbandonPopup";
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
@@ -12,15 +12,15 @@ import Selection from "@/exams/Selection";
import Speaking from "@/exams/Speaking"; import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing"; import Writing from "@/exams/Writing";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam"; import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam";
import {Stat, User} from "@/interfaces/user"; import { Stat, User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; import { evaluateSpeakingAnswer, evaluateWritingAnswer } from "@/utils/evaluation";
import {defaultExamUserSolutions, getExam} from "@/utils/exams"; import { defaultExamUserSolutions, getExam } from "@/utils/exams";
import axios from "axios"; import axios from "axios";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {toast, ToastContainer} from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import {v4 as uuidv4} from "uuid"; import { v4 as uuidv4 } from "uuid";
import useSessions from "@/hooks/useSessions"; import useSessions from "@/hooks/useSessions";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import clsx from "clsx"; import clsx from "clsx";
@@ -31,10 +31,11 @@ import { mapBy } from "@/utils";
interface Props { interface Props {
page: "exams" | "exercises"; page: "exams" | "exercises";
user: User; user: User;
destination?: string
hideSidebar?: boolean hideSidebar?: boolean
} }
export default function ExamPage({page, user, hideSidebar = false}: Props) { export default function ExamPage({ page, user, destination = "/exam", hideSidebar = false }: Props) {
const [variant, setVariant] = useState<Variant>("full"); const [variant, setVariant] = useState<Variant>("full");
const [avoidRepeated, setAvoidRepeated] = useState(false); const [avoidRepeated, setAvoidRepeated] = useState(false);
const [hasBeenUploaded, setHasBeenUploaded] = useState(false); const [hasBeenUploaded, setHasBeenUploaded] = useState(false);
@@ -49,18 +50,18 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
const assignment = useExamStore((state) => state.assignment); const assignment = useExamStore((state) => state.assignment);
const initialTimeSpent = useExamStore((state) => state.timeSpent); const initialTimeSpent = useExamStore((state) => state.timeSpent);
const {exam, setExam} = useExamStore((state) => state); const { exam, setExam } = useExamStore((state) => state);
const {exams, setExams} = useExamStore((state) => state); const { exams, setExams } = useExamStore((state) => state);
const {sessionId, setSessionId} = useExamStore((state) => state); const { sessionId, setSessionId } = useExamStore((state) => state);
const {partIndex, setPartIndex} = useExamStore((state) => state); const { partIndex, setPartIndex } = useExamStore((state) => state);
const {moduleIndex, setModuleIndex} = useExamStore((state) => state); const { moduleIndex, setModuleIndex } = useExamStore((state) => state);
const {questionIndex, setQuestionIndex} = useExamStore((state) => state); const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state); const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
const {userSolutions, setUserSolutions} = useExamStore((state) => state); const { userSolutions, setUserSolutions } = useExamStore((state) => state);
const {showSolutions, setShowSolutions} = useExamStore((state) => state); const { showSolutions, setShowSolutions } = useExamStore((state) => state);
const {selectedModules, setSelectedModules} = useExamStore((state) => state); const { selectedModules, setSelectedModules } = useExamStore((state) => state);
const {inactivity, setInactivity} = useExamStore((state) => state); const { inactivity, setInactivity } = useExamStore((state) => state);
const {bgColor, setBgColor} = useExamStore((state) => state); const { bgColor, setBgColor } = useExamStore((state) => state);
const setShuffleMaps = useExamStore((state) => state.setShuffles); const setShuffleMaps = useExamStore((state) => state.setShuffles);
const router = useRouter(); const router = useRouter();
@@ -262,11 +263,11 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
date: new Date().getTime(), date: new Date().getTime(),
isDisabled: solution.isDisabled, isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps, shuffleMaps: solution.shuffleMaps,
...(assignment ? {assignment: assignment.id} : {}), ...(assignment ? { assignment: assignment.id } : {}),
})); }));
axios axios
.post<{ok: boolean}>("/api/stats", newStats) .post<{ ok: boolean }>("/api/stats", newStats)
.then((response) => setHasBeenUploaded(response.data.ok)) .then((response) => setHasBeenUploaded(response.data.ok))
.catch(() => setHasBeenUploaded(false)); .catch(() => setHasBeenUploaded(false));
} }
@@ -329,7 +330,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
), ),
}), }),
); );
return Object.assign(exam, {parts}); return Object.assign(exam, { parts });
} }
const exercises = exam.exercises.map((x) => const exercises = exam.exercises.map((x) =>
@@ -337,7 +338,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions, userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions,
}), }),
); );
return Object.assign(exam, {exercises}); return Object.assign(exam, { exercises });
}; };
const onFinish = async (solutions: UserSolution[]) => { const onFinish = async (solutions: UserSolution[]) => {
@@ -392,7 +393,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
correct: 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 };
} = { } = {
reading: { reading: {
total: 0, total: 0,
@@ -434,7 +435,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
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 renderScreen = () => { const renderScreen = () => {
@@ -465,6 +466,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
timeSpent, timeSpent,
inactivity: totalInactivity, inactivity: totalInactivity,
}} }}
destination={destination}
onViewResults={(index?: number) => { onViewResults={(index?: number) => {
if (exams[0].module === "level") { if (exams[0].module === "level") {
const levelExam = exams[0] as LevelExam; const levelExam = exams[0] as LevelExam;

View File

@@ -6,43 +6,43 @@ import ProgressBar from "@/components/Low/ProgressBar";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import Separator from "@/components/Low/Separator"; import Separator from "@/components/Low/Separator";
import useExams from "@/hooks/useExams"; import useExams from "@/hooks/useExams";
import {useListSearch} from "@/hooks/useListSearch"; import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination"; import usePagination from "@/hooks/usePagination";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {EntityWithRoles} from "@/interfaces/entity"; import { EntityWithRoles } from "@/interfaces/entity";
import {InstructorGender, Variant} from "@/interfaces/exam"; import { InstructorGender, Variant } from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import {Group, User} from "@/interfaces/user"; import { Group, User } from "@/interfaces/user";
import {sessionOptions} from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import {mapBy, redirect, serialize} from "@/utils"; import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import {getAssignment} from "@/utils/assignments.be"; import { getAssignment } from "@/utils/assignments.be";
import {getEntitiesWithRoles} from "@/utils/entities.be"; import { getEntitiesWithRoles } from "@/utils/entities.be";
import {getGroups, getGroupsByEntities} from "@/utils/groups.be"; import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
import {checkAccess, doesEntityAllow, findAllowedEntities} from "@/utils/permissions"; import { checkAccess, doesEntityAllow, findAllowedEntities } from "@/utils/permissions";
import {calculateAverageLevel} from "@/utils/score"; import { calculateAverageLevel } from "@/utils/score";
import {getEntitiesUsers, getUsers} from "@/utils/users.be"; import { getEntitiesUsers, getUsers } from "@/utils/users.be";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import {capitalize} from "lodash"; import { capitalize } from "lodash";
import moment from "moment"; import moment from "moment";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {generate} from "random-words"; import { generate } from "random-words";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
import {BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
if (!user) return redirect("/login") if (!user) return redirect("/login")
res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59"); res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59");
const {id} = params as {id: string}; const { id } = params as { id: string };
const entityIDS = mapBy(user.entities, "id") || []; const entityIDS = mapBy(user.entities, "id") || [];
const assignment = await getAssignment(id); const assignment = await getAssignment(id);
@@ -59,7 +59,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id'))); const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
const groups = await (checkAccess(user, ["developer", "admin"]) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id'))); const groups = await (checkAccess(user, ["developer", "admin"]) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id')));
return {props: serialize({user, users, entities: allowedEntities, assignment, groups})}; return { props: serialize({ user, users, entities: allowedEntities, assignment, groups }) };
}, sessionOptions); }, sessionOptions);
interface Props { interface Props {
@@ -72,7 +72,7 @@ interface Props {
const SIZE = 9; const SIZE = 9;
export default function AssignmentsPage({assignment, user, users, entities, groups}: Props) { export default function AssignmentsPage({ assignment, user, users, entities, groups }: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>(assignment.exams.map((e) => e.module)); const [selectedModules, setSelectedModules] = useState<Module[]>(assignment.exams.map((e) => e.module));
const [assignees, setAssignees] = useState<string[]>(assignment.assignees); const [assignees, setAssignees] = useState<string[]>(assignment.assignees);
const [teachers, setTeachers] = useState<string[]>(assignment.teachers || []); const [teachers, setTeachers] = useState<string[]>(assignment.teachers || []);
@@ -90,12 +90,11 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
const [released, setReleased] = useState<boolean>(assignment.released || false); const [released, setReleased] = useState<boolean>(assignment.released || false);
const [autoStart, setAutostart] = useState<boolean>(assignment.autoStart || false); const [autoStart, setAutostart] = useState<boolean>(assignment.autoStart || false);
const [autoStartDate, setAutoStartDate] = useState<Date | null>(moment(assignment.autoStartDate).toDate());
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 }[]>([]);
const {exams} = useExams(); const { exams } = useExams();
const router = useRouter(); const router = useRouter();
const classrooms = useMemo(() => groups.filter((e) => e.entity === entity), [entity, groups]); const classrooms = useMemo(() => groups.filter((e) => e.entity === entity), [entity, groups]);
@@ -103,11 +102,11 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]); const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]);
const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]); const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]);
const {rows: filteredStudentsRows, renderSearch: renderStudentSearch} = useListSearch([["name"], ["email"]], userStudents); const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents);
const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch} = useListSearch([["name"], ["email"]], userTeachers); const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers);
const {items: studentRows, renderMinimal: renderStudentPagination} = usePagination(filteredStudentsRows, SIZE); const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE);
const {items: teacherRows, renderMinimal: renderTeacherPagination} = usePagination(filteredTeachersRows, SIZE); const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE);
useEffect(() => { useEffect(() => {
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
@@ -148,7 +147,6 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
instructorGender, instructorGender,
released, released,
autoStart, autoStart,
autoStartDate,
}) })
.then(() => { .then(() => {
toast.success(`The assignment "${name}" has been updated successfully!`); toast.success(`The assignment "${name}" has been updated successfully!`);
@@ -316,9 +314,9 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required /> <Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
<Select <Select
label="Entity" label="Entity"
options={entities.map((e) => ({value: e.id, label: e.label}))} options={entities.map((e) => ({ value: e.id, label: e.label }))}
onChange={(v) => setEntity(v ? v.value! : undefined)} onChange={(v) => setEntity(v ? v.value! : undefined)}
defaultValue={{value: entities[0]?.id, label: entities[0]?.label}} defaultValue={{ value: entities[0]?.id, label: entities[0]?.label }}
/> />
</div> </div>
@@ -355,24 +353,6 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
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") && (
@@ -386,9 +366,9 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)} onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
disabled={!selectedModules.includes("speaking") || !!assignment} disabled={!selectedModules.includes("speaking") || !!assignment}
options={[ options={[
{value: "male", label: "Male"}, { value: "male", label: "Male" },
{value: "female", label: "Female"}, { value: "female", label: "Female" },
{value: "varied", label: "Varied"}, { value: "varied", label: "Varied" },
]} ]}
/> />
</div> </div>
@@ -412,14 +392,14 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
onChange={(value) => onChange={(value) =>
value value
? setExamIDs((prev) => [ ? setExamIDs((prev) => [
...prev.filter((x) => x.module !== module), ...prev.filter((x) => x.module !== module),
{id: value.value!, module}, { id: value.value!, module },
]) ])
: setExamIDs((prev) => prev.filter((x) => x.module !== module)) : setExamIDs((prev) => prev.filter((x) => x.module !== module))
} }
options={exams options={exams
.filter((x) => !x.isDiagnostic && x.module === module) .filter((x) => !x.isDiagnostic && x.module === module)
.map((x) => ({value: x.id, label: x.id}))} .map((x) => ({ value: x.id, label: x.id }))}
/> />
</div> </div>
))} ))}
@@ -446,7 +426,7 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) && users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
"!bg-mti-purple-light !text-white", "!bg-mti-purple-light !text-white",
)}> )}>
{g.name} {g.name}
</button> </button>
@@ -510,7 +490,7 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) && users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
"!bg-mti-purple-light !text-white", "!bg-mti-purple-light !text-white",
)}> )}>
{g.name} {g.name}
</button> </button>

View File

@@ -6,37 +6,37 @@ import ProgressBar from "@/components/Low/ProgressBar";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import Separator from "@/components/Low/Separator"; import Separator from "@/components/Low/Separator";
import useExams from "@/hooks/useExams"; import useExams from "@/hooks/useExams";
import {useListSearch} from "@/hooks/useListSearch"; import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination"; import usePagination from "@/hooks/usePagination";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {EntityWithRoles, WithEntity} from "@/interfaces/entity"; import { EntityWithRoles, WithEntity } from "@/interfaces/entity";
import {InstructorGender, Variant} from "@/interfaces/exam"; import { InstructorGender, Variant } from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import {Group, User} from "@/interfaces/user"; import { Group, User } from "@/interfaces/user";
import {sessionOptions} from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import {mapBy, redirect, serialize} from "@/utils"; import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import {getEntitiesWithRoles} from "@/utils/entities.be"; import { getEntitiesWithRoles } from "@/utils/entities.be";
import {getGroups, getGroupsByEntities} from "@/utils/groups.be"; import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
import {checkAccess, findAllowedEntities} from "@/utils/permissions"; import { checkAccess, findAllowedEntities } from "@/utils/permissions";
import {calculateAverageLevel} from "@/utils/score"; import { calculateAverageLevel } from "@/utils/score";
import { isAdmin } from "@/utils/users"; import { isAdmin } from "@/utils/users";
import {getEntitiesUsers, getUsers} from "@/utils/users.be"; import { getEntitiesUsers, getUsers } from "@/utils/users.be";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import {capitalize} from "lodash"; import { capitalize } from "lodash";
import moment from "moment"; import moment from "moment";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {generate} from "random-words"; import { generate } from "random-words";
import {useEffect, useMemo, useState} from "react"; import { useEffect, useMemo, useState } from "react";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
import {BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
if (!user) return redirect("/login") if (!user) return redirect("/login")
@@ -49,7 +49,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const users = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id'))); const users = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
const groups = await (isAdmin(user) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id'))); const groups = await (isAdmin(user) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id')));
return {props: serialize({user, users, entities, groups})}; return { props: serialize({ user, users, entities, groups }) };
}, sessionOptions); }, sessionOptions);
interface Props { interface Props {
@@ -62,7 +62,7 @@ interface Props {
const SIZE = 9; const SIZE = 9;
export default function AssignmentsPage({user, users, groups, entities}: Props) { export default function AssignmentsPage({ user, users, groups, entities }: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>([]); const [selectedModules, setSelectedModules] = useState<Module[]>([]);
const [assignees, setAssignees] = useState<string[]>([]); const [assignees, setAssignees] = useState<string[]>([]);
const [teachers, setTeachers] = useState<string[]>([...(user.type === "teacher" ? [user.id] : [])]); const [teachers, setTeachers] = useState<string[]>([...(user.type === "teacher" ? [user.id] : [])]);
@@ -88,12 +88,11 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
const [released, setReleased] = useState<boolean>(false); const [released, setReleased] = useState<boolean>(false);
const [autoStart, setAutostart] = useState<boolean>(false); const [autoStart, setAutostart] = useState<boolean>(false);
const [autoStartDate, setAutoStartDate] = useState<Date | null>(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 }[]>([]);
const {exams} = useExams(); const { exams } = useExams();
const router = useRouter(); const router = useRouter();
const classrooms = useMemo(() => groups.filter((e) => e.entity?.id === entity), [entity, groups]); const classrooms = useMemo(() => groups.filter((e) => e.entity?.id === entity), [entity, groups]);
@@ -102,11 +101,11 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
const userStudents = useMemo(() => allowedUsers.filter((x) => x.type === "student"), [allowedUsers]); const userStudents = useMemo(() => allowedUsers.filter((x) => x.type === "student"), [allowedUsers]);
const userTeachers = useMemo(() => allowedUsers.filter((x) => x.type === "teacher"), [allowedUsers]); const userTeachers = useMemo(() => allowedUsers.filter((x) => x.type === "teacher"), [allowedUsers]);
const {rows: filteredStudentsRows, renderSearch: renderStudentSearch} = useListSearch([["name"], ["email"]], userStudents); const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents);
const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch} = useListSearch([["name"], ["email"]], userTeachers); const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers);
const {items: studentRows, renderMinimal: renderStudentPagination} = usePagination(filteredStudentsRows, SIZE); const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE);
const {items: teacherRows, renderMinimal: renderTeacherPagination} = usePagination(filteredTeachersRows, SIZE); const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE);
useEffect(() => { useEffect(() => {
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
@@ -148,7 +147,6 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
instructorGender, instructorGender,
released, released,
autoStart, autoStart,
autoStartDate,
}) })
.then((result) => { .then((result) => {
toast.success(`The assignment "${name}" has been created successfully!`); toast.success(`The assignment "${name}" has been created successfully!`);
@@ -274,9 +272,9 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required /> <Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
<Select <Select
label="Entity" label="Entity"
options={entities.map((e) => ({value: e.id, label: e.label}))} options={entities.map((e) => ({ value: e.id, label: e.label }))}
onChange={(v) => setEntity(v ? v.value! : undefined)} onChange={(v) => setEntity(v ? v.value! : undefined)}
defaultValue={{value: entities[0]?.id, label: entities[0]?.label}} defaultValue={{ value: entities[0]?.id, label: entities[0]?.label }}
/> />
</div> </div>
@@ -313,24 +311,6 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
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") && (
@@ -344,9 +324,9 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)} onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
disabled={!selectedModules.includes("speaking")} disabled={!selectedModules.includes("speaking")}
options={[ options={[
{value: "male", label: "Male"}, { value: "male", label: "Male" },
{value: "female", label: "Female"}, { value: "female", label: "Female" },
{value: "varied", label: "Varied"}, { value: "varied", label: "Varied" },
]} ]}
/> />
</div> </div>
@@ -370,14 +350,14 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
onChange={(value) => onChange={(value) =>
value value
? setExamIDs((prev) => [ ? setExamIDs((prev) => [
...prev.filter((x) => x.module !== module), ...prev.filter((x) => x.module !== module),
{id: value.value!, module}, { id: value.value!, module },
]) ])
: setExamIDs((prev) => prev.filter((x) => x.module !== module)) : setExamIDs((prev) => prev.filter((x) => x.module !== module))
} }
options={exams options={exams
.filter((x) => !x.isDiagnostic && x.module === module) .filter((x) => !x.isDiagnostic && x.module === module)
.map((x) => ({value: x.id, label: x.id}))} .map((x) => ({ value: x.id, label: x.id }))}
/> />
</div> </div>
))} ))}
@@ -404,7 +384,7 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) && users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
"!bg-mti-purple-light !text-white", "!bg-mti-purple-light !text-white",
)}> )}>
{g.name} {g.name}
</button> </button>
@@ -468,7 +448,7 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) && users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
"!bg-mti-purple-light !text-white", "!bg-mti-purple-light !text-white",
)}> )}>
{g.name} {g.name}
</button> </button>

View File

@@ -1,11 +1,11 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import {withIronSessionSsr} from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import {shouldRedirectHome} from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage"; import ExamPage from "./(exam)/ExamPage";
import Head from "next/head"; import Head from "next/head";
import {User} from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { filterBy, findBy, redirect, serialize } from "@/utils"; import { filterBy, findBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be"; import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be";
@@ -20,36 +20,38 @@ import { useRouter } from "next/router";
import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be"; import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be";
import { Session } from "@/hooks/useSessions"; import { Session } from "@/hooks/useSessions";
import moment from "moment"; import moment from "moment";
import { activeAssignmentFilter } from "@/utils/assignments";
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
const destination = Buffer.from(req.url || "/").toString("base64") const loginDestination = Buffer.from(req.url || "/").toString("base64")
if (!user) return redirect(`/login?destination=${destination}`) if (!user) return redirect(`/login?destination=${loginDestination}`)
if (shouldRedirectHome(user)) return redirect("/") if (shouldRedirectHome(user)) return redirect("/")
const {assignment: assignmentID} = query as {assignment?: string} const { assignment: assignmentID, destination } = query as { assignment?: string, destination?: string }
const destinationURL = !!destination ? Buffer.from(destination, 'base64').toString() : undefined
if (assignmentID) { if (assignmentID) {
const assignment = await getAssignment(assignmentID) const assignment = await getAssignment(assignmentID)
if (!assignment) return redirect("/exam") if (!assignment) return redirect(destinationURL || "/exam")
if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type)) if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type))
return redirect("/exam") return redirect(destinationURL || "/exam")
if (filterBy(assignment.results, 'user', user.id).length > 0) if (filterBy(assignment.results, 'user', user.id).length > 0)
return redirect("/exam") return redirect(destinationURL || "/exam")
const exams = await getExamsByIds(uniqBy(assignment.exams, "id")) const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
const session = await getSessionByAssignment(assignmentID) const session = await getSessionByAssignment(assignmentID)
return { return {
props: serialize({user, assignment, exams, session: session ?? undefined}) props: serialize({ user, assignment, exams, destinationURL, session: session ?? undefined })
} }
} }
return { return {
props: serialize({user}), props: serialize({ user, destinationURL }),
}; };
}, sessionOptions); }, sessionOptions);
@@ -58,16 +60,17 @@ interface Props {
assignment?: Assignment assignment?: Assignment
exams?: Exam[] exams?: Exam[]
session?: Session session?: Session
destinationURL?: string
} }
export default function Page({user, assignment, exams = [], session}: Props) { export default function Page({ user, assignment, exams = [], destinationURL = "/exam", session }: Props) {
const router = useRouter() const router = useRouter()
const state = useExamStore((state) => state) const state = useExamStore((state) => state)
useEffect(() => { useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !session) { if (assignment && exams.length > 0 && !state.assignment && !session) {
if ((moment(assignment.startDate).isAfter(moment()) || moment(assignment.endDate).isBefore(moment())) && assignment.start) return if (!activeAssignmentFilter(assignment)) return
state.setUserSolutions([]); state.setUserSolutions([]);
state.setShowSolutions(false); state.setShowSolutions(false);
@@ -82,12 +85,12 @@ export default function Page({user, assignment, exams = [], session}: Props) {
router.replace(router.asPath) router.replace(router.asPath)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session]) }, [assignment, exams, session])
useEffect(() => { useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !!session) { if (assignment && exams.length > 0 && !state.assignment && !!session) {
state.setShuffles(session.userSolutions.map((x) => ({exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : []}))); state.setShuffles(session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })));
state.setSelectedModules(session.selectedModules); state.setSelectedModules(session.selectedModules);
state.setExam(session.exam); state.setExam(session.exam);
state.setExams(session.exams); state.setExams(session.exams);
@@ -103,7 +106,7 @@ export default function Page({user, assignment, exams = [], session}: Props) {
router.replace(router.asPath) router.replace(router.asPath)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session]) }, [assignment, exams, session])
return ( return (
@@ -117,7 +120,7 @@ export default function Page({user, assignment, exams = [], session}: Props) {
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<ExamPage page="exams" user={user} hideSidebar={!!assignment || !!state.assignment} /> <ExamPage page="exams" destination={destinationURL} user={user} hideSidebar={!!assignment || !!state.assignment} />
</> </>
); );
} }

View File

@@ -10,31 +10,57 @@ import clsx from "clsx";
import { MODULE_ARRAY } from "@/utils/moduleUtils"; import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { capitalize } from "lodash"; import { capitalize } from "lodash";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import { checkAccess } from "@/utils/permissions"; import { checkAccess, findAllowedEntities } from "@/utils/permissions";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import useExamEditorStore from "@/stores/examEditor"; import useExamEditorStore from "@/stores/examEditor";
import ExamEditorStore from "@/stores/examEditor/types"; import ExamEditorStore from "@/stores/examEditor/types";
import ExamEditor from "@/components/ExamEditor"; import ExamEditor from "@/components/ExamEditor";
import MultipleAudioUploader from "@/components/ExamEditor/Shared/AudioEdit"; import MultipleAudioUploader from "@/components/ExamEditor/Shared/AudioEdit";
import { redirect, serialize } from "@/utils"; import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { Module } from "@/interfaces";
import { getExam, getExams } from "@/utils/exams.be";
import { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
import { useEffect } from "react"; import { useEffect } from "react";
import { Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam"; import { getEntitiesWithRoles } from "@/utils/entities.be";
import { isAdmin } from "@/utils/users";
import axios from "axios"; import axios from "axios";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { type Permission = { [key in Module]: boolean }
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
if (!user) return redirect("/login") if (!user) return redirect("/login")
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "mastercorporate", "developer", "corporate"])) if (shouldRedirectHome(user)) return redirect("/")
return redirect("/")
const entityIDs = mapBy(user.entities, 'id')
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs)
const permissions: Permission = {
reading: findAllowedEntities(user, entities, `generate_reading`).length > 0,
listening: findAllowedEntities(user, entities, `generate_listening`).length > 0,
writing: findAllowedEntities(user, entities, `generate_writing`).length > 0,
speaking: findAllowedEntities(user, entities, `generate_speaking`).length > 0,
level: findAllowedEntities(user, entities, `generate_level`).length > 0,
}
if (Object.keys(permissions).every(p => !permissions[p as Module])) return redirect("/")
const { id, module } = query as { id?: string, module?: Module }
if (!id || !module) return { props: serialize({ user, permissions }) };
if (!permissions[module]) return redirect("/generation")
const exam = await getExam(module, id)
if (!exam) return redirect("/generation")
return { return {
props: serialize({ user }), props: serialize({ user, exam, permissions }),
}; };
}, sessionOptions); }, sessionOptions);
export default function Generation({ user }: { user: User; }) { export default function Generation({ user, exam, permissions }: { user: User; exam?: Exam, permissions: Permission }) {
const { title, currentModule, modules, dispatch } = useExamEditorStore(); const { title, currentModule, modules, dispatch } = useExamEditorStore();
const updateRoot = (updates: Partial<ExamEditorStore>) => { const updateRoot = (updates: Partial<ExamEditorStore>) => {
@@ -97,6 +123,9 @@ export default function Generation({ user }: { user: User; }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
if (exam) { }
}, [exam])
return ( return (
<> <>
@@ -130,7 +159,7 @@ export default function Generation({ user }: { user: User; }) {
value={currentModule} value={currentModule}
onChange={(currentModule) => updateRoot({ currentModule })} onChange={(currentModule) => updateRoot({ currentModule })}
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between"> className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
{[...MODULE_ARRAY].map((x) => ( {[...MODULE_ARRAY].filter(m => permissions[m]).map((x) => (
<Radio value={x} key={x}> <Radio value={x} key={x}>
{({ checked }) => ( {({ checked }) => (
<span <span

View File

@@ -4,34 +4,34 @@ import Layout from "@/components/High/Layout";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import Separator from "@/components/Low/Separator"; import Separator from "@/components/Low/Separator";
import ProfileSummary from "@/components/ProfileSummary"; import ProfileSummary from "@/components/ProfileSummary";
import {Session} from "@/hooks/useSessions"; import { Session } from "@/hooks/useSessions";
import {Grading} from "@/interfaces"; import { Grading } from "@/interfaces";
import {EntityWithRoles} from "@/interfaces/entity"; import { EntityWithRoles } from "@/interfaces/entity";
import {Exam} from "@/interfaces/exam"; import { Exam } from "@/interfaces/exam";
import { InviteWithEntity } from "@/interfaces/invite"; import { InviteWithEntity } from "@/interfaces/invite";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import {Stat, User} from "@/interfaces/user"; import { Stat, User } from "@/interfaces/user";
import {sessionOptions} from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {findBy, mapBy, redirect, serialize} from "@/utils"; import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import {activeAssignmentFilter} from "@/utils/assignments"; import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments";
import {getAssignmentsByAssignee} from "@/utils/assignments.be"; import { getAssignmentsByAssignee } from "@/utils/assignments.be";
import {getEntitiesWithRoles} from "@/utils/entities.be"; import { getEntitiesWithRoles } from "@/utils/entities.be";
import {getExamsByIds} from "@/utils/exams.be"; import { getExamsByIds } from "@/utils/exams.be";
import {sortByModule} from "@/utils/moduleUtils"; import { sortByModule } from "@/utils/moduleUtils";
import {checkAccess} from "@/utils/permissions"; import { checkAccess } from "@/utils/permissions";
import {getSessionsByUser} from "@/utils/sessions.be"; import { getSessionsByUser } from "@/utils/sessions.be";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import {uniqBy} from "lodash"; import { uniqBy } from "lodash";
import moment from "moment"; import moment from "moment";
import Head from "next/head"; import Head from "next/head";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import {BsArrowRepeat} from "react-icons/bs"; import { BsArrowRepeat } from "react-icons/bs";
import {ToastContainer} from "react-toastify"; import { ToastContainer } from "react-toastify";
interface Props { interface Props {
user: User; user: User;
@@ -44,7 +44,7 @@ interface Props {
grading: Grading; grading: Grading;
} }
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
const destination = Buffer.from(req.url || "/").toString("base64") const destination = Buffer.from(req.url || "/").toString("base64")
if (!user) return redirect(`/login?destination=${destination}`) if (!user) return redirect(`/login?destination=${destination}`)
@@ -55,21 +55,23 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const entityIDS = mapBy(user.entities, "id") || []; const entityIDS = mapBy(user.entities, "id") || [];
const entities = await getEntitiesWithRoles(entityIDS); const entities = await getEntitiesWithRoles(entityIDS);
const assignments = await getAssignmentsByAssignee(user.id, {archived: {$ne: true}}); const assignments = await getAssignmentsByAssignee(user.id, { archived: { $ne: true } });
const sessions = await getSessionsByUser(user.id, 0, { "assignment.id": { $in: mapBy(assignments, 'id') } }); const sessions = await getSessionsByUser(user.id, 0, { "assignment.id": { $in: mapBy(assignments, 'id') } });
const examIDs = uniqBy( const examIDs = uniqBy(
assignments.flatMap((a) => assignments.flatMap((a) =>
a.exams.filter((e) => e.assignee === user.id).map((e) => ({module: e.module, id: e.id, key: `${e.module}_${e.id}`})), filterBy(a.exams, 'assignee', user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })),
), ),
"key", "key",
); );
const exams = await getExamsByIds(examIDs); const exams = await getExamsByIds(examIDs);
return {props: serialize({user, entities, assignments, exams, sessions})}; return { props: serialize({ user, entities, assignments, exams, sessions }) };
}, sessionOptions); }, sessionOptions);
export default function OfficialExam({user, entities, assignments, sessions, exams}: Props) { const destination = Buffer.from("/official-exam").toString("base64")
export default function OfficialExam({ user, entities, assignments, sessions, exams }: Props) {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const router = useRouter(); const router = useRouter();
@@ -93,12 +95,12 @@ export default function OfficialExam({user, entities, assignments, sessions, exa
state.setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module')); state.setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module'));
state.setAssignment(assignment); state.setAssignment(assignment);
router.push("/exam"); router.push(`/exam?assignment=${assignment.id}&destination=${destination}`);
} }
}; };
const loadSession = async (session: Session) => { const loadSession = async (session: Session) => {
state.setShuffles(session.userSolutions.map((x) => ({exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : []}))); state.setShuffles(session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })));
state.setSelectedModules(session.selectedModules); state.setSelectedModules(session.selectedModules);
state.setExam(session.exam); state.setExam(session.exam);
state.setExams(session.exams); state.setExams(session.exams);
@@ -112,16 +114,20 @@ export default function OfficialExam({user, entities, assignments, sessions, exa
state.setShowSolutions(false); state.setShowSolutions(false);
state.setQuestionIndex(session.questionIndex); state.setQuestionIndex(session.questionIndex);
router.push("/exam"); router.push(`/exam?assignment=${session.assignment?.id}&destination=${destination}`);
}; };
const logout = async () => { const logout = async () => {
axios.post("/api/logout").finally(() => { axios.post("/api/logout").finally(() => {
setTimeout(() => router.reload(), 500); setTimeout(() => router.reload(), 500);
}); });
}; };
const studentAssignments = useMemo(() => [
...assignments.filter(activeAssignmentFilter), ...assignments.filter(futureAssignmentFilter)],
[assignments]
);
const studentAssignments = useMemo(() => assignments.filter(activeAssignmentFilter), [assignments]);
const assignmentSessions = useMemo(() => sessions.filter(s => mapBy(studentAssignments, 'id').includes(s.assignment?.id || "")), [sessions, studentAssignments]) const assignmentSessions = useMemo(() => sessions.filter(s => mapBy(studentAssignments, 'id').includes(s.assignment?.id || "")), [sessions, studentAssignments])
return ( return (

View File

@@ -1,19 +1,19 @@
import client from "@/lib/mongodb"; import client from "@/lib/mongodb";
import {Assignment} from "@/interfaces/results"; import { Assignment } from "@/interfaces/results";
import {getAllAssignersByCorporate} from "@/utils/groups.be"; import { getAllAssignersByCorporate } from "@/utils/groups.be";
import {Type} from "@/interfaces/user"; import { Type } from "@/interfaces/user";
const db = client.db(process.env.MONGODB_DB); const db = client.db(process.env.MONGODB_DB);
export const getAssignmentsByAssigner = async (id: string, startDate?: Date, endDate?: Date) => { export const getAssignmentsByAssigner = async (id: string, startDate?: Date, endDate?: Date) => {
let query: any = {assigner: id}; let query: any = { assigner: id };
if (startDate) { if (startDate) {
query.startDate = {$gte: startDate.toISOString()}; query.startDate = { $gte: startDate.toISOString() };
} }
if (endDate) { if (endDate) {
query.endDate = {$lte: endDate.toISOString()}; query.endDate = { $lte: endDate.toISOString() };
} }
return await db.collection("assignments").find<Assignment>(query).toArray(); return await db.collection("assignments").find<Assignment>(query).toArray();
@@ -24,39 +24,39 @@ export const getAssignments = async () => {
}; };
export const getAssignment = async (id: string) => { export const getAssignment = async (id: string) => {
return await db.collection("assignments").findOne<Assignment>({id}); return await db.collection("assignments").findOne<Assignment>({ id });
}; };
export const getAssignmentsByAssignee = async (id: string, filter?: {[key in keyof Partial<Assignment>]: any}) => { export const getAssignmentsByAssignee = async (id: string, filter?: { [key in keyof Partial<Assignment>]: any }) => {
return await db return await db
.collection("assignments") .collection("assignments")
.find<Assignment>({assignees: [id], ...(!filter ? {} : filter)}) .find<Assignment>({ assignees: id, ...(!filter ? {} : filter) })
.toArray(); .toArray();
}; };
export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => { export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => {
return await db.collection("assignments").find<Assignment>({assigner: id}).toArray(); return await db.collection("assignments").find<Assignment>({ assigner: id }).toArray();
}; };
export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, endDate?: Date) => { export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, endDate?: Date) => {
return await db return await db
.collection("assignments") .collection("assignments")
.find<Assignment>({ .find<Assignment>({
assigner: {$in: ids}, assigner: { $in: ids },
...(!!startDate ? {startDate: {$gte: startDate.toISOString()}} : {}), ...(!!startDate ? { startDate: { $gte: startDate.toISOString() } } : {}),
...(!!endDate ? {endDate: {$lte: endDate.toISOString()}} : {}), ...(!!endDate ? { endDate: { $lte: endDate.toISOString() } } : {}),
}) })
.toArray(); .toArray();
}; };
export const getEntityAssignments = async (id: string) => { export const getEntityAssignments = async (id: string) => {
return await db.collection("assignments").find<Assignment>({entity: id}).toArray(); return await db.collection("assignments").find<Assignment>({ entity: id }).toArray();
}; };
export const getEntitiesAssignments = async (ids: string[]) => { export const getEntitiesAssignments = async (ids: string[]) => {
return await db return await db
.collection("assignments") .collection("assignments")
.find<Assignment>({entity: {$in: ids}}) .find<Assignment>({ entity: { $in: ids } })
.toArray(); .toArray();
}; };

View File

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

View File

@@ -1,13 +1,13 @@
import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and} from "firebase/firestore"; import { collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and } from "firebase/firestore";
import {groupBy, shuffle} from "lodash"; import { groupBy, shuffle } from "lodash";
import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam"; import { Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam } from "@/interfaces/exam";
import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user"; import { DeveloperUser, Stat, StudentUser, User } from "@/interfaces/user";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {getCorporateUser} from "@/resources/user"; import { getCorporateUser } from "@/resources/user";
import {getUserCorporate} from "./groups.be"; import { getUserCorporate } from "./groups.be";
import {Db, ObjectId} from "mongodb"; import { Db, ObjectId } from "mongodb";
import client from "@/lib/mongodb"; import client from "@/lib/mongodb";
import {MODULE_ARRAY} from "./moduleUtils"; import { MODULE_ARRAY } from "./moduleUtils";
import { mapBy } from "."; import { mapBy } from ".";
const db = client.db(process.env.MONGODB_DB); const db = client.db(process.env.MONGODB_DB);
@@ -21,7 +21,7 @@ export async function getSpecificExams(ids: string[]) {
async (module) => async (module) =>
await db await db
.collection(module) .collection(module)
.find<Exam>({id: {$in: ids}}) .find<Exam>({ id: { $in: ids } })
.toArray(), .toArray(),
), ),
) )
@@ -30,7 +30,11 @@ export async function getSpecificExams(ids: string[]) {
return exams; return exams;
} }
export const getExamsByIds = async (ids: {module: Module; id: string}[]) => { export const getExam = async (module: Module, id: string) => {
return await db.collection(module).findOne<Exam>({ id }) ?? undefined
};
export const getExamsByIds = async (ids: { module: Module; id: string }[]) => {
const groupedByModule = groupBy(ids, "module"); const groupedByModule = groupBy(ids, "module");
const exams: Exam[] = ( const exams: Exam[] = (
await Promise.all( await Promise.all(
@@ -38,7 +42,7 @@ export const getExamsByIds = async (ids: {module: Module; id: string}[]) => {
async (m) => async (m) =>
await db await db
.collection(m) .collection(m)
.find<Exam>({id: {$in: mapBy(groupedByModule[m], 'id')}}) .find<Exam>({ id: { $in: mapBy(groupedByModule[m], 'id') } })
.toArray(), .toArray(),
), ),
) )
@@ -121,7 +125,7 @@ const filterByOwners = async (exams: Exam[], userID?: string) => {
const filterByDifficulty = async (db: Db, exams: Exam[], module: Module, userID?: string) => { const filterByDifficulty = async (db: Db, exams: Exam[], module: Module, userID?: string) => {
if (!userID) return exams; if (!userID) return exams;
const user = await db.collection("users").findOne<User>({id: userID}); const user = await db.collection("users").findOne<User>({ id: userID });
if (!user) return exams; if (!user) return exams;
const difficulty = user.levels[module] <= 3 ? "easy" : user.levels[module] <= 6 ? "medium" : "hard"; const difficulty = user.levels[module] <= 3 ? "easy" : user.levels[module] <= 6 ? "medium" : "hard";
@@ -135,7 +139,7 @@ const filterByPreference = async (db: Db, exams: Exam[], module: Module, userID?
if (!userID) return exams; if (!userID) return exams;
const user = await db.collection("users").findOne<StudentUser | DeveloperUser>({id: userID}); const user = await db.collection("users").findOne<StudentUser | DeveloperUser>({ id: userID });
if (!user) return exams; if (!user) return exams;
if (!["developer", "student"].includes(user.type)) return exams; if (!["developer", "student"].includes(user.type)) return exams;