import Button from "@/components/Low/Button"; import ProgressBar from "@/components/Low/ProgressBar"; import { Grading, Module } from "@/interfaces"; import { Assignment } from "@/interfaces/results"; import { Stat, User } from "@/interfaces/user"; import useExamStore from "@/stores/exam"; import { getExamById } from "@/utils/exams"; import { sortByModule } from "@/utils/moduleUtils"; import { calculateBandScore, getGradingLabel } from "@/utils/score"; import { getUserName } from "@/utils/users"; import axios from "axios"; import clsx from "clsx"; import { capitalize, uniqBy } from "lodash"; import moment from "moment"; import { useRouter } from "next/router"; import { BsBook, BsBuilding, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, } from "react-icons/bs"; import { toast } from "react-toastify"; import { futureAssignmentFilter } from "@/utils/assignments"; import { withIronSessionSsr } from "iron-session/next"; import { checkAccess, doesEntityAllow } from "@/utils/permissions"; import { mapBy, redirect, serialize } from "@/utils"; import { getAssignment } from "@/utils/assignments.be"; import { getEntityUsers, getUsers } from "@/utils/users.be"; import { getEntityWithRoles } from "@/utils/entities.be"; import { sessionOptions } from "@/lib/session"; import { EntityWithRoles } from "@/interfaces/entity"; import Head from "next/head"; import Separator from "@/components/Low/Separator"; import Link from "next/link"; import { requestUser } from "@/utils/api"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; import { getGradingSystemByEntity } from "@/utils/grading.be"; export const getServerSideProps = withIronSessionSsr( async ({ req, res, params }) => { const user = await requestUser(req, res); if (!user) return redirect("/login"); if ( !checkAccess(user, [ "admin", "developer", "corporate", "teacher", "mastercorporate", ]) ) return redirect("/assignments"); res.setHeader( "Cache-Control", "public, s-maxage=10, stale-while-revalidate=59" ); const { id } = params as { id: string }; const assignment = await getAssignment(id); if (!assignment) return redirect("/assignments"); const entity = await getEntityWithRoles(assignment.entity || ""); if (!entity) { const users = await getUsers( {}, 0, {}, { _id: 0, id: 1, name: 1, email: 1, } ); return { props: serialize({ user, users, assignment }) }; } if (!doesEntityAllow(user, entity, "view_assignments")) return redirect("/assignments"); const [users, gradingSystem] = await Promise.all([ await (checkAccess(user, ["developer", "admin"]) ? getUsers( {}, 0, {}, { _id: 0, id: 1, name: 1, email: 1, } ) : getEntityUsers( entity.id, 0, {}, { _id: 0, id: 1, name: 1, email: 1, } )), getGradingSystemByEntity(entity.id), ]); return { props: serialize({ user, users, entity, assignment, gradingSystem }), }; }, sessionOptions ); interface Props { user: User; users: User[]; assignment: Assignment; entity?: EntityWithRoles; gradingSystem?: Grading; } export default function AssignmentView({ user, users, entity, assignment, gradingSystem, }: Props) { const canDeleteAssignment = useEntityPermission( user, entity, "delete_assignment" ); const canStartAssignment = useEntityPermission( user, entity, "start_assignment" ); const router = useRouter(); const dispatch = useExamStore((state) => state.dispatch); const deleteAssignment = async () => { if (!canDeleteAssignment) return; 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(() => router.push("/assignments")); }; const startAssignment = () => { if (!canStartAssignment) return; if (!confirm("Are you sure you want to start this assignment?")) return; axios .post(`/api/assignments/${assignment.id}/start`) .then(() => { toast.success( `The assignment "${assignment.name}" has been started successfully!` ); router.replace(router.asPath); }) .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"; return date.format(formatter); }; const calculateAverageModuleScore = (module: Module) => { if (!assignment) return -1; 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); }); 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, }, }; stats .filter((x) => !x.isPractice) .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] })); }; const levelAverage = ( aggregatedLevels: { module: Module; level: number }[] ) => aggregatedLevels.reduce( (accumulator, current) => accumulator + current.level, 0 ) / aggregatedLevels.length; const renderLevelScore = ( stats: Stat[], aggregatedLevels: { module: Module; level: number }[] ) => { const defaultLevelScore = levelAverage(aggregatedLevels).toFixed(1); if (!stats.every((s) => s.module === "level")) return defaultLevelScore; if (!gradingSystem) return defaultLevelScore; const score = { correct: stats.reduce((acc, curr) => acc + curr.score.correct, 0), total: stats.reduce((acc, curr) => acc + curr.score.total, 0), }; const level: number = calculateBandScore( score.correct, score.total, "level", user.focus ); return getGradingLabel(level, gradingSystem.steps); }; 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 timeSpent = stats[0].timeSpent; const selectExam = () => { const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam) ); Promise.all(examPromises).then((exams) => { if (exams.every((x) => !!x)) { dispatch({ type: "INIT_SOLUTIONS", payload: { exams: exams.map((x) => x!).sort(sortByModule), modules: exams .map((x) => x!) .sort(sortByModule) .map((x) => x!.module), stats, }, }); router.push("/exam"); } }); }; 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 {renderLevelScore(stats, aggregatedLevels)}
{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}
); }; const shouldRenderStart = () => { if (assignment) { if (futureAssignmentFilter(assignment)) { return true; } } return false; }; const removeInactiveAssignees = () => { const mappedResults = mapBy(assignment.results, "user"); const inactiveAssignees = assignment.assignees.filter( (a) => !mappedResults.includes(a) ); const activeAssignees = assignment.assignees.filter((a) => mappedResults.includes(a) ); if ( !confirm( `Are you sure you want to remove ${inactiveAssignees.length} assignees?` ) ) return; axios .patch(`/api/assignments/${assignment.id}`, { assignees: activeAssignees, }) .then(() => { toast.success( `The assignment "${assignment.name}" has been updated successfully!` ); router.replace(router.asPath); }) .catch((e) => { console.log(e); toast.error("Something went wrong, please try again later!"); }); }; const copyLink = async () => { const origin = window.location.origin; await navigator.clipboard.writeText( `${origin}/exam?assignment=${assignment.id}` ); toast.success( "The URL to the assignment has been copied to your clipboard!" ); }; return ( <> {assignment.name} | EnCoach <>

{assignment.name}

{!!entity && ( {entity.label} )}
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))}
{assignment.assignees.length !== 0 && assignment.results.length !== assignment.assignees.length && ( )}
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() && ( )}
); }