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