From dc13a4a7b7f22a1515f1178f7db1efaf7ef0c5b1 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 30 Oct 2023 14:29:41 +0000 Subject: [PATCH] Updated the Dashboard for Corporate accounts --- src/constants/userPermissions.ts | 4 + src/dashboards/Corporate.tsx | 249 ++++++++++++++++++++++++ src/dashboards/IconCard.tsx | 30 +++ src/dashboards/Owner.tsx | 104 ++++------ src/interfaces/user.ts | 4 +- src/pages/api/reset/sendVerification.ts | 5 +- src/pages/api/stats/index.ts | 3 +- src/pages/index.tsx | 3 +- src/utils/stats.ts | 1 + 9 files changed, 333 insertions(+), 70 deletions(-) create mode 100644 src/dashboards/Corporate.tsx create mode 100644 src/dashboards/IconCard.tsx diff --git a/src/constants/userPermissions.ts b/src/constants/userPermissions.ts index 3fdb9e97..94ce5ba7 100644 --- a/src/constants/userPermissions.ts +++ b/src/constants/userPermissions.ts @@ -6,6 +6,7 @@ export const PERMISSIONS = { teacher: ["corporate", "developer", "owner"], corporate: ["owner", "developer"], owner: ["developer", "owner"], + agent: ["developer", "owner"], developer: ["developer"], }, deleteUser: { @@ -13,6 +14,7 @@ export const PERMISSIONS = { teacher: ["corporate", "developer", "owner"], corporate: ["owner", "developer"], owner: ["developer", "owner"], + agent: ["developer", "owner"], developer: ["developer"], }, updateUser: { @@ -20,6 +22,7 @@ export const PERMISSIONS = { teacher: ["corporate", "developer", "owner"], corporate: ["owner", "developer"], owner: ["developer", "owner"], + agent: ["developer", "owner"], developer: ["developer"], }, updateExpiryDate: { @@ -27,6 +30,7 @@ export const PERMISSIONS = { teacher: ["developer", "owner"], corporate: ["owner", "developer"], owner: ["developer", "owner"], + agent: ["developer", "owner"], developer: ["developer"], }, examManagement: { diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx new file mode 100644 index 00000000..2e9dfaa1 --- /dev/null +++ b/src/dashboards/Corporate.tsx @@ -0,0 +1,249 @@ +/* eslint-disable @next/next/no-img-element */ +import Modal from "@/components/Modal"; +import useStats from "@/hooks/useStats"; +import useUsers from "@/hooks/useUsers"; +import {Group, Stat, User} from "@/interfaces/user"; +import UserList from "@/pages/(admin)/Lists/UserList"; +import {dateSorter} from "@/utils"; +import moment from "moment"; +import {useEffect, useState} from "react"; +import { + BsArrowLeft, + BsClipboard2Data, + BsClipboard2DataFill, + BsClock, + BsGlobeCentralSouthAsia, + BsPaperclip, + BsPerson, + BsPersonFill, + BsPersonFillGear, + BsPersonGear, + BsPersonLinesFill, +} from "react-icons/bs"; +import UserCard from "@/components/UserCard"; +import useGroups from "@/hooks/useGroups"; +import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; +import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import {Module} from "@/interfaces"; +import {groupByExam} from "@/utils/stats"; +import IconCard from "./IconCard"; + +interface Props { + user: User; +} + +export default function CorporateDashboard({user}: Props) { + const [page, setPage] = useState(""); + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); + + const {stats} = useStats(); + const {users, reload} = useUsers(); + const {groups} = useGroups(user.id); + + useEffect(() => { + setShowModal(!!selectedUser && page === ""); + }, [selectedUser, page]); + + const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id); + const teacherFilter = (user: User) => user.type === "teacher" && groups.flatMap((g) => g.participants).includes(user.id); + + const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); + + const UserDisplay = (displayUser: User) => ( +
setSelectedUser(displayUser)} + className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> + {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + const StudentsList = () => { + const filter = (x: User) => + x.type === "student" && + (!!selectedUser + ? groups + .filter((g) => g.admin === selectedUser.id) + .flatMap((g) => g.participants) + .includes(x.id) || false + : groups.flatMap((g) => g.participants).includes(x.id)); + + return ( + <> +
+
setPage("")} + 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 ({users.filter(filter).length})

+
+ + + + ); + }; + + const TeachersList = () => { + const filter = (x: User) => + x.type === "teacher" && + (!!selectedUser + ? groups + .filter((g) => g.admin === selectedUser.id) + .flatMap((g) => g.participants) + .includes(x.id) || false + : groups.flatMap((g) => g.participants).includes(x.id)); + + return ( + <> +
+
setPage("")} + 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 ({users.filter(filter).length})

+
+ + + + ); + }; + + const averageLevelCalculator = (studentStats: Stat[]) => { + const formattedStats = studentStats + .map((s) => ({focus: users.find((u) => u.id === s.user)?.focus, score: s.score, module: s.module})) + .filter((f) => !!f.focus); + const bandScores = formattedStats.map((s) => ({ + module: s.module, + level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), + })); + + const levels: {[key in Module]: number} = {reading: 0, listening: 0, writing: 0, speaking: 0}; + bandScores.forEach((b) => (levels[b.module] += b.level)); + + return calculateAverageLevel(levels); + }; + + const DefaultDashboard = () => ( +
+
+ setPage("students")} + Icon={BsPersonFill} + label="Students" + value={users.filter(studentFilter).length} + color="purple" + /> + setPage("teachers")} + Icon={BsPersonLinesFill} + label="Teachers" + value={users.filter(teacherFilter).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" + /> + +
+ +
+
+ Latest students +
+ {users + .filter(studentFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {users + .filter(teacherFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {users + .filter(studentFilter) + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {users + .filter(studentFilter) + .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) reload(); + }} + onViewStudents={ + selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined + } + onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined} + {...selectedUser} + /> +
+ )} + +
+ {page === "students" && } + {page === "teachers" && } + {page === "" && } + + ); +} diff --git a/src/dashboards/IconCard.tsx b/src/dashboards/IconCard.tsx new file mode 100644 index 00000000..46b2ad15 --- /dev/null +++ b/src/dashboards/IconCard.tsx @@ -0,0 +1,30 @@ +import clsx from "clsx"; +import {IconType} from "react-icons"; + +interface Props { + Icon: IconType; + label: string; + value: string | number; + color: "purple" | "rose" | "red"; + onClick?: () => void; +} + +export default function IconCard({Icon, label, value, color, onClick}: Props) { + const colorClasses: {[key in typeof color]: string} = { + purple: "text-mti-purple-light", + red: "text-mti-red-light", + rose: "text-mti-rose-light", + }; + + return ( +
+ + + {label} + {value} + +
+ ); +} diff --git a/src/dashboards/Owner.tsx b/src/dashboards/Owner.tsx index 7f876bca..ffe11236 100644 --- a/src/dashboards/Owner.tsx +++ b/src/dashboards/Owner.tsx @@ -10,6 +10,7 @@ import {useEffect, useState} from "react"; import {BsArrowLeft, BsGlobeCentralSouthAsia, BsPerson, BsPersonFill, BsPersonFillGear, BsPersonGear, BsPersonLinesFill} from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; +import IconCard from "./IconCard"; interface Props { user: User; @@ -153,72 +154,53 @@ export default function OwnerDashboard({user}: Props) { const DefaultDashboard = () => ( <>
-
x.type === "student").length} onClick={() => setPage("students")} - className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> - - - Students - {users.filter((x) => x.type === "student").length} - -
-
+ x.type === "teacher").length} onClick={() => setPage("teachers")} - className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> - - - Teachers - {users.filter((x) => x.type === "teacher").length} - -
-
+ x.type === "corporate").length} onClick={() => setPage("corporate")} - className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> - - - Corporate - {users.filter((x) => x.type === "corporate").length} - -
-
- - - Countries - - {[...new Set(users.filter((x) => x.demographicInformation).map((x) => x.demographicInformation?.country))].length} - - -
-
+ x.demographicInformation).map((x) => x.demographicInformation?.country))].length} + color="purple" + /> + setPage("inactiveStudents")} - className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> - - - Inactive Students - - { - users.filter( - (x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)), - ).length - } - - -
-
x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) + .length + } + color="rose" + /> + setPage("inactiveCorporate")} - className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> - - - Inactive Corporate - - { - users.filter( - (x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)), - ).length - } - - -
+ Icon={BsPerson} + label="Inactive Corporate" + value={ + users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) + .length + } + color="rose" + />
diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 33fc7fe7..39c34479 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -77,5 +77,5 @@ export interface Group { disableEditing?: boolean; } -export type Type = "student" | "teacher" | "corporate" | "owner" | "developer"; -export const userTypes: Type[] = ["student", "teacher", "corporate", "owner", "developer"]; +export type Type = "student" | "teacher" | "corporate" | "owner" | "developer" | "agent"; +export const userTypes: Type[] = ["student", "teacher", "corporate", "owner", "developer", "agent"]; diff --git a/src/pages/api/reset/sendVerification.ts b/src/pages/api/reset/sendVerification.ts index 69e36b91..cf028f59 100644 --- a/src/pages/api/reset/sendVerification.ts +++ b/src/pages/api/reset/sendVerification.ts @@ -13,8 +13,6 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) { const short = new ShortUniqueId(); if (req.session.user) { - console.log("ME HERE"); - const transport = prepareMailer("verification"); const mailOptions = prepareMailOptions( { @@ -27,8 +25,7 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) { "verification", ); - const result = await transport.sendMail(mailOptions); - console.log(result); + await transport.sendMail(mailOptions); res.status(200).json({ok: true}); return; diff --git a/src/pages/api/stats/index.ts b/src/pages/api/stats/index.ts index 555ff86a..618aed16 100644 --- a/src/pages/api/stats/index.ts +++ b/src/pages/api/stats/index.ts @@ -21,8 +21,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { return; } - const user = req.session.user; - const q = query(collection(db, "stats"), where("user", "==", user.id)); + const q = query(collection(db, "stats")); const snapshot = await getDocs(q); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ea949562..29fa8ba5 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -24,6 +24,7 @@ import {MODULE_ARRAY} from "@/utils/moduleUtils"; import ProfileSummary from "@/components/ProfileSummary"; import StudentDashboard from "@/dashboards/Student"; import OwnerDashboard from "@/dashboards/Owner"; +import CorporateDashboard from "@/dashboards/Corporate"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -163,7 +164,7 @@ export default function Home() { {user.type === "student" && } {user.type === "teacher" && } - {user.type === "corporate" && } + {user.type === "corporate" && } {user.type === "owner" && } {user.type === "developer" && } diff --git a/src/utils/stats.ts b/src/utils/stats.ts index d42c5c2e..8f8e7e97 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -121,6 +121,7 @@ export const getExamsBySession = (stats: Stat[], session: string) => { export const groupBySession = (stats: Stat[]) => groupBy(stats, "session"); export const groupByDate = (stats: Stat[]) => groupBy(stats, "date"); +export const groupByExam = (stats: Stat[]) => groupBy(stats, "exam"); export const groupByModule = (stats: Stat[]) => groupBy(stats, "module"); export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {