Corrected a bug related to groups of a master corporate

This commit is contained in:
Tiago Ribeiro
2024-08-20 14:37:33 +01:00
parent 247f192a0a
commit 4315a7b17c
2 changed files with 304 additions and 367 deletions

View File

@@ -4,9 +4,9 @@ import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList"; import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils"; import {dateSorter} from "@/utils";
import moment from "moment"; import moment from "moment";
import { useEffect, useState } from "react"; import {useEffect, useState} from "react";
import { import {
BsArrowLeft, BsArrowLeft,
BsClipboard2Data, BsClipboard2Data,
@@ -22,7 +22,7 @@ import {
BsPlus, BsPlus,
BsPersonFillGear, BsPersonFillGear,
BsFilter, BsFilter,
BsDatabase BsDatabase,
} from "react-icons/bs"; } from "react-icons/bs";
import UserCard from "@/components/UserCard"; import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
@@ -34,10 +34,10 @@ import {groupByExam} from "@/utils/stats";
import IconCard from "./IconCard"; import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList"; import GroupList from "@/pages/(admin)/Lists/GroupList";
import useFilterStore from "@/stores/listFilterStore"; import useFilterStore from "@/stores/listFilterStore";
import { useRouter } from "next/router"; import {useRouter} from "next/router";
import useCodes from "@/hooks/useCodes"; import useCodes from "@/hooks/useCodes";
import useAssignments from "@/hooks/useAssignments"; import useAssignments from "@/hooks/useAssignments";
import { Assignment } from "@/interfaces/results"; import {Assignment} from "@/interfaces/results";
import AssignmentView from "./AssignmentView"; import AssignmentView from "./AssignmentView";
import AssignmentCreator from "./AssignmentCreator"; import AssignmentCreator from "./AssignmentCreator";
import clsx from "clsx"; import clsx from "clsx";
@@ -309,21 +309,11 @@ export default function MasterCorporateDashboard({user}: Props) {
const {codes} = useCodes(user.id); const {codes} = useCodes(user.id);
const {groups} = useGroups({admin: user.id, userType: user.type}); const {groups} = useGroups({admin: user.id, userType: user.type});
const masterCorporateUserGroups = [ const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
...new Set(
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
),
];
const corporateUserGroups = [ const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];
...new Set(groups.flatMap((g) => g.participants)),
];
const { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
assignments,
isLoading: isAssignmentsLoading,
reload: reloadAssignments,
} = useAssignments({ corporate: user.id });
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -350,13 +340,8 @@ export default function MasterCorporateDashboard({user}: Props) {
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
<div <div
onClick={() => setSelectedUser(displayUser)} onClick={() => 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" className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
> <img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span> <span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span> <span className="text-sm opacity-75">{displayUser.email}</span>
@@ -366,10 +351,7 @@ export default function MasterCorporateDashboard({user}: Props) {
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -379,8 +361,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -393,10 +374,7 @@ export default function MasterCorporateDashboard({user}: Props) {
const TeachersList = () => { const TeachersList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "teacher" && x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -406,8 +384,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -419,10 +396,7 @@ export default function MasterCorporateDashboard({user}: Props) {
}; };
const corporateUserFilter = (x: User) => const corporateUserFilter = (x: User) =>
x.type === "corporate" && x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
(!!selectedUser
? masterCorporateUserGroups.includes(x.id) || false
: masterCorporateUserGroups.includes(x.id));
const CorporateList = () => { const CorporateList = () => {
return ( return (
@@ -433,8 +407,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -451,8 +424,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -646,22 +618,18 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<h2 className="text-2xl font-semibold">Master Statistical</h2> <h2 className="text-2xl font-semibold">Master Statistical</h2>
</div> </div>
<MasterStatistical <MasterStatistical
users={masterCorporateUserGroups.reduce( users={masterCorporateUserGroups.reduce((accm: CorporateUser[], id) => {
(accm: CorporateUser[], id) => {
const user = users.find((u) => u.id === id) as CorporateUser; const user = users.find((u) => u.id === id) as CorporateUser;
if (user) return [...accm, user]; if (user) return [...accm, user];
return accm; return accm;
}, }, [])}
[]
)}
/> />
</> </>
); );
@@ -775,11 +743,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(studentFilter) .filter(studentFilter)
.sort( .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
))} ))}
@@ -792,8 +756,7 @@ export default function MasterCorporateDashboard({user}: Props) {
.filter(studentFilter) .filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
Object.keys(groupByExam(getStatsByStudent(a))).length
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -817,8 +780,7 @@ export default function MasterCorporateDashboard({user}: Props) {
if (shouldReload) reload(); if (shouldReload) reload();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "teacher"
selectedUser.type === "teacher"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-students", id: "view-students",
@@ -828,11 +790,7 @@ export default function MasterCorporateDashboard({user}: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });
@@ -842,8 +800,7 @@ export default function MasterCorporateDashboard({user}: Props) {
: undefined : undefined
} }
onViewTeachers={ onViewTeachers={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "student"
selectedUser.type === "student"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-teachers", id: "view-teachers",
@@ -853,11 +810,7 @@ export default function MasterCorporateDashboard({user}: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });

View File

@@ -1,20 +1,12 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import { app } from "@/firebase"; import {app} from "@/firebase";
import { import {getFirestore, collection, getDocs, setDoc, doc, query, where} from "firebase/firestore";
getFirestore, import {withIronSessionApiRoute} from "iron-session/next";
collection, import {sessionOptions} from "@/lib/session";
getDocs, import {Group} from "@/interfaces/user";
setDoc, import {v4} from "uuid";
doc, import {updateExpiryDateOnGroup, getGroupsForUser} from "@/utils/groups.be";
query,
where,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Group } from "@/interfaces/user";
import { v4 } from "uuid";
import { updateExpiryDateOnGroup, getGroupsForUser } from "@/utils/groups.be";
const db = getFirestore(app); const db = getFirestore(app);
@@ -22,7 +14,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) { if (!req.session.user) {
res.status(401).json({ ok: false }); res.status(401).json({ok: false});
return; return;
} }
@@ -31,7 +23,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
async function get(req: NextApiRequest, res: NextApiResponse) { async function get(req: NextApiRequest, res: NextApiResponse) {
const { admin, participant } = req.query as { const {admin, participant} = req.query as {
admin: string; admin: string;
participant: string; participant: string;
}; };
@@ -39,29 +31,25 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
if (req.session?.user?.type === "mastercorporate") { if (req.session?.user?.type === "mastercorporate") {
try { try {
const masterCorporateGroups = await getGroupsForUser(admin, participant); const masterCorporateGroups = await getGroupsForUser(admin, participant);
const corporatesFromMaster = masterCorporateGroups const corporatesFromMaster = masterCorporateGroups.filter((g) => g.name === "Corporate").flatMap((g) => g.participants);
.filter((g) => g.name === "Corporate")
.flatMap((g) => g.participants);
if (corporatesFromMaster.length === 0) { if (corporatesFromMaster.length === 0) {
res.status(200).json([]); res.status(200).json(masterCorporateGroups);
return; return;
} }
Promise.all( Promise.all(corporatesFromMaster.map((c) => getGroupsForUser(c, participant)))
corporatesFromMaster.map((c) => getGroupsForUser(c, participant))
)
.then((groups) => { .then((groups) => {
res.status(200).json([...masterCorporateGroups, ...groups.flat()]); res.status(200).json([...masterCorporateGroups, ...groups.flat()]);
return; return;
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
res.status(500).json({ ok: false }); res.status(500).json({ok: false});
return; return;
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
res.status(500).json({ ok: false }); res.status(500).json({ok: false});
return; return;
} }
return; return;
@@ -72,23 +60,19 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json(groups); res.status(200).json(groups);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
res.status(500).json({ ok: false }); res.status(500).json({ok: false});
} }
} }
async function post(req: NextApiRequest, res: NextApiResponse) { async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Group; const body = req.body as Group;
await Promise.all( await Promise.all(body.participants.map(async (p) => await updateExpiryDateOnGroup(p, body.admin)));
body.participants.map(
async (p) => await updateExpiryDateOnGroup(p, body.admin)
)
);
await setDoc(doc(db, "groups", v4()), { await setDoc(doc(db, "groups", v4()), {
name: body.name, name: body.name,
admin: body.admin, admin: body.admin,
participants: body.participants, participants: body.participants,
}); });
res.status(200).json({ ok: true }); res.status(200).json({ok: true});
} }