From 3be0d158e36c19902e85f38cf3a6c6522ae65417 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 7 Sep 2024 17:34:41 +0100 Subject: [PATCH] Improved the performance of the MasterCorporate --- src/dashboards/Admin.tsx | 15 +- src/dashboards/MasterCorporate.tsx | 703 ------------------ .../MasterCorporate/MasterStatistical.tsx | 371 +++++++++ .../MasterCorporate/MasterStatisticalPage.tsx | 52 ++ .../StudentPerformanceList.tsx | 252 +++++++ .../StudentPerformancePage.tsx | 46 ++ src/dashboards/MasterCorporate/index.tsx | 442 +++++++++++ src/dashboards/MasterStatistical.tsx | 424 ----------- src/hooks/useUsers.tsx | 4 +- src/pages/api/users/list.ts | 11 +- src/utils/users.be.ts | 11 +- 11 files changed, 1197 insertions(+), 1134 deletions(-) delete mode 100644 src/dashboards/MasterCorporate.tsx create mode 100644 src/dashboards/MasterCorporate/MasterStatistical.tsx create mode 100644 src/dashboards/MasterCorporate/MasterStatisticalPage.tsx create mode 100644 src/dashboards/MasterCorporate/StudentPerformanceList.tsx create mode 100644 src/dashboards/MasterCorporate/StudentPerformancePage.tsx create mode 100644 src/dashboards/MasterCorporate/index.tsx delete mode 100644 src/dashboards/MasterStatistical.tsx diff --git a/src/dashboards/Admin.tsx b/src/dashboards/Admin.tsx index 4ceb0751..e3bc48ce 100644 --- a/src/dashboards/Admin.tsx +++ b/src/dashboards/Admin.tsx @@ -36,8 +36,7 @@ export default function AdminDashboard({user}: Props) { const [selectedUser, setSelectedUser] = useState(); const [showModal, setShowModal] = useState(false); - const {data: stats} = useFilterRecordsByUser(user.id); - const {users, reload} = useUsers(); + const {users, reload, isLoading} = useUsers(); const {groups} = useGroups({}); const {pending, done} = usePaymentStatusUsers(); @@ -280,6 +279,7 @@ export default function AdminDashboard({user}: Props) {
x.type === "student").length} onClick={() => router.push("/#students")} @@ -287,6 +287,7 @@ export default function AdminDashboard({user}: Props) { /> x.type === "teacher").length} onClick={() => router.push("/#teachers")} @@ -294,6 +295,7 @@ export default function AdminDashboard({user}: Props) { /> x.type === "corporate").length} onClick={() => router.push("/#corporate")} @@ -301,6 +303,7 @@ export default function AdminDashboard({user}: Props) { /> x.type === "agent").length} onClick={() => router.push("/#agents")} @@ -308,6 +311,7 @@ export default function AdminDashboard({user}: Props) { /> x.demographicInformation).map((x) => x.demographicInformation?.country))].length} color="purple" @@ -315,6 +319,7 @@ export default function AdminDashboard({user}: Props) { router.push("/#inactiveStudents")} Icon={BsPersonFill} + isLoading={isLoading} label="Inactive Students" value={ users.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) @@ -325,6 +330,7 @@ export default function AdminDashboard({user}: Props) { router.push("/#inactiveCountryManagers")} Icon={BsBriefcaseFill} + isLoading={isLoading} label="Inactive Country Managers" value={users.filter(inactiveCountryManagerFilter).length} color="rose" @@ -332,6 +338,7 @@ export default function AdminDashboard({user}: Props) { router.push("/#inactiveCorporate")} Icon={BsBank} + isLoading={isLoading} label="Inactive Corporate" value={ users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) @@ -342,6 +349,7 @@ export default function AdminDashboard({user}: Props) { router.push("/#paymentdone")} Icon={BsCurrencyDollar} + isLoading={isLoading} label="Payment Done" value={done.length} color="purple" @@ -349,6 +357,7 @@ export default function AdminDashboard({user}: Props) { router.push("/#paymentpending")} Icon={BsCurrencyDollar} + isLoading={isLoading} label="Pending Payment" value={pending.length} color="rose" @@ -356,12 +365,14 @@ export default function AdminDashboard({user}: Props) { router.push("https://cms.encoach.com/admin")} Icon={BsLayoutSidebar} + isLoading={isLoading} label="Content Management System (CMS)" color="green" /> router.push("/#corporatestudentslevels")} Icon={BsPersonFill} + isLoading={isLoading} label="Corporate Students Levels" color="purple" /> diff --git a/src/dashboards/MasterCorporate.tsx b/src/dashboards/MasterCorporate.tsx deleted file mode 100644 index ee6dba4d..00000000 --- a/src/dashboards/MasterCorporate.tsx +++ /dev/null @@ -1,703 +0,0 @@ -/* 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 UserList from "@/pages/(admin)/Lists/UserList"; -import {dateSorter} from "@/utils"; -import moment from "moment"; -import {useEffect, useState, useMemo} from "react"; -import { - BsArrowLeft, - BsClipboard2Data, - BsClock, - BsPaperclip, - BsPersonFill, - BsPencilSquare, - BsPersonCheck, - BsPeople, - BsBank, - BsEnvelopePaper, - BsArrowRepeat, - BsPlus, - BsPersonFillGear, - BsFilter, - 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 IconCard from "./IconCard"; -import GroupList from "@/pages/(admin)/Lists/GroupList"; -import useFilterStore from "@/stores/listFilterStore"; -import {useRouter} from "next/router"; -import useCodes from "@/hooks/useCodes"; -import useAssignments from "@/hooks/useAssignments"; -import {Assignment} from "@/interfaces/results"; -import AssignmentView from "./AssignmentView"; -import AssignmentCreator from "./AssignmentCreator"; -import clsx from "clsx"; -import AssignmentCard from "./AssignmentCard"; -import {createColumn, createColumnHelper} from "@tanstack/react-table"; -import List from "@/components/List"; -import {getUserCorporate} from "@/utils/groups"; -import {getCorporateUser, getUserCompanyName} from "@/resources/user"; -import Checkbox from "@/components/Low/Checkbox"; -import {groupBy, uniq, uniqBy} from "lodash"; -import Select from "@/components/Low/Select"; -import {Menu, MenuButton, MenuItem, MenuItems} from "@headlessui/react"; -import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; -import MasterStatistical from "./MasterStatistical"; -import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments"; -import useUserBalance from "@/hooks/useUserBalance"; -import AssignmentsPage from "./views/AssignmentsPage"; -import { group } from "console"; - -interface Props { - user: MasterCorporateUser; -} - -type StudentPerformanceItem = User & { - corporate?: CorporateUser; - group?: Group; -}; -const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => { - const [isShowingAmount, setIsShowingAmount] = useState(false); - const [availableCorporates] = useState( - uniqBy( - items.map((x) => x.corporate), - "id", - ), - ); - const [availableGroups] = useState( - uniqBy( - items.map((x) => x.group), - "id", - ), - ); - - const [selectedCorporate, setSelectedCorporate] = useState(null); - const [selectedGroup, setSelectedGroup] = useState(null); - - 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("demographicInformation.passport_id", { - header: "ID", - cell: (info) => info.getValue() || "N/A", - }), - columnHelper.accessor("group", { - header: "Group", - cell: (info) => info.getValue()?.name || "N/A", - }), - columnHelper.accessor("corporate", { - header: "Corporate", - cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"), - }), - columnHelper.accessor("levels.reading", { - header: "Reading", - cell: (info) => - !isShowingAmount - ? calculateBandScore( - stats - .filter((x) => x.module === "reading" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.correct, 0), - stats - .filter((x) => x.module === "reading" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.total, 0), - "level", - info.row.original.focus || "academic", - ) || 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 - ? calculateBandScore( - stats - .filter((x) => x.module === "listening" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.correct, 0), - stats - .filter((x) => x.module === "listening" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.total, 0), - "level", - info.row.original.focus || "academic", - ) || 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 - ? calculateBandScore( - stats - .filter((x) => x.module === "writing" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.correct, 0), - stats - .filter((x) => x.module === "writing" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.total, 0), - "level", - info.row.original.focus || "academic", - ) || 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 - ? calculateBandScore( - stats - .filter((x) => x.module === "speaking" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.correct, 0), - stats - .filter((x) => x.module === "speaking" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.total, 0), - "level", - info.row.original.focus || "academic", - ) || 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 - ? calculateBandScore( - stats - .filter((x) => x.module === "level" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.correct, 0), - stats - .filter((x) => x.module === "level" && x.user === info.row.original.id) - .reduce((acc, curr) => acc + curr.score.total, 0), - "level", - info.row.original.focus || "academic", - ) || 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( - users, - 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 filterUsers = (data: StudentPerformanceItem[]) => { - const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id; - const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id; - - const filters: ((item: StudentPerformanceItem) => boolean)[] = []; - if (selectedCorporate !== null) filters.push(filterByCorporate); - if (selectedGroup !== null) filters.push(filterByGroup); - - return filters.reduce((d, f) => d.filter(f), data); - }; - - return ( -
-
- - Show Utilization - - - -
- -
-
- -
- Filters - ({ - value: x?.id || "N/A", - label: x?.name || "N/A", - }))} - isClearable - value={ - selectedGroup === null - ? null - : { - value: selectedGroup?.id || "N/A", - label: selectedGroup?.name || "N/A", - } - } - placeholder="Select a Group..." - onChange={(value) => - !value - ? setSelectedGroup(null) - : setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value)) - } - /> -
-
-
-
- - data={filterUsers( - items.sort( - (a, b) => - averageLevelCalculator( - users, - stats.filter((x) => x.user === b.id), - ) - - averageLevelCalculator( - users, - stats.filter((x) => x.user === a.id), - ), - ), - )} - columns={columns} - /> -
- ); -}; - -export default function MasterCorporateDashboard({user}: Props) { - const [selectedUser, setSelectedUser] = useState(); - const [showModal, setShowModal] = useState(false); - const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]); - - const {data: stats} = useFilterRecordsByUser(); - - const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(userHashStudent); - const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(userHashTeacher); - const {users: corporates, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(userHashCorporate); - - const {groups} = useGroups({admin: user.id, userType: user.type}); - const {balance} = useUserBalance(); - - const users = useMemo(() => uniqBy([...students, ...teachers, ...corporates, user], "id"), [corporates, students, teachers, user]); - - 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(); - - useEffect(() => { - setShowModal(!!selectedUser && router.asPath === "/"); - }, [selectedUser, router.asPath]); - - useEffect(() => { - setCorporateAssignments( - assignments.filter(activeAssignmentFilter).map((a) => { - const assigner = [...teachers, ...corporates].find((x) => x.id === a.assigner); - - return { - ...a, - corporate: assigner ? getCorporateUser(assigner, [...teachers, ...corporates], groups) : undefined, - }; - }), - ); - }, [assignments, groups, teachers, corporates]); - 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 groupedByNameCorporates = groupBy(corporates, (x: CorporateUser) => x.corporateInformation?.companyInformation?.name || 'N/A'); - const groupedByNameCorporatesKeys = Object.keys(groupedByNameCorporates); - const groupedByNameCorporateIds = groupedByNameCorporatesKeys.reduce((accm, x) => { - const corporateUserIds = (groupedByNameCorporates[x] as CorporateUser[]).map((y) => y.id); - return { ...accm, [x]: corporateUserIds }; - }, {}); - - console.log("groupedByNameCorporates", groupedByNameCorporates, groupedByNameCorporateIds); - const GroupsList = () => { - 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.length})

-
- - - - ); - }; - - const StudentPerformancePage = () => { - 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 -
-
- Reload - -
-
- - - ); - }; - - 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 DefaultDashboard = () => ( - <> -
- router.push("/#students")} - Icon={BsPersonFill} - isLoading={isStudentsLoading} - label="Students" - value={students.length} - color="purple" - /> - router.push("/#teachers")} - Icon={BsPencilSquare} - isLoading={isTeachersLoading} - label="Teachers" - value={teachers.length} - 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("/#corporate")} - /> - router.push("/#corporate")} - /> - 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) => ( - - ))} -
-
-
- - ); - - return ( - <> - setSelectedUser(undefined)}> - <> - {selectedUser && ( -
- { - setSelectedUser(undefined); - if (shouldReload && selectedUser!.type === "student") reloadStudents(); - if (shouldReload && selectedUser!.type === "teacher") reloadTeachers(); - if (shouldReload && selectedUser!.type === "corporate") reloadCorporates(); - }} - 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 - } - user={selectedUser} - /> -
- )} - -
- {router.asPath === "/#students" && ( - ( -
-
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})

-
- )} - /> - )} - {router.asPath === "/#teachers" && ( - ( -
-
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})

-
- )} - /> - )} - {router.asPath === "/#groups" && } - {router.asPath === "/#corporate" && ( - ( -
-
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 -
-

Corporate ({total})

-
- )} - /> - )} - {router.asPath === "/#assignments" && ( - router.push("/")} - /> - )} - {router.asPath === "/#studentsPerformance" && } - {router.asPath === "/#statistical" && } - {router.asPath === "/" && } - - ); -} diff --git a/src/dashboards/MasterCorporate/MasterStatistical.tsx b/src/dashboards/MasterCorporate/MasterStatistical.tsx new file mode 100644 index 00000000..dde1a2ef --- /dev/null +++ b/src/dashboards/MasterCorporate/MasterStatistical.tsx @@ -0,0 +1,371 @@ +import React from "react"; +import {CorporateUser, User} from "@/interfaces/user"; +import {BsFileExcel, BsBank, BsPersonFill} from "react-icons/bs"; +import IconCard from "../IconCard"; + +import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates"; +import ReactDatePicker from "react-datepicker"; + +import moment from "moment"; +import {AssignmentWithCorporateId} from "@/interfaces/results"; +import {flexRender, createColumnHelper, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import Checkbox from "@/components/Low/Checkbox"; +import {useListSearch} from "@/hooks/useListSearch"; +import axios from "axios"; +import {toast} from "react-toastify"; +import Button from "@/components/Low/Button"; + +interface GroupedCorporateUsers { + // list of user Ids + [key: string]: string[]; +} + +interface Props { + corporateUsers: GroupedCorporateUsers; + users: User[]; +} + +interface TableData { + user: string; + email: string; + correct: number; + corporate: string; + submitted: boolean; + date: moment.Moment; + assignment: string; + corporateId: string; +} + +interface UserCount { + userCount: number; + maxUserCount: number; +} + +const searchFilters = [["email"], ["user"], ["userId"]]; + +const MasterStatistical = (props: Props) => { + const {users, corporateUsers} = props; + + // const corporateRelevantUsers = React.useMemo( + // () => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[], + // [corporateUsers] + // ); + + const corporates = React.useMemo(() => Object.values(corporateUsers).flat(), [corporateUsers]); + + const [selectedCorporates, setSelectedCorporates] = React.useState(corporates); + const [startDate, setStartDate] = React.useState(moment("01/01/2023").toDate()); + const [endDate, setEndDate] = React.useState(moment().endOf("year").toDate()); + + const {assignments} = useAssignmentsCorporates({ + // corporates: [...corporates, "tYU0HTiJdjMsS8SB7XJsUdMMP892"], + corporates: selectedCorporates, + startDate, + endDate, + }); + + const [downloading, setDownloading] = React.useState(false); + + 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 || "", + email: userData?.email || "", + userId: assignee, + corporateId: a.corporateId, + corporate, + assignment: a.name, + }; + if (userStats.length === 0) { + return { + ...commonData, + correct: 0, + submitted: false, + // date: moment(), + }; + } + + 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], + ); + + const getCorporateScores = (corporateId: string): UserCount => { + const corporateAssignmentsUsers = assignments.filter((a) => a.corporateId === corporateId).reduce((acc, a) => acc + a.assignees.length, 0); + + const corporateResults = tableResults.filter((r) => r.corporateId === corporateId).length; + + return { + maxUserCount: corporateAssignmentsUsers, + userCount: corporateResults, + }; + }; + + const getCorporatesScoresHash = (data: string[]) => + data.reduce( + (accm, id) => ({ + ...accm, + [id]: getCorporateScores(id), + }), + {}, + ) as Record; + + const getConsolidateScore = (data: Record) => + Object.values(data).reduce( + (acc: UserCount, {userCount, maxUserCount}: UserCount) => ({ + userCount: acc.userCount + userCount, + maxUserCount: acc.maxUserCount + maxUserCount, + }), + {userCount: 0, maxUserCount: 0}, + ); + + const corporateScores = getCorporatesScoresHash(corporates); + const consolidateScore = getConsolidateScore(corporateScores); + + const getConsolidateScoreStr = (data: UserCount) => `${data.userCount}/${data.maxUserCount}`; + + const columnHelper = createColumnHelper(); + + const defaultColumns = [ + columnHelper.accessor("user", { + header: "User", + id: "user", + cell: (info) => { + return {info.getValue()}; + }, + }), + columnHelper.accessor("email", { + header: "Email", + id: "email", + cell: (info) => { + return {info.getValue()}; + }, + }), + columnHelper.accessor("corporate", { + header: "Corporate", + id: "corporate", + cell: (info) => { + return {info.getValue()}; + }, + }), + columnHelper.accessor("assignment", { + header: "Assignment", + id: "assignment", + cell: (info) => { + return {info.getValue()}; + }, + }), + columnHelper.accessor("submitted", { + header: "Submitted", + id: "submitted", + cell: (info) => { + return ( + {}}> + + + ); + }, + }), + columnHelper.accessor("correct", { + header: "Correct", + id: "correct", + cell: (info) => { + return {info.getValue()}; + }, + }), + columnHelper.accessor("date", { + header: "Date", + id: "date", + cell: (info) => { + const date = info.getValue(); + if (date) { + return {date.format("DD/MM/YYYY")}; + } + + return {""}; + }, + }), + ]; + + const {rows: filteredRows, renderSearch, text: searchText} = useListSearch(searchFilters, tableResults); + + const table = useReactTable({ + data: filteredRows, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); + + const areAllSelected = selectedCorporates.length === corporates.length; + + const getStudentsConsolidateScore = () => { + if (tableResults.length === 0) { + return {highest: null, lowest: null}; + } + + // Find the student with the highest and lowest score + return tableResults.reduce( + (acc, curr) => { + if (curr.correct > acc.highest.correct) { + acc.highest = curr; + } + if (curr.correct < acc.lowest.correct) { + acc.lowest = curr; + } + return acc; + }, + {highest: tableResults[0], lowest: tableResults[0]}, + ); + }; + + const triggerDownload = async () => { + try { + setDownloading(true); + const res = await axios.post("/api/assignments/statistical/excel", { + ids: selectedCorporates, + ...(startDate ? {startDate: startDate.toISOString()} : {}), + ...(endDate ? {endDate: endDate.toISOString()} : {}), + searchText, + }); + toast.success("Report ready!"); + const link = document.createElement("a"); + link.href = res.data; + // download should have worked but there are some CORS issues + // https://firebase.google.com/docs/storage/web/download-files#cors_configuration + // link.download="report.pdf"; + link.target = "_blank"; + link.rel = "noreferrer"; + link.click(); + setDownloading(false); + } catch (err) { + toast.error("Failed to display the report!"); + console.error(err); + setDownloading(false); + } + }; + + const consolidateResults = getStudentsConsolidateScore(); + return ( + <> +
+ { + if (areAllSelected) { + setSelectedCorporates([]); + return; + } + setSelectedCorporates(corporates); + }} + isSelected={areAllSelected} + /> + {Object.keys(corporateUsers).map((corporateName) => { + const group = corporateUsers[corporateName]; + const isSelected = group.every((id) => selectedCorporates.includes(id)); + + const valueHash = getCorporatesScoresHash(group); + const value = getConsolidateScoreStr(getConsolidateScore(valueHash)); + return ( + { + if (isSelected) { + setSelectedCorporates((prev) => prev.filter((x) => !group.includes(x))); + return; + } + setSelectedCorporates((prev) => [...new Set([...prev, ...group])]); + }} + isSelected={isSelected} + /> + ); + })} +
+
+
+ + { + setStartDate(initialDate ?? moment("01/01/2023").toDate()); + if (finalDate) { + // basicly selecting a final day works as if I'm selecting the first + // minute of that day. this way it covers the whole day + setEndDate(moment(finalDate).endOf("day").toDate()); + return; + } + setEndDate(null); + }} + /> +
+ {renderSearch()} +
+ +
+
+ +
+ + + {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())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+ {consolidateResults.highest && ( + {}} Icon={BsPersonFill} label={`Highest result: ${consolidateResults.highest.user}`} color="purple" /> + )} + {consolidateResults.lowest && ( + {}} Icon={BsPersonFill} label={`Lowest result: ${consolidateResults.lowest.user}`} color="purple" /> + )} +
+ + ); +}; + +export default MasterStatistical; diff --git a/src/dashboards/MasterCorporate/MasterStatisticalPage.tsx b/src/dashboards/MasterCorporate/MasterStatisticalPage.tsx new file mode 100644 index 00000000..4dcb173b --- /dev/null +++ b/src/dashboards/MasterCorporate/MasterStatisticalPage.tsx @@ -0,0 +1,52 @@ +import useUsers from "@/hooks/useUsers"; +import {CorporateUser, User} from "@/interfaces/user"; +import {groupBy} from "lodash"; +import {useRouter} from "next/router"; +import {useMemo} from "react"; +import {BsArrowLeft} from "react-icons/bs"; +import MasterStatistical from "./MasterStatistical"; + +interface Props { + user: User; +} + +const MasterStatisticalPage = () => { + const {users} = useUsers(); + + const router = useRouter(); + + const groupedByNameCorporates = useMemo( + () => + groupBy( + users.filter((x) => x.type === "corporate"), + (x: CorporateUser) => x.corporateInformation?.companyInformation?.name || "N/A", + ), + [users], + ); + + const groupedByNameCorporateIds = useMemo( + () => + Object.keys(groupedByNameCorporates).reduce((accm, x) => { + const corporateUserIds = (groupedByNameCorporates[x] as CorporateUser[]).map((y) => y.id); + return {...accm, [x]: corporateUserIds}; + }, {}), + [groupedByNameCorporates], + ); + + 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

+
+ + + ); +}; + +export default MasterStatisticalPage; diff --git a/src/dashboards/MasterCorporate/StudentPerformanceList.tsx b/src/dashboards/MasterCorporate/StudentPerformanceList.tsx new file mode 100644 index 00000000..53e20ba5 --- /dev/null +++ b/src/dashboards/MasterCorporate/StudentPerformanceList.tsx @@ -0,0 +1,252 @@ +/* eslint-disable @next/next/no-img-element */ +import {CorporateUser, Group, Stat, User} from "@/interfaces/user"; +import {useState} from "react"; +import {BsFilter} from "react-icons/bs"; + +import {averageLevelCalculator, calculateBandScore} from "@/utils/score"; +import {groupByExam} from "@/utils/stats"; +import {createColumnHelper} from "@tanstack/react-table"; +import List from "@/components/List"; +import {getUserCompanyName} from "@/resources/user"; +import Checkbox from "@/components/Low/Checkbox"; +import {uniqBy} from "lodash"; +import Select from "@/components/Low/Select"; +import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; + +type StudentPerformanceItem = User & { + corporate?: CorporateUser; + group?: Group; +}; + +const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => { + const [isShowingAmount, setIsShowingAmount] = useState(false); + const [availableCorporates] = useState( + uniqBy( + items.map((x) => x.corporate), + "id", + ), + ); + const [availableGroups] = useState( + uniqBy( + items.map((x) => x.group), + "id", + ), + ); + + const [selectedCorporate, setSelectedCorporate] = useState(null); + const [selectedGroup, setSelectedGroup] = useState(null); + + 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("demographicInformation.passport_id", { + header: "ID", + cell: (info) => info.getValue() || "N/A", + }), + columnHelper.accessor("group", { + header: "Group", + cell: (info) => info.getValue()?.name || "N/A", + }), + columnHelper.accessor("corporate", { + header: "Corporate", + cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"), + }), + columnHelper.accessor("levels.reading", { + header: "Reading", + cell: (info) => + !isShowingAmount + ? calculateBandScore( + stats + .filter((x) => x.module === "reading" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.correct, 0), + stats + .filter((x) => x.module === "reading" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.total, 0), + "level", + info.row.original.focus || "academic", + ) || 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 + ? calculateBandScore( + stats + .filter((x) => x.module === "listening" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.correct, 0), + stats + .filter((x) => x.module === "listening" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.total, 0), + "level", + info.row.original.focus || "academic", + ) || 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 + ? calculateBandScore( + stats + .filter((x) => x.module === "writing" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.correct, 0), + stats + .filter((x) => x.module === "writing" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.total, 0), + "level", + info.row.original.focus || "academic", + ) || 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 + ? calculateBandScore( + stats + .filter((x) => x.module === "speaking" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.correct, 0), + stats + .filter((x) => x.module === "speaking" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.total, 0), + "level", + info.row.original.focus || "academic", + ) || 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 + ? calculateBandScore( + stats + .filter((x) => x.module === "level" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.correct, 0), + stats + .filter((x) => x.module === "level" && x.user === info.row.original.id) + .reduce((acc, curr) => acc + curr.score.total, 0), + "level", + info.row.original.focus || "academic", + ) || 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( + users, + 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 filterUsers = (data: StudentPerformanceItem[]) => { + const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id; + const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id; + + const filters: ((item: StudentPerformanceItem) => boolean)[] = []; + if (selectedCorporate !== null) filters.push(filterByCorporate); + if (selectedGroup !== null) filters.push(filterByGroup); + + return filters.reduce((d, f) => d.filter(f), data); + }; + + return ( +
+
+ + Show Utilization + + + +
+ +
+
+ +
+ Filters + ({ + value: x?.id || "N/A", + label: x?.name || "N/A", + }))} + isClearable + value={ + selectedGroup === null + ? null + : { + value: selectedGroup?.id || "N/A", + label: selectedGroup?.name || "N/A", + } + } + placeholder="Select a Group..." + onChange={(value) => + !value + ? setSelectedGroup(null) + : setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value)) + } + /> +
+
+
+
+ + data={filterUsers( + items.sort( + (a, b) => + averageLevelCalculator( + users, + stats.filter((x) => x.user === b.id), + ) - + averageLevelCalculator( + users, + stats.filter((x) => x.user === a.id), + ), + ), + )} + columns={columns} + /> +
+ ); +}; + +export default StudentPerformanceList; diff --git a/src/dashboards/MasterCorporate/StudentPerformancePage.tsx b/src/dashboards/MasterCorporate/StudentPerformancePage.tsx new file mode 100644 index 00000000..72136e8a --- /dev/null +++ b/src/dashboards/MasterCorporate/StudentPerformancePage.tsx @@ -0,0 +1,46 @@ +import useAssignments from "@/hooks/useAssignments"; +import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; +import useGroups from "@/hooks/useGroups"; +import useUsers, {userHashCorporate, userHashStudent} from "@/hooks/useUsers"; +import {Stat, User} from "@/interfaces/user"; +import clsx from "clsx"; +import {useRouter} from "next/router"; +import {BsArrowLeft, BsArrowRepeat} from "react-icons/bs"; +import StudentPerformanceList from "./StudentPerformanceList"; + +interface Props { + user: User; +} + +const StudentPerformancePage = ({user}: Props) => { + const {users: students} = useUsers(userHashStudent); + const {users: corporates} = useUsers(userHashCorporate); + const {groups} = useGroups({admin: user.id, userType: user.type}); + const {data: stats} = useFilterRecordsByUser(); + + const {reload: reloadAssignments, isLoading: isAssignmentsLoading} = useAssignments({corporate: user.id}); + + const router = useRouter(); + + 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 +
+
+ Reload + +
+
+ + + ); +}; + +export default StudentPerformancePage; diff --git a/src/dashboards/MasterCorporate/index.tsx b/src/dashboards/MasterCorporate/index.tsx new file mode 100644 index 00000000..2ca8b282 --- /dev/null +++ b/src/dashboards/MasterCorporate/index.tsx @@ -0,0 +1,442 @@ +/* eslint-disable @next/next/no-img-element */ +import Modal from "@/components/Modal"; +import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; +import useUsers from "@/hooks/useUsers"; +import {CorporateUser, MasterCorporateUser, Stat, User} from "@/interfaces/user"; +import UserList from "@/pages/(admin)/Lists/UserList"; +import {dateSorter} from "@/utils"; +import moment from "moment"; +import {useEffect, useState, useMemo} from "react"; +import { + BsArrowLeft, + BsClipboard2Data, + BsClock, + BsPaperclip, + BsPersonFill, + BsPencilSquare, + BsPersonCheck, + BsPeople, + BsBank, + BsEnvelopePaper, + BsArrowRepeat, + BsPersonFillGear, + BsDatabase, +} from "react-icons/bs"; +import UserCard from "@/components/UserCard"; +import useGroups from "@/hooks/useGroups"; + +import {averageLevelCalculator, calculateAverageLevel} from "@/utils/score"; +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 useAssignments from "@/hooks/useAssignments"; +import {Assignment} from "@/interfaces/results"; +import clsx from "clsx"; +import {getCorporateUser} from "@/resources/user"; +import {groupBy, uniqBy} from "lodash"; +import MasterStatistical from "./MasterStatistical"; +import {activeAssignmentFilter} from "@/utils/assignments"; +import useUserBalance from "@/hooks/useUserBalance"; +import AssignmentsPage from "../views/AssignmentsPage"; +import StudentPerformanceList from "./StudentPerformanceList"; +import StudentPerformancePage from "./StudentPerformancePage"; +import MasterStatisticalPage from "./MasterStatisticalPage"; + +interface Props { + user: MasterCorporateUser; +} + +const studentHash = { + type: "student", + size: 25, + orderBy: "registrationDate", +}; + +const teacherHash = { + type: "teacher", + size: 25, + orderBy: "registrationDate", +}; + +const corporateHash = { + type: "corporate", + size: 25, + orderBy: "registrationDate", +}; + +export default function MasterCorporateDashboard({user}: Props) { + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); + const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]); + + const {data: stats} = useFilterRecordsByUser(); + + const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash); + const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash); + const {users: corporates, total: totalCorporate, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(corporateHash); + + const {groups} = useGroups({admin: user.id, userType: user.type}); + const {balance} = useUserBalance(); + + 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(); + + useEffect(() => { + setShowModal(!!selectedUser && router.asPath === "/"); + }, [selectedUser, router.asPath]); + + useEffect(() => { + setCorporateAssignments( + assignments.filter(activeAssignmentFilter).map((a) => { + const assigner = [...teachers, ...corporates].find((x) => x.id === a.assigner); + + return { + ...a, + corporate: assigner ? getCorporateUser(assigner, [...teachers, ...corporates], groups) : undefined, + }; + }), + ); + }, [assignments, groups, teachers, corporates]); + 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 GroupsList = () => { + 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.length})

