Merged in feature/ExamGenRework (pull request #132)

Removed a non sense entity ownership classroom name check I added a while back, patched the same entity + same name + same admin query

Approved-by: Tiago Ribeiro
This commit is contained in:
carlos.mesquita
2025-01-06 21:21:05 +00:00
committed by Tiago Ribeiro
4 changed files with 17 additions and 166 deletions

View File

@@ -21,7 +21,6 @@ const ClassroomImportSummary: React.FC<{ state: ClassroomTransferState }> = ({ s
const [showNotFoundModal, setShowNotFoundModal] = useState(false); const [showNotFoundModal, setShowNotFoundModal] = useState(false);
const [showOtherEntityModal, setShowOtherEntityModal] = useState(false); const [showOtherEntityModal, setShowOtherEntityModal] = useState(false);
const [showAlreadyInClassModal, setShowAlreadyInClassModal] = useState(false); const [showAlreadyInClassModal, setShowAlreadyInClassModal] = useState(false);
const [showNotOwnedModal, setShowNotOwnedModal] = useState(false);
const [showMismatchesModal, setShowMismatchesModal] = useState(false); const [showMismatchesModal, setShowMismatchesModal] = useState(false);
const errorCount = state.parsedExcel?.errors ? const errorCount = state.parsedExcel?.errors ?
@@ -114,21 +113,6 @@ const ClassroomImportSummary: React.FC<{ state: ClassroomTransferState }> = ({ s
</div> </div>
)} )}
{state.notOwnedClassrooms.length > 0 && (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FaLock className="h-5 w-5 text-red-500" />
<span>{`${state.notOwnedClassrooms.length} classroom${state.notOwnedClassrooms.length !== 1 ? 's' : ''} not owned`}</span>
</div>
<button
onClick={() => setShowNotOwnedModal(true)}
className="inline-flex items-center justify-center px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors"
>
View details
</button>
</div>
)}
{state.duplicatedRows.length > 0 && ( {state.duplicatedRows.length > 0 && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -176,8 +160,7 @@ const ClassroomImportSummary: React.FC<{ state: ClassroomTransferState }> = ({ s
</div> </div>
{(state.duplicatedRows.length > 0 || state.userMismatches.length > 0 || errorCount > 0 || {(state.duplicatedRows.length > 0 || state.userMismatches.length > 0 || errorCount > 0 ||
state.notFoundUsers.length > 0 || state.otherEntityUsers.length > 0 || state.notFoundUsers.length > 0 || state.otherEntityUsers.length > 0) && (
state.notOwnedClassrooms.length > 0) && (
<div className="mt-6 rounded-lg border border-red-100 bg-white p-6"> <div className="mt-6 rounded-lg border border-red-100 bg-white p-6">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="mt-1"> <div className="mt-1">
@@ -202,16 +185,6 @@ const ClassroomImportSummary: React.FC<{ state: ClassroomTransferState }> = ({ s
</div> </div>
</li> </li>
)} )}
{state.notOwnedClassrooms.length > 0 && (
<li>
<div className="text-gray-700">
<span className="font-medium">{state.notOwnedClassrooms.length}</span> classrooms not owned:
<div className="mt-1 ml-4 text-sm text-gray-600">
{state.notOwnedClassrooms.join(', ')}
</div>
</div>
</li>
)}
{state.duplicatedRows.length > 0 && ( {state.duplicatedRows.length > 0 && (
<li> <li>
<div className="text-gray-700"> <div className="text-gray-700">
@@ -288,25 +261,6 @@ const ClassroomImportSummary: React.FC<{ state: ClassroomTransferState }> = ({ s
</> </>
</Modal> </Modal>
<Modal isOpen={showNotOwnedModal} onClose={() => setShowNotOwnedModal(false)}>
<>
<div className="flex items-center gap-2 mb-6">
<FaLock className="w-5 h-5 text-red-500" />
<h2 className="text-lg font-semibold text-gray-900">Classrooms Not Owned</h2>
</div>
<div className="space-y-3">
{state.notOwnedClassrooms.map(classroom => (
<div
key={classroom}
className="flex justify-between items-center rounded-lg border border-gray-200 bg-gray-50 p-3"
>
<span className="text-gray-700">{classroom}</span>
</div>
))}
</div>
</>
</Modal>
<Modal isOpen={showDuplicatesModal} onClose={() => setShowDuplicatesModal(false)}> <Modal isOpen={showDuplicatesModal} onClose={() => setShowDuplicatesModal(false)}>
<> <>
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">

View File

@@ -4,7 +4,6 @@ import Modal from "../Modal";
import { useFilePicker } from "use-file-picker"; import { useFilePicker } from "use-file-picker";
import readXlsxFile from "read-excel-file"; import readXlsxFile from "read-excel-file";
import countryCodes from "country-codes-list"; import countryCodes from "country-codes-list";
import { ExcelUserDuplicatesMap } from "../ImportSummaries/User";
import { UserImport } from "@/interfaces/IUserImport"; import { UserImport } from "@/interfaces/IUserImport";
import axios from "axios"; import axios from "axios";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -47,8 +46,6 @@ export interface ClassroomTransferState {
notFoundUsers: UserImport[]; notFoundUsers: UserImport[];
otherEntityUsers: UserImport[]; otherEntityUsers: UserImport[];
alreadyInClass: UserImport[]; alreadyInClass: UserImport[];
notOwnedClassrooms: string[];
validClassrooms: string[];
} }
@@ -66,8 +63,6 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
notFoundUsers: [], notFoundUsers: [],
otherEntityUsers: [], otherEntityUsers: [],
alreadyInClass: [], alreadyInClass: [],
notOwnedClassrooms: [],
validClassrooms: []
}) })
const router = useRouter(); const router = useRouter();
@@ -322,7 +317,8 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
email: info.email, email: info.email,
classroom: info.groupName classroom: info.groupName
})), })),
entity entity,
userId: user.id
}); });
const excludeEmails = new Set([ const excludeEmails = new Set([
@@ -361,38 +357,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
crossRefUsers(); crossRefUsers();
} }
}, [classroomTransferState.imports, user.entities, classroomTransferState.stage, entity]) }, [classroomTransferState.imports, user.entities, classroomTransferState.stage, entity, user.id])
// Stage 3 - Classroom Filter
// - See if there are classrooms with same name but different admin
// - Find which new classrooms need to be created
useEffect(() => {
const crossRefClassrooms = async () => {
const classrooms = Array.from(new Set(classroomTransferState.imports.map((i) => i.groupName)));
try {
const { data: notOwnedClassroomsSameName } = await axios.post("/api/groups/controller?op=crossRefOwnership", {
userId: user.id,
classrooms
});
setClassroomTransferState((prev) => ({
...prev,
stage: 3,
notOwnedClassrooms: notOwnedClassroomsSameName,
validClassrooms: Array.from(classrooms).filter(
(name) => !new Set(notOwnedClassroomsSameName).has(name)
)
}))
} catch (error) {
toast.error("Something went wrong, please try again later!");
}
};
if (classroomTransferState.imports.length > 0 && classroomTransferState.stage === 2) {
crossRefClassrooms();
}
}, [classroomTransferState.imports, classroomTransferState.stage, user.id, entity])
const clearAndReset = () => { const clearAndReset = () => {
setIsLoading(false); setIsLoading(false);
@@ -405,8 +370,6 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
notFoundUsers: [], notFoundUsers: [],
otherEntityUsers: [], otherEntityUsers: [],
alreadyInClass: [], alreadyInClass: [],
notOwnedClassrooms: [],
validClassrooms: []
}); });
clear(); clear();
}; };
@@ -425,6 +388,8 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
try { try {
setIsLoading(true); setIsLoading(true);
const classrooms = Array.from(new Set(classroomTransferState.imports.map((i) => i.groupName)));
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", {
@@ -458,7 +423,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
}); });
const { data: existingGroupsMap } = await axios.post("/api/groups/controller?op=existantGroupIds", { const { data: existingGroupsMap } = await axios.post("/api/groups/controller?op=existantGroupIds", {
names: classroomTransferState.validClassrooms names: classrooms
}); });
const groupedUsers = imports.reduce((acc, user) => { const groupedUsers = imports.reduce((acc, user) => {
@@ -475,7 +440,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
const newGroupUsers = Object.fromEntries( const newGroupUsers = Object.fromEntries(
Object.entries(groupedUsers) Object.entries(groupedUsers)
.filter(([groupName]) => .filter(([groupName]) =>
classroomTransferState.validClassrooms.includes(groupName) && classrooms.includes(groupName) &&
!existingGroupsMap[groupName] !existingGroupsMap[groupName]
) )
); );
@@ -496,7 +461,7 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
const allExistingUsers = Object.fromEntries( const allExistingUsers = Object.fromEntries(
Object.entries(groupedUsers) Object.entries(groupedUsers)
.filter(([groupName]) => .filter(([groupName]) =>
!classroomTransferState.validClassrooms.includes(groupName) || !classrooms.includes(groupName) ||
existingGroupsMap[groupName] existingGroupsMap[groupName]
) )
); );

View File

@@ -22,9 +22,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
case 'existantGroupIds': case 'existantGroupIds':
res.status(200).json(await existantGroupIds(req.body.names)); res.status(200).json(await existantGroupIds(req.body.names));
break; break;
case 'crossRefOwnership':
res.status(200).json(await crossRefOwnership(req.body));
break;
case 'getIds': case 'getIds':
res.status(200).json(await getIds(req.body)); res.status(200).json(await getIds(req.body));
break; break;
@@ -40,45 +37,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
} }
async function crossRefOwnership(body: any): Promise<string[]> {
const { userId, classrooms, entity } = body;
const existingClassrooms = await db.collection('groups')
.find({
name: { $in: classrooms },
admin: { $ne: userId }
})
.project({ name: 1, admin: 1, _id: 0 })
.toArray();
if (existingClassrooms.length === 0) {
return [];
}
const adminUsers = await db.collection('users')
.find({
id: { $in: existingClassrooms.map(classroom => classroom.admin) }
})
.project({ id: 1, entities: 1, _id: 0 })
.toArray();
const adminEntitiesMap = new Map(
adminUsers.map(admin => [
admin.id,
admin.entities?.map((e: any) => e.id) || []
])
);
return Array.from(new Set(
existingClassrooms
.filter(classroom => {
const adminEntities = adminEntitiesMap.get(classroom.admin) || [];
return adminEntities.includes(entity);
})
.map(classroom => classroom.name)
));
}
async function getIds(body: any): Promise<Record<string, string>> { async function getIds(body: any): Promise<Record<string, string>> {
const { names, userEmails } = body; const { names, userEmails } = body;

View File

@@ -169,7 +169,11 @@ async function entityCheck(body: Record<string, any>): Promise<Array<{
} }
async function crossRefClassrooms(body: any) { async function crossRefClassrooms(body: any) {
const { sets, entity } = body as { sets: { email: string, classroom: string }[], entity: string }; const { sets, entity, userId } = body as {
sets: { email: string, classroom: string }[],
entity: string,
userId: string
};
const pipeline = [ const pipeline = [
// Match users with the provided emails // Match users with the provided emails
@@ -189,6 +193,8 @@ async function crossRefClassrooms(body: any) {
$expr: { $expr: {
$and: [ $and: [
{ $in: ['$$userId', '$participants'] }, { $in: ['$$userId', '$participants'] },
{ $eq: ['$admin', userId] },
{ $eq: ['$entity', entity] },
{ {
$let: { $let: {
vars: { vars: {
@@ -210,38 +216,6 @@ async function crossRefClassrooms(body: any) {
] ]
} }
} }
},
// Lookup admin's entities
{
$lookup: {
from: 'users',
let: { adminId: '$admin' },
pipeline: [
{
$match: {
$expr: { $eq: ['$id', '$$adminId'] }
}
}
],
as: 'adminInfo'
}
},
// Filter where admin has the target entity
{
$match: {
$expr: {
$in: [
entity,
{
$map: {
input: { $arrayElemAt: ['$adminInfo.entities', 0] },
as: 'entityObj',
in: '$$entityObj.id'
}
}
]
}
}
} }
], ],
as: 'matchingGroups' as: 'matchingGroups'