diff --git a/src/components/High/AssignmentCard.tsx b/src/components/High/AssignmentCard.tsx index e53bb846..0c23ed03 100644 --- a/src/components/High/AssignmentCard.tsx +++ b/src/components/High/AssignmentCard.tsx @@ -1,7 +1,10 @@ import { Session } from "@/hooks/useSessions"; import { Assignment } from "@/interfaces/results"; import { User } from "@/interfaces/user"; -import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments"; +import { + activeAssignmentFilter, + futureAssignmentFilter, +} from "@/utils/assignments"; import { sortByModuleName } from "@/utils/moduleUtils"; import clsx from "clsx"; import moment from "moment"; @@ -11,102 +14,124 @@ import Button from "../Low/Button"; import ModuleBadge from "../ModuleBadge"; interface Props { - assignment: Assignment - user: User - session?: Session - startAssignment: (assignment: Assignment) => void - resumeAssignment: (session: Session) => void + assignment: Assignment; + user: User; + session?: Session; + startAssignment: (assignment: Assignment) => void; + resumeAssignment: (session: Session) => void; } -export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) { - const router = useRouter() +export default function AssignmentCard({ + user, + assignment, + session, + startAssignment, + resumeAssignment, +}: Props) { + const hasBeenSubmitted = useMemo( + () => assignment.results.map((r) => r.user).includes(user.id), + [assignment.results, user.id] + ); - const hasBeenSubmitted = useMemo(() => assignment.results.map((r) => r.user).includes(user.id), [assignment.results, user.id]) - - return ( -
r.user).includes(user.id) && "border-mti-green-light", - )} - key={assignment.id}> -
-

{assignment.name}

- - {moment(assignment.startDate).format("DD/MM/YY, HH:mm")} - - - {moment(assignment.endDate).format("DD/MM/YY, HH:mm")} - -
-
-
- {assignment.exams - .filter((e) => e.assignee === user.id) - .map((e) => e.module) - .sort(sortByModuleName) - .map((module) => ( - - ))} -
- {futureAssignmentFilter(assignment) && !hasBeenSubmitted && ( - - )} - {activeAssignmentFilter(assignment) && !hasBeenSubmitted && ( - <> -
- -
- {!session && ( -
- -
- )} - {!!session && ( -
- -
- )} - - )} - {hasBeenSubmitted && ( - - )} -
-
- ) + return ( +
r.user).includes(user.id) && + "border-mti-green-light" + )} + key={assignment.id} + > +
+

+ {assignment.name} +

+ + {moment(assignment.startDate).format("DD/MM/YY, HH:mm")} + - + {moment(assignment.endDate).format("DD/MM/YY, HH:mm")} + +
+
+
+ {assignment.exams + .filter((e) => e.assignee === user.id) + .map((e) => e.module) + .sort(sortByModuleName) + .map((module) => ( + + ))} +
+ {futureAssignmentFilter(assignment) && !hasBeenSubmitted && ( + + )} + {activeAssignmentFilter(assignment) && !hasBeenSubmitted && ( + <> +
+ +
+ {!session && ( +
+ +
+ )} + {!!session && ( +
+ +
+ )} + + )} + {hasBeenSubmitted && ( + + )} +
+
+ ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f5b32313..6702ac02 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -61,7 +61,14 @@ export default function App({ Component, pageProps }: AppProps) { return pageProps?.user ? ( - {loading ? : } + {loading ? ( + // TODO: Change this later to a better loading screen (example: skeletons for each page) +
+ +
+ ) : ( + + )}
) : ( diff --git a/src/pages/assignments/[id].tsx b/src/pages/assignments/[id].tsx index 6a806362..987ff1cc 100644 --- a/src/pages/assignments/[id].tsx +++ b/src/pages/assignments/[id].tsx @@ -13,15 +13,23 @@ 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 { + 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 { 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"; @@ -31,437 +39,629 @@ 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") +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") + if ( + !checkAccess(user, [ + "admin", + "developer", + "corporate", + "teacher", + "mastercorporate", + ]) + ) + return redirect("/assignments"); - 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 assignment = await getAssignment(id); - if (!assignment) return redirect("/assignments") + const assignment = await getAssignment(id); + if (!assignment) return redirect("/assignments"); - const entity = await getEntityWithRoles(assignment.entity || "") - if (!entity) { - const users = await getUsers() - return { props: serialize({ user, users, assignment }) }; - } + 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") + if (!doesEntityAllow(user, entity, "view_assignments")) + return redirect("/assignments"); - const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntityUsers(entity.id)); - const gradingSystem = await getGradingSystemByEntity(entity.id) + 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); + return { + props: serialize({ user, users, entity, assignment, gradingSystem }), + }; + }, + sessionOptions +); interface Props { - user: User; - users: User[]; - assignment: Assignment; - entity?: EntityWithRoles - gradingSystem?: Grading + 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') +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 router = useRouter(); - const dispatch = useExamStore((state) => state.dispatch); + const dispatch = useExamStore((state) => state.dispatch); - const deleteAssignment = async () => { - if (!canDeleteAssignment) return - if (!confirm("Are you sure you want to delete this assignment?")) return; + 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")); - }; + 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; + 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!"); - }); - }; + 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"; + 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.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, - }; - }); + 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] })); - }; + 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 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 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 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); + const level: number = calculateBandScore( + score.correct, + score.total, + "level", + user.focus + ); - return getGradingLabel(level, gradingSystem.steps) - } + 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 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)) { - 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"); - } - }); - }; + 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)} - -
+ 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)} -
- ))} -
-
- - ); +
+
+ {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} +
+
+ ); + }; - const shouldRenderStart = () => { - if (assignment) { - if (futureAssignmentFilter(assignment)) { - return true; - } - } + const shouldRenderStart = () => { + if (assignment) { + if (futureAssignmentFilter(assignment)) { + return true; + } + } - return false; - }; + 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)) + 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 + 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!"); - }); - } + 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!") - } + 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))} -
-
+ 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 && ( - - )} + {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...} -
-
+
+ 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() && ( - - )} - -
-
- - - ); +
+ + {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/pages/assignments/creator/[id].tsx b/src/pages/assignments/creator/[id].tsx index 5c9d18bb..d97505c8 100644 --- a/src/pages/assignments/creator/[id].tsx +++ b/src/pages/assignments/creator/[id].tsx @@ -18,7 +18,11 @@ import { requestUser } from "@/utils/api"; import { getAssignment } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.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 { getEntitiesUsers, getUsers } from "@/utils/users.be"; import axios from "axios"; @@ -32,563 +36,834 @@ import { useRouter } from "next/router"; import { generate } from "random-words"; import { useEffect, useMemo, useState } from "react"; 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"; -export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") +export const getServerSideProps = withIronSessionSsr( + async ({ req, res, params }) => { + const user = await requestUser(req, res); + 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 entityIDS = mapBy(user.entities, "id") || []; + const { id } = params as { id: string }; + const entityIDS = mapBy(user.entities, "id") || []; + const isAdmin = checkAccess(user, ["developer", "admin"]); - const assignment = await getAssignment(id); - if (!assignment) return redirect("/assignments") + const [assignment, entities] = await Promise.all([ + getAssignment(id), + getEntitiesWithRoles(isAdmin ? undefined : entityIDS), + ]); - const entities = await (checkAccess(user, ["developer", "admin"]) ? getEntitiesWithRoles() : getEntitiesWithRoles(entityIDS)); - const entity = entities.find((e) => e.id === assignment.entity) + if (!assignment) return redirect("/assignments"); + const entity = entities.find((e) => e.id === assignment.entity); - if (!entity) return redirect("/assignments") - if (!doesEntityAllow(user, entity, 'edit_assignment')) return redirect("/assignments") + if (!entity) return redirect("/assignments"); - const allowedEntities = findAllowedEntities(user, entities, 'edit_assignment') + if (!doesEntityAllow(user, entity, "edit_assignment")) + return redirect("/assignments"); - 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 allowedEntities = findAllowedEntities( + user, + entities, + "edit_assignment" + ); - return { props: serialize({ user, users, entities: allowedEntities, assignment, groups }) }; -}, sessionOptions); + const allowEntitiesIDs = mapBy(allowedEntities, "id"); + + const [users, groups] = await Promise.all([ + isAdmin + ? getUsers( + {}, + 0, + {}, + { + _id: 0, + id: 1, + type: 1, + name: 1, + email: 1, + levels: 1, + } + ) + : getEntitiesUsers(allowEntitiesIDs, {}, 0, { + _id: 0, + id: 1, + type: 1, + name: 1, + email: 1, + levels: 1, + }), + isAdmin ? getGroups() : getGroupsByEntities(allowEntitiesIDs), + ]); + + return { + props: serialize({ + user, + users, + entities: allowedEntities, + assignment, + groups, + }), + }; + }, + sessionOptions +); interface Props { - assignment: Assignment; - groups: Group[]; - user: User; - users: User[]; - entities: EntityWithRoles[]; + assignment: Assignment; + groups: Group[]; + user: User; + users: User[]; + entities: EntityWithRoles[]; } const SIZE = 9; -export default function AssignmentsPage({ assignment, user, users, entities, groups }: Props) { - const [selectedModules, setSelectedModules] = useState(assignment.exams.map((e) => e.module)); - const [assignees, setAssignees] = useState(assignment.assignees); - const [teachers, setTeachers] = useState(assignment.teachers || []); - const [entity, setEntity] = useState(assignment.entity || entities[0]?.id); - const [name, setName] = useState(assignment.name); - const [isLoading, setIsLoading] = useState(false); +export default function AssignmentsPage({ + assignment, + user, + users, + entities, + groups, +}: Props) { + const [selectedModules, setSelectedModules] = useState( + assignment.exams.map((e) => e.module) + ); + const [assignees, setAssignees] = useState(assignment.assignees); + const [teachers, setTeachers] = useState(assignment.teachers || []); + const [entity, setEntity] = useState( + assignment.entity || entities[0]?.id + ); + const [name, setName] = useState(assignment.name); + const [isLoading, setIsLoading] = useState(false); - const [startDate, setStartDate] = useState(moment(assignment.startDate).toDate()); - const [endDate, setEndDate] = useState(moment(assignment.endDate).toDate()); + const [startDate, setStartDate] = useState( + moment(assignment.startDate).toDate() + ); + const [endDate, setEndDate] = useState( + moment(assignment.endDate).toDate() + ); - const [variant, setVariant] = useState("full"); - const [instructorGender, setInstructorGender] = useState(assignment?.instructorGender || "varied"); + const [variant, setVariant] = useState("full"); + const [instructorGender, setInstructorGender] = useState( + assignment?.instructorGender || "varied" + ); - const [generateMultiple, setGenerateMultiple] = useState(false); - const [released, setReleased] = useState(assignment.released || false); + const [generateMultiple, setGenerateMultiple] = useState(false); + const [released, setReleased] = useState( + assignment.released || false + ); - const [autoStart, setAutostart] = useState(assignment.autoStart || false); + const [autoStart, setAutostart] = useState( + assignment.autoStart || false + ); - const [useRandomExams, setUseRandomExams] = useState(true); - const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]); + const [useRandomExams, setUseRandomExams] = useState(true); + const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]); - const { exams } = useExams(); - const router = useRouter(); + const { exams } = useExams(); + 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] + ); - const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]); - const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]); + const userStudents = useMemo( + () => users.filter((x) => x.type === "student"), + [users] + ); + const userTeachers = useMemo( + () => users.filter((x) => x.type === "teacher"), + [users] + ); - const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents); - const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers); + const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = + useListSearch([["name"], ["email"]], userStudents); + const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = + useListSearch([["name"], ["email"]], userTeachers); - const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE); - const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE); + const { items: studentRows, renderMinimal: renderStudentPagination } = + usePagination(filteredStudentsRows, SIZE); + const { items: teacherRows, renderMinimal: renderTeacherPagination } = + usePagination(filteredTeachersRows, SIZE); - useEffect(() => { - setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); - }, [selectedModules]); + useEffect(() => { + setExamIDs((prev) => + prev.filter((x) => selectedModules.includes(x.module)) + ); + }, [selectedModules]); - useEffect(() => { - setAssignees([]); - setTeachers([]); - }, [entity]); + useEffect(() => { + setAssignees([]); + setTeachers([]); + }, [entity]); - const toggleModule = (module: Module) => { - const modules = selectedModules.filter((x) => x !== module); - setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module])); - }; + const toggleModule = (module: Module) => { + const modules = selectedModules.filter((x) => x !== module); + setSelectedModules((prev) => + prev.includes(module) ? modules : [...modules, module] + ); + }; - const toggleAssignee = (user: User) => { - setAssignees((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id])); - }; + const toggleAssignee = (user: User) => { + setAssignees((prev) => + prev.includes(user.id) + ? prev.filter((a) => a !== user.id) + : [...prev, user.id] + ); + }; - const toggleTeacher = (user: User) => { - setTeachers((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id])); - }; + const toggleTeacher = (user: User) => { + setTeachers((prev) => + prev.includes(user.id) + ? prev.filter((a) => a !== user.id) + : [...prev, user.id] + ); + }; - const createAssignment = () => { - setIsLoading(true); + const createAssignment = () => { + setIsLoading(true); - (assignment ? axios.patch : axios.post)(`/api/assignments/${assignment.id}`, { - assignees, - name, - startDate, - examIDs: !useRandomExams ? examIDs : undefined, - endDate, - selectedModules, - generateMultiple, - entity, - teachers, - variant, - instructorGender, - released, - autoStart, - }) - .then(() => { - toast.success(`The assignment "${name}" has been updated successfully!`); - router.push(`/assignments/${assignment.id}`); - }) - .catch((e) => { - console.log(e); - toast.error("Something went wrong, please try again later!"); - }) - .finally(() => setIsLoading(false)); - }; + (assignment ? axios.patch : axios.post)( + `/api/assignments/${assignment.id}`, + { + assignees, + name, + startDate, + examIDs: !useRandomExams ? examIDs : undefined, + endDate, + selectedModules, + generateMultiple, + entity, + teachers, + variant, + instructorGender, + released, + autoStart, + } + ) + .then(() => { + toast.success( + `The assignment "${name}" has been updated successfully!` + ); + router.push(`/assignments/${assignment.id}`); + }) + .catch((e) => { + console.log(e); + toast.error("Something went wrong, please try again later!"); + }) + .finally(() => setIsLoading(false)); + }; - const deleteAssignment = () => { - if (!confirm(`Are you sure you want to delete the "${assignment.name}" assignment?`)) return; - console.log("GOT HERE"); - setIsLoading(true); + const deleteAssignment = () => { + if ( + !confirm( + `Are you sure you want to delete the "${assignment.name}" assignment?` + ) + ) + return; + console.log("GOT HERE"); + setIsLoading(true); - axios - .delete(`/api/assignments/${assignment.id}`) - .then(() => { - toast.success(`The assignment "${name}" has been deleted successfully!`); - router.push("/assignments"); - }) - .catch((e) => { - console.log(e); - toast.error("Something went wrong, please try again later!"); - }) - .finally(() => setIsLoading(false)); - }; + axios + .delete(`/api/assignments/${assignment.id}`) + .then(() => { + toast.success( + `The assignment "${name}" has been deleted successfully!` + ); + router.push("/assignments"); + }) + .catch((e) => { + console.log(e); + toast.error("Something went wrong, please try again later!"); + }) + .finally(() => setIsLoading(false)); + }; - const startAssignment = () => { - if (assignment) { - setIsLoading(true); + const startAssignment = () => { + if (assignment) { + setIsLoading(true); - axios - .post(`/api/assignments/${assignment.id}/start`) - .then(() => { - toast.success(`The assignment "${name}" has been started successfully!`); - router.push(`/assignments/${assignment.id}`); - }) - .catch((e) => { - console.log(e); - toast.error("Something went wrong, please try again later!"); - }) - .finally(() => setIsLoading(false)); - } - }; + axios + .post(`/api/assignments/${assignment.id}/start`) + .then(() => { + toast.success( + `The assignment "${name}" has been started successfully!` + ); + router.push(`/assignments/${assignment.id}`); + }) + .catch((e) => { + console.log(e); + toast.error("Something went wrong, please try again later!"); + }) + .finally(() => setIsLoading(false)); + } + }; - 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!") - } + 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 ( - <> - - Edit {assignment.name} | EnCoach - - - - - <> -
-
- - - -

