Merge branch 'main' into develop

This commit is contained in:
Tiago Ribeiro
2024-09-09 17:52:04 +01:00
11 changed files with 494 additions and 402 deletions

View File

@@ -1,14 +1,37 @@
import {Column, flexRender, getCoreRowModel, getSortedRowModel, useReactTable} from "@tanstack/react-table"; 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<T>({data, columns}: {data: T[]; columns: any[]}) { export default function List<T>({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({ const table = useReactTable({
data, data: items,
columns: columns, columns: columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
}); });
return ( return (
<div className="w-full h-full flex flex-col gap-2">
<div className="w-full flex gap-2 justify-between">
<Button className="w-full max-w-[200px]" disabled={page === 0} onClick={() => setPage((prev) => prev - 1)}>
Previous Page
</Button>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{page * SIZE + 1} - {(page + 1) * SIZE > data.length ? data.length : (page + 1) * SIZE} / {data.length}
</span>
<Button className="w-[200px]" disabled={(page + 1) * SIZE >= data.length} onClick={() => setPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full"> <table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead> <thead>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@@ -19,7 +42,9 @@ export default function List<T>({data, columns}: {data: T[]; columns: any[]}) {
<> <>
<div <div
{...{ {...{
className: header.column.getCanSort() ? "cursor-pointer select-none py-4 text-left first:pl-4" : "", className: header.column.getCanSort()
? "cursor-pointer select-none py-4 text-left first:pl-4"
: "",
onClick: header.column.getToggleSortingHandler(), onClick: header.column.getToggleSortingHandler(),
}}> }}>
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(header.column.columnDef.header, header.getContext())}
@@ -47,5 +72,6 @@ export default function List<T>({data, columns}: {data: T[]; columns: any[]}) {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
); );
} }

View File

@@ -63,7 +63,7 @@ export default function AdminDashboard({user}: Props) {
const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash); const {users: students, total: totalStudents, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(studentHash);
const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash); const {users: teachers, total: totalTeachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(teacherHash);
const {users: corporates, total: totalCorporate, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(corporateHash); const {users: corporates, total: totalCorporate, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(corporateHash);
const {users: agents, total: totalAgents, reload: reloadAgents, isLoading: isAgentsLoading} = useUsers(corporateHash); const {users: agents, total: totalAgents, reload: reloadAgents, isLoading: isAgentsLoading} = useUsers(agentsHash);
const {groups} = useGroups({}); const {groups} = useGroups({});
const {pending, done} = usePaymentStatusUsers(); const {pending, done} = usePaymentStatusUsers();

View File

@@ -21,6 +21,7 @@ import Checkbox from "@/components/Low/Checkbox";
import {InstructorGender, Variant} from "@/interfaces/exam"; import {InstructorGender, Variant} from "@/interfaces/exam";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import useExams from "@/hooks/useExams"; import useExams from "@/hooks/useExams";
import {useListSearch} from "@/hooks/useListSearch";
interface Props { interface Props {
isCreating: boolean; isCreating: boolean;
@@ -31,7 +32,12 @@ interface Props {
cancelCreation: () => void; cancelCreation: () => void;
} }
const SIZE = 12;
export default function AssignmentCreator({isCreating, assignment, user, groups, users, cancelCreation}: Props) { 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<Module[]>(assignment?.exams.map((e) => e.module) || []); const [selectedModules, setSelectedModules] = useState<Module[]>(assignment?.exams.map((e) => e.module) || []);
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []); const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
const [teachers, setTeachers] = useState<string[]>(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]); const [teachers, setTeachers] = useState<string[]>(!!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 userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]);
const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [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(() => { useEffect(() => {
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
}, [selectedModules]); }, [selectedModules]);
@@ -347,9 +376,9 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
</div> </div>
)} )}
<section className="w-full flex flex-col gap-3"> <section className="w-full flex flex-col gap-4">
<span className="font-semibold">Assignees ({assignees.length} selected)</span> <span className="font-semibold">Assignees ({assignees.length} selected)</span>
<div className="flex gap-4 overflow-x-scroll scrollbar-hide"> <div className="grid grid-cols-5 gap-4">
{groups.map((g) => ( {groups.map((g) => (
<button <button
key={g.id} key={g.id}
@@ -371,8 +400,11 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
</button> </button>
))} ))}
</div> </div>
{renderStudentSearch()}
<div className="flex flex-wrap -md:justify-center gap-4"> <div className="flex flex-wrap -md:justify-center gap-4">
{userStudents.map((user) => ( {studentRows.map((user) => (
<div <div
onClick={() => toggleAssignee(user)} onClick={() => toggleAssignee(user)}
className={clsx( className={clsx(
@@ -402,12 +434,32 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
</div> </div>
))} ))}
</div> </div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={studentsPage === 0} onClick={() => setStudentsPage((prev) => prev - 1)}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{studentsPage * SIZE + 1} -{" "}
{(studentsPage + 1) * SIZE > filteredStudentsRows.length ? filteredStudentsRows.length : (studentsPage + 1) * SIZE} /{" "}
{filteredStudentsRows.length}
</span>
<Button
className="w-[200px]"
disabled={(studentsPage + 1) * SIZE >= filteredStudentsRows.length}
onClick={() => setStudentsPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
</section> </section>
{user.type !== "teacher" && ( {user.type !== "teacher" && (
<section className="w-full flex flex-col gap-3"> <section className="w-full flex flex-col gap-3">
<span className="font-semibold">Teachers ({teachers.length} selected)</span> <span className="font-semibold">Teachers ({teachers.length} selected)</span>
<div className="flex gap-4 overflow-x-scroll scrollbar-hide"> <div className="grid grid-cols-5 gap-4">
{groups.map((g) => ( {groups.map((g) => (
<button <button
key={g.id} key={g.id}
@@ -429,8 +481,11 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
</button> </button>
))} ))}
</div> </div>
{renderTeacherSearch()}
<div className="flex flex-wrap -md:justify-center gap-4"> <div className="flex flex-wrap -md:justify-center gap-4">
{userTeachers.map((user) => ( {teacherRows.map((user) => (
<div <div
onClick={() => toggleTeacher(user)} onClick={() => toggleTeacher(user)}
className={clsx( className={clsx(
@@ -453,6 +508,29 @@ export default function AssignmentCreator({isCreating, assignment, user, groups,
</div> </div>
))} ))}
</div> </div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={teachersPage === 0} onClick={() => setTeachersPage((prev) => prev - 1)}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{teachersPage * SIZE + 1} -{" "}
{(teachersPage + 1) * SIZE > filteredTeachersRows.length
? filteredTeachersRows.length
: (teachersPage + 1) * SIZE}{" "}
/ {filteredTeachersRows.length}
</span>
<Button
className="w-[200px]"
disabled={(teachersPage + 1) * SIZE >= filteredTeachersRows.length}
onClick={() => setTeachersPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
</section> </section>
)} )}

View File

@@ -1,5 +1,5 @@
import React from "react"; import React, {useEffect, useMemo, useState} from "react";
import {CorporateUser, User} from "@/interfaces/user"; import {CorporateUser, StudentUser, User} from "@/interfaces/user";
import {BsFileExcel, BsBank, BsPersonFill} from "react-icons/bs"; import {BsFileExcel, BsBank, BsPersonFill} from "react-icons/bs";
import IconCard from "../IconCard"; import IconCard from "../IconCard";
@@ -14,6 +14,7 @@ import {useListSearch} from "@/hooks/useListSearch";
import axios from "axios"; import axios from "axios";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import {getUserName} from "@/utils/users";
interface GroupedCorporateUsers { interface GroupedCorporateUsers {
// list of user Ids // list of user Ids
@@ -27,7 +28,7 @@ interface Props {
} }
interface TableData { interface TableData {
user: string; user: User | undefined;
email: string; email: string;
correct: number; correct: number;
corporate: string; corporate: string;
@@ -42,10 +43,13 @@ interface UserCount {
maxUserCount: number; maxUserCount: number;
} }
const searchFilters = [["email"], ["user"], ["userId"]]; const searchFilters = [["email"], ["user"], ["userId"], ["exams"], ["assignment"]];
const SIZE = 16;
const MasterStatistical = (props: Props) => { const MasterStatistical = (props: Props) => {
const {users, corporateUsers, displaySelection = true} = props; const {users, corporateUsers, displaySelection = true} = props;
const [page, setPage] = useState(0);
// const corporateRelevantUsers = React.useMemo( // const corporateRelevantUsers = React.useMemo(
// () => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[], // () => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[],
@@ -58,8 +62,7 @@ const MasterStatistical = (props: Props) => {
const [startDate, setStartDate] = React.useState<Date | null>(moment("01/01/2023").toDate()); const [startDate, setStartDate] = React.useState<Date | null>(moment("01/01/2023").toDate());
const [endDate, setEndDate] = React.useState<Date | null>(moment().endOf("year").toDate()); const [endDate, setEndDate] = React.useState<Date | null>(moment().endOf("year").toDate());
const {assignments} = useAssignmentsCorporates({ const {assignments, isLoading} = useAssignmentsCorporates({
// corporates: [...corporates, "tYU0HTiJdjMsS8SB7XJsUdMMP892"],
corporates: selectedCorporates, corporates: selectedCorporates,
startDate, startDate,
endDate, endDate,
@@ -73,12 +76,15 @@ const MasterStatistical = (props: Props) => {
const userResults = a.assignees.map((assignee) => { const userResults = a.assignees.map((assignee) => {
const userStats = a.results.find((r) => r.user === assignee)?.stats || []; const userStats = a.results.find((r) => r.user === assignee)?.stats || [];
const userData = users.find((u) => u.id === assignee); const userData = users.find((u) => u.id === assignee);
const corporate = users.find((u) => u.id === a.assigner)?.name || ""; if (!!userData) console.log(assignee, userData.name);
const corporate = getUserName(users.find((u) => u.id === a.assigner));
const commonData = { const commonData = {
user: userData?.name || "", user: userData,
email: userData?.email || "", email: userData?.email || "N/A",
userId: assignee, userId: assignee,
corporateId: a.corporateId, corporateId: a.corporateId,
exams: a.exams.map((x) => x.id).join(", "),
corporate, corporate,
assignment: a.name, assignment: a.name,
}; };
@@ -87,7 +93,7 @@ const MasterStatistical = (props: Props) => {
...commonData, ...commonData,
correct: 0, correct: 0,
submitted: false, submitted: false,
// date: moment(), date: null,
}; };
} }
@@ -104,6 +110,8 @@ const MasterStatistical = (props: Props) => {
[assignments, users], [assignments, users],
); );
useEffect(() => console.log(assignments), [assignments]);
const getCorporateScores = (corporateId: string): UserCount => { const getCorporateScores = (corporateId: string): UserCount => {
const corporateAssignmentsUsers = assignments.filter((a) => a.corporateId === corporateId).reduce((acc, a) => acc + a.assignees.length, 0); const corporateAssignmentsUsers = assignments.filter((a) => a.corporateId === corporateId).reduce((acc, a) => acc + a.assignees.length, 0);
@@ -145,7 +153,7 @@ const MasterStatistical = (props: Props) => {
header: "User", header: "User",
id: "user", id: "user",
cell: (info) => { cell: (info) => {
return <span>{info.getValue()}</span>; return <span>{info.getValue()?.name || "N/A"}</span>;
}, },
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
@@ -155,6 +163,13 @@ const MasterStatistical = (props: Props) => {
return <span>{info.getValue()}</span>; return <span>{info.getValue()}</span>;
}, },
}), }),
columnHelper.accessor("user", {
header: "Student ID",
id: "studentID",
cell: (info) => {
return <span>{(info.getValue() as StudentUser).studentID || "N/A"}</span>;
},
}),
...(displaySelection ...(displaySelection
? [ ? [
columnHelper.accessor("corporate", { columnHelper.accessor("corporate", {
@@ -166,13 +181,6 @@ const MasterStatistical = (props: Props) => {
}), }),
] ]
: []), : []),
columnHelper.accessor("corporate", {
header: "Corporate",
id: "corporate",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("assignment", { columnHelper.accessor("assignment", {
header: "Assignment", header: "Assignment",
id: "assignment", id: "assignment",
@@ -192,7 +200,7 @@ const MasterStatistical = (props: Props) => {
}, },
}), }),
columnHelper.accessor("correct", { columnHelper.accessor("correct", {
header: "Correct", header: "Score",
id: "correct", id: "correct",
cell: (info) => { cell: (info) => {
return <span>{info.getValue()}</span>; return <span>{info.getValue()}</span>;
@@ -204,7 +212,7 @@ const MasterStatistical = (props: Props) => {
cell: (info) => { cell: (info) => {
const date = info.getValue(); const date = info.getValue();
if (date) { if (date) {
return <span>{date.format("DD/MM/YYYY")}</span>; return <span>{!!date ? date.format("DD/MM/YYYY") : "N/A"}</span>;
} }
return <span>{""}</span>; return <span>{""}</span>;
@@ -214,8 +222,14 @@ const MasterStatistical = (props: Props) => {
const {rows: filteredRows, renderSearch, text: searchText} = useListSearch(searchFilters, tableResults); 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({ const table = useReactTable({
data: filteredRows, data: rows,
columns: defaultColumns, columns: defaultColumns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
@@ -276,6 +290,7 @@ const MasterStatistical = (props: Props) => {
<IconCard <IconCard
Icon={BsBank} Icon={BsBank}
label="Consolidate" label="Consolidate"
isLoading={isLoading}
value={getConsolidateScoreStr(consolidateScore)} value={getConsolidateScoreStr(consolidateScore)}
color="purple" color="purple"
onClick={() => { onClick={() => {
@@ -297,6 +312,7 @@ const MasterStatistical = (props: Props) => {
<IconCard <IconCard
key={corporateName} key={corporateName}
Icon={BsBank} Icon={BsBank}
isLoading={isLoading}
label={corporateName} label={corporateName}
value={value} value={value}
color="purple" color="purple"
@@ -338,13 +354,28 @@ const MasterStatistical = (props: Props) => {
</div> </div>
{renderSearch()} {renderSearch()}
<div className="flex flex-col gap-3 justify-end"> <div className="flex flex-col gap-3 justify-end">
<Button className="max-w-[200px] h-[70px]" variant="outline" onClick={triggerDownload}> <Button className="w-[200px] h-[70px]" variant="outline" isLoading={downloading} onClick={triggerDownload}>
Download Download
</Button> </Button>
</div> </div>
</div> </div>
<div> <div className="w-full h-full flex flex-col gap-4">
<div className="w-full flex gap-2 justify-between">
<Button className="w-full max-w-[200px]" disabled={page === 0} onClick={() => setPage((prev) => prev - 1)}>
Previous Page
</Button>
<div className="flex items-center gap-4 w-fit">
<span className="opacity-80">
{page * SIZE + 1} - {(page + 1) * SIZE > filteredRows.length ? filteredRows.length : (page + 1) * SIZE} /{" "}
{filteredRows.length}
</span>
<Button className="w-[200px]" disabled={(page + 1) * SIZE >= filteredRows.length} onClick={() => setPage((prev) => prev + 1)}>
Next Page
</Button>
</div>
</div>
<table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full"> <table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full">
<thead> <thead>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (

View File

@@ -83,18 +83,6 @@ export default function MasterCorporateDashboard({user}: Props) {
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); 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 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 appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -131,7 +119,6 @@ export default function MasterCorporateDashboard({user}: Props) {
const {users} = useUsers(); const {users} = useUsers();
const groupedByNameCorporates = useMemo( const groupedByNameCorporates = useMemo(
() => () =>
groupBy( groupBy(
@@ -374,12 +361,18 @@ export default function MasterCorporateDashboard({user}: Props) {
color="purple" color="purple"
onClick={() => router.push("/#corporate")} onClick={() => router.push("/#corporate")}
/> />
<IconCard Icon={BsBank} label="Corporate" value={groupedByNameCorporatesKeys.length} isLoading={isCorporatesLoading} color="purple" /> <IconCard
Icon={BsBank}
label="Corporate"
value={groupedByNameCorporatesKeys.length}
isLoading={isCorporatesLoading}
color="purple"
/>
<IconCard <IconCard
Icon={BsPersonFillGear} Icon={BsPersonFillGear}
isLoading={isStudentsLoading} isLoading={isStudentsLoading}
label="Student Performance" label="Student Performance"
value={students.length} value={totalStudents}
color="purple" color="purple"
onClick={() => router.push("/#studentsPerformance")} onClick={() => router.push("/#studentsPerformance")}
/> />

View File

@@ -12,7 +12,7 @@ import {calculateAverageLevel} from "@/utils/score";
import {sortByModuleName} from "@/utils/moduleUtils"; import {sortByModuleName} from "@/utils/moduleUtils";
import {capitalize} from "lodash"; import {capitalize} from "lodash";
import ProfileSummary from "@/components/ProfileSummary"; import ProfileSummary from "@/components/ProfileSummary";
import {Variant} from "@/interfaces/exam"; import {ShuffleMap, Shuffles, Variant} from "@/interfaces/exam";
import useSessions, {Session} from "@/hooks/useSessions"; import useSessions, {Session} from "@/hooks/useSessions";
import SessionCard from "@/components/Medium/SessionCard"; import SessionCard from "@/components/Medium/SessionCard";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
@@ -41,6 +41,7 @@ export default function Selection({user, page, onStart, disableSelection = false
}; };
const loadSession = async (session: Session) => { const loadSession = async (session: Session) => {
state.setShuffles(session.userSolutions.map((x) => ({exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : []})));
state.setSelectedModules(session.selectedModules); state.setSelectedModules(session.selectedModules);
state.setExam(session.exam); state.setExam(session.exam);
state.setExams(session.exams); state.setExams(session.exams);

View File

@@ -642,12 +642,12 @@ export default function UserList({
</Button> </Button>
<div className="flex items-center gap-4 w-fit"> <div className="flex items-center gap-4 w-fit">
<span className="opacity-80"> <span className="opacity-80">
{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}
</span> </span>
<Button <Button
isLoading={isLoading} isLoading={isLoading}
className="w-[200px]" className="w-[200px]"
disabled={page * 16 >= total} disabled={(page + 1) * userHash.size >= total}
onClick={() => setPage((prev) => prev + 1)}> onClick={() => setPage((prev) => prev + 1)}>
Next Page Next Page
</Button> </Button>

View File

@@ -11,7 +11,7 @@ import { checkAccess } from "@/utils/permissions";
import {getAssignmentsForCorporates} from "@/utils/assignments.be"; import {getAssignmentsForCorporates} from "@/utils/assignments.be";
import {search} from "@/utils/search"; import {search} from "@/utils/search";
import {getGradingSystem} from "@/utils/grading.be"; import {getGradingSystem} from "@/utils/grading.be";
import { User } from "@/interfaces/user"; import {StudentUser, User} from "@/interfaces/user";
import {calculateBandScore, getGradingLabel} from "@/utils/score"; import {calculateBandScore, getGradingLabel} from "@/utils/score";
import {Module} from "@/interfaces"; import {Module} from "@/interfaces";
@@ -19,6 +19,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
interface TableData { interface TableData {
user: string; user: string;
studentID: string;
email: string; email: string;
correct: number; correct: number;
corporate: string; corporate: string;
@@ -45,9 +46,7 @@ const searchFilters = [["email"], ["user"], ["userId"]];
async function post(req: NextApiRequest, res: NextApiResponse) { async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export // verify if it's a logged user that is trying to export
if (req.session.user) { if (req.session.user) {
if ( if (!checkAccess(req.session.user, ["mastercorporate", "corporate", "developer", "admin"])) {
!checkAccess(req.session.user, ["mastercorporate", "corporate", "developer", "admin"])
) {
return res.status(403).json({error: "Unauthorized"}); return res.status(403).json({error: "Unauthorized"});
} }
const { const {
@@ -65,15 +64,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}; };
const startDateParsed = startDate ? new Date(startDate) : undefined; const startDateParsed = startDate ? new Date(startDate) : undefined;
const endDateParsed = endDate ? new Date(endDate) : undefined; const endDateParsed = endDate ? new Date(endDate) : undefined;
const assignments = await getAssignmentsForCorporates( const assignments = await getAssignmentsForCorporates(ids, startDateParsed, endDateParsed);
ids,
startDateParsed,
endDateParsed
);
const assignmentUsers = [ const assignmentUsers = [...new Set(assignments.flatMap((a) => a.assignees))];
...new Set(assignments.flatMap((a) => a.assignees)),
];
const assigners = [...new Set(assignments.map((a) => a.assigner))]; const assigners = [...new Set(assignments.map((a) => a.assigner))];
const users = await getSpecificUsers(assignmentUsers); const users = await getSpecificUsers(assignmentUsers);
const assignerUsers = await getSpecificUsers(assigners); const assignerUsers = await getSpecificUsers(assigners);
@@ -83,7 +76,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const data = await getGradingSystem(user); const data = await getGradingSystem(user);
// in this context I need to override as I'll have to match to the assigner // in this context I need to override as I'll have to match to the assigner
return {...data, user: user.id}; return {...data, user: user.id};
}) }),
); );
const getGradingSystemHelper = ( const getGradingSystemHelper = (
@@ -91,19 +84,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
assigner: string, assigner: string,
user: User, user: User,
correct: number, correct: number,
total: number total: number,
) => { ) => {
if (exams.some((e) => e.module === "level")) { if (exams.some((e) => e.module === "level")) {
const gradingSystem = assignerUsersGradingSystems.find( const gradingSystem = assignerUsersGradingSystems.find((gs) => gs.user === assigner);
(gs) => gs.user === assigner
);
if (gradingSystem) { if (gradingSystem) {
const bandScore = calculateBandScore( const bandScore = calculateBandScore(correct, total, "level", user?.focus || "academic");
correct,
total,
"level",
user.focus
);
return { return {
label: getGradingLabel(bandScore, gradingSystem?.steps || []), label: getGradingLabel(bandScore, gradingSystem?.steps || []),
score: bandScore, score: bandScore,
@@ -117,24 +103,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const tableResults = assignments const tableResults = assignments
.reduce((accmA: TableData[], a: AssignmentWithCorporateId) => { .reduce((accmA: TableData[], a: AssignmentWithCorporateId) => {
const userResults = a.assignees.map((assignee) => { const userResults = a.assignees.map((assignee) => {
const userStats = const userStats = a.results.find((r) => r.user === assignee)?.stats || [];
a.results.find((r) => r.user === assignee)?.stats || [];
const userData = users.find((u) => u.id === assignee); const userData = users.find((u) => u.id === assignee);
const corporateUser = users.find((u) => u.id === a.assigner); const corporateUser = users.find((u) => u.id === a.assigner);
const correct = userStats.reduce((n, e) => n + e.score.correct, 0); const correct = userStats.reduce((n, e) => n + e.score.correct, 0);
const total = userStats.reduce((n, e) => n + e.score.total, 0); const total = userStats.reduce((n, e) => n + e.score.total, 0);
const { label: level, score } = getGradingSystemHelper( const {label: level, score} = getGradingSystemHelper(a.exams, a.assigner, userData!, correct, total);
a.exams,
a.assigner,
userData!,
correct,
total
);
console.log("Level", level);
const commonData = { const commonData = {
user: userData?.name || "", user: userData?.name || "",
email: userData?.email || "", email: userData?.email || "",
studentID: (userData as StudentUser).studentID || "",
userId: assignee, userId: assignee,
corporateId: a.corporateId, corporateId: a.corporateId,
corporate: corporateUser?.name || "", corporate: corporateUser?.name || "",
@@ -186,6 +165,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
label: "Email", label: "Email",
value: (entry: TableData) => entry.email, value: (entry: TableData) => entry.email,
}, },
{
label: "Student ID",
value: (entry: TableData) => entry.studentID,
},
...(displaySelection ...(displaySelection
? [ ? [
{ {
@@ -223,9 +206,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
})), })),
]; ];
const filteredSearch = searchText const filteredSearch = searchText ? search(searchText, searchFilters, tableResults) : tableResults;
? search(searchText, searchFilters, tableResults)
: tableResults;
worksheet.addRow(headers.map((h) => h.label)); worksheet.addRow(headers.map((h) => h.label));
(filteredSearch as TableData[]).forEach((entry) => { (filteredSearch as TableData[]).forEach((entry) => {
@@ -241,8 +222,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const fileRef = ref(storage, refName); const fileRef = ref(storage, refName);
// upload the pdf to storage // upload the pdf to storage
await uploadBytes(fileRef, buffer, { await uploadBytes(fileRef, buffer, {
contentType: contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}); });
const url = await getDownloadURL(fileRef); const url = await getDownloadURL(fileRef);

View File

@@ -50,11 +50,7 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
} }
const user = req.session.user; const user = req.session.user;
if ( if (user.type === "admin" || user.type === "developer" || user.id === group.admin) {
user.type === "admin" ||
user.type === "developer" ||
user.id === group.admin
) {
await db.collection("groups").deleteOne({id: id}); await db.collection("groups").deleteOne({id: id});
res.status(200).json({ok: true}); res.status(200).json({ok: true});
@@ -79,27 +75,13 @@ async function patch(req: NextApiRequest, res: NextApiResponse) {
} }
const user = req.session.user; const user = req.session.user;
if ( if (user.type === "admin" || user.type === "developer" || user.id === group.admin) {
user.type === "admin" ||
user.type === "developer" ||
user.id === group.admin
) {
if ("participants" in req.body) { if ("participants" in req.body) {
const newParticipants = (req.body.participants as string[]).filter( const newParticipants = (req.body.participants as string[]).filter((x) => !group.participants.includes(x));
(x) => !group.participants.includes(x), await Promise.all(newParticipants.map(async (p) => await updateExpiryDateOnGroup(p, group.admin)));
);
await Promise.all(
newParticipants.map(
async (p) => await updateExpiryDateOnGroup(p, group.admin),
),
);
} }
await db.collection("grading").updateOne( await db.collection("groups").updateOne({id: req.session.user.id}, {$set: {id, ...req.body}}, {upsert: true});
{ id: req.session.user.id },
{ $set: {id: id, ...req.body} },
{ upsert: true }
);
res.status(200).json({ok: true}); res.status(200).json({ok: true});
return; return;

View File

@@ -26,7 +26,14 @@ export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate
}; };
export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, endDate?: Date) => { 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<Assignment>({
assigner: {$in: ids},
...(!!startDate ? {startDate: {$gte: startDate.toISOString()}} : {}),
...(!!endDate ? {endDate: {$lte: endDate.toISOString()}} : {}),
})
.toArray();
}; };
export const getAssignmentsForCorporates = async (idsList: string[], startDate?: Date, endDate?: Date) => { export const getAssignmentsForCorporates = async (idsList: string[], startDate?: Date, endDate?: Date) => {
@@ -57,4 +64,4 @@ export const getAssignmentsForCorporates = async (idsList: string[], startDate?:
); );
return assignments.flat(); return assignments.flat();
} };

View File

@@ -1,8 +1,9 @@
import {app} from "@/firebase"; 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, User} from "@/interfaces/user";
import client from "@/lib/mongodb"; import client from "@/lib/mongodb";
import moment from "moment"; import moment from "moment";
import {getUser} from "./users.be"; import {getLinkedUsers, getUser} from "./users.be";
import {getSpecificUsers} from "./users.be"; import {getSpecificUsers} from "./users.be";
const db = client.db(process.env.MONGODB_DB); 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<string[]> => { export const getAllAssignersByCorporate = async (corporateID: string): Promise<string[]> => {
const groups = await getUserGroups(corporateID); const linkedTeachers = await getLinkedUsers(corporateID, "mastercorporate", "teacher");
const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))) const linkedCorporates = await getLinkedUsers(corporateID, "mastercorporate", "corporate");
.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,
),
);
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) => { export const getGroupsForUser = async (admin?: string, participant?: string) => {