diff --git a/src/components/Medium/SessionCard.tsx b/src/components/Medium/SessionCard.tsx index 71e9af97..6fdfb55c 100644 --- a/src/components/Medium/SessionCard.tsx +++ b/src/components/Medium/SessionCard.tsx @@ -40,61 +40,71 @@ export default function SessionCard({ }; return ( -
- - ID: - {session.sessionId} - - - Date: - {moment(session.date).format("DD/MM/YYYY - HH:mm")} - -
-
- {session.selectedModules.sort(sortByModuleName).map((module) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } -
- ))} -
+
+
+ + ID: + {session.sessionId} + + + Date: + {moment(session.date).format("DD/MM/YYYY - HH:mm")} + + {session.assignment && ( + + Assignment: + {session.assignment.name} + + )}
-
- - +
+
+
+ {session.selectedModules.sort(sortByModuleName).map((module) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } +
+ ))} +
+
+
+ + +
); diff --git a/src/components/Medium/StatGridItem.tsx b/src/components/Medium/StatGridItem.tsx index b0c3622b..c4c31040 100644 --- a/src/components/Medium/StatGridItem.tsx +++ b/src/components/Medium/StatGridItem.tsx @@ -277,7 +277,11 @@ const StatsGridItem: React.FC = ({ selectedTrainingExams.some((exam) => exam.includes(timestamp)) && "border-2 border-slate-600", )} - onClick={examNumber === undefined ? selectExam : undefined} + onClick={() => { + if (!!assignment && !assignment.released) return; + if (examNumber === undefined) return selectExam(); + return; + }} style={{ ...(width !== undefined && {width}), ...(height !== undefined && {height}), diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx index 3de8527b..23a7c45a 100644 --- a/src/dashboards/Corporate.tsx +++ b/src/dashboards/Corporate.tsx @@ -207,64 +207,6 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
); - 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 ( - ( -
-
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})

-
- )} - /> - ); - }; - - 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 ( - ( -
-
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})

-
- )} - /> - ); - }; - const GroupsList = () => { const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id); @@ -518,8 +460,40 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) { )} - {router.asPath === "/#students" && } - {router.asPath === "/#teachers" && } + {router.asPath === "/#students" && ( + x.type === "student"]} + renderHeader={(total) => ( +
+
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" && ( + x.type === "teacher"]} + renderHeader={(total) => ( +
+
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 === "/#assignments" && ( user.type === "student" && corporateUserGroups.includes(user.id); - const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id); + const studentFilter = (user: User) => user.type === "student"; + const teacherFilter = (user: User) => user.type === "teacher"; const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); const UserDisplay = (displayUser: User) => ( @@ -365,74 +365,7 @@ export default function MasterCorporateDashboard({user}: Props) {
); - const StudentsList = () => { - const filter = (x: User) => - x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id)); - - 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})

-
- )} - /> - ); - }; - - const TeachersList = () => { - const filter = (x: User) => - x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id)); - - 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})

-
- )} - /> - ); - }; - - const corporateUserFilter = (x: User) => - x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id)); - - const CorporateList = () => { - 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 -
-

Corporates ({total})

-
- )} - /> - ); - }; + const corporateUserFilter = (x: User) => x.type === "corporate"; const GroupsList = () => { return ( @@ -704,10 +637,58 @@ export default function MasterCorporateDashboard({user}: Props) { )} - {router.asPath === "/#students" && } - {router.asPath === "/#teachers" && } + {router.asPath === "/#students" && ( + x.type === "student"]} + renderHeader={(total) => ( +
+
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" && ( + x.type === "teacher"]} + renderHeader={(total) => ( +
+
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.asPath === "/#corporate" && ( + x.type === "corporate"]} + renderHeader={(total) => ( +
+
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" && ( (user.id, !user?.id); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id}); const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id}); @@ -160,12 +162,20 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props) Start
- +
x.assignment?.id === assignment.id).length > 0 && "tooltip", + )}> + +
)} {assignment.results.map((r) => r.user).includes(user.id) && ( diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index 448e4144..def70ba5 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -51,6 +51,7 @@ import usePermissions from "@/hooks/usePermissions"; import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments"; import AssignmentsPage from "./views/AssignmentsPage"; import {useRouter} from "next/router"; +import useFilterStore from "@/stores/listFilterStore"; interface Props { user: User; @@ -67,6 +68,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) { const {permissions} = usePermissions(user.id); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id}); + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]); @@ -90,7 +92,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) { setShowModal(!!selectedUser && router.asPath === "/#"); }, [selectedUser, router.asPath]); - const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id); + const studentFilter = (user: User) => user.type === "student"; const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); @@ -106,35 +108,6 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) { ); - 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 ( - ( -
-
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})