Edit {assignment.name}

-
- -
-
-
-
toggleModule("reading") : undefined} - className={clsx( - "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", - selectedModules.includes("reading") ? "border-mti-purple-light" : "border-mti-gray-platinum", - )}> -
- -
- Reading - {!selectedModules.includes("reading") && !selectedModules.includes("level") && ( -
- )} - {selectedModules.includes("level") && } - {selectedModules.includes("reading") && } -
-
toggleModule("listening") : undefined} - className={clsx( - "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", - selectedModules.includes("listening") ? "border-mti-purple-light" : "border-mti-gray-platinum", - )}> -
- -
- Listening - {!selectedModules.includes("listening") && !selectedModules.includes("level") && ( -
- )} - {selectedModules.includes("level") && } - {selectedModules.includes("listening") && } -
-
toggleModule("level") - : undefined - } - className={clsx( - "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", - selectedModules.includes("level") ? "border-mti-purple-light" : "border-mti-gray-platinum", - )}> -
- -
- Level - {!selectedModules.includes("level") && selectedModules.length === 0 && ( -
- )} - {!selectedModules.includes("level") && selectedModules.length > 0 && } - {selectedModules.includes("level") && } -
-
toggleModule("writing") : undefined} - className={clsx( - "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", - selectedModules.includes("writing") ? "border-mti-purple-light" : "border-mti-gray-platinum", - )}> -
- -
- Writing - {!selectedModules.includes("writing") && !selectedModules.includes("level") && ( -
- )} - {selectedModules.includes("level") && } - {selectedModules.includes("writing") && } -
-
toggleModule("speaking") : undefined} - className={clsx( - "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", - selectedModules.includes("speaking") ? "border-mti-purple-light" : "border-mti-gray-platinum", - )}> -
- -
- Speaking - {!selectedModules.includes("speaking") && !selectedModules.includes("level") && ( -
- )} - {selectedModules.includes("level") && } - {selectedModules.includes("speaking") && } -
-
+ return ( + <> + + Edit {assignment.name} | EnCoach + + + + + <> +
+
+ + + +

Edit {assignment.name}

+
+ +
+
+
+
toggleModule("reading") + : undefined + } + className={clsx( + "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", + selectedModules.includes("reading") + ? "border-mti-purple-light" + : "border-mti-gray-platinum" + )} + > +
+ +
+ Reading + {!selectedModules.includes("reading") && + !selectedModules.includes("level") && ( +
+ )} + {selectedModules.includes("level") && ( + + )} + {selectedModules.includes("reading") && ( + + )} +
+
toggleModule("listening") + : undefined + } + className={clsx( + "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", + selectedModules.includes("listening") + ? "border-mti-purple-light" + : "border-mti-gray-platinum" + )} + > +
+ +
+ Listening + {!selectedModules.includes("listening") && + !selectedModules.includes("level") && ( +
+ )} + {selectedModules.includes("level") && ( + + )} + {selectedModules.includes("listening") && ( + + )} +
+
toggleModule("level") + : undefined + } + className={clsx( + "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", + selectedModules.includes("level") + ? "border-mti-purple-light" + : "border-mti-gray-platinum" + )} + > +
+ +
+ Level + {!selectedModules.includes("level") && + selectedModules.length === 0 && ( +
+ )} + {!selectedModules.includes("level") && + selectedModules.length > 0 && ( + + )} + {selectedModules.includes("level") && ( + + )} +
+
toggleModule("writing") + : undefined + } + className={clsx( + "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", + selectedModules.includes("writing") + ? "border-mti-purple-light" + : "border-mti-gray-platinum" + )} + > +
+ +
+ Writing + {!selectedModules.includes("writing") && + !selectedModules.includes("level") && ( +
+ )} + {selectedModules.includes("level") && ( + + )} + {selectedModules.includes("writing") && ( + + )} +
+
toggleModule("speaking") + : undefined + } + className={clsx( + "w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer", + selectedModules.includes("speaking") + ? "border-mti-purple-light" + : "border-mti-gray-platinum" + )} + > +
+ +
+ Speaking + {!selectedModules.includes("speaking") && + !selectedModules.includes("level") && ( +
+ )} + {selectedModules.includes("level") && ( + + )} + {selectedModules.includes("speaking") && ( + + )} +
+
-
- setName(e)} defaultValue={name} label="Assignment Name" required /> - setName(e)} + defaultValue={name} + label="Assignment Name" + required + /> + (value ? setInstructorGender(value.value as InstructorGender) : null)} - disabled={!selectedModules.includes("speaking") || !!assignment} - options={[ - { value: "male", label: "Male" }, - { value: "female", label: "Female" }, - { value: "varied", label: "Varied" }, - ]} - /> -
- )} + {selectedModules.includes("speaking") && ( +
+ + e.module === module)?.id || null, - label: examIDs.find((e) => e.module === module)?.id || "", - }} - onChange={(value) => - value - ? setExamIDs((prev) => [ - ...prev.filter((x) => x.module !== module), - { id: value.value!, module }, - ]) - : setExamIDs((prev) => prev.filter((x) => x.module !== module)) - } - options={exams - .filter((x) => !x.isDiagnostic && x.module === module) - .map((x) => ({ value: x.id, label: x.id }))} - /> -
- ))} -
- )} -
- )} + {selectedModules.length > 0 && ( +
+ + Random Exams + + {!useRandomExams && ( +
+ {selectedModules.map((module) => ( +
+ + setName(e)} defaultValue={name} label="Assignment Name" required /> - setName(e)} + defaultValue={name} + label="Assignment Name" + required + /> + (value ? setInstructorGender(value.value as InstructorGender) : null)} - disabled={!selectedModules.includes("speaking")} - options={[ - { value: "male", label: "Male" }, - { value: "female", label: "Female" }, - { value: "varied", label: "Varied" }, - ]} - /> -
- )} + {selectedModules.includes("speaking") && ( +
+ + e.module === module)?.id || null, - label: examIDs.find((e) => e.module === module)?.id || "", - }} - onChange={(value) => - value - ? setExamIDs((prev) => [ - ...prev.filter((x) => x.module !== module), - { id: value.value!, module }, - ]) - : setExamIDs((prev) => prev.filter((x) => x.module !== module)) - } - options={exams - .filter((x) => !x.isDiagnostic && x.module === module) - .map((x) => ({ value: x.id, label: x.id }))} - /> -
- ))} -
- )} -
- )} + {selectedModules.length > 0 && ( +
+ + Random Exams + + {!useRandomExams && ( +
+ {selectedModules.map((module) => ( +
+ + -
-
- Entity: - +
+
+ Entity: + + setPaymentPrice(e ? parseInt(e) : undefined) + } + type="number" + defaultValue={entity.payment?.price || 0} + thin + /> + setPaymentPrice(e ? parseInt(e) : undefined)} - type="number" - defaultValue={entity.payment?.price || 0} - thin - /> - -
+ return ( + <> + + Create Entity | EnCoach + + + + + + <> +
+
+
+ + + +

Create Entity

+
+
+ +
+
+ +
+
+ Entity Label: + +
-
- Licenses: - setLicenses(parseInt(v))} type="number" placeholder="12" /> -
-
- -
- Members ({selectedUsers.length} selected): -
-
- {renderSearch()} - {renderMinimal()} -
-
+
+ Licenses: + setLicenses(parseInt(v))} + type="number" + placeholder="12" + /> +
+
+ +
+ + Members ({selectedUsers.length} selected): + +
+
+ {renderSearch()} + {renderMinimal()} +
+ -
- {items.map((u) => ( - - ))} -
- - - ); +
+ + + + + {u.email} + + + + + + {u.subscriptionExpirationDate + ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") + : "Unlimited"} + + + + + + {u.lastLogin + ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") + : "N/A"} + +
+ + ))} + + + + ); } diff --git a/src/pages/entities/index.tsx b/src/pages/entities/index.tsx index d9ef0924..95e18210 100644 --- a/src/pages/entities/index.tsx +++ b/src/pages/entities/index.tsx @@ -35,17 +35,35 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { ); const allowedEntities = findAllowedEntities(user, entities, "view_entities"); - const entitiesWithCount = await Promise.all( - allowedEntities.map(async (e) => ({ - entity: e, - count: await countEntityUsers(e.id, { - type: { $in: ["student", "teacher", "corporate", "mastercorporate"] }, - }), - users: await getEntityUsers(e.id, 5, { - type: { $in: ["student", "teacher", "corporate", "mastercorporate"] }, - }), - })) - ); + const [counts, users] = await Promise.all([ + await Promise.all( + allowedEntities.map(async (e) => + countEntityUsers(e.id, { + type: { $in: ["student", "teacher", "corporate", "mastercorporate"] }, + }) + ) + ), + await Promise.all( + allowedEntities.map(async (e) => + getEntityUsers( + e.id, + 5, + { + type: { + $in: ["student", "teacher", "corporate", "mastercorporate"], + }, + }, + { name: 1 } + ) + ) + ), + ]); + + const entitiesWithCount = allowedEntities.map<{ + entity: EntityWithRoles; + users: User[]; + count: number; + }>((e, i) => ({ entity: e, users: users[i], count: counts[i] })); return { props: serialize({ user, entities: entitiesWithCount }), diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index 99f1e63c..6ddac55c 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -21,93 +21,124 @@ import { getSessionByAssignment } from "@/utils/sessions.be"; import { Session } from "@/hooks/useSessions"; import { activeAssignmentFilter } from "@/utils/assignments"; -export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => { - const user = await requestUser(req, res) - const loginDestination = Buffer.from(req.url || "/").toString("base64") - if (!user) return redirect(`/login?destination=${loginDestination}`) +export const getServerSideProps = withIronSessionSsr( + async ({ req, res, query }) => { + const user = await requestUser(req, res); + const loginDestination = Buffer.from(req.url || "/").toString("base64"); + if (!user) return redirect(`/login?destination=${loginDestination}`); - if (shouldRedirectHome(user)) return redirect("/") + if (shouldRedirectHome(user)) return redirect("/"); - const { assignment: assignmentID, destination } = query as { assignment?: string, destination?: string } - const destinationURL = !!destination ? Buffer.from(destination, 'base64').toString() : undefined + const { assignment: assignmentID, destination } = query as { + assignment?: string; + destination?: string; + }; + const destinationURL = !!destination + ? Buffer.from(destination, "base64").toString() + : undefined; - if (!!assignmentID) { - const assignment = await getAssignment(assignmentID) + if (!!assignmentID) { + const assignment = await getAssignment(assignmentID); - if (!assignment) return redirect(destinationURL || "/exam") - if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type)) - return redirect(destinationURL || "/exam") + if (!assignment) return redirect(destinationURL || "/exam"); + if ( + !assignment.assignees.includes(user.id) && + !["admin", "developer"].includes(user.type) + ) + return redirect(destinationURL || "/exam"); - if (filterBy(assignment.results, 'user', user.id).length > 0) - return redirect(destinationURL || "/exam") + if (filterBy(assignment.results, "user", user.id).length > 0) + return redirect(destinationURL || "/exam"); + + const [exams, session] = await Promise.all([ + getExamsByIds(uniqBy(assignment.exams, "id")), + getSessionByAssignment(assignmentID), + ]); - const exams = await getExamsByIds(uniqBy(assignment.exams, "id")) - const session = await getSessionByAssignment(assignmentID) + return { + props: serialize({ + user, + assignment, + exams, + destinationURL, + session: session ?? undefined, + }), + }; + } - return { - props: serialize({ user, assignment, exams, destinationURL, session: session ?? undefined }) - } - } - - return { - props: serialize({ user, destinationURL }), - }; -}, sessionOptions); + return { + props: serialize({ user, destinationURL }), + }; + }, + sessionOptions +); interface Props { - user: User; - assignment?: Assignment - exams?: Exam[] - session?: Session - destinationURL?: string + user: User; + assignment?: Assignment; + exams?: Exam[]; + session?: Session; + destinationURL?: string; } -const Page: React.FC = ({ user, assignment, exams = [], destinationURL = "/exam", session }) => { - const router = useRouter() - const { assignment: storeAssignment, dispatch } = useExamStore(); +const Page: React.FC = ({ + user, + assignment, + exams = [], + destinationURL = "/exam", + session, +}) => { + const router = useRouter(); + const { assignment: storeAssignment, dispatch } = useExamStore(); - useEffect(() => { - if (assignment && exams.length > 0 && !storeAssignment && !session) { - if (!activeAssignmentFilter(assignment)) return - dispatch({ - type: "INIT_EXAM", payload: { - exams: exams.sort(sortByModule), - modules: exams - .map((x) => x!) - .sort(sortByModule) - .map((x) => x!.module), - assignment - } - }); + useEffect(() => { + if (assignment && exams.length > 0 && !storeAssignment && !session) { + if (!activeAssignmentFilter(assignment)) return; + dispatch({ + type: "INIT_EXAM", + payload: { + exams: exams.sort(sortByModule), + modules: exams + .map((x) => x!) + .sort(sortByModule) + .map((x) => x!.module), + assignment, + }, + }); - router.replace(router.asPath) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [assignment, exams, session]) + router.replace(router.asPath); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assignment, exams, session]); - useEffect(() => { - if (assignment && exams.length > 0 && !storeAssignment && !!session) { - dispatch({ type: "SET_SESSION", payload: { session } }) - router.replace(router.asPath) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [assignment, exams, session]) + useEffect(() => { + if (assignment && exams.length > 0 && !storeAssignment && !!session) { + dispatch({ type: "SET_SESSION", payload: { session } }); + router.replace(router.asPath); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assignment, exams, session]); - return ( - <> - - Exams | EnCoach - - - - - - - ); -} + return ( + <> + + Exams | EnCoach + + + + + + + ); +}; //Page.whyDidYouRender = true; export default Page; diff --git a/src/pages/exercises.tsx b/src/pages/exercises.tsx index b35ce2c2..6925587c 100644 --- a/src/pages/exercises.tsx +++ b/src/pages/exercises.tsx @@ -21,92 +21,100 @@ import { getSessionByAssignment } from "@/utils/sessions.be"; import { Session } from "@/hooks/useSessions"; import moment from "moment"; -export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => { - const user = await requestUser(req, res) - const destination = Buffer.from(req.url || "/").toString("base64") - if (!user) return redirect(`/login?destination=${destination}`) +export const getServerSideProps = withIronSessionSsr( + async ({ req, res, query }) => { + const user = await requestUser(req, res); + const destination = Buffer.from(req.url || "/").toString("base64"); + if (!user) return redirect(`/login?destination=${destination}`); - if (shouldRedirectHome(user)) return redirect("/") + if (shouldRedirectHome(user)) return redirect("/"); - const { assignment: assignmentID } = query as { assignment?: string } + const { assignment: assignmentID } = query as { assignment?: string }; - if (assignmentID) { - const assignment = await getAssignment(assignmentID) + if (assignmentID) { + const assignment = await getAssignment(assignmentID); - if (!assignment) return redirect("/exam") - if (!["admin", "developer"].includes(user.type) && !assignment.assignees.includes(user.id)) return redirect("/exercises") + if (!assignment) return redirect("/exam"); + if ( + !["admin", "developer"].includes(user.type) && + !assignment.assignees.includes(user.id) + ) + return redirect("/exercises"); + if ( + filterBy(assignment.results, "user", user.id) || + moment(assignment.startDate).isBefore(moment()) || + moment(assignment.endDate).isAfter(moment()) + ) + return redirect("/exam"); + const [exams, session] = await Promise.all([ + getExamsByIds(uniqBy(assignment.exams, "id")), + getSessionByAssignment(assignmentID), + ]); - const exams = await getExamsByIds(uniqBy(assignment.exams, "id")) - const session = await getSessionByAssignment(assignmentID) + return { + props: serialize({ user, assignment, exams, session }), + }; + } - if ( - filterBy(assignment.results, 'user', user.id) || - moment(assignment.startDate).isBefore(moment()) || - moment(assignment.endDate).isAfter(moment()) - ) - return redirect("/exam") - - return { - props: serialize({ user, assignment, exams, session }) - } - } - - return { - props: serialize({ user }), - }; -}, sessionOptions); + return { + props: serialize({ user }), + }; + }, + sessionOptions +); interface Props { - user: User; - assignment?: Assignment - exams?: Exam[] - session?: Session + user: User; + assignment?: Assignment; + exams?: Exam[]; + session?: Session; } export default function Page({ user, assignment, exams = [], session }: Props) { - const router = useRouter() + const router = useRouter(); - const { assignment: storeAssignment, dispatch } = useExamStore() + const { assignment: storeAssignment, dispatch } = useExamStore(); - useEffect(() => { - if (assignment && exams.length > 0 && !storeAssignment && !session) { - dispatch({ - type: "INIT_EXAM", payload: { - exams: exams.sort(sortByModule), - modules: exams - .map((x) => x!) - .sort(sortByModule) - .map((x) => x!.module), - assignment - } - }) + useEffect(() => { + if (assignment && exams.length > 0 && !storeAssignment && !session) { + dispatch({ + type: "INIT_EXAM", + payload: { + exams: exams.sort(sortByModule), + modules: exams + .map((x) => x!) + .sort(sortByModule) + .map((x) => x!.module), + assignment, + }, + }); - router.replace(router.asPath) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [assignment, exams, session]) + router.replace(router.asPath); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assignment, exams, session]); - useEffect(() => { - if (assignment && exams.length > 0 && !storeAssignment && !!session) { - dispatch({ type: "SET_SESSION", payload: { session } }); + useEffect(() => { + if (assignment && exams.length > 0 && !storeAssignment && !!session) { + dispatch({ type: "SET_SESSION", payload: { session } }); - router.replace(router.asPath) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [assignment, exams, session]) + router.replace(router.asPath); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assignment, exams, session]); - return ( - <> - - Exams | EnCoach - - - - - - - ); + return ( + <> + + Exams | EnCoach + + + + + + + ); } diff --git a/src/pages/official-exam.tsx b/src/pages/official-exam.tsx index d472f9d9..b7b50638 100644 --- a/src/pages/official-exam.tsx +++ b/src/pages/official-exam.tsx @@ -4,7 +4,7 @@ import Button from "@/components/Low/Button"; import Separator from "@/components/Low/Separator"; import ProfileSummary from "@/components/ProfileSummary"; import { Session } from "@/hooks/useSessions"; -import { Grading } from "@/interfaces"; +import { Grading, Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; import { Exam } from "@/interfaces/exam"; import { InviteWithEntity } from "@/interfaces/invite"; @@ -12,14 +12,13 @@ import { Assignment } from "@/interfaces/results"; import { Stat, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import useExamStore from "@/stores/exam"; -import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils"; +import { findBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { activeAssignmentFilter, futureAssignmentFilter, } from "@/utils/assignments"; import { getAssignmentsByAssignee } from "@/utils/assignments.be"; -import { getEntitiesWithRoles } from "@/utils/entities.be"; import { getExamsByIds } from "@/utils/exams.be"; import { sortByModule } from "@/utils/moduleUtils"; import { checkAccess } from "@/utils/permissions"; @@ -53,32 +52,59 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { if (!checkAccess(user, ["admin", "developer", "student"])) return redirect("/"); + const assignments = (await getAssignmentsByAssignee( + user.id, + { + archived: { $ne: true }, + }, + { + _id: 0, + id: 1, + name: 1, + startDate: 1, + endDate: 1, + exams: 1, + results: 1, + }, + { + sort: { startDate: 1 }, + } + )) as Assignment[]; - const entityIDS = mapBy(user.entities, "id") || []; - - const entities = await getEntitiesWithRoles(entityIDS); - 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") }, + }, + { + _id: 0, + id: 1, + assignment: 1, + } + ); const examIDs = uniqBy( - assignments.flatMap((a) => - filterBy(a.exams, "assignee", user.id).map( - (e: any) => ({ - module: e.module, - id: e.id, - key: `${e.module}_${e.id}`, - }) - ) + assignments.reduce<{ module: Module; id: string; key: string }[]>( + (acc, a) => { + a.exams.forEach((e) => { + if (e.assignee === user.id) + acc.push({ + module: e.module, + id: e.id, + key: `${e.module}_${e.id}`, + }); + }); + return acc; + }, + [] ), "key" ); + const exams = await getExamsByIds(examIDs); - return { props: serialize({ user, entities, assignments, exams, sessions }) }; + return { props: serialize({ user, assignments, exams, sessions }) }; }, sessionOptions); const destination = Buffer.from("/official-exam").toString("base64"); @@ -109,11 +135,12 @@ export default function OfficialExam({ }); if (assignmentExams.every((x) => !!x)) { + const sortedAssignmentExams = assignmentExams.sort(sortByModule); dispatch({ type: "INIT_EXAM", payload: { - exams: assignmentExams.sort(sortByModule), - modules: mapBy(assignmentExams.sort(sortByModule), "module"), + exams: sortedAssignmentExams, + modules: mapBy(sortedAssignmentExams, "module"), assignment, }, }); @@ -144,12 +171,16 @@ export default function OfficialExam({ [assignments] ); - const assignmentSessions = useMemo( - () => - sessions.filter((s) => - mapBy(studentAssignments, "id").includes(s.assignment?.id || "") - ), - [sessions, studentAssignments] + const assignmentSessions = useMemo(() => { + const studentAssignmentsIDs = mapBy(studentAssignments, "id"); + return sessions.filter((s) => + studentAssignmentsIDs.includes(s.assignment?.id || "") + ); + }, [sessions, studentAssignments]); + + const entityLabels = useMemo( + () => mapBy(entities, "label")?.join(","), + [entities] ); return ( @@ -167,7 +198,7 @@ export default function OfficialExam({ <> {entities.length > 0 && (
- {mapBy(entities, "label")?.join(", ")} + {entityLabels}
)} @@ -191,20 +222,18 @@ export default function OfficialExam({ {studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."} - {studentAssignments - .sort((a, b) => moment(a.startDate).diff(b.startDate)) - .map((a) => ( - s.assignment?.id === a.id - )} - startAssignment={startAssignment} - resumeAssignment={loadSession} - /> - ))} + {studentAssignments.map((a) => ( + s.assignment?.id === a.id + )} + startAssignment={startAssignment} + resumeAssignment={loadSession} + /> + ))} diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 618d9621..edc8ed1b 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -31,7 +31,7 @@ import { useListSearch } from "@/hooks/useListSearch"; import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions"; import { requestUser } from "@/utils/api"; import { mapBy, redirect, serialize } from "@/utils"; -import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; import { isAdmin } from "@/utils/users"; import { Entity, EntityWithRoles } from "@/interfaces/entity"; diff --git a/src/pages/payment.tsx b/src/pages/payment.tsx index 7c253eed..0585aba4 100644 --- a/src/pages/payment.tsx +++ b/src/pages/payment.tsx @@ -21,11 +21,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { if (!user) return redirect("/login") const entityIDs = mapBy(user.entities, 'id') - const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs) - const domain = user.email.split("@").pop() - const discounts = await db.collection("discounts").find({ domain }).toArray() - const packages = await db.collection("packages").find().toArray() + const [entities, discounts, packages] = await Promise.all([ + getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs), + db.collection("discounts").find({ domain }).toArray(), + db.collection("packages").find().toArray(), + ]) return { props: serialize({ user, entities, discounts, packages }), diff --git a/src/pages/permissions/[id].tsx b/src/pages/permissions/[id].tsx index 6e79b2d3..723cee7b 100644 --- a/src/pages/permissions/[id].tsx +++ b/src/pages/permissions/[id].tsx @@ -7,7 +7,7 @@ import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { Permission, PermissionType } from "@/interfaces/permissions"; import { getPermissionDoc } from "@/utils/permissions.be"; import { User } from "@/interfaces/user"; -import { LayoutContext } from "@/components/High/Layout"; +import { LayoutContext } from "@/components/High/Layout"; import { getUsers } from "@/utils/users.be"; import { BsTrash } from "react-icons/bs"; import Select from "@/components/Low/Select"; @@ -18,6 +18,7 @@ import { Type as UserType } from "@/interfaces/user"; import { getGroups } from "@/utils/groups.be"; import { requestUser } from "@/utils/api"; import { redirect } from "@/utils"; +import { G } from "@react-pdf/renderer"; interface BasicUser { id: string; name: string; @@ -40,31 +41,25 @@ export const getServerSideProps = withIronSessionSsr( if (!params?.id) return redirect("/permissions"); // Fetch data from external API - const permission: Permission = await getPermissionDoc(params.id as string); - - const allUserData: User[] = await getUsers(); - const groups = await getGroups(); + const [permission, users, groups] = await Promise.all([ + getPermissionDoc(params.id as string), + getUsers({}, 0, {}, { _id: 0, id: 1, name: 1, type: 1 }), + getGroups(), + ]); const userGroups = groups.filter((x) => x.admin === user.id); + const userGroupsParticipants = userGroups.flatMap((x) => x.participants); const filteredGroups = user.type === "corporate" ? userGroups : user.type === "mastercorporate" - ? groups.filter((x) => - userGroups.flatMap((y) => y.participants).includes(x.admin) - ) + ? groups.filter((x) => userGroupsParticipants.includes(x.admin)) : groups; - - const users = allUserData.map((u) => ({ - id: u.id, - name: u.name, - type: u.type, - })) as BasicUser[]; - + const filteredGroupsParticipants = filteredGroups.flatMap( + (g) => g.participants + ); const filteredUsers = ["mastercorporate", "corporate"].includes(user.type) - ? users.filter((u) => - filteredGroups.flatMap((g) => g.participants).includes(u.id) - ) + ? users.filter((u) => filteredGroupsParticipants.includes(u.id)) : users; // const res = await fetch("api/permissions"); @@ -158,12 +153,14 @@ export default function Page(props: Props) {