Merge remote-tracking branch 'origin/develop' into feature/ExamGenRework
This commit is contained in:
@@ -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>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -86,9 +96,9 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
)}
|
)}
|
||||||
{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>
|
||||||
|
|||||||
@@ -3,39 +3,30 @@ 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 {
|
||||||
|
exam: LevelExam
|
||||||
showSolutions: boolean;
|
showSolutions: boolean;
|
||||||
runOnClick: ((index: number) => void) | undefined;
|
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 currentExercise = (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise;
|
|
||||||
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) => {
|
const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => {
|
||||||
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
|
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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;
|
||||||
@@ -54,6 +55,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
|||||||
// In case client want to switch back
|
// In case client want to switch back
|
||||||
const textRenderDisabled = true;
|
const textRenderDisabled = true;
|
||||||
|
|
||||||
|
const [timesListened, setTimesListened] = useState(0);
|
||||||
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
||||||
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
||||||
const [continueAnyways, setContinueAnyways] = useState(false);
|
const [continueAnyways, setContinueAnyways] = useState(false);
|
||||||
@@ -176,6 +178,8 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimesListened(0);
|
||||||
setPartIndex(partIndex + 1);
|
setPartIndex(partIndex + 1);
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
setQuestionIndex(0);
|
setQuestionIndex(0);
|
||||||
@@ -256,6 +260,40 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
|||||||
}, 0) + (questionIndex + 1);
|
}, 0) + (questionIndex + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderAudioPlayer = () => (
|
||||||
|
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
||||||
|
{exam?.parts[partIndex]?.audio?.source ? (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const renderText = () => (
|
const renderText = () => (
|
||||||
<>
|
<>
|
||||||
<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={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
|
||||||
@@ -274,7 +312,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
|||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||||
{exam.parts[partIndex].context &&
|
{(exam.parts[partIndex].context || exam.parts[partIndex].text) &&
|
||||||
<TextComponent
|
<TextComponent
|
||||||
part={exam.parts[partIndex]}
|
part={exam.parts[partIndex]}
|
||||||
contextWords={contextWords}
|
contextWords={contextWords}
|
||||||
@@ -436,13 +474,15 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
|||||||
renderText() :
|
renderText() :
|
||||||
<>
|
<>
|
||||||
{exam.parts[partIndex]?.context && renderText()}
|
{exam.parts[partIndex]?.context && renderText()}
|
||||||
|
{exam.parts[partIndex]?.audio && renderAudioPlayer()}
|
||||||
{(showSolutions) ?
|
{(showSolutions) ?
|
||||||
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
||||||
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</>)
|
</>
|
||||||
|
)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [textRender, currentExercise, changedPrompt]);
|
}, [textRender, currentExercise, changedPrompt]);
|
||||||
|
|
||||||
@@ -483,7 +523,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
|
|||||||
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}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -413,13 +393,13 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
|
|||||||
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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -371,13 +351,13 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
|
|||||||
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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -87,7 +90,7 @@ export default function Page({user, assignment, exams = [], session}: Props) {
|
|||||||
|
|
||||||
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);
|
||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +114,7 @@ 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 () => {
|
||||||
@@ -121,7 +123,11 @@ export default function OfficialExam({user, entities, assignments, sessions, exa
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const studentAssignments = useMemo(() => assignments.filter(activeAssignmentFilter), [assignments]);
|
const studentAssignments = useMemo(() => [
|
||||||
|
...assignments.filter(activeAssignmentFilter), ...assignments.filter(futureAssignmentFilter)],
|
||||||
|
[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 (
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,20 @@
|
|||||||
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;
|
if (a.archived) return false;
|
||||||
}
|
|
||||||
|
|
||||||
return moment(a.endDate).isBefore(currentDate);
|
return moment(a.endDate).isBefore(currentDate);
|
||||||
}
|
}
|
||||||
@@ -41,37 +23,18 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user