ENCOA-309
This commit is contained in:
@@ -22,10 +22,26 @@ import { EntityWithRoles } from "@/interfaces/entity";
|
||||
|
||||
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||
|
||||
export interface DuplicateClassroom {
|
||||
rowNumber: number;
|
||||
email: string;
|
||||
classroom: string;
|
||||
}
|
||||
|
||||
export interface Mismatches {
|
||||
email: string;
|
||||
rows: number[];
|
||||
mismatches: {
|
||||
field: string;
|
||||
values: any[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ClassroomTransferState {
|
||||
stage: number;
|
||||
parsedExcel: { rows?: any[]; errors?: any[] } | undefined;
|
||||
duplicatedRows: { duplicates: ExcelUserDuplicatesMap, count: number } | undefined;
|
||||
duplicatedRows: DuplicateClassroom[];
|
||||
userMismatches: Mismatches[];
|
||||
imports: UserImport[];
|
||||
notFoundUsers: UserImport[];
|
||||
otherEntityUsers: UserImport[];
|
||||
@@ -43,7 +59,8 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
|
||||
const [classroomTransferState, setClassroomTransferState] = useState<ClassroomTransferState>({
|
||||
stage: 0,
|
||||
parsedExcel: undefined,
|
||||
duplicatedRows: undefined,
|
||||
duplicatedRows: [],
|
||||
userMismatches: [],
|
||||
imports: [],
|
||||
notFoundUsers: [],
|
||||
otherEntityUsers: [],
|
||||
@@ -169,79 +186,116 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
|
||||
}, [filesContent]);
|
||||
|
||||
// Stage 1 - Excel Parsing
|
||||
// - Find duplicate rows
|
||||
// - Group rows by emails
|
||||
// - Find duplicate rows (email + classroom)
|
||||
// - Find email and other fields mismatch except classroom
|
||||
// - Parsing errors
|
||||
// - Set the data
|
||||
useEffect(() => {
|
||||
if (classroomTransferState.parsedExcel && classroomTransferState.parsedExcel.rows) {
|
||||
const duplicates: ExcelUserDuplicatesMap = {
|
||||
studentID: new Map(),
|
||||
email: new Map(),
|
||||
passport_id: new Map(),
|
||||
phone: new Map()
|
||||
};
|
||||
const duplicateValues = new Set<string>();
|
||||
const duplicateRowIndices = new Set<number>();
|
||||
|
||||
const errorRowIndices = new Set(
|
||||
classroomTransferState.parsedExcel.errors?.map(error => error.row) || []
|
||||
);
|
||||
|
||||
|
||||
const emailClassroomMap = new Map<string, Map<string, number[]>>(); // email -> (group -> row numbers)
|
||||
const emailRowMap = new Map<string, Map<number, any>>(); // email -> (row number -> row data)
|
||||
|
||||
// Group rows by email and assign row data
|
||||
classroomTransferState.parsedExcel.rows.forEach((row, index) => {
|
||||
if (!errorRowIndices.has(index + 2)) {
|
||||
(Object.keys(duplicates) as Array<keyof ExcelUserDuplicatesMap>).forEach(field => {
|
||||
if (row !== null) {
|
||||
const value = row[field];
|
||||
if (value) {
|
||||
if (!duplicates[field].has(value)) {
|
||||
duplicates[field].set(value, [index + 2]);
|
||||
} else {
|
||||
const existingRows = duplicates[field].get(value);
|
||||
if (existingRows) {
|
||||
existingRows.push(index + 2);
|
||||
duplicateValues.add(value);
|
||||
existingRows.forEach(rowNum => duplicateRowIndices.add(rowNum));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const rowNum = index + 2;
|
||||
if (!errorRowIndices.has(rowNum) && row !== null) {
|
||||
const email = row.email.toString().trim().toLowerCase();
|
||||
const classroom = row.group;
|
||||
|
||||
if (!emailRowMap.has(email)) {
|
||||
emailRowMap.set(email, new Map());
|
||||
}
|
||||
emailRowMap.get(email)!.set(rowNum, row);
|
||||
|
||||
if (!emailClassroomMap.has(email)) {
|
||||
emailClassroomMap.set(email, new Map());
|
||||
}
|
||||
const classroomMap = emailClassroomMap.get(email)!;
|
||||
if (!classroomMap.has(classroom)) {
|
||||
classroomMap.set(classroom, []);
|
||||
}
|
||||
classroomMap.get(classroom)!.push(rowNum);
|
||||
}
|
||||
});
|
||||
|
||||
const infos = classroomTransferState.parsedExcel.rows
|
||||
.map((row, index) => {
|
||||
if (errorRowIndices.has(index + 2) || duplicateRowIndices.has(index + 2) || row === null) {
|
||||
return undefined;
|
||||
|
||||
const duplicatedRows: DuplicateClassroom[] = [];
|
||||
const userMismatches: Mismatches[] = [];
|
||||
const validRows = [];
|
||||
|
||||
for (const [email, classroomMap] of emailClassroomMap) {
|
||||
const rowDataMap = emailRowMap.get(email)!;
|
||||
const allRowsForEmail = Array.from(rowDataMap.keys());
|
||||
|
||||
// Check for duplicates (same email + classroom)
|
||||
for (const [classroom, rowNumbers] of classroomMap) {
|
||||
if (rowNumbers.length > 1) {
|
||||
rowNumbers.forEach(row => duplicatedRows.push({
|
||||
rowNumber: row,
|
||||
email,
|
||||
classroom
|
||||
}));
|
||||
} else {
|
||||
validRows.push(rowNumbers[0]);
|
||||
}
|
||||
const { firstName, lastName, studentID, passport_id, email, phone, group, country } = row;
|
||||
if (!email || !EMAIL_REGEX.test(email.toString().trim())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check for mismatches in other fields
|
||||
if (allRowsForEmail.length > 1) {
|
||||
const fields = ['firstName', 'lastName', 'studentID', 'passport_id', 'phone'];
|
||||
const mismatches: {field: string; values: any[]}[] = [];
|
||||
|
||||
fields.forEach(field => {
|
||||
const uniqueValues = new Set(
|
||||
allRowsForEmail.map(rowNum => rowDataMap.get(rowNum)![field])
|
||||
);
|
||||
if (uniqueValues.size > 1) {
|
||||
mismatches.push({
|
||||
field,
|
||||
values: Array.from(uniqueValues)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (mismatches.length > 0) {
|
||||
userMismatches.push({
|
||||
email,
|
||||
rows: allRowsForEmail,
|
||||
mismatches
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
email: email.toString().trim().toLowerCase(),
|
||||
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
|
||||
passport_id: passport_id?.toString().trim() || undefined,
|
||||
groupName: group,
|
||||
studentID,
|
||||
demographicInformation: {
|
||||
country: country?.countryCode,
|
||||
passport_id: passport_id?.toString().trim() || undefined,
|
||||
phone: phone.toString(),
|
||||
},
|
||||
entity: undefined,
|
||||
type: undefined
|
||||
} as UserImport;
|
||||
})
|
||||
.filter((item): item is UserImport => item !== undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const imports = validRows
|
||||
.map(rowNum => classroomTransferState.parsedExcel!.rows![rowNum - 2])
|
||||
.filter((row): row is any => row !== null)
|
||||
.map(row => ({
|
||||
email: row.email.toString().trim().toLowerCase(),
|
||||
name: `${row.firstName ?? ""} ${row.lastName ?? ""}`.trim(),
|
||||
passport_id: row.passport_id?.toString().trim() || undefined,
|
||||
groupName: row.group,
|
||||
studentID: row.studentID,
|
||||
demographicInformation: {
|
||||
country: row.country?.countryCode,
|
||||
passport_id: row.passport_id?.toString().trim() || undefined,
|
||||
phone: row.phone.toString(),
|
||||
},
|
||||
entity: undefined,
|
||||
type: undefined
|
||||
} as UserImport));
|
||||
|
||||
// On import reset state except excel parsing
|
||||
setClassroomTransferState((prev) => ({
|
||||
...prev,
|
||||
stage: 1,
|
||||
duplicatedRows: { duplicates, count: duplicateRowIndices.size },
|
||||
imports: infos,
|
||||
duplicatedRows,
|
||||
userMismatches,
|
||||
imports,
|
||||
notFoundUsers: [],
|
||||
otherEntityUsers: [],
|
||||
notOwnedClassrooms: [],
|
||||
@@ -342,7 +396,8 @@ const StudentClassroomTransfer: React.FC<{ user: User; entities?: EntityWithRole
|
||||
setClassroomTransferState({
|
||||
stage: 0,
|
||||
parsedExcel: undefined,
|
||||
duplicatedRows: undefined,
|
||||
duplicatedRows: [],
|
||||
userMismatches: [],
|
||||
imports: [],
|
||||
notFoundUsers: [],
|
||||
otherEntityUsers: [],
|
||||
|
||||
Reference in New Issue
Block a user