diff --git a/src/dashboards/Corporate/index.tsx b/src/dashboards/Corporate/index.tsx index f353d055..e16028b3 100644 --- a/src/dashboards/Corporate/index.tsx +++ b/src/dashboards/Corporate/index.tsx @@ -1,546 +1,405 @@ /* eslint-disable @next/next/no-img-element */ import Modal from "@/components/Modal"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import useUsers, { - userHashStudent, - userHashTeacher, - userHashCorporate, -} from "@/hooks/useUsers"; -import { - CorporateUser, - Group, - MasterCorporateUser, - Stat, - User, -} from "@/interfaces/user"; +import useUsers, {userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers"; +import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; -import { dateSorter } from "@/utils"; +import {dateSorter} from "@/utils"; import moment from "moment"; -import { useEffect, useMemo, useState } from "react"; +import {useEffect, useMemo, useState} from "react"; import { - BsArrowLeft, - BsClipboard2Data, - BsClipboard2DataFill, - BsClock, - BsGlobeCentralSouthAsia, - BsPaperclip, - BsPerson, - BsPersonAdd, - BsPersonFill, - BsPersonFillGear, - BsPersonGear, - BsPencilSquare, - BsPersonBadge, - BsPersonCheck, - BsPeople, - BsArrowRepeat, - BsPlus, - BsEnvelopePaper, - BsDatabase, + BsArrowLeft, + BsClipboard2Data, + BsClipboard2DataFill, + BsClock, + BsGlobeCentralSouthAsia, + BsPaperclip, + BsPerson, + BsPersonAdd, + BsPersonFill, + BsPersonFillGear, + BsPersonGear, + BsPencilSquare, + BsPersonBadge, + BsPersonCheck, + BsPeople, + BsArrowRepeat, + BsPlus, + BsEnvelopePaper, + BsDatabase, } from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; -import { - averageLevelCalculator, - calculateAverageLevel, - calculateBandScore, -} from "@/utils/score"; -import { MODULE_ARRAY } from "@/utils/moduleUtils"; -import { Module } from "@/interfaces"; -import { groupByExam } from "@/utils/stats"; +import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score"; +import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import {Module} from "@/interfaces"; +import {groupByExam} from "@/utils/stats"; import IconCard from "../IconCard"; import GroupList from "@/pages/(admin)/Lists/GroupList"; import useFilterStore from "@/stores/listFilterStore"; -import { useRouter } from "next/router"; +import {useRouter} from "next/router"; import useCodes from "@/hooks/useCodes"; -import { getUserCorporate } from "@/utils/groups"; +import {getUserCorporate} from "@/utils/groups"; import useAssignments from "@/hooks/useAssignments"; -import { Assignment } from "@/interfaces/results"; +import {Assignment} from "@/interfaces/results"; import AssignmentView from "../AssignmentView"; import AssignmentCreator from "../AssignmentCreator"; import clsx from "clsx"; import AssignmentCard from "../AssignmentCard"; -import { createColumnHelper } from "@tanstack/react-table"; +import {createColumnHelper} from "@tanstack/react-table"; import Checkbox from "@/components/Low/Checkbox"; import List from "@/components/List"; -import { getUserCompanyName } from "@/resources/user"; -import { - futureAssignmentFilter, - pastAssignmentFilter, - archivedAssignmentFilter, - activeAssignmentFilter, -} from "@/utils/assignments"; +import {getUserCompanyName} from "@/resources/user"; +import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments"; import useUserBalance from "@/hooks/useUserBalance"; import AssignmentsPage from "../views/AssignmentsPage"; import StudentPerformancePage from "./StudentPerformancePage"; import MasterStatistical from "../MasterCorporate/MasterStatistical"; +import MasterStatisticalPage from "./MasterStatisticalPage"; interface Props { - user: CorporateUser; - linkedCorporate?: CorporateUser | MasterCorporateUser; + user: CorporateUser; + linkedCorporate?: CorporateUser | MasterCorporateUser; } const studentHash = { - type: "student", - orderBy: "registrationDate", - size: 25, + type: "student", + orderBy: "registrationDate", + size: 25, }; const teacherHash = { - type: "teacher", - orderBy: "registrationDate", - size: 25, + type: "teacher", + orderBy: "registrationDate", + size: 25, }; -export default function CorporateDashboard({ user, linkedCorporate }: Props) { - const [selectedUser, setSelectedUser] = useState(); - const [showModal, setShowModal] = useState(false); +export default function CorporateDashboard({user, linkedCorporate}: Props) { + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); - const { data: stats } = useFilterRecordsByUser(); - const { groups } = useGroups({ admin: user.id }); - const { - assignments, - isLoading: isAssignmentsLoading, - reload: reloadAssignments, - } = useAssignments({ corporate: user.id }); - const { balance } = useUserBalance(); + const {data: stats} = useFilterRecordsByUser(); + const {groups} = useGroups({admin: user.id}); + const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); + const {balance} = useUserBalance(); - const { - users: students, - total: totalStudents, - reload: reloadStudents, - isLoading: isStudentsLoading, - } = useUsers(studentHash); - const { - users: teachers, - total: totalTeachers, - reload: reloadTeachers, - isLoading: isTeachersLoading, - } = useUsers(teacherHash); + const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash); + const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash); - const appendUserFilters = useFilterStore((state) => state.appendUserFilter); - const router = useRouter(); + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); + const router = useRouter(); - const assignmentsGroups = useMemo( - () => - groups.filter( - (x) => x.admin === user.id || x.participants.includes(user.id) - ), - [groups, user.id] - ); + const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]); - const assignmentsUsers = useMemo( - () => - [...teachers, ...students].filter((x) => - !!selectedUser - ? groups - .filter((g) => g.admin === selectedUser.id) - .flatMap((g) => g.participants) - .includes(x.id) || false - : groups.flatMap((g) => g.participants).includes(x.id) - ), - [groups, teachers, students, selectedUser] - ); + const assignmentsUsers = useMemo( + () => + [...teachers, ...students].filter((x) => + !!selectedUser + ? groups + .filter((g) => g.admin === selectedUser.id) + .flatMap((g) => g.participants) + .includes(x.id) || false + : groups.flatMap((g) => g.participants).includes(x.id), + ), + [groups, teachers, students, selectedUser], + ); - useEffect(() => { - setShowModal(!!selectedUser && router.asPath === "/#"); - }, [selectedUser, router.asPath]); + useEffect(() => { + setShowModal(!!selectedUser && router.asPath === "/#"); + }, [selectedUser, router.asPath]); - const getStatsByStudent = (user: User) => - stats.filter((s) => s.user === user.id); + const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); - const UserDisplay = (displayUser: User) => ( -
setSelectedUser(displayUser)} - className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" - > - {displayUser.name} -
- {displayUser.name} - {displayUser.email} -
-
- ); + const UserDisplay = (displayUser: User) => ( +
setSelectedUser(displayUser)} + className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> + {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); - // this workaround will allow us toreuse the master statistical due to master corporate restraints - // while still being able to use the corporate user - const groupedByNameCorporateIds = useMemo( - () => ({ - [user.corporateInformation?.companyInformation?.name || user.name]: [ - user.id, - ], - }), - [user] - ); - const teachersAndStudents = useMemo( - () => [...students, ...teachers], - [students, teachers] - ); - const MasterStatisticalPage = () => { - return ( - <> -
-
router.push("/")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Master Statistical