+
+ + + + ); + }; + + if (router.asPath === "/#studentsPerformance") return ; + if (router.asPath === "/#statistical") return ; + if (router.asPath === "/#groups") return ; + + 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 === "/#assignments") + return ( + router.push("/")} + /> + ); + + if (router.asPath === "/#corporate") + 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 +
+

Corporate ({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})

+
+ )} + /> + ); + + return ( + <> + setSelectedUser(undefined)}> + <> + {selectedUser && ( +
+ { + setSelectedUser(undefined); + if (shouldReload && selectedUser!.type === "student") reloadStudents(); + if (shouldReload && selectedUser!.type === "teacher") reloadTeachers(); + if (shouldReload && selectedUser!.type === "corporate") reloadCorporates(); + }} + 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 + } + user={selectedUser} + /> +
+ )} + +
+ + <> +
+ router.push("/#students")} + Icon={BsPersonFill} + isLoading={isStudentsLoading} + label="Students" + value={totalStudents} + color="purple" + /> + router.push("/#teachers")} + Icon={BsPencilSquare} + isLoading={isTeachersLoading} + 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("/#corporate")} + /> + + 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) => ( + + ))} +
+
+
+ + + ); +} diff --git a/src/dashboards/MasterStatistical.tsx b/src/dashboards/MasterStatistical.tsx deleted file mode 100644 index 1b6e2261..00000000 --- a/src/dashboards/MasterStatistical.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import React from "react"; -import { CorporateUser, User } from "@/interfaces/user"; -import { BsFileExcel, BsBank, BsPersonFill } from "react-icons/bs"; -import IconCard from "./IconCard"; - -import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates"; -import ReactDatePicker from "react-datepicker"; - -import moment from "moment"; -import { AssignmentWithCorporateId } from "@/interfaces/results"; -import { - flexRender, - createColumnHelper, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; -import Checkbox from "@/components/Low/Checkbox"; -import { useListSearch } from "@/hooks/useListSearch"; -import axios from "axios"; -import { toast } from "react-toastify"; -import Button from "@/components/Low/Button"; - -interface GroupedCorporateUsers { - // list of user Ids - [key: string]: string[]; -} - -interface Props { - corporateUsers: GroupedCorporateUsers; - users: User[]; -} - -interface TableData { - user: string; - email: string; - correct: number; - corporate: string; - submitted: boolean; - date: moment.Moment; - assignment: string; - corporateId: string; -} - -interface UserCount { - userCount: number; - maxUserCount: number; -} - -const searchFilters = [["email"], ["user"], ["userId"]]; - -const MasterStatistical = (props: Props) => { - const { users, corporateUsers } = props; - - // const corporateRelevantUsers = React.useMemo( - // () => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[], - // [corporateUsers] - // ); - - const corporates = React.useMemo( - () => Object.values(corporateUsers).flat(), - [corporateUsers] - ); - - const [selectedCorporates, setSelectedCorporates] = - React.useState(corporates); - const [startDate, setStartDate] = React.useState( - moment("01/01/2023").toDate() - ); - const [endDate, setEndDate] = React.useState( - moment().endOf("year").toDate() - ); - - const { assignments } = useAssignmentsCorporates({ - // corporates: [...corporates, "tYU0HTiJdjMsS8SB7XJsUdMMP892"], - corporates: selectedCorporates, - startDate, - endDate, - }); - - const [downloading, setDownloading] = React.useState(false); - - 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 || "", - email: userData?.email || "", - userId: assignee, - corporateId: a.corporateId, - corporate, - assignment: a.name, - }; - if (userStats.length === 0) { - return { - ...commonData, - correct: 0, - submitted: false, - // date: moment(), - }; - } - - 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] - ); - - const getCorporateScores = (corporateId: string): UserCount => { - const corporateAssignmentsUsers = assignments - .filter((a) => a.corporateId === corporateId) - .reduce((acc, a) => acc + a.assignees.length, 0); - - const corporateResults = tableResults.filter( - (r) => r.corporateId === corporateId - ).length; - - return { - maxUserCount: corporateAssignmentsUsers, - userCount: corporateResults, - }; - }; - - const getCorporatesScoresHash = (data: string[]) => - data.reduce( - (accm, id) => ({ - ...accm, - [id]: getCorporateScores(id), - }), - {} - ) as Record; - - const getConsolidateScore = (data: Record) => - Object.values(data).reduce( - (acc: UserCount, { userCount, maxUserCount }: UserCount) => ({ - userCount: acc.userCount + userCount, - maxUserCount: acc.maxUserCount + maxUserCount, - }), - { userCount: 0, maxUserCount: 0 } - ); - - const corporateScores = getCorporatesScoresHash(corporates); - const consolidateScore = getConsolidateScore(corporateScores); - - const getConsolidateScoreStr = (data: UserCount) => - `${data.userCount}/${data.maxUserCount}`; - - const columnHelper = createColumnHelper(); - - const defaultColumns = [ - columnHelper.accessor("user", { - header: "User", - id: "user", - cell: (info) => { - return {info.getValue()}; - }, - }), - columnHelper.accessor("email", { - header: "Email", - id: "email", - cell: (info) => { - return {info.getValue()}; - }, - }), - columnHelper.accessor("corporate", { - header: "Corporate", - id: "corporate", - cell: (info) => { - return {info.getValue()}; - }, - }), - columnHelper.accessor("assignment", { - header: "Assignment", - id: "assignment", - cell: (info) => { - return {info.getValue()}; - }, - }), - columnHelper.accessor("submitted", { - header: "Submitted", - id: "submitted", - cell: (info) => { - return ( - {}}> - - - ); - }, - }), - columnHelper.accessor("correct", { - header: "Correct", - id: "correct", - cell: (info) => { - return {info.getValue()}; - }, - }), - columnHelper.accessor("date", { - header: "Date", - id: "date", - cell: (info) => { - const date = info.getValue(); - if (date) { - return {date.format("DD/MM/YYYY")}; - } - - return {""}; - }, - }), - ]; - - const { - rows: filteredRows, - renderSearch, - text: searchText, - } = useListSearch(searchFilters, tableResults); - - const table = useReactTable({ - data: filteredRows, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }); - - const areAllSelected = selectedCorporates.length === corporates.length; - - const getStudentsConsolidateScore = () => { - if (tableResults.length === 0) { - return { highest: null, lowest: null }; - } - - // Find the student with the highest and lowest score - return tableResults.reduce( - (acc, curr) => { - if (curr.correct > acc.highest.correct) { - acc.highest = curr; - } - if (curr.correct < acc.lowest.correct) { - acc.lowest = curr; - } - return acc; - }, - { highest: tableResults[0], lowest: tableResults[0] } - ); - }; - - const triggerDownload = async () => { - try { - setDownloading(true); - const res = await axios.post("/api/assignments/statistical/excel", { - ids: selectedCorporates, - ...(startDate ? { startDate: startDate.toISOString() } : {}), - ...(endDate ? { endDate: endDate.toISOString() } : {}), - searchText, - }); - toast.success("Report ready!"); - const link = document.createElement("a"); - link.href = res.data; - // download should have worked but there are some CORS issues - // https://firebase.google.com/docs/storage/web/download-files#cors_configuration - // link.download="report.pdf"; - link.target = "_blank"; - link.rel = "noreferrer"; - link.click(); - setDownloading(false); - } catch (err) { - toast.error("Failed to display the report!"); - console.error(err); - setDownloading(false); - } - }; - - const consolidateResults = getStudentsConsolidateScore(); - return ( - <> -
- { - if (areAllSelected) { - setSelectedCorporates([]); - return; - } - setSelectedCorporates(corporates); - }} - isSelected={areAllSelected} - /> - {Object.keys(corporateUsers).map((corporateName) => { - const group = corporateUsers[corporateName]; - const isSelected = group.every((id) => - selectedCorporates.includes(id) - ); - - const valueHash = getCorporatesScoresHash(group); - const value = getConsolidateScoreStr(getConsolidateScore(valueHash)); - return ( - { - if (isSelected) { - setSelectedCorporates((prev) => - prev.filter((x) => !group.includes(x)) - ); - return; - } - setSelectedCorporates((prev) => [ - ...new Set([...prev, ...group]), - ]); - }} - isSelected={isSelected} - /> - ); - })} -
-
-
- - { - setStartDate(initialDate ?? moment("01/01/2023").toDate()); - if (finalDate) { - // basicly selecting a final day works as if I'm selecting the first - // minute of that day. this way it covers the whole day - setEndDate(moment(finalDate).endOf("day").toDate()); - return; - } - setEndDate(null); - }} - /> -
- {renderSearch()} -
- -
-
- -
- - - {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() - )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
-
- {consolidateResults.highest && ( - {}} - Icon={BsPersonFill} - label={`Highest result: ${consolidateResults.highest.user}`} - color="purple" - /> - )} - {consolidateResults.lowest && ( - {}} - Icon={BsPersonFill} - label={`Lowest result: ${consolidateResults.lowest.user}`} - color="purple" - /> - )} -
- - ); -}; - -export default MasterStatistical; diff --git a/src/hooks/useUsers.tsx b/src/hooks/useUsers.tsx index 64fd920e..c3c4f5cc 100644 --- a/src/hooks/useUsers.tsx +++ b/src/hooks/useUsers.tsx @@ -9,7 +9,7 @@ export const userHashStudent = {type: "student"} as {type: Type}; export const userHashTeacher = {type: "teacher"} as {type: Type}; export const userHashCorporate = {type: "corporate"} as {type: Type}; -export default function useUsers(props?: {type?: Type; page?: number; size?: number}) { +export default function useUsers(props?: {type?: string; page?: number; size?: number; orderBy?: string; direction?: "asc" | "desc"}) { const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [isLoading, setIsLoading] = useState(false); @@ -35,7 +35,7 @@ export default function useUsers(props?: {type?: Type; page?: number; size?: num .finally(() => setIsLoading(false)); }; - useEffect(getData, [props?.page, props?.size, props?.type]); + useEffect(getData, [props?.page, props?.size, props?.type, props?.orderBy, props?.direction]); return {users, total, isLoading, isError, reload: getData}; } diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index d0a39c3f..41d509ca 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -13,8 +13,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const {size, type, page} = req.query as {size?: string; type?: Type; page?: string}; - console.log(size, type, page); + const { + size, + type, + page, + orderBy, + direction = "desc", + } = req.query as {size?: string; type?: Type; page?: string; orderBy?: string; direction?: "asc" | "desc"}; const {users, total} = await getLinkedUsers( req.session.user?.id, @@ -22,6 +27,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { type, page !== undefined ? parseInt(page) : undefined, size !== undefined ? parseInt(size) : undefined, + orderBy, + direction, ); res.status(200).json({users, total}); diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index c85144b6..c6343c21 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -44,7 +44,15 @@ export async function getSpecificUsers(ids: string[]) { .toArray(); } -export async function getLinkedUsers(userID?: string, userType?: Type, type?: Type, page?: number, size?: number) { +export async function getLinkedUsers( + userID?: string, + userType?: Type, + type?: Type, + page?: number, + size?: number, + sort?: string, + direction?: "asc" | "desc", +) { const filters = { ...(!!type ? {type} : {}), }; @@ -53,6 +61,7 @@ export async function getLinkedUsers(userID?: string, userType?: Type, type?: Ty const users = await db .collection("users") .find(filters) + .sort(sort ? {[sort]: direction === "desc" ? -1 : 1} : {}) .skip(page && size ? page * size : 0) .limit(size || 0) .toArray();