-
- )} - /> - ); - }; - const GroupsList = () => { const filter = (x: Group) => x.admin === user.id; @@ -285,16 +258,68 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) { if (shouldReload) reload(); }} onViewStudents={ - selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => router.push("/#students") : undefined + 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 } - onViewTeachers={selectedUser.type === "corporate" ? () => router.push("/#teachers") : undefined} user={selectedUser} /> )} - {router.asPath === "/#students" && } + {router.asPath === "/#students" && ( + x.type === "student"]} + renderHeader={(total) => ( +
+
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 === "/#groups" && } {router.asPath === "/#assignments" && ( )} - {assignment && !assignment.released && ( + {assignment && !assignment.released && !isLoading && (
{/* */} @@ -223,7 +223,7 @@ export default function Finish({user, scores, modules, information, solutions, i
)} - {!isLoading && ( + {!isLoading && !(assignment && !assignment.released) && (
{moduleResultText(selectedModule, bandScore)}
diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index e3379c7e..d9bfbe8c 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -80,19 +80,15 @@ export default function UserList({ useEffect(() => { (async () => { - if (user && users) { - const filterUsers = ["corporate", "teacher", "mastercorporate"].includes(user.type) - ? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id)) - : users; - - const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers); + if (users) { + const filteredUsers = filters.reduce((d, f) => d.filter(f), users); const sortedUsers = await asyncSorter(filteredUsers, sortFunction); setDisplayUsers([...sortedUsers]); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user, users, sorter, groups]); + }, [users, sorter]); const deleteAccount = (user: User) => { if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return; diff --git a/src/pages/api/groups/index.ts b/src/pages/api/groups/index.ts index 3493c22e..d93dd686 100644 --- a/src/pages/api/groups/index.ts +++ b/src/pages/api/groups/index.ts @@ -30,30 +30,10 @@ async function get(req: NextApiRequest, res: NextApiResponse) { participant: string; }; - if (req.session?.user?.type === "mastercorporate") { - try { - const masterCorporateGroups = await getGroupsForUser(admin, participant); - const participants = uniq(masterCorporateGroups.flatMap((g) => g.participants)); - const corporatesFromMaster = (await Promise.all(participants.map(getUser))).filter((x) => x.type === "corporate"); - - if (corporatesFromMaster.length === 0) return res.status(200).json(masterCorporateGroups); - - const groups = await Promise.all(corporatesFromMaster.map((c) => getGroupsForUser(c.id, participant))); - return res.status(200).json([...masterCorporateGroups, ...uniqBy(groups.flat(), "id")]); - } catch (e) { - console.error(e); - res.status(500).json({ok: false}); - return; - } - } - - try { - const groups = await getGroupsForUser(admin, participant); - res.status(200).json(groups); - } catch (e) { - console.error(e); - res.status(500).json({ok: false}); - } + const adminGroups = await getGroupsForUser(admin, participant); + const participants = uniq(adminGroups.flatMap((g) => g.participants)); + const groups = await Promise.all(participants.map(async (c) => await getGroupsForUser(c, participant))); + return res.status(200).json([...adminGroups, ...uniqBy(groups.flat(), "id")]); } async function post(req: NextApiRequest, res: NextApiResponse) { diff --git a/src/pages/api/make_user.ts b/src/pages/api/make_user.ts index 062a543d..a1956f74 100644 --- a/src/pages/api/make_user.ts +++ b/src/pages/api/make_user.ts @@ -4,10 +4,12 @@ import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, de import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {v4} from "uuid"; -import {CorporateUser, Group} from "@/interfaces/user"; +import {CorporateUser, Group, Type} from "@/interfaces/user"; import {createUserWithEmailAndPassword, getAuth} from "firebase/auth"; import ShortUniqueId from "short-unique-id"; -import {getUserCorporate} from "@/utils/groups.be"; +import {getUserCorporate, getUserGroups} from "@/utils/groups.be"; +import {uniq} from "lodash"; +import {getUser} from "@/utils/users.be"; const DEFAULT_DESIRED_LEVELS = { reading: 9, @@ -28,6 +30,13 @@ const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); +const getUsersOfType = async (admin: string, type: Type) => { + const groups = await getUserGroups(admin); + const users = await Promise.all(uniq(groups.flatMap((x) => x.participants)).map(getUser)); + + return users.filter((x) => x.type === type).map((x) => x.id); +}; + async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") return post(req, res); @@ -106,11 +115,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }); if (type === "corporate") { + const students = maker.type === "corporate" ? await getUsersOfType(maker.id, "student") : []; + const teachers = maker.type === "corporate" ? await getUsersOfType(maker.id, "teacher") : []; + const defaultTeachersGroup: Group = { admin: userId, id: v4(), name: "Teachers", - participants: [], + participants: teachers, disableEditing: true, }; @@ -118,21 +130,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) { admin: userId, id: v4(), name: "Students", - participants: [], - disableEditing: true, - }; - - const defaultCorporateGroup: Group = { - admin: userId, - id: v4(), - name: "Corporate", - participants: [], + participants: students, disableEditing: true, }; await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup); await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup); - await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup); } if (!!corporate) { diff --git a/src/pages/api/sessions/index.ts b/src/pages/api/sessions/index.ts index c5b30692..16afdeca 100644 --- a/src/pages/api/sessions/index.ts +++ b/src/pages/api/sessions/index.ts @@ -4,6 +4,8 @@ import {app} from "@/firebase"; import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; +import {Session} from "@/hooks/useSessions"; +import moment from "moment"; const db = getFirestore(app); @@ -24,12 +26,17 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const q = user ? query(collection(db, "sessions"), where("user", "==", user)) : collection(db, "sessions"); const snapshot = await getDocs(q); + const sessions = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Session[]; res.status(200).json( - snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })), + sessions.filter((x) => { + if (!x.assignment) return true; + if (x.assignment.results.filter((y) => y.user === user).length > 0) return false; + return !moment().isAfter(moment(x.assignment.endDate)); + }), ); } diff --git a/src/pages/api/users/list.ts b/src/pages/api/users/list.ts index ac569e8d..d5e8c93d 100644 --- a/src/pages/api/users/list.ts +++ b/src/pages/api/users/list.ts @@ -4,6 +4,8 @@ import {app} from "@/firebase"; import {getFirestore, collection, getDocs} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; +import {getGroupsForUser} from "@/utils/groups.be"; +import {uniq} from "lodash"; const db = getFirestore(app); @@ -16,11 +18,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } const snapshot = await getDocs(collection(db, "users")); + const users = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); - res.status(200).json( - snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })), - ); + if (!req.session.user) return res.status(200).json(users); + if (req.session.user.type === "admin" || req.session.user.type === "developer") return res.status(200).json(users); + + const adminGroups = await getGroupsForUser(req.session.user.id); + const groups = await Promise.all(adminGroups.flatMap((x) => x.participants).map(async (x) => await getGroupsForUser(x))); + const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...groups.flat().flatMap((x) => x.participants)]); + + res.status(200).json(users.filter((x) => participants.includes(x.id))); } diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index b36b8921..0a911581 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -40,7 +40,7 @@ export const getUserCorporate = async (id: string) => { const groups = await getParticipantGroups(id); const admins = await Promise.all(groups.map((x) => x.admin).map(getUser)); - const corporates = admins.filter((x) => x.type === "corporate" || x.type === "mastercorporate"); + const corporates = admins.filter((x) => (user.type === "corporate" ? x.type === "mastercorporate" : x.type === "corporate")); if (corporates.length === 0) return undefined; return corporates.shift() as CorporateUser | MasterCorporateUser;