diff --git a/src/components/Imports/StudentClassroomTransfer.tsx b/src/components/Imports/StudentClassroomTransfer.tsx index ae7d127c..dcbb1157 100644 --- a/src/components/Imports/StudentClassroomTransfer.tsx +++ b/src/components/Imports/StudentClassroomTransfer.tsx @@ -18,6 +18,7 @@ import UserTable from "../Tables/UserTable"; import ClassroomImportSummary from "../ImportSummaries/Classroom"; import Select from "../Low/Select"; import { EntityWithRoles } from "@/interfaces/entity"; +import { useRouter } from "next/router"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); @@ -47,7 +48,7 @@ export interface ClassroomTransferState { otherEntityUsers: UserImport[]; alreadyInClass: UserImport[]; notOwnedClassrooms: string[]; - classroomsToCreate: string[]; + validClassrooms: string[]; } @@ -66,9 +67,11 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole otherEntityUsers: [], alreadyInClass: [], notOwnedClassrooms: [], - classroomsToCreate: [] + validClassrooms: [] }) + const router = useRouter(); + const { openFilePicker, filesContent, clear } = useFilePicker({ accept: ".xlsx", multiple: false, @@ -183,7 +186,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filesContent]); + }, [filesContent, entity]); // Stage 1 - Excel Parsing // - Group rows by emails @@ -299,10 +302,10 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole notFoundUsers: [], otherEntityUsers: [], notOwnedClassrooms: [], - classroomsToCreate: [], + validClassrooms: [], })); } - }, [classroomTransferState.parsedExcel]); + }, [classroomTransferState.parsedExcel, entity]); // Stage 2 - Student Filter // - Filter non existant students @@ -377,7 +380,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole ...prev, stage: 3, notOwnedClassrooms: notOwnedClassroomsSameName, - classroomsToCreate: Array.from(classrooms).filter( + validClassrooms: Array.from(classrooms).filter( (name) => !new Set(notOwnedClassroomsSameName).has(name) ) })) @@ -388,7 +391,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole if (classroomTransferState.imports.length > 0 && classroomTransferState.stage === 2) { crossRefClassrooms(); } - }, [classroomTransferState.imports, classroomTransferState.stage, user.id]) + }, [classroomTransferState.imports, classroomTransferState.stage, user.id, entity]) const clearAndReset = () => { @@ -403,7 +406,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole otherEntityUsers: [], alreadyInClass: [], notOwnedClassrooms: [], - classroomsToCreate: [] + validClassrooms: [] }); clear(); }; @@ -422,7 +425,6 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole try { setIsLoading(true); - const getIds = async () => { try { const { data: emailIdMap } = await axios.post("/api/users/controller?op=getIds", { @@ -445,7 +447,6 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole if (!imports) return; - await axios.post("/api/groups/controller?op=deletePriorEntitiesGroups", { ids: imports.map((u) => u.id), entity @@ -456,6 +457,9 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole entity }); + const { data: existingGroupsMap } = await axios.post("/api/groups/controller?op=existantGroupIds", { + names: classroomTransferState.validClassrooms + }); const groupedUsers = imports.reduce((acc, user) => { if (!acc[user.groupName]) { @@ -464,12 +468,18 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole acc[user.groupName].push(user); return acc; }, {} as Record); - + + // ############## + // # New Groups # + // ############## const newGroupUsers = Object.fromEntries( Object.entries(groupedUsers) - .filter(([groupName]) => classroomTransferState.classroomsToCreate.includes(groupName)) + .filter(([groupName]) => + classroomTransferState.validClassrooms.includes(groupName) && + !existingGroupsMap[groupName] + ) ); - + const createGroupPromises = Object.entries(newGroupUsers).map(([groupName, users]) => { const groupData: Partial = { admin: user.id, @@ -479,25 +489,33 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole }; return axios.post('/api/groups', groupData); }); - - - const existingGroupUsers = Object.fromEntries( + + // ################### + // # Existant Groups # + // ################### + const allExistingUsers = Object.fromEntries( Object.entries(groupedUsers) - .filter(([groupName]) => !classroomTransferState.classroomsToCreate.includes(groupName)) + .filter(([groupName]) => + !classroomTransferState.validClassrooms.includes(groupName) || + existingGroupsMap[groupName] + ) ); - + let updatePromises: Promise[] = []; + + if (Object.keys(allExistingUsers).length > 0) { + const { data: { groups: groupNameToId, users: userEmailToId } } = await axios.post('/api/groups/controller?op=getIds', { + names: Object.keys(allExistingUsers), + userEmails: Object.values(allExistingUsers).flat().map(user => user.email) + }); - if (Object.keys(existingGroupUsers).length > 0) { - const { groups: groupNameToId, users: userEmailToId } = await axios.post('/api/groups?op=getIds', { - names: Object.keys(existingGroupUsers), - userEmails: Object.values(existingGroupUsers).flat().map(user => user.email) - }).then(response => response.data); - - const existingGroupsWithIds = Object.entries(existingGroupUsers).reduce((acc, [groupName, users]) => { - acc[groupNameToId[groupName]] = users; + const existingGroupsWithIds = Object.entries(allExistingUsers).reduce((acc, [groupName, users]) => { + const groupId = groupNameToId[groupName]; + if (groupId) { + acc[groupId] = users; + } return acc; - }, {} as Record); + }, {} as Record); updatePromises = Object.entries(existingGroupsWithIds).map(([groupId, users]) => { const userIds = users.map(user => userEmailToId[user.email]); @@ -506,13 +524,14 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole }); }); } - + await Promise.all([ ...createGroupPromises, ...updatePromises ]); toast.success(`Successfully assigned all ${classroomTransferState.imports.length} user(s)!`); + router.replace("/classrooms"); onFinish(); } catch (error) { console.error(error); diff --git a/src/pages/api/groups/controller.ts b/src/pages/api/groups/controller.ts index 73a8ed6c..6fe67631 100644 --- a/src/pages/api/groups/controller.ts +++ b/src/pages/api/groups/controller.ts @@ -19,6 +19,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } else if (req.method === 'POST') { switch (op) { + case 'existantGroupIds': + res.status(200).json(await existantGroupIds(req.body.names)); + break; case 'crossRefOwnership': res.status(200).json(await crossRefOwnership(req.body)); break; @@ -41,7 +44,7 @@ async function crossRefOwnership(body: any): Promise { const { userId, classrooms, entity } = body; const existingClassrooms = await db.collection('groups') - .find({ + .find({ name: { $in: classrooms }, admin: { $ne: userId } }) @@ -61,7 +64,7 @@ async function crossRefOwnership(body: any): Promise { const adminEntitiesMap = new Map( adminUsers.map(admin => [ - admin.id, + admin.id, admin.entities?.map((e: any) => e.id) || [] ]) ); @@ -104,7 +107,6 @@ async function getIds(body: any): Promise> { async function deletePriorEntitiesGroups(body: any) { const { ids, entity } = body; - if (!Array.isArray(ids) || ids.length === 0 || !entity) { return; } @@ -123,8 +125,33 @@ async function deletePriorEntitiesGroups(body: any) { return; } - db.collection('groups').updateMany( + const affectedGroups = await db.collection('groups') + .find({ participants: { $in: toDeleteUserIds } }) + .project({ id: 1, _id: 0 }) + .toArray(); + + await db.collection('groups').updateMany( { participants: { $in: toDeleteUserIds } }, { $pull: { participants: { $in: toDeleteUserIds } } } as any ); + + // delete groups that were updated and have no participants + await db.collection('groups').deleteMany({ + id: { $in: affectedGroups.map(g => g.id) }, + participants: { $size: 0 } + }); +} + +async function existantGroupIds(names: string[]) { + const existingGroups = await db.collection('groups') + .find({ + name: { $in: names } + }) + .project({ id: 1, name: 1, _id: 0 }) + .toArray(); + + return existingGroups.reduce((acc, group) => { + acc[group.name] = group.id; + return acc; + }, {} as Record); }