From c49b1c80704b6e4db6a5418ade98ffcfc93cec4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Lima?= Date: Tue, 4 Mar 2025 23:12:26 +0000 Subject: [PATCH] Fix student performance freeze and search users in create entities TODO: pagination in student performance freeze --- package.json | 3 +- .../(admin)/Lists/StudentPerformanceList.tsx | 246 +++++++++++------- src/pages/entities/create.tsx | 2 +- src/pages/users/performance.tsx | 39 ++- src/utils/exams.ts | 22 +- src/utils/score.ts | 35 +-- src/utils/users.be.ts | 11 +- tsconfig.json | 2 +- 8 files changed, 220 insertions(+), 140 deletions(-) diff --git a/package.json b/package.json index 43102b99..34acca90 100644 --- a/package.json +++ b/package.json @@ -114,5 +114,6 @@ "husky": "^8.0.3", "postcss": "^8.4.21", "tailwindcss": "^3.2.4" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/pages/(admin)/Lists/StudentPerformanceList.tsx b/src/pages/(admin)/Lists/StudentPerformanceList.tsx index 59a2a7ea..b9d63edf 100644 --- a/src/pages/(admin)/Lists/StudentPerformanceList.tsx +++ b/src/pages/(admin)/Lists/StudentPerformanceList.tsx @@ -1,110 +1,158 @@ /* eslint-disable @next/next/no-img-element */ -import {Stat, StudentUser, User} from "@/interfaces/user"; -import {useState} from "react"; -import {averageLevelCalculator} from "@/utils/score"; -import {groupByExam} from "@/utils/stats"; -import {createColumnHelper} from "@tanstack/react-table"; +import { Stat, StudentUser, User } from "@/interfaces/user"; +import { useState } from "react"; +import { averageLevelCalculator } from "@/utils/score"; +import { groupByExam } from "@/utils/stats"; +import { createColumnHelper } from "@tanstack/react-table"; import Checkbox from "@/components/Low/Checkbox"; import Table from "@/components/High/Table"; -type StudentPerformanceItem = StudentUser & {entitiesLabel: string; group: string}; +type StudentPerformanceItem = StudentUser & { + entitiesLabel: string; + group: string; + userStats: Stat[]; +}; -const StudentPerformanceList = ({items = [], stats}: {items: StudentPerformanceItem[]; stats: Stat[]}) => { - const [isShowingAmount, setIsShowingAmount] = useState(false); +const StudentPerformanceList = ({ + items = [], +}: { + items: StudentPerformanceItem[]; +}) => { + const [isShowingAmount, setIsShowingAmount] = useState(false); - const columnHelper = createColumnHelper(); + const columnHelper = createColumnHelper(); - const columns = [ - columnHelper.accessor("name", { - header: "Student Name", - cell: (info) => info.getValue(), - }), - columnHelper.accessor("email", { - header: "E-mail", - cell: (info) => info.getValue(), - }), - columnHelper.accessor("studentID", { - header: "ID", - cell: (info) => info.getValue() || "N/A", - }), - columnHelper.accessor("group", { - header: "Group", - cell: (info) => info.getValue(), - }), - columnHelper.accessor("entitiesLabel", { - header: "Entities", - cell: (info) => info.getValue() || "N/A", - }), - columnHelper.accessor("levels.reading", { - header: "Reading", - cell: (info) => - !isShowingAmount - ? info.getValue() || 0 - : `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`, - }), - columnHelper.accessor("levels.listening", { - header: "Listening", - cell: (info) => - !isShowingAmount - ? info.getValue() || 0 - : `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`, - }), - columnHelper.accessor("levels.writing", { - header: "Writing", - cell: (info) => - !isShowingAmount - ? info.getValue() || 0 - : `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`, - }), - columnHelper.accessor("levels.speaking", { - header: "Speaking", - cell: (info) => - !isShowingAmount - ? info.getValue() || 0 - : `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`, - }), - columnHelper.accessor("levels.level", { - header: "Level", - cell: (info) => - !isShowingAmount - ? info.getValue() || 0 - : `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`, - }), - columnHelper.accessor("levels", { - id: "overall_level", - header: "Overall", - cell: (info) => - !isShowingAmount - ? averageLevelCalculator( - items, - stats.filter((x) => x.user === info.row.original.id), - ).toFixed(1) - : `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`, - }), - ]; + const columns = [ + columnHelper.accessor("name", { + header: "Student Name", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("email", { + header: "E-mail", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("studentID", { + header: "ID", + cell: (info) => info.getValue() || "N/A", + }), + columnHelper.accessor("group", { + header: "Group", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("entitiesLabel", { + header: "Entities", + cell: (info) => info.getValue() || "N/A", + }), + columnHelper.accessor("levels.reading", { + header: "Reading", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${ + Object.keys( + groupByExam( + info.row.original.userStats.filter( + (x) => x.module === "reading" + ) + ) + ).length + } exams`, + }), + columnHelper.accessor("levels.listening", { + header: "Listening", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${ + Object.keys( + groupByExam( + info.row.original.userStats.filter( + (x) => x.module === "listening" + ) + ) + ).length + } exams`, + }), + columnHelper.accessor("levels.writing", { + header: "Writing", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${ + Object.keys( + groupByExam( + info.row.original.userStats.filter( + (x) => x.module === "writing" + ) + ) + ).length + } exams`, + }), + columnHelper.accessor("levels.speaking", { + header: "Speaking", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${ + Object.keys( + groupByExam( + info.row.original.userStats.filter( + (x) => x.module === "speaking" + ) + ) + ).length + } exams`, + }), + columnHelper.accessor("levels.level", { + header: "Level", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${ + Object.keys( + groupByExam( + info.row.original.userStats.filter( + (x) => x.module === "level" + ) + ) + ).length + } exams`, + }), + columnHelper.accessor("userStats", { + id: "overall_level", + header: "Overall", + cell: (info) => + !isShowingAmount + ? averageLevelCalculator( + info.row.original.focus, + info.getValue() + ).toFixed(1) + : `${Object.keys(groupByExam(info.getValue())).length} exams`, + }), + ]; - return ( -
- - Show Utilization - - - data={items.sort( - (a, b) => - averageLevelCalculator( - items, - stats.filter((x) => x.user === b.id), - ) - - averageLevelCalculator( - items, - stats.filter((x) => x.user === a.id), - ), - )} - columns={columns} - searchFields={[["name"], ["email"], ["studentID"], ["entitiesLabel"], ["group"]]} - /> -
- ); + return ( +
+ + Show Utilization + + + data={items.sort( + (a, b) => + averageLevelCalculator(b.focus, b.userStats) - + averageLevelCalculator(a.focus, a.userStats) + )} + columns={columns} + searchFields={[ + ["name"], + ["email"], + ["studentID"], + ["entitiesLabel"], + ["group"], + ]} + /> +
+ ); }; export default StudentPerformanceList; diff --git a/src/pages/entities/create.tsx b/src/pages/entities/create.tsx index 606f17ea..58bac7d7 100644 --- a/src/pages/entities/create.tsx +++ b/src/pages/entities/create.tsx @@ -70,7 +70,7 @@ export default function Home({ user, users }: Props) { const [licenses, setLicenses] = useState(0); const { rows, renderSearch } = useListSearch( - [["name"], ["corporateInformation", "companyInformation", "name"]], + [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]], users ); const { items, renderMinimal } = usePagination(rows, 16); diff --git a/src/pages/users/performance.tsx b/src/pages/users/performance.tsx index c8191c63..4267cbeb 100644 --- a/src/pages/users/performance.tsx +++ b/src/pages/users/performance.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import { BsChevronLeft } from "react-icons/bs"; import { mapBy, serialize } from "@/utils"; import { withIronSessionSsr } from "iron-session/next"; -import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { getUsersWithStats } from "@/utils/users.be"; import { sessionOptions } from "@/lib/session"; import { checkAccess, findAllowedEntities } from "@/utils/permissions"; import { getEntitiesWithRoles } from "@/utils/entities.be"; @@ -33,8 +33,32 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { if (allowedEntities.length === 0) return redirect("/"); const students = await (checkAccess(user, ["admin", "developer"]) - ? getUsers({ type: "student" }) - : getEntitiesUsers(mapBy(allowedEntities, "id"), { type: "student" })); + ? getUsersWithStats( + { type: "student" }, + { + id: 1, + entities: 1, + focus: 1, + email: 1, + name: 1, + levels: 1, + userStats: 1, + studentID: 1, + } + ) + : getUsersWithStats( + { type: "student", "entities.id": { in: mapBy(entities, "id") } }, + { + id: 1, + entities: 1, + focus: 1, + email: 1, + name: 1, + levels: 1, + userStats: 1, + studentID: 1, + } + )); const groups = await getParticipantsGroups(mapBy(students, "id")); return { @@ -44,14 +68,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { interface Props { user: User; - students: StudentUser[]; + students: (StudentUser & { userStats: Stat[] })[]; entities: Entity[]; groups: Group[]; } -const StudentPerformance = ({ user, students, entities, groups }: Props) => { - const { data: stats } = useFilterRecordsByUser(); - +const StudentPerformance = ({ students, entities, groups }: Props) => { const router = useRouter(); const performanceStudents = students.map((u) => ({ @@ -76,7 +98,6 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => { - <>
- + ); diff --git a/src/utils/exams.ts b/src/utils/exams.ts index d100762a..b268d8e6 100644 --- a/src/utils/exams.ts +++ b/src/utils/exams.ts @@ -1,4 +1,4 @@ -import {Module} from "@/interfaces"; +import { Module } from "@/interfaces"; import { Exam, ReadingExam, @@ -70,9 +70,9 @@ export const getExamById = async (module: Module, id: string): Promise { if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") - return exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam)); + return (exam.parts.flatMap((x) => x.exercises) ?? []).map((x) => defaultUserSolutions(x, exam)); - return exam.exercises.map((x) => defaultUserSolutions(x, exam)); + return (exam.exercises ?? []).map((x) => defaultUserSolutions(x, exam)); }; export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSolution => { @@ -88,26 +88,26 @@ export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSoluti switch (exercise.type) { case "fillBlanks": total = exercise.text.match(/({{\d+}})/g)?.length || 0; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; case "matchSentences": total = exercise.sentences.length; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; case "multipleChoice": total = exercise.questions.length; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; case "writeBlanks": total = exercise.text.match(/({{\d+}})/g)?.length || 0; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; case "trueFalse": total = exercise.questions.length; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; case "writing": total = 1; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; case "speaking": total = 1; - return {...defaultSettings, score: {correct: 0, total, missing: total}}; + return { ...defaultSettings, score: { correct: 0, total, missing: total } }; default: - return {...defaultSettings, score: {correct: 0, total: 0, missing: 0}}; + return { ...defaultSettings, score: { correct: 0, total: 0, missing: 0 } }; } }; diff --git a/src/utils/score.ts b/src/utils/score.ts index 58ac2be5..460747ec 100644 --- a/src/utils/score.ts +++ b/src/utils/score.ts @@ -1,9 +1,9 @@ -import {Module, Step} from "@/interfaces"; -import {Stat, User} from "@/interfaces/user"; +import { Module, Step } from "@/interfaces"; +import { Stat, User } from "@/interfaces/user"; type Type = "academic" | "general"; -export const writingReverseMarking: {[key: number]: number} = { +export const writingReverseMarking: { [key: number]: number } = { 9: 90, 8.5: 85, 8: 80, @@ -25,7 +25,7 @@ export const writingReverseMarking: {[key: number]: number} = { 0: 0, }; -export const speakingReverseMarking: {[key: number]: number} = { +export const speakingReverseMarking: { [key: number]: number } = { 9: 90, 8.5: 85, 8: 80, @@ -47,7 +47,7 @@ export const speakingReverseMarking: {[key: number]: number} = { 0: 0, }; -export const writingMarking: {[key: number]: number} = { +export const writingMarking: { [key: number]: number } = { 90: 9, 80: 8, 70: 7, @@ -60,7 +60,7 @@ export const writingMarking: {[key: number]: number} = { 0: 0, }; -const readingGeneralMarking: {[key: number]: number} = { +const readingGeneralMarking: { [key: number]: number } = { 100: 9, 97.5: 8.5, 92.5: 8, @@ -77,7 +77,7 @@ const readingGeneralMarking: {[key: number]: number} = { 15: 2.5, }; -const academicMarking: {[key: number]: number} = { +const academicMarking: { [key: number]: number } = { 97.5: 9, 92.5: 8.5, 87.5: 8, @@ -94,7 +94,7 @@ const academicMarking: {[key: number]: number} = { 10: 2.5, }; -const levelMarking: {[key: number]: number} = { +const levelMarking: { [key: number]: number } = { 88: 9, // Advanced 64: 8, // Upper-Intermediate 52: 6, // Intermediate @@ -103,7 +103,7 @@ const levelMarking: {[key: number]: number} = { 0: 0, // Beginner }; -const moduleMarkings: {[key in Module | "overall"]: {[key in Type]: {[key: number]: number}}} = { +const moduleMarkings: { [key in Module | "overall"]: { [key in Type]: { [key: number]: number } } } = { reading: { academic: academicMarking, general: readingGeneralMarking, @@ -147,7 +147,7 @@ export const calculateBandScore = (correct: number, total: number, module: Modul return 0; }; -export const calculateAverageLevel = (levels: {[key in Module]: number}) => { +export const calculateAverageLevel = (levels: { [key in Module]: number }) => { return ( Object.keys(levels) .filter((x) => x !== "level") @@ -193,20 +193,21 @@ export const getGradingLabel = (score: number, grading: Step[]) => { return "N/A"; }; -export const averageLevelCalculator = (users: User[], studentStats: Stat[]) => { - const formattedStats = studentStats +export const averageLevelCalculator = (focus: Type, studentStats: Stat[]) => { + /* const formattedStats = studentStats .map((s) => ({ - focus: users.find((u) => u.id === s.user)?.focus, + focus: focus, score: s.score, module: s.module, })) - .filter((f) => !!f.focus); - const bandScores = formattedStats.map((s) => ({ + .filter((f) => !!f.focus); */ + + const bandScores = studentStats.map((s) => ({ module: s.module, - level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), + level: calculateBandScore(s.score.correct, s.score.total, s.module, focus), })); - const levels: {[key in Module]: number} = { + const levels: { [key in Module]: number } = { reading: 0, listening: 0, writing: 0, diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index 221fd36c..6a8d4e5c 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -6,7 +6,7 @@ import client from "@/lib/mongodb"; import { EntityWithRoles, WithEntities } from "@/interfaces/entity"; import { getEntity } from "./entities.be"; import { getRole } from "./roles.be"; -import { groupAllowedEntitiesByPermissions } from "./permissions"; +import { groupAllowedEntitiesByPermissions } from "./permissions"; import { mapBy } from "."; const db = client.db(process.env.MONGODB_DB); @@ -20,6 +20,15 @@ export async function getUsers(filter?: object, limit = 0, sort = {}, projection .toArray(); } +export async function getUsersWithStats(filter?: object, projection = {}, limit = 0, sort = {}) { + return await db + .collection("usersWithStats") + .find(filter || {}, { projection: { _id: 0, ...projection } }) + .limit(limit) + .sort(sort) + .toArray(); +} + export async function searchUsers(searchInput?: string, limit = 50, page = 0, sort: object = { "name": 1 }, projection = {}, filter?: object) { const compoundFilter = { "compound": { diff --git a/tsconfig.json b/tsconfig.json index e3cc9a30..a35f939e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,7 +36,7 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx" - ], +, "scripts/updatePrivateFieldExams.js" ], "exclude": [ "node_modules" ]