diff --git a/src/components/List.tsx b/src/components/List.tsx index 0e76b663..f62e7172 100644 --- a/src/components/List.tsx +++ b/src/components/List.tsx @@ -1,51 +1,77 @@ import {Column, flexRender, getCoreRowModel, getSortedRowModel, useReactTable} from "@tanstack/react-table"; +import {useMemo, useState} from "react"; +import Button from "./Low/Button"; + +const SIZE = 25; export default function List({data, columns}: {data: T[]; columns: any[]}) { + const [page, setPage] = useState(0); + + const items = useMemo(() => data.slice(page * SIZE, (page + 1) * SIZE > data.length ? data.length : (page + 1) * SIZE), [data, page]); + const table = useReactTable({ - data, + data: items, columns: columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder ? null : ( - <> -
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: " 🔼", - desc: " 🔽", - }[header.column.getIsSorted() as string] ?? null} -
- - )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+
+
+ +
+ + {page * SIZE + 1} - {(page + 1) * SIZE > data.length ? data.length : (page + 1) * SIZE} / {data.length} + + +
+
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: " 🔼", + desc: " 🔽", + }[header.column.getIsSorted() as string] ?? null} +
+ + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
); } diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index fa677f5b..c3bdd792 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -21,6 +21,7 @@ import Checkbox from "@/components/Low/Checkbox"; import {InstructorGender, Variant} from "@/interfaces/exam"; import Select from "@/components/Low/Select"; import useExams from "@/hooks/useExams"; +import {useListSearch} from "@/hooks/useListSearch"; interface Props { isCreating: boolean; @@ -31,7 +32,12 @@ interface Props { cancelCreation: () => void; } +const SIZE = 12; + export default function AssignmentCreator({isCreating, assignment, user, groups, users, cancelCreation}: Props) { + const [studentsPage, setStudentsPage] = useState(0); + const [teachersPage, setTeachersPage] = useState(0); + const [selectedModules, setSelectedModules] = useState(assignment?.exams.map((e) => e.module) || []); const [assignees, setAssignees] = useState(assignment?.assignees || []); const [teachers, setTeachers] = useState(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]); @@ -69,6 +75,29 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, 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, text: studentText} = useListSearch([["name"], ["email"]], userStudents); + const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch, text: teacherText} = useListSearch([["name"], ["email"]], userTeachers); + + useEffect(() => setStudentsPage(0), [studentText]); + const studentRows = useMemo( + () => + filteredStudentsRows.slice( + studentsPage * SIZE, + (studentsPage + 1) * SIZE > filteredStudentsRows.length ? filteredStudentsRows.length : (studentsPage + 1) * SIZE, + ), + [filteredStudentsRows, studentsPage], + ); + + useEffect(() => setTeachersPage(0), [teacherText]); + const teacherRows = useMemo( + () => + filteredTeachersRows.slice( + teachersPage * SIZE, + (teachersPage + 1) * SIZE > filteredTeachersRows.length ? filteredTeachersRows.length : (teachersPage + 1) * SIZE, + ), + [filteredTeachersRows, teachersPage], + ); + useEffect(() => { setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); }, [selectedModules]); @@ -347,9 +376,9 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, )} -
+
Assignees ({assignees.length} selected) -
+
{groups.map((g) => (
+ + {renderStudentSearch()} +
- {userStudents.map((user) => ( + {studentRows.map((user) => (
toggleAssignee(user)} className={clsx( @@ -402,12 +434,32 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
))}
+
+
+ +
+
+ + {studentsPage * SIZE + 1} -{" "} + {(studentsPage + 1) * SIZE > filteredStudentsRows.length ? filteredStudentsRows.length : (studentsPage + 1) * SIZE} /{" "} + {filteredStudentsRows.length} + + +
+
{user.type !== "teacher" && (
Teachers ({teachers.length} selected) -
+
{groups.map((g) => (
+ + {renderTeacherSearch()} +
- {userTeachers.map((user) => ( + {teacherRows.map((user) => (
toggleTeacher(user)} className={clsx( @@ -453,6 +508,29 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
))}
+ +
+
+ +
+
+ + {teachersPage * SIZE + 1} -{" "} + {(teachersPage + 1) * SIZE > filteredTeachersRows.length + ? filteredTeachersRows.length + : (teachersPage + 1) * SIZE}{" "} + / {filteredTeachersRows.length} + + +
+
)} diff --git a/src/dashboards/MasterCorporate/MasterStatistical.tsx b/src/dashboards/MasterCorporate/MasterStatistical.tsx index 17d0708c..8978395a 100644 --- a/src/dashboards/MasterCorporate/MasterStatistical.tsx +++ b/src/dashboards/MasterCorporate/MasterStatistical.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useEffect, useMemo, useState} from "react"; import {CorporateUser, User} from "@/interfaces/user"; import {BsFileExcel, BsBank, BsPersonFill} from "react-icons/bs"; import IconCard from "../IconCard"; @@ -14,6 +14,7 @@ import {useListSearch} from "@/hooks/useListSearch"; import axios from "axios"; import {toast} from "react-toastify"; import Button from "@/components/Low/Button"; +import {getUserName} from "@/utils/users"; interface GroupedCorporateUsers { // list of user Ids @@ -42,10 +43,13 @@ interface UserCount { maxUserCount: number; } -const searchFilters = [["email"], ["user"], ["userId"]]; +const searchFilters = [["email"], ["user"], ["userId"], ["exams"], ["assignment"]]; + +const SIZE = 16; const MasterStatistical = (props: Props) => { const {users, corporateUsers, displaySelection = true} = props; + const [page, setPage] = useState(0); // const corporateRelevantUsers = React.useMemo( // () => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[], @@ -68,43 +72,44 @@ const MasterStatistical = (props: Props) => { const tableResults = React.useMemo( () => - 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); - const corporate = users.find((u) => u.id === a.assigner)?.name || ""; - const commonData = { - user: userData?.name || "N/A", - email: userData?.email || "N/A", - userId: assignee, - corporateId: a.corporateId, - corporate, - assignment: a.name, - }; - if (userStats.length === 0) { - return { - ...commonData, - correct: 0, - submitted: false, - // date: moment(), - }; - } - + 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); + const corporate = getUserName(users.find((u) => u.id === a.assigner)); + const commonData = { + user: userData?.name || "N/A", + email: userData?.email || "N/A", + userId: assignee, + corporateId: a.corporateId, + exams: a.exams.map((x) => x.id).join(", "), + corporate, + assignment: a.name, + }; + if (userStats.length === 0) { return { ...commonData, - correct: userStats.reduce((n, e) => n + e.score.correct, 0), - submitted: true, - date: moment.max(userStats.map((e) => moment(e.date))), + correct: 0, + submitted: false, + date: null, }; - }) as TableData[]; + } - return [...accmA, ...userResults]; - }, []) - .filter((x) => x.user !== "N/A"), + return { + ...commonData, + correct: userStats.reduce((n, e) => n + e.score.correct, 0), + submitted: true, + date: moment.max(userStats.map((e) => moment(e.date))), + }; + }) as TableData[]; + + return [...accmA, ...userResults]; + }, []), [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,13 +172,6 @@ const MasterStatistical = (props: Props) => { }), ] : []), - columnHelper.accessor("corporate", { - header: "Corporate", - id: "corporate", - cell: (info) => { - return {info.getValue()}; - }, - }), columnHelper.accessor("assignment", { header: "Assignment", id: "assignment", @@ -193,7 +191,7 @@ const MasterStatistical = (props: Props) => { }, }), columnHelper.accessor("correct", { - header: "Correct", + header: "Score", id: "correct", cell: (info) => { return {info.getValue()}; @@ -205,7 +203,7 @@ const MasterStatistical = (props: Props) => { cell: (info) => { const date = info.getValue(); if (date) { - return {date.format("DD/MM/YYYY")}; + return {!!date ? date.format("DD/MM/YYYY") : "N/A"}; } return {""}; @@ -215,8 +213,14 @@ const MasterStatistical = (props: Props) => { const {rows: filteredRows, renderSearch, text: searchText} = useListSearch(searchFilters, tableResults); + useEffect(() => setPage(0), [searchText]); + const rows = useMemo( + () => filteredRows.slice(page * SIZE, (page + 1) * SIZE > filteredRows.length ? filteredRows.length : (page + 1) * SIZE), + [filteredRows, page], + ); + const table = useReactTable({ - data: filteredRows, + data: rows, columns: defaultColumns, getCoreRowModel: getCoreRowModel(), }); @@ -345,7 +349,22 @@ const MasterStatistical = (props: Props) => { -
+
+
+ +
+ + {page * SIZE + 1} - {(page + 1) * SIZE > filteredRows.length ? filteredRows.length : (page + 1) * SIZE} /{" "} + {filteredRows.length} + + +
+
+ {table.getHeaderGroups().map((headerGroup) => ( diff --git a/src/dashboards/MasterCorporate/index.tsx b/src/dashboards/MasterCorporate/index.tsx index 584eaf28..efa958e0 100644 --- a/src/dashboards/MasterCorporate/index.tsx +++ b/src/dashboards/MasterCorporate/index.tsx @@ -83,18 +83,6 @@ export default function MasterCorporateDashboard({user}: Props) { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]); - const assignmentsUsers = useMemo( - () => - [...students, ...teachers].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, selectedUser, teachers, students], - ); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); @@ -131,7 +119,6 @@ export default function MasterCorporateDashboard({user}: Props) { const {users} = useUsers(); - const groupedByNameCorporates = useMemo( () => groupBy( @@ -374,12 +361,18 @@ export default function MasterCorporateDashboard({user}: Props) { color="purple" onClick={() => router.push("/#corporate")} /> - + router.push("/#studentsPerformance")} /> diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 9b0ea9df..2396bb2a 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -632,12 +632,12 @@ export default function UserList({
- {page * 16 + 1} - {(page + 1) * 16 > total ? total : (page + 1) * 16} / {total} + {page * userHash.size + 1} - {(page + 1) * userHash.size > total ? total : (page + 1) * userHash.size} / {total} diff --git a/src/pages/api/groups/[id].ts b/src/pages/api/groups/[id].ts index 31c7b6c7..48df6782 100644 --- a/src/pages/api/groups/[id].ts +++ b/src/pages/api/groups/[id].ts @@ -1,109 +1,91 @@ // 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 { Group } from "@/interfaces/user"; -import { updateExpiryDateOnGroup } from "@/utils/groups.be"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {Group} from "@/interfaces/user"; +import {updateExpiryDateOnGroup} from "@/utils/groups.be"; const db = client.db(process.env.MONGODB_DB); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "GET") return await get(req, res); - if (req.method === "DELETE") return await del(req, res); - if (req.method === "PATCH") return await patch(req, res); + if (req.method === "GET") return await get(req, res); + if (req.method === "DELETE") return await del(req, res); + if (req.method === "PATCH") return await patch(req, res); - res.status(404).json(undefined); + res.status(404).json(undefined); } async function get(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ ok: false }); - return; - } + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } - const { id } = req.query as { id: string }; + const {id} = req.query as {id: string}; - const snapshot = await db.collection("groups").findOne({ id: id}); + const snapshot = await db.collection("groups").findOne({id: id}); - if (snapshot) { - res.status(200).json({ ...snapshot }); - } else { - res.status(404).json(undefined); - } + if (snapshot) { + res.status(200).json({...snapshot}); + } else { + res.status(404).json(undefined); + } } async function del(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ ok: false }); - return; - } + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } - const { id } = req.query as { id: string }; - const group = await db.collection("groups").findOne({id: id}); + const {id} = req.query as {id: string}; + const group = await db.collection("groups").findOne({id: id}); - if (!group) { - res.status(404); - return; - } + if (!group) { + res.status(404); + return; + } - const user = req.session.user; - if ( - user.type === "admin" || - user.type === "developer" || - user.id === group.admin - ) { - await db.collection("groups").deleteOne({ id: id }); + const user = req.session.user; + if (user.type === "admin" || user.type === "developer" || user.id === group.admin) { + await db.collection("groups").deleteOne({id: id}); - res.status(200).json({ ok: true }); - return; - } + res.status(200).json({ok: true}); + return; + } - res.status(403).json({ ok: false }); + res.status(403).json({ok: false}); } async function patch(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ ok: false }); - return; - } + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } - const { id } = req.query as { id: string }; + const {id} = req.query as {id: string}; - const group = await db.collection("groups").findOne({id: id}); - if (!group) { - res.status(404); - return; - } + const group = await db.collection("groups").findOne({id: id}); + if (!group) { + res.status(404); + return; + } - const user = req.session.user; - if ( - user.type === "admin" || - user.type === "developer" || - user.id === group.admin - ) { - if ("participants" in req.body) { - const newParticipants = (req.body.participants as string[]).filter( - (x) => !group.participants.includes(x), - ); - await Promise.all( - newParticipants.map( - async (p) => await updateExpiryDateOnGroup(p, group.admin), - ), - ); - } + const user = req.session.user; + if (user.type === "admin" || user.type === "developer" || user.id === group.admin) { + if ("participants" in req.body) { + const newParticipants = (req.body.participants as string[]).filter((x) => !group.participants.includes(x)); + await Promise.all(newParticipants.map(async (p) => await updateExpiryDateOnGroup(p, group.admin))); + } - await db.collection("grading").updateOne( - { id: req.session.user.id }, - { $set: {id: id, ...req.body} }, - { upsert: true } - ); + await db.collection("groups").updateOne({id: req.session.user.id}, {$set: {id, ...req.body}}, {upsert: true}); - res.status(200).json({ ok: true }); - return; - } + res.status(200).json({ok: true}); + return; + } - res.status(403).json({ ok: false }); + res.status(403).json({ok: false}); } diff --git a/src/utils/assignments.be.ts b/src/utils/assignments.be.ts index e3e9ff45..e2341020 100644 --- a/src/utils/assignments.be.ts +++ b/src/utils/assignments.be.ts @@ -1,18 +1,18 @@ import client from "@/lib/mongodb"; -import { Assignment } from "@/interfaces/results"; -import { getAllAssignersByCorporate } from "@/utils/groups.be"; +import {Assignment} from "@/interfaces/results"; +import {getAllAssignersByCorporate} from "@/utils/groups.be"; const db = client.db(process.env.MONGODB_DB); export const getAssignmentsByAssigner = async (id: string, startDate?: Date, endDate?: Date) => { - let query: any = { assigner: id }; + let query: any = {assigner: id}; if (startDate) { - query.startDate = { $gte: startDate.toISOString() }; + query.startDate = {$gte: startDate.toISOString()}; } if (endDate) { - query.endDate = { $lte: endDate.toISOString() }; + query.endDate = {$lte: endDate.toISOString()}; } return await db.collection("assignments").find(query).toArray(); @@ -26,7 +26,14 @@ export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate }; export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, endDate?: Date) => { - return (await Promise.all(ids.map((id) => getAssignmentsByAssigner(id, startDate, endDate)))).flat(); + return await db + .collection("assignments") + .find({ + assigner: {$in: ids}, + ...(!!startDate ? {startDate: {$gte: startDate.toISOString()}} : {}), + ...(!!endDate ? {endDate: {$lte: endDate.toISOString()}} : {}), + }) + .toArray(); }; export const getAssignmentsForCorporates = async (idsList: string[], startDate?: Date, endDate?: Date) => { @@ -57,4 +64,4 @@ export const getAssignmentsForCorporates = async (idsList: string[], startDate?: ); return assignments.flat(); -} \ No newline at end of file +}; diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 42fe68f9..7757ecd8 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -1,8 +1,9 @@ import {app} from "@/firebase"; +import {Assignment} from "@/interfaces/results"; import {CorporateUser, Group, MasterCorporateUser, StudentUser, TeacherUser, User} from "@/interfaces/user"; import client from "@/lib/mongodb"; import moment from "moment"; -import {getUser} from "./users.be"; +import {getLinkedUsers, getUser} from "./users.be"; import {getSpecificUsers} from "./users.be"; const db = client.db(process.env.MONGODB_DB); @@ -71,17 +72,10 @@ export const getUsersGroups = async (ids: string[]) => { }; export const getAllAssignersByCorporate = async (corporateID: string): Promise => { - const groups = await getUserGroups(corporateID); - const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))) - .flat() - .filter((x) => !!x) as User[]; - const teacherPromises = await Promise.all( - groupUsers.map(async (u) => - u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined, - ), - ); + const linkedTeachers = await getLinkedUsers(corporateID, "mastercorporate", "teacher"); + const linkedCorporates = await getLinkedUsers(corporateID, "mastercorporate", "corporate"); - return teacherPromises.filter((x) => !!x).flat() as string[]; + return [...linkedTeachers.users.map((x) => x.id), ...linkedCorporates.users.map((x) => x.id)]; }; export const getGroupsForUser = async (admin?: string, participant?: string) => {