diff --git a/src/components/Medium/StatGridItem.tsx b/src/components/Medium/StatGridItem.tsx index 3470b20c..b0c3622b 100644 --- a/src/components/Medium/StatGridItem.tsx +++ b/src/components/Medium/StatGridItem.tsx @@ -2,7 +2,7 @@ import React from "react"; import {BsClock, BsXCircle} from "react-icons/bs"; import clsx from "clsx"; import {Stat, User} from "@/interfaces/user"; -import {Module} from "@/interfaces"; +import {Module, Step} from "@/interfaces"; import ai_usage from "@/utils/ai.detection"; import {calculateBandScore} from "@/utils/score"; import moment from "moment"; @@ -77,6 +77,7 @@ interface StatsGridItemProps { assignments: Assignment[]; users: User[]; training?: boolean; + gradingSystem?: Step[]; selectedTrainingExams?: string[]; maxTrainingExams?: number; setSelectedTrainingExams?: React.Dispatch>; @@ -97,6 +98,7 @@ const StatsGridItem: React.FC = ({ users, training, selectedTrainingExams, + gradingSystem, setSelectedTrainingExams, setExams, setShowSolutions, @@ -214,10 +216,14 @@ const StatsGridItem: React.FC = ({
- - Level{" "} - {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} - + {!!assignment && (assignment.released || assignment.released === undefined) && ( + + Level{" "} + {( + aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length + ).toFixed(1)} + + )} {shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)}
{examNumber === undefined ? ( @@ -242,9 +248,9 @@ const StatsGridItem: React.FC = ({
- {aggregatedLevels.map(({module, level}) => ( - - ))} + {!!assignment && + (assignment.released || assignment.released === undefined) && + aggregatedLevels.map(({module, level}) => )}
{assignment && ( diff --git a/src/components/ModuleBadge.tsx b/src/components/ModuleBadge.tsx index fc763487..0a791bd6 100644 --- a/src/components/ModuleBadge.tsx +++ b/src/components/ModuleBadge.tsx @@ -1,24 +1,28 @@ +import {Step} from "@/interfaces"; +import {getGradingLabel, getLevelLabel} from "@/utils/score"; import clsx from "clsx"; -import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs"; +import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; -const ModuleBadge: React.FC<{ module: string; level?: number }> = ({ module, level }) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {/* do not switch to level && it will convert the 0.0 to 0*/} - {level !== undefined && ({level.toFixed(1)})} -
+const ModuleBadge: React.FC<{module: string; level?: number; gradingSystem?: Step[]}> = ({module, level, gradingSystem}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {/* do not switch to level && it will convert the 0.0 to 0*/} + {level !== undefined && ( + {module === "level" && gradingSystem ? getGradingLabel(level, gradingSystem) : level.toFixed(1)} + )} +
); -export default ModuleBadge; \ No newline at end of file +export default ModuleBadge; diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index bf1f9c41..4952e14e 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -25,14 +25,16 @@ import useExams from "@/hooks/useExams"; interface Props { isCreating: boolean; users: User[]; + user: User; groups: Group[]; assignment?: Assignment; cancelCreation: () => void; } -export default function AssignmentCreator({isCreating, assignment, groups, users, cancelCreation}: Props) { +export default function AssignmentCreator({isCreating, assignment, user, groups, users, cancelCreation}: Props) { const [selectedModules, setSelectedModules] = useState(assignment?.exams.map((e) => e.module) || []); const [assignees, setAssignees] = useState(assignment?.assignees || []); + const [teachers, setTeachers] = useState(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]); const [name, setName] = useState( assignment?.name || generate({ @@ -53,6 +55,8 @@ export default function AssignmentCreator({isCreating, assignment, groups, users const [instructorGender, setInstructorGender] = useState(assignment?.instructorGender || "varied"); // creates a new exam for each assignee or just one exam for all assignees const [generateMultiple, setGenerateMultiple] = useState(false); + const [released, setReleased] = useState(false); + const [useRandomExams, setUseRandomExams] = useState(true); const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]); @@ -82,8 +86,10 @@ export default function AssignmentCreator({isCreating, assignment, groups, users endDate, selectedModules, generateMultiple, + teachers, variant, instructorGender, + released, }) .then(() => { toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`); @@ -373,6 +379,9 @@ export default function AssignmentCreator({isCreating, assignment, groups, users setGenerateMultiple((d) => !d)}> Generate different exams + setReleased((d) => !d)}> + Release automatically +
)} - {!isLoading && false && ( + {!isLoading && (
{moduleResultText(selectedModule, bandScore)}
diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 4e65ec22..33feb7e0 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -12,6 +12,7 @@ interface ExamBase { isDiagnostic: boolean; variant?: Variant; difficulty?: Difficulty; + owners?: string[]; shuffle?: boolean; createdBy?: string; // option as it has been added later createdAt?: string; // option as it has been added later diff --git a/src/interfaces/results.ts b/src/interfaces/results.ts index f14c0481..63aee893 100644 --- a/src/interfaces/results.ts +++ b/src/interfaces/results.ts @@ -26,6 +26,7 @@ export interface Assignment { instructorGender?: InstructorGender; startDate: Date; endDate: Date; + teachers?: string[]; archived?: boolean; released?: boolean; // unless start is active, the assignment is not visible to the assignees diff --git a/src/pages/(admin)/Lists/ExamList.tsx b/src/pages/(admin)/Lists/ExamList.tsx index 2c990677..fbb4bd1f 100644 --- a/src/pages/(admin)/Lists/ExamList.tsx +++ b/src/pages/(admin)/Lists/ExamList.tsx @@ -1,4 +1,4 @@ -import {useMemo} from "react"; +import {useMemo, useState} from "react"; import {PERMISSIONS} from "@/constants/userPermissions"; import useExams from "@/hooks/useExams"; import useUsers from "@/hooks/useUsers"; @@ -11,11 +11,15 @@ import {countExercises} from "@/utils/moduleUtils"; import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; +import {capitalize, uniq} from "lodash"; import {useRouter} from "next/router"; -import {BsBan, BsBanFill, BsCheck, BsCircle, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs"; +import {BsBan, BsBanFill, BsCheck, BsCircle, BsPencil, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs"; import {toast} from "react-toastify"; import {useListSearch} from "@/hooks/useListSearch"; +import Modal from "@/components/Modal"; +import {checkAccess} from "@/utils/permissions"; +import useGroups from "@/hooks/useGroups"; +import Button from "@/components/Low/Button"; const searchFields = [["module"], ["id"], ["createdBy"]]; @@ -29,9 +33,40 @@ const CLASSES: {[key in Module]: string} = { const columnHelper = createColumnHelper(); +const ExamOwnerSelector = ({options, exam, onSave}: {options: User[]; exam: Exam; onSave: (owners: string[]) => void}) => { + const [owners, setOwners] = useState(exam.owners || []); + + return ( +
+
+ {options.map((c) => ( + + ))} +
+ +
+ ); +}; + export default function ExamList({user}: {user: User}) { + const [selectedExam, setSelectedExam] = useState(); + const {exams, reload} = useExams(); const {users} = useUsers(); + const {groups} = useGroups({admin: user?.id, userType: user?.type}); + + const filteredCorporates = useMemo(() => { + const participantsAndAdmins = uniq(groups.flatMap((x) => [...x.participants, x.admin])).filter((x) => x !== user?.id); + return users.filter((x) => participantsAndAdmins.includes(x.id) && x.type === "corporate"); + }, [users, groups, user]); const parsedExams = useMemo(() => { return exams.map((exam) => { @@ -94,6 +129,29 @@ export default function ExamList({user}: {user: User}) { .finally(reload); }; + const updateExam = async (exam: Exam, body: object) => { + if (!confirm(`Are you sure you want to update this ${capitalize(exam.module)} exam?`)) return; + + axios + .patch(`/api/exam/${exam.module}/${exam.id}`, body) + .then(() => toast.success(`Updated the "${exam.id}" exam`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Exam not found!"); + return; + } + + if (reason.response.status === 403) { + toast.error("You do not have permission to update this exam!"); + return; + } + + toast.error("Something went wrong, please try again later."); + }) + .finally(reload) + .finally(() => setSelectedExam(undefined)); + }; + const deleteExam = async (exam: Exam) => { if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return; @@ -166,12 +224,21 @@ export default function ExamList({user}: {user: User}) { cell: ({row}: {row: {original: Exam}}) => { return (
- + {(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && ( + <> + + {checkAccess(user, ["admin", "developer", "mastercorporate"]) && ( + + )} + + )}