diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index 4952e14e..1724da6d 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -47,7 +47,8 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, }), ); const [isLoading, setIsLoading] = useState(false); - const [startDate, setStartDate] = useState(assignment ? moment(assignment.startDate).toDate() : new Date()); + const [startDate, setStartDate] = useState(assignment ? moment(assignment.startDate).toDate() : moment().add(1, 'hour').toDate()); + const [endDate, setEndDate] = useState( assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(), ); @@ -55,7 +56,10 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, const [instructorGender, setInstructorGender] = useState(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 [released, setReleased] = useState(assignment?.released || false); + + const [autoStart, setAutostart] = useState(assignment?.autoStart || false); + const [autoStartDate, setAutoStartDate] = useState(assignment ? moment(assignment.autoStartDate).toDate() : new Date()); const [useRandomExams, setUseRandomExams] = useState(true); const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]); @@ -90,6 +94,8 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, variant, instructorGender, released, + autoStart, + autoStartDate, }) .then(() => { toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`); @@ -233,7 +239,7 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
- + setEndDate(date)} />
+ {autoStart && (
+ + moment(date).isSameOrAfter(new Date())} + dateFormat="dd/MM/yyyy HH:mm" + selected={autoStartDate} + showTimeSelect + onChange={(date) => setAutoStartDate(date)} + /> +
+ )}
{selectedModules.includes("speaking") && ( @@ -380,7 +403,10 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, Generate different exams setReleased((d) => !d)}> - Release automatically + Auto release results + + setAutostart((d) => !d)}> + Auto start exam
diff --git a/src/dashboards/AssignmentView.tsx b/src/dashboards/AssignmentView.tsx index 03f5baff..2565e532 100644 --- a/src/dashboards/AssignmentView.tsx +++ b/src/dashboards/AssignmentView.tsx @@ -2,329 +2,433 @@ import Button from "@/components/Low/Button"; import ProgressBar from "@/components/Low/ProgressBar"; import Modal from "@/components/Modal"; import useUsers from "@/hooks/useUsers"; -import {Module} from "@/interfaces"; -import {Assignment} from "@/interfaces/results"; -import {Stat, User} from "@/interfaces/user"; +import { Module } from "@/interfaces"; +import { Assignment } from "@/interfaces/results"; +import { Stat, User } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; -import {getExamById} from "@/utils/exams"; -import {sortByModule} from "@/utils/moduleUtils"; -import {calculateBandScore} from "@/utils/score"; -import {convertToUserSolutions} from "@/utils/stats"; -import {getUserName} from "@/utils/users"; +import { getExamById } from "@/utils/exams"; +import { sortByModule } from "@/utils/moduleUtils"; +import { calculateBandScore } from "@/utils/score"; +import { convertToUserSolutions } from "@/utils/stats"; +import { getUserName } from "@/utils/users"; import axios from "axios"; import clsx from "clsx"; -import {capitalize, uniqBy} from "lodash"; +import { capitalize, uniqBy } from "lodash"; import moment from "moment"; -import {useRouter} from "next/router"; -import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs"; -import {toast} from "react-toastify"; +import { useRouter } from "next/router"; +import { + BsBook, + BsClipboard, + BsHeadphones, + BsMegaphone, + BsPen, +} from "react-icons/bs"; +import { toast } from "react-toastify"; +import { futureAssignmentFilter } from "@/utils/assignments"; interface Props { - isOpen: boolean; - assignment?: Assignment; - onClose: () => void; + isOpen: boolean; + assignment?: Assignment; + onClose: () => void; } -export default function AssignmentView({isOpen, assignment, onClose}: Props) { - const {users} = useUsers(); - const router = useRouter(); +export default function AssignmentView({ isOpen, assignment, onClose }: Props) { + const { users } = useUsers(); + const router = useRouter(); - const setExams = useExamStore((state) => state.setExams); - const setShowSolutions = useExamStore((state) => state.setShowSolutions); - const setUserSolutions = useExamStore((state) => state.setUserSolutions); - const setSelectedModules = useExamStore((state) => state.setSelectedModules); + const setExams = useExamStore((state) => state.setExams); + const setShowSolutions = useExamStore((state) => state.setShowSolutions); + const setUserSolutions = useExamStore((state) => state.setUserSolutions); + const setSelectedModules = useExamStore((state) => state.setSelectedModules); - const deleteAssignment = async () => { - if (!confirm("Are you sure you want to delete this assignment?")) return; + const deleteAssignment = async () => { + if (!confirm("Are you sure you want to delete this assignment?")) return; - axios - .delete(`/api/assignments/${assignment?.id}`) - .then(() => toast.success(`Successfully deleted the assignment "${assignment?.name}".`)) - .catch(() => toast.error("Something went wrong, please try again later.")) - .finally(onClose); - }; + axios + .delete(`/api/assignments/${assignment?.id}`) + .then(() => + toast.success( + `Successfully deleted the assignment "${assignment?.name}".` + ) + ) + .catch(() => toast.error("Something went wrong, please try again later.")) + .finally(onClose); + }; - const startAssignment = () => { - if (assignment) { - axios - .post(`/api/assignments/${assignment.id}/start`) - .then(() => { - toast.success(`The assignment "${assignment.name}" has been started successfully!`); - }) - .catch((e) => { - console.log(e); - toast.error("Something went wrong, please try again later!"); - }); - } - }; + const startAssignment = () => { + if (assignment) { + axios + .post(`/api/assignments/${assignment.id}/start`) + .then(() => { + toast.success( + `The assignment "${assignment.name}" has been started successfully!` + ); + }) + .catch((e) => { + console.log(e); + toast.error("Something went wrong, please try again later!"); + }); + } + }; - const formatTimestamp = (timestamp: string) => { - const date = moment(parseInt(timestamp)); - const formatter = "YYYY/MM/DD - HH:mm"; + const formatTimestamp = (timestamp: string) => { + const date = moment(parseInt(timestamp)); + const formatter = "YYYY/MM/DD - HH:mm"; - return date.format(formatter); - }; + return date.format(formatter); + }; - const calculateAverageModuleScore = (module: Module) => { - if (!assignment) return -1; + const calculateAverageModuleScore = (module: Module) => { + if (!assignment) return -1; - const resultModuleBandScores = assignment.results.map((r) => { - const moduleStats = r.stats.filter((s) => s.module === module); + const resultModuleBandScores = assignment.results.map((r) => { + const moduleStats = r.stats.filter((s) => s.module === module); - const correct = moduleStats.reduce((acc, curr) => acc + curr.score.correct, 0); - const total = moduleStats.reduce((acc, curr) => acc + curr.score.total, 0); - return calculateBandScore(correct, total, module, r.type); - }); + const correct = moduleStats.reduce( + (acc, curr) => acc + curr.score.correct, + 0 + ); + const total = moduleStats.reduce( + (acc, curr) => acc + curr.score.total, + 0 + ); + return calculateBandScore(correct, total, module, r.type); + }); - return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / assignment.results.length; - }; + return resultModuleBandScores.length === 0 + ? -1 + : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / + assignment.results.length; + }; - const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => { - const scores: { - [key in Module]: {total: number; missing: number; correct: number}; - } = { - reading: { - total: 0, - correct: 0, - missing: 0, - }, - listening: { - total: 0, - correct: 0, - missing: 0, - }, - writing: { - total: 0, - correct: 0, - missing: 0, - }, - speaking: { - total: 0, - correct: 0, - missing: 0, - }, - level: { - total: 0, - correct: 0, - missing: 0, - }, - }; + const aggregateScoresByModule = ( + stats: Stat[] + ): { module: Module; total: number; missing: number; correct: number }[] => { + const scores: { + [key in Module]: { total: number; missing: number; correct: number }; + } = { + reading: { + total: 0, + correct: 0, + missing: 0, + }, + listening: { + total: 0, + correct: 0, + missing: 0, + }, + writing: { + total: 0, + correct: 0, + missing: 0, + }, + speaking: { + total: 0, + correct: 0, + missing: 0, + }, + level: { + total: 0, + correct: 0, + missing: 0, + }, + }; - stats.forEach((x) => { - scores[x.module!] = { - total: scores[x.module!].total + x.score.total, - correct: scores[x.module!].correct + x.score.correct, - missing: scores[x.module!].missing + x.score.missing, - }; - }); + stats.forEach((x) => { + scores[x.module!] = { + total: scores[x.module!].total + x.score.total, + correct: scores[x.module!].correct + x.score.correct, + missing: scores[x.module!].missing + x.score.missing, + }; + }); - return Object.keys(scores) - .filter((x) => scores[x as Module].total > 0) - .map((x) => ({module: x as Module, ...scores[x as Module]})); - }; + return Object.keys(scores) + .filter((x) => scores[x as Module].total > 0) + .map((x) => ({ module: x as Module, ...scores[x as Module] })); + }; - const customContent = (stats: Stat[], user: string, focus: "academic" | "general") => { - const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0); - const total = stats.reduce((accumulator, current) => accumulator + current.score.total, 0); - const aggregatedScores = aggregateScoresByModule(stats).filter((x) => x.total > 0); + const customContent = ( + stats: Stat[], + user: string, + focus: "academic" | "general" + ) => { + const correct = stats.reduce( + (accumulator, current) => accumulator + current.score.correct, + 0 + ); + const total = stats.reduce( + (accumulator, current) => accumulator + current.score.total, + 0 + ); + const aggregatedScores = aggregateScoresByModule(stats).filter( + (x) => x.total > 0 + ); - const aggregatedLevels = aggregatedScores.map((x) => ({ - module: x.module, - level: calculateBandScore(x.correct, x.total, x.module, focus), - })); + const aggregatedLevels = aggregatedScores.map((x) => ({ + module: x.module, + level: calculateBandScore(x.correct, x.total, x.module, focus), + })); - const timeSpent = stats[0].timeSpent; + const timeSpent = stats[0].timeSpent; - const selectExam = () => { - const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam)); + const selectExam = () => { + const examPromises = uniqBy(stats, "exam").map((stat) => + getExamById(stat.module, stat.exam) + ); - Promise.all(examPromises).then((exams) => { - if (exams.every((x) => !!x)) { - setUserSolutions(convertToUserSolutions(stats)); - setShowSolutions(true); - setExams(exams.map((x) => x!).sort(sortByModule)); - setSelectedModules( - exams - .map((x) => x!) - .sort(sortByModule) - .map((x) => x!.module), - ); - router.push("/exercises"); - } - }); - }; + Promise.all(examPromises).then((exams) => { + if (exams.every((x) => !!x)) { + setUserSolutions(convertToUserSolutions(stats)); + setShowSolutions(true); + setExams(exams.map((x) => x!).sort(sortByModule)); + setSelectedModules( + exams + .map((x) => x!) + .sort(sortByModule) + .map((x) => x!.module) + ); + router.push("/exercises"); + } + }); + }; - const content = ( - <> -
-
- {formatTimestamp(stats[0].date.toString())} - {timeSpent && ( - <> - - {Math.floor(timeSpent / 60)} minutes - - )} -
- = 0.7 && "text-mti-purple", - correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", - correct / total < 0.3 && "text-mti-rose", - )}> - Level{" "} - {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} - -
+ const content = ( + <> +
+
+ + {formatTimestamp(stats[0].date.toString())} + + {timeSpent && ( + <> + + + {Math.floor(timeSpent / 60)} minutes + + + )} +
+ = 0.7 && "text-mti-purple", + correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", + correct / total < 0.3 && "text-mti-rose" + )} + > + Level{" "} + {( + aggregatedLevels.reduce( + (accumulator, current) => accumulator + current.level, + 0 + ) / aggregatedLevels.length + ).toFixed(1)} + +
-
-
- {aggregatedLevels.map(({module, level}) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {level.toFixed(1)} -
- ))} -
-
- - ); +
+
+ {aggregatedLevels.map(({ module, level }) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {level.toFixed(1)} +
+ ))} +
+
+ + ); - return ( -
- - {(() => { - const student = users.find((u) => u.id === user); - return `${student?.name} (${student?.email})`; - })()} - -
= 0.7 && "hover:border-mti-purple", - correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", - correct / total < 0.3 && "hover:border-mti-rose", - )} - onClick={selectExam} - role="button"> - {content} -
-
= 0.7 && "hover:border-mti-purple", - correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", - correct / total < 0.3 && "hover:border-mti-rose", - )} - data-tip="Your screen size is too small to view previous exams." - role="button"> - {content} -
-
- ); - }; + return ( +
+ + {(() => { + const student = users.find((u) => u.id === user); + return `${student?.name} (${student?.email})`; + })()} + +
= 0.7 && "hover:border-mti-purple", + correct / total >= 0.3 && + correct / total < 0.7 && + "hover:border-mti-red", + correct / total < 0.3 && "hover:border-mti-rose" + )} + onClick={selectExam} + role="button" + > + {content} +
+
= 0.7 && "hover:border-mti-purple", + correct / total >= 0.3 && + correct / total < 0.7 && + "hover:border-mti-red", + correct / total < 0.3 && "hover:border-mti-rose" + )} + data-tip="Your screen size is too small to view previous exams." + role="button" + > + {content} +
+
+ ); + }; - return ( - -
- -
-
- Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")} - End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")} -
-
- - Assignees:{" "} - {users - .filter((u) => assignment?.assignees.includes(u.id)) - .map((u) => `${u.name} (${u.email})`) - .join(", ")} - - Assigner: {getUserName(users.find((x) => x.id === assignment?.assigner))} -
-
-
- Average Scores -
- {assignment && - uniqBy(assignment.exams, (x) => x.module).map(({module}) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {calculateAverageModuleScore(module) > -1 && ( - {calculateAverageModuleScore(module).toFixed(1)} - )} -
- ))} -
-
-
- - Results ({assignment?.results.length}/{assignment?.assignees.length}) - -
- {assignment && assignment?.results.length > 0 && ( -
- {assignment.results.map((r) => customContent(r.stats, r.user, r.type))} -
- )} - {assignment && assignment?.results.length === 0 && No results yet...} -
-
+ const shouldRenderStart = () => { + if (assignment) { + if (futureAssignmentFilter(assignment)) { + return true; + } + } -
- {assignment && (assignment.results.length === assignment.assignees.length || moment().isAfter(moment(assignment.endDate))) && ( - - )} - {assignment && (assignment.results.length === 0 || moment().isAfter(moment(assignment.startDate))) && ( - - )} - -
-
-
- ); + return false; + }; + + return ( + +
+ +
+
+ + Start Date:{" "} + {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")} + + + End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")} + +
+
+ + Assignees:{" "} + {users + .filter((u) => assignment?.assignees.includes(u.id)) + .map((u) => `${u.name} (${u.email})`) + .join(", ")} + + + Assigner:{" "} + {getUserName(users.find((x) => x.id === assignment?.assigner))} + +
+
+
+ Average Scores +
+ {assignment && + uniqBy(assignment.exams, (x) => x.module).map(({ module }) => ( +
+ {module === "reading" && } + {module === "listening" && ( + + )} + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {calculateAverageModuleScore(module) > -1 && ( + + {calculateAverageModuleScore(module).toFixed(1)} + + )} +
+ ))} +
+
+
+ + Results ({assignment?.results.length}/{assignment?.assignees.length} + ) + +
+ {assignment && assignment?.results.length > 0 && ( +
+ {assignment.results.map((r) => + customContent(r.stats, r.user, r.type) + )} +
+ )} + {assignment && assignment?.results.length === 0 && ( + No results yet... + )} +
+
+ +
+ {assignment && + (assignment.results.length === assignment.assignees.length || + moment().isAfter(moment(assignment.endDate))) && ( + + )} + {/** if the assignment is not deemed as active yet, display start */} + {shouldRenderStart() && ( + + )} + +
+
+
+ ); } diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index 3a8609d9..354121dc 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -27,6 +27,7 @@ import {useRouter} from "next/router"; import {useEffect, useState} from "react"; import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs"; import {toast} from "react-toastify"; +import { activeAssignmentFilter } from "@/utils/assignments"; interface Props { user: User; @@ -69,6 +70,9 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props) }); }; + const studentAssignments = assignments + .filter(activeAssignmentFilter); + return ( <> {linkedCorporate && ( @@ -119,10 +123,9 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props)
- {assignments.filter((a) => moment(a.endDate).isSameOrAfter(moment())).length === 0 && + {studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."} - {assignments - .filter((a) => moment(a.endDate).isSameOrAfter(moment())) + {studentAssignments .sort((a, b) => moment(a.startDate).diff(b.startDate)) .map((assignment) => (
-