-
- - - ); - }; + const GroupsList = () => { + const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id); - const GroupsList = () => { - const filter = (x: Group) => - x.admin === user.id || x.participants.includes(user.id); + return ( + <> +
+
router.push("/")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Groups ({groups.filter(filter).length})

+
- return ( - <> -
-
router.push("/")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Groups ({groups.filter(filter).length}) -

-
+ + + ); + }; - - - ); - }; + const averageLevelCalculator = (studentStats: Stat[]) => { + const formattedStats = studentStats + .map((s) => ({ + focus: students.find((u) => u.id === s.user)?.focus, + score: s.score, + module: s.module, + })) + .filter((f) => !!f.focus); + const bandScores = formattedStats.map((s) => ({ + module: s.module, + level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), + })); - const averageLevelCalculator = (studentStats: Stat[]) => { - const formattedStats = studentStats - .map((s) => ({ - focus: students.find((u) => u.id === s.user)?.focus, - score: s.score, - module: s.module, - })) - .filter((f) => !!f.focus); - const bandScores = formattedStats.map((s) => ({ - module: s.module, - level: calculateBandScore( - s.score.correct, - s.score.total, - s.module, - s.focus! - ), - })); + const levels: {[key in Module]: number} = { + reading: 0, + listening: 0, + writing: 0, + speaking: 0, + level: 0, + }; + bandScores.forEach((b) => (levels[b.module] += b.level)); - const levels: { [key in Module]: number } = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, - level: 0, - }; - bandScores.forEach((b) => (levels[b.module] += b.level)); + return calculateAverageLevel(levels); + }; - return calculateAverageLevel(levels); - }; + if (router.asPath === "/#students") + return ( + ( +
+
router.push("/")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Students ({total})

+
+ )} + /> + ); - if (router.asPath === "/#students") - return ( - ( -
-
router.push("/")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Students ({total})

-
- )} - /> - ); + if (router.asPath === "/#teachers") + return ( + ( +
+
router.push("/")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Teachers ({total})

+
+ )} + /> + ); - if (router.asPath === "/#teachers") - return ( - ( -
-
router.push("/")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Teachers ({total})

-
- )} - /> - ); + if (router.asPath === "/#groups") return ; + if (router.asPath === "/#studentsPerformance") return ; - if (router.asPath === "/#groups") return ; - if (router.asPath === "/#studentsPerformance") - return ; + if (router.asPath === "/#assignments") + return ( + router.push("/")} + /> + ); - if (router.asPath === "/#assignments") - return ( - router.push("/")} - /> - ); + if (router.asPath === "/#statistical") return ; - if (router.asPath === "/#statistical") return ; + return ( + <> + setSelectedUser(undefined)}> + <> + {selectedUser && ( +
+ { + setSelectedUser(undefined); + if (shouldReload && selectedUser!.type === "student") reloadStudents(); + if (shouldReload && selectedUser!.type === "teacher") reloadTeachers(); + }} + onViewStudents={ + selectedUser.type === "corporate" || selectedUser.type === "teacher" + ? () => { + appendUserFilters({ + id: "view-students", + filter: (x: User) => x.type === "student", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) + .flatMap((g) => g.participants) + .includes(x.id), + }); - return ( - <> - setSelectedUser(undefined)}> - <> - {selectedUser && ( -
- { - setSelectedUser(undefined); - if (shouldReload && selectedUser!.type === "student") - reloadStudents(); - if (shouldReload && selectedUser!.type === "teacher") - reloadTeachers(); - }} - onViewStudents={ - selectedUser.type === "corporate" || - selectedUser.type === "teacher" - ? () => { - appendUserFilters({ - id: "view-students", - filter: (x: User) => x.type === "student", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter( - (g) => - g.admin === selectedUser.id || - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => g.participants) - .includes(x.id), - }); + router.push("/list/users"); + } + : undefined + } + onViewTeachers={ + selectedUser.type === "corporate" || selectedUser.type === "student" + ? () => { + appendUserFilters({ + id: "view-teachers", + filter: (x: User) => x.type === "teacher", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) + .flatMap((g) => g.participants) + .includes(x.id), + }); - router.push("/list/users"); - } - : undefined - } - onViewTeachers={ - selectedUser.type === "corporate" || - selectedUser.type === "student" - ? () => { - appendUserFilters({ - id: "view-teachers", - filter: (x: User) => x.type === "teacher", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter( - (g) => - g.admin === selectedUser.id || - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => g.participants) - .includes(x.id), - }); + router.push("/list/users"); + } + : undefined + } + user={selectedUser} + /> +
+ )} + +
- router.push("/list/users"); - } - : undefined - } - user={selectedUser} - /> -
- )} - -
+ <> + {!!linkedCorporate && ( +
+ Linked to: {linkedCorporate?.corporateInformation?.companyInformation.name || linkedCorporate.name} +
+ )} +
+ router.push("/#students")} + isLoading={isStudentsLoading} + Icon={BsPersonFill} + label="Students" + value={totalStudents} + color="purple" + /> + router.push("/#teachers")} + isLoading={isTeachersLoading} + Icon={BsPencilSquare} + label="Teachers" + value={totalTeachers} + color="purple" + /> + groups.flatMap((g) => g.participants).includes(s.user)).length} + color="purple" + /> + groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} + color="purple" + /> + router.push("/#groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" /> + + + router.push("/#studentsPerformance")} + /> + router.push("/#statistical")} /> + +
- <> - {!!linkedCorporate && ( -
- Linked to:{" "} - - {linkedCorporate?.corporateInformation?.companyInformation.name || - linkedCorporate.name} - -
- )} -
- router.push("/#students")} - isLoading={isStudentsLoading} - Icon={BsPersonFill} - label="Students" - value={totalStudents} - color="purple" - /> - router.push("/#teachers")} - isLoading={isTeachersLoading} - Icon={BsPencilSquare} - label="Teachers" - value={totalTeachers} - color="purple" - /> - - groups.flatMap((g) => g.participants).includes(s.user) - ).length - } - color="purple" - /> - - groups.flatMap((g) => g.participants).includes(s.user) - ) - ).toFixed(1)} - color="purple" - /> - router.push("/#groups")} - Icon={BsPeople} - label="Groups" - value={groups.length} - color="purple" - /> - - - router.push("/#studentsPerformance")} - /> - router.push("/#statistical")} - /> - -
- -
-
- Latest students -
- {students - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Latest teachers -
- {teachers - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Highest level students -
- {students - .sort( - (a, b) => - calculateAverageLevel(b.levels) - - calculateAverageLevel(a.levels) - ) - .map((x) => ( - - ))} -
-
-
- Highest exam count students -
- {students - .sort( - (a, b) => - Object.keys(groupByExam(getStatsByStudent(b))).length - - Object.keys(groupByExam(getStatsByStudent(a))).length - ) - .map((x) => ( - - ))} -
-
-
- - - ); +
+
+ Latest students +
+ {students + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {teachers + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {students + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {students + .sort( + (a, b) => + Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length, + ) + .map((x) => ( + + ))} +
+
+
+ + + ); } diff --git a/src/dashboards/MasterCorporate/MasterStatistical.tsx b/src/dashboards/MasterCorporate/MasterStatistical.tsx index ec205d6a..28e8e674 100644 --- a/src/dashboards/MasterCorporate/MasterStatistical.tsx +++ b/src/dashboards/MasterCorporate/MasterStatistical.tsx @@ -74,10 +74,8 @@ const MasterStatistical = (props: Props) => { () => assignments.reduce((accmA: TableData[], a: AssignmentWithCorporateId) => { const userResults = a.assignees.map((assignee) => { - const userStats = a.results.find((r) => r.user === assignee)?.stats || []; const userData = users.find((u) => u.id === assignee); - if (!!userData) console.log(assignee, userData.name); - + const userStats = a.results.find((r) => r.user === assignee)?.stats || []; const corporate = getUserName(users.find((u) => u.id === a.assigner)); const commonData = { user: userData, @@ -88,6 +86,7 @@ const MasterStatistical = (props: Props) => { corporate, assignment: a.name, }; + if (userStats.length === 0) { return { ...commonData, @@ -110,8 +109,6 @@ const MasterStatistical = (props: Props) => { [assignments, users], ); - useEffect(() => console.log(assignments), [assignments]); - const getCorporateScores = (corporateId: string): UserCount => { const corporateAssignmentsUsers = assignments.filter((a) => a.corporateId === corporateId).reduce((acc, a) => acc + a.assignees.length, 0); @@ -167,7 +164,7 @@ const MasterStatistical = (props: Props) => { header: "Student ID", id: "studentID", cell: (info) => { - return {(info.getValue() as StudentUser).studentID || "N/A"}; + return {(info.getValue() as StudentUser)?.studentID || "N/A"}; }, }), ...(displaySelection diff --git a/src/dashboards/views/AssignmentsPage.tsx b/src/dashboards/views/AssignmentsPage.tsx index 07513fb2..805f364d 100644 --- a/src/dashboards/views/AssignmentsPage.tsx +++ b/src/dashboards/views/AssignmentsPage.tsx @@ -55,7 +55,7 @@ export default function AssignmentsPage({assignments, corporateAssignments, user { diff --git a/src/hooks/useListSearch.tsx b/src/hooks/useListSearch.tsx index 4f2a23ac..512a644a 100644 --- a/src/hooks/useListSearch.tsx +++ b/src/hooks/useListSearch.tsx @@ -1,6 +1,6 @@ import {useState, useMemo} from "react"; import Input from "@/components/Low/Input"; -import { search } from "@/utils/search"; +import {search} from "@/utils/search"; export function useListSearch(fields: string[][], rows: T[]) { const [text, setText] = useState(""); @@ -8,7 +8,8 @@ export function useListSearch(fields: string[][], rows: T[]) { const renderSearch = () => ; const updatedRows = useMemo(() => { - return search(text, fields, rows); + if (text.length > 0) return search(text, fields, rows); + return rows; }, [fields, rows, text]); return { diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx new file mode 100644 index 00000000..490c6c9e --- /dev/null +++ b/src/hooks/usePagination.tsx @@ -0,0 +1,28 @@ +import Button from "@/components/Low/Button"; +import {useMemo, useState} from "react"; + +export default function usePagination(list: T[], size = 25) { + const [page, setPage] = useState(0); + + const items = useMemo(() => list.slice(page * size, (page + 1) * size), [page, size, list]); + + const render = () => ( +
+
+ +
+
+ + {page * size + 1} - {(page + 1) * size > list.length ? list.length : (page + 1) * size} / {list.length} + + +
+
+ ); + + return {page, items, setPage, render}; +} diff --git a/src/hooks/useUsers.tsx b/src/hooks/useUsers.tsx index 7b860637..982eb9f7 100644 --- a/src/hooks/useUsers.tsx +++ b/src/hooks/useUsers.tsx @@ -23,8 +23,6 @@ export default function useUsers(props?: {type?: string; page?: number; size?: n if (props[key as keyof typeof props] !== undefined) params.append(key, props[key as keyof typeof props]!.toString()); }); - console.log(params.toString()); - setIsLoading(true); axios .get<{users: User[]; total: number}>(`/api/users/list?${params.toString()}`, {headers: {page: "register"}}) diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 335e57ba..619630fa 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -28,6 +28,7 @@ import {checkAccess} from "@/utils/permissions"; import {PermissionType} from "@/interfaces/permissions"; import usePermissions from "@/hooks/usePermissions"; import useUserBalance from "@/hooks/useUserBalance"; +import usePagination from "@/hooks/usePagination"; const columnHelper = createColumnHelper(); const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; @@ -62,15 +63,12 @@ export default function UserList({ const [sorter, setSorter] = useState(); const [displayUsers, setDisplayUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(); - const [page, setPage] = useState(0); const userHash = useMemo( () => ({ type, - size: 16, - page, }), - [type, page], + [type], ); const {users, total, isLoading, reload} = useUsers(userHash); @@ -102,9 +100,9 @@ export default function UserList({ (async () => { if (users && users.length > 0) { const filteredUsers = filters.reduce((d, f) => d.filter(f), users); - const sortedUsers = await asyncSorter(filteredUsers, sortFunction); + // const sortedUsers = await asyncSorter(filteredUsers, sortFunction); - setDisplayUsers([...sortedUsers]); + setDisplayUsers([...filteredUsers]); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -513,10 +511,14 @@ export default function UserList({ return a.id.localeCompare(b.id); }; - const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); + const {rows: filteredRows, renderSearch, text: searchText} = useListSearch(searchFields, displayUsers); + const {items, setPage, render: renderPagination} = usePagination(filteredRows, 16); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => setPage(0), [searchText]); const table = useReactTable({ - data: filteredRows, + data: items, columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any, getCoreRowModel: getCoreRowModel(), }); @@ -632,27 +634,7 @@ export default function UserList({ Download List -
- -
- - {page * userHash.size + 1} - {(page + 1) * userHash.size > total ? total : (page + 1) * userHash.size} / {total} - - -
-
+ {renderPagination()} {table.getHeaderGroups().map((headerGroup) => ( diff --git a/src/pages/api/assignments/corporate/[id].ts b/src/pages/api/assignments/corporate/[id].ts index 728273d8..242690b8 100644 --- a/src/pages/api/assignments/corporate/[id].ts +++ b/src/pages/api/assignments/corporate/[id].ts @@ -22,7 +22,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { async function GET(req: NextApiRequest, res: NextApiResponse) { const {id} = req.query as {id: string}; - const assigners = await getAllAssignersByCorporate(id); + const assigners = await getAllAssignersByCorporate(id, req.session.user!.type); const assignments = await getAssignmentsByAssigners([...assigners, id]); res.status(200).json(uniqBy(assignments, "id")); diff --git a/src/pages/api/assignments/corporate/index.ts b/src/pages/api/assignments/corporate/index.ts index e2557fe3..2b543250 100644 --- a/src/pages/api/assignments/corporate/index.ts +++ b/src/pages/api/assignments/corporate/index.ts @@ -29,7 +29,7 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { try { const idsList = ids.split(","); - const assignments = await getAssignmentsForCorporates(idsList, startDateParsed, endDateParsed); + const assignments = await getAssignmentsForCorporates(req.session.user!.type, idsList, startDateParsed, endDateParsed); res.status(200).json(assignments); } catch (err: any) { res.status(500).json({error: err.message}); diff --git a/src/pages/api/assignments/statistical/excel.ts b/src/pages/api/assignments/statistical/excel.ts index da5befe0..b0000814 100644 --- a/src/pages/api/assignments/statistical/excel.ts +++ b/src/pages/api/assignments/statistical/excel.ts @@ -14,12 +14,18 @@ import {getGradingSystem} from "@/utils/grading.be"; import {StudentUser, User} from "@/interfaces/user"; import {calculateBandScore, getGradingLabel} from "@/utils/score"; import {Module} from "@/interfaces"; +import {uniq} from "lodash"; +import {getUserName} from "@/utils/users"; +import {LevelExam} from "@/interfaces/exam"; +import {getSpecificExams} from "@/utils/exams.be"; export default withIronSessionApiRoute(handler, sessionOptions); interface TableData { user: string; studentID: string; + passportID: string; + exams: string; email: string; correct: number; corporate: string; @@ -41,7 +47,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") return await post(req, res); } -const searchFilters = [["email"], ["user"], ["userId"]]; +const searchFilters = [["email"], ["user"], ["userId"], ["assignment"], ["exams"]]; async function post(req: NextApiRequest, res: NextApiResponse) { // verify if it's a logged user that is trying to export @@ -64,12 +70,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }; const startDateParsed = startDate ? new Date(startDate) : undefined; const endDateParsed = endDate ? new Date(endDate) : undefined; - const assignments = await getAssignmentsForCorporates(ids, startDateParsed, endDateParsed); + const assignments = await getAssignmentsForCorporates(req.session.user.type, ids, startDateParsed, endDateParsed); - const assignmentUsers = [...new Set(assignments.flatMap((a) => a.assignees))]; + const assignmentUsers = uniq([...assignments.flatMap((x) => x.assignees), ...assignments.flatMap((x) => x.assigner)]); const assigners = [...new Set(assignments.map((a) => a.assigner))]; const users = await getSpecificUsers(assignmentUsers); const assignerUsers = await getSpecificUsers(assigners); + const exams = await getSpecificExams(uniq(assignments.flatMap((x) => x.exams.map((x) => x.id)))); const assignerUsersGradingSystems = await Promise.all( assignerUsers.map(async (user: User) => { @@ -91,7 +98,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { if (gradingSystem) { const bandScore = calculateBandScore(correct, total, "level", user?.focus || "academic"); return { - label: getGradingLabel(bandScore, gradingSystem?.steps || []), + label: getGradingLabel(bandScore, gradingSystem.steps || []), score: bandScore, }; } @@ -113,10 +120,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const commonData = { user: userData?.name || "", email: userData?.email || "", - studentID: (userData as StudentUser).studentID || "", + studentID: (userData as StudentUser)?.studentID || "", + passportID: (userData as StudentUser)?.demographicInformation?.passport_id || "", userId: assignee, + exams: a.exams.map((x) => x.id).join(", "), corporateId: a.corporateId, - corporate: corporateUser?.name || "", + corporate: !corporateUser ? "" : getUserName(corporateUser), assignment: a.name, level, score, @@ -130,14 +139,22 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }; } - const partsData = userStats.every((e) => e.module === "level") - ? userStats.reduce((acc, e, index) => { - return { - ...acc, - [`part${index}`]: `${e.score.correct}/${e.score.total}`, - }; - }, {}) - : {}; + let data: {total: number; correct: number}[] = []; + if (a.exams.every((x) => x.module === "level")) { + const exam = exams.find((x) => x.id === a.exams.find((x) => x.assignee === assignee)?.id) as LevelExam; + data = exam.parts.map((x) => { + const exerciseIDs = x.exercises.map((x) => x.id); + const stats = userStats.filter((x) => exerciseIDs.includes(x.exercise)); + + const total = stats.reduce((acc, curr) => acc + curr.score.total, 0); + const correct = stats.reduce((acc, curr) => acc + curr.score.correct, 0); + + return {total, correct}; + }); + } + + const partsData = + data.length > 0 ? data.reduce((acc, e, index) => ({...acc, [`part${index}`]: `${e.correct}/${e.total}`}), {}) : {}; return { ...commonData, @@ -169,6 +186,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) { label: "Student ID", value: (entry: TableData) => entry.studentID, }, + { + label: "Passport ID", + value: (entry: TableData) => entry.passportID, + }, ...(displaySelection ? [ { @@ -186,7 +207,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { value: (entry: TableData) => (entry.submitted ? "Yes" : "No"), }, { - label: "Correct", + label: "Score", value: (entry: TableData) => entry.correct, }, { @@ -206,7 +227,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { })), ]; - const filteredSearch = searchText ? search(searchText, searchFilters, tableResults) : tableResults; + const filteredSearch = !!searchText ? search(searchText, searchFilters, tableResults) : tableResults; worksheet.addRow(headers.map((h) => h.label)); (filteredSearch as TableData[]).forEach((entry) => { diff --git a/src/pages/api/evaluate/writing.ts b/src/pages/api/evaluate/writing.ts index af1850d8..1645e8a1 100644 --- a/src/pages/api/evaluate/writing.ts +++ b/src/pages/api/evaluate/writing.ts @@ -1,11 +1,11 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; import client from "@/lib/mongodb"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import axios, {AxiosResponse} from "axios"; -import {Stat} from "@/interfaces/user"; -import {writingReverseMarking} from "@/utils/score"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import axios, { AxiosResponse } from "axios"; +import { Stat } from "@/interfaces/user"; +import { writingReverseMarking } from "@/utils/score"; interface Body { question: string; @@ -24,7 +24,7 @@ export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) { - res.status(401).json({ok: false}); + res.status(401).json({ ok: false }); return; } @@ -36,27 +36,29 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const correspondingStat = await getCorrespondingStat(req.body.id, 1); - const solutions = correspondingStat.solutions.map((x) => ({...x, evaluation: backendRequest.data})); + const solutions = correspondingStat.solutions.map((x) => ({ ...x, evaluation: backendRequest.data })); await db.collection("stats").updateOne( - { id: (req.body as Body).id}, - { - id: (req.body as Body).id, - solutions, - score: { - correct: writingReverseMarking[backendRequest.data.overall], - total: 100, - missing: 0, - }, - isDisabled: false, + { id: (req.body as Body).id }, + { + $set: { + id: (req.body as Body).id, + solutions, + score: { + correct: writingReverseMarking[backendRequest.data.overall], + total: 100, + missing: 0, + }, + isDisabled: false, + } }, - {upsert: true}, + { upsert: true }, ); console.log("🌱 - Updated the DB"); } async function getCorrespondingStat(id: string, index: number): Promise { console.log(`🌱 - Try number ${index} - ${id}`); - const correspondingStat = await db.collection("stats").findOne({ id: id}); + const correspondingStat = await db.collection("stats").findOne({ id: id }); if (correspondingStat) return correspondingStat; diff --git a/src/pages/api/stats/[id]/index.ts b/src/pages/api/stats/[id]/index.ts index f531284b..75cd2f41 100644 --- a/src/pages/api/stats/[id]/index.ts +++ b/src/pages/api/stats/[id]/index.ts @@ -16,5 +16,5 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { const snapshot = await db.collection("stats").findOne({ id: id as string}); if (!snapshot) return res.status(404).json({id: id as string}); - res.status(200).json({...snapshot.data(), id: snapshot.id}); + res.status(200).json({...snapshot, id: snapshot.id}); } diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index 41d509ca..4914257a 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -4,6 +4,7 @@ import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {getLinkedUsers} from "@/utils/users.be"; import {Type} from "@/interfaces/user"; +import {uniqBy} from "lodash"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -31,5 +32,5 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { direction, ); - res.status(200).json({users, total}); + res.status(200).json({users: uniqBy([...users], "id"), total}); } diff --git a/src/utils/assignments.be.ts b/src/utils/assignments.be.ts index e2341020..261f04b6 100644 --- a/src/utils/assignments.be.ts +++ b/src/utils/assignments.be.ts @@ -1,6 +1,7 @@ import client from "@/lib/mongodb"; import {Assignment} from "@/interfaces/results"; import {getAllAssignersByCorporate} from "@/utils/groups.be"; +import {Type} from "@/interfaces/user"; const db = client.db(process.env.MONGODB_DB); @@ -36,10 +37,10 @@ export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, .toArray(); }; -export const getAssignmentsForCorporates = async (idsList: string[], startDate?: Date, endDate?: Date) => { +export const getAssignmentsForCorporates = async (userType: Type, idsList: string[], startDate?: Date, endDate?: Date) => { const assigners = await Promise.all( idsList.map(async (id) => { - const assigners = await getAllAssignersByCorporate(id); + const assigners = await getAllAssignersByCorporate(id, userType); return { corporateId: id, assigners, diff --git a/src/utils/exams.be.ts b/src/utils/exams.be.ts index 8b7b83df..70bcd685 100644 --- a/src/utils/exams.be.ts +++ b/src/utils/exams.be.ts @@ -6,6 +6,28 @@ import {Module} from "@/interfaces"; import {getCorporateUser} from "@/resources/user"; import {getUserCorporate} from "./groups.be"; import {Db, ObjectId} from "mongodb"; +import client from "@/lib/mongodb"; +import {MODULE_ARRAY} from "./moduleUtils"; + +const db = client.db(process.env.MONGODB_DB); + +export async function getSpecificExams(ids: string[]) { + if (ids.length === 0) return []; + + const exams: Exam[] = ( + await Promise.all( + MODULE_ARRAY.flatMap( + async (module) => + await db + .collection(module) + .find({id: {$in: ids}}) + .toArray(), + ), + ) + ).flat(); + + return exams; +} export const getExams = async ( db: Db, diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 7757ecd8..4c344dc8 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -1,6 +1,6 @@ import {app} from "@/firebase"; import {Assignment} from "@/interfaces/results"; -import {CorporateUser, Group, MasterCorporateUser, StudentUser, TeacherUser, User} from "@/interfaces/user"; +import {CorporateUser, Group, MasterCorporateUser, StudentUser, TeacherUser, Type, User} from "@/interfaces/user"; import client from "@/lib/mongodb"; import moment from "moment"; import {getLinkedUsers, getUser} from "./users.be"; @@ -71,9 +71,9 @@ export const getUsersGroups = async (ids: string[]) => { .toArray(); }; -export const getAllAssignersByCorporate = async (corporateID: string): Promise => { - const linkedTeachers = await getLinkedUsers(corporateID, "mastercorporate", "teacher"); - const linkedCorporates = await getLinkedUsers(corporateID, "mastercorporate", "corporate"); +export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise => { + const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher"); + const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate"); return [...linkedTeachers.users.map((x) => x.id), ...linkedCorporates.users.map((x) => x.id)]; }; diff --git a/src/utils/search.ts b/src/utils/search.ts index eeb1fbc4..87b2cfc1 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -1,4 +1,3 @@ - /*fields example = [ ['id'], ['companyInformation', 'companyInformation', 'name'] @@ -13,17 +12,17 @@ const getFieldValue = (fields: string[], data: any): string => { }; export const search = (text: string, fields: string[][], rows: any[]) => { - const searchText = text.toLowerCase(); - return rows.filter((row) => { - return fields.some((fieldsKeys) => { - const value = getFieldValue(fieldsKeys, row); - if (typeof value === "string") { - return value.toLowerCase().includes(searchText); - } + const searchText = text.toLowerCase(); + return rows.filter((row) => { + return fields.some((fieldsKeys) => { + const value = getFieldValue(fieldsKeys, row); + if (typeof value === "string") { + return value.toLowerCase().includes(searchText); + } - if (typeof value === "number") { - return (value as Number).toString().includes(searchText); - } - }); - }); -} \ No newline at end of file + if (typeof value === "number") { + return (value as Number).toString().includes(searchText); + } + }); + }); +}; diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index cd5c1c1a..31ecbe05 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -8,11 +8,11 @@ import client from "@/lib/mongodb"; const db = client.db(process.env.MONGODB_DB); export async function getUsers() { - return await db.collection("users").find({}).toArray(); + return await db.collection("users").find({}, { projection: { _id: 0 } }).toArray(); } export async function getUser(id: string): Promise { - const user = await db.collection("users").findOne({id}); + const user = await db.collection("users").findOne({id: id}, { projection: { _id: 0 } }); return !!user ? user : undefined; } @@ -21,7 +21,7 @@ export async function getSpecificUsers(ids: string[]) { return await db .collection("users") - .find({id: {$in: ids}}) + .find({id: {$in: ids}}, { projection: { _id: 0 } }) .toArray(); }