ENCOA-309
This commit is contained in:
@@ -14,19 +14,28 @@ import UserTable from '../Tables/UserTable';
|
||||
import ParseExcelErrors from './ExcelError';
|
||||
import { errorsByRows } from '@/utils/excel.errors';
|
||||
import { ClassroomTransferState } from '../Imports/StudentClassroomTransfer';
|
||||
import { ExcelUserDuplicatesMap } from './User';
|
||||
|
||||
const ClassroomImportSummary: React.FC<{state: ClassroomTransferState}> = ({ state }) => {
|
||||
const ClassroomImportSummary: React.FC<{ state: ClassroomTransferState }> = ({ state }) => {
|
||||
const [showErrorsModal, setShowErrorsModal] = useState(false);
|
||||
const [showDuplicatesModal, setShowDuplicatesModal] = useState(false);
|
||||
const [showNotFoundModal, setShowNotFoundModal] = useState(false);
|
||||
const [showOtherEntityModal, setShowOtherEntityModal] = useState(false);
|
||||
const [showAlreadyInClassModal, setShowAlreadyInClassModal] = useState(false);
|
||||
const [showNotOwnedModal, setShowNotOwnedModal] = useState(false);
|
||||
const [showMismatchesModal, setShowMismatchesModal] = useState(false);
|
||||
|
||||
const errorCount = state.parsedExcel?.errors ?
|
||||
const errorCount = state.parsedExcel?.errors ?
|
||||
Object.entries(errorsByRows(state.parsedExcel.errors)).length : 0;
|
||||
|
||||
const fieldMapper: { [key: string]: string } = {
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"studentID": "Student ID",
|
||||
"phone": "Phone Number",
|
||||
"passport_id": "Passport/National ID",
|
||||
"country": "Country"
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
@@ -120,11 +129,11 @@ const ClassroomImportSummary: React.FC<{state: ClassroomTransferState}> = ({ sta
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.duplicatedRows && state.duplicatedRows.count > 0 && (
|
||||
{state.duplicatedRows.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaExclamationCircle className="h-5 w-5 text-yellow-500" />
|
||||
<span>{state.duplicatedRows.count} duplicate entries in file</span>
|
||||
<span>{state.duplicatedRows.length} duplicate entries in file</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowDuplicatesModal(true)}
|
||||
@@ -135,6 +144,21 @@ const ClassroomImportSummary: React.FC<{state: ClassroomTransferState}> = ({ sta
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.userMismatches.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaExclamationTriangle className="h-5 w-5 text-orange-500" />
|
||||
<span>{state.userMismatches.length} users with mismatched information</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowMismatchesModal(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 mismatches
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorCount > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -151,73 +175,79 @@ const ClassroomImportSummary: React.FC<{state: ClassroomTransferState}> = ({ sta
|
||||
)}
|
||||
</div>
|
||||
|
||||
{((state.duplicatedRows?.count ?? 0) > 0 || errorCount > 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="flex items-start gap-4">
|
||||
<div className="mt-1">
|
||||
<FaExclamationTriangle className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
<p className="font-medium text-gray-900">
|
||||
The following will be excluded from transfer:
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{state.notFoundUsers.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.notFoundUsers.length}</span> users not found
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{state.otherEntityUsers.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.otherEntityUsers.length}</span> users from different entities
|
||||
</div>
|
||||
</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(', ')}
|
||||
{(state.duplicatedRows.length > 0 || state.userMismatches.length > 0 || errorCount > 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="flex items-start gap-4">
|
||||
<div className="mt-1">
|
||||
<FaExclamationTriangle className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
<p className="font-medium text-gray-900">
|
||||
The following will be excluded from transfer:
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{state.notFoundUsers.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.notFoundUsers.length}</span> users not found
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{state.duplicatedRows && state.duplicatedRows.count > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.duplicatedRows.count}</span> duplicate entries
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{state.alreadyInClass && state.alreadyInClass.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.alreadyInClass.length}</span> users that are already assigned to the classroom
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{errorCount > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{errorCount}</span> rows with invalid information
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
{state.otherEntityUsers.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.otherEntityUsers.length}</span> users from different entities
|
||||
</div>
|
||||
</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 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.duplicatedRows.length}</span> duplicate entries
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{state.userMismatches.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.userMismatches.length}</span> users with mismatched information
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{state.alreadyInClass && state.alreadyInClass.length > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{state.alreadyInClass.length}</span> users that are already assigned to the classroom
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{errorCount > 0 && (
|
||||
<li>
|
||||
<div className="text-gray-700">
|
||||
<span className="font-medium">{errorCount}</span> rows with invalid information
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Modals */}
|
||||
<Modal isOpen={showErrorsModal} onClose={() => setShowErrorsModal(false)}>
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
@@ -283,53 +313,59 @@ const ClassroomImportSummary: React.FC<{state: ClassroomTransferState}> = ({ sta
|
||||
<FaExclamationCircle className="w-5 h-5 text-yellow-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Duplicate Entries</h2>
|
||||
</div>
|
||||
{state.duplicatedRows && (
|
||||
<div className="space-y-6">
|
||||
{(Object.keys(state.duplicatedRows.duplicates) as Array<keyof ExcelUserDuplicatesMap>).map(field => {
|
||||
const duplicates = Array.from(state.duplicatedRows!.duplicates[field].entries())
|
||||
.filter((entry): entry is [string, number[]] => entry[1].length > 1);
|
||||
|
||||
if (duplicates.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={field} className="relative">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h2 className="text-md font-medium text-gray-700">
|
||||
{field} duplicates
|
||||
</h2>
|
||||
<span className="text-xs text-gray-500 ml-auto">
|
||||
{duplicates.length} {duplicates.length === 1 ? 'duplicate' : 'duplicates'}
|
||||
</span>
|
||||
<div className="space-y-3">
|
||||
{state.duplicatedRows.map((duplicate) => (
|
||||
<div
|
||||
key={`${duplicate.email}-${duplicate.classroom}-${duplicate.rowNumber}`}
|
||||
className="group relative rounded-lg border border-gray-200 bg-gray-50 p-3 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<span className="font-medium text-gray-900">Row {duplicate.rowNumber}</span>
|
||||
<div className="mt-1 text-sm text-gray-600">
|
||||
Email: <span className="text-blue-600 font-medium">{duplicate.email}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{duplicates.map(([value, rows]) => (
|
||||
<div
|
||||
key={value}
|
||||
className="group relative rounded-lg border border-gray-200 bg-gray-50 p-3 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<span className="font-medium text-gray-900">{value}</span>
|
||||
<div className="mt-1 text-sm text-gray-600">
|
||||
Appears in rows:
|
||||
<span className="ml-1 text-blue-600 font-medium">
|
||||
{rows.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{rows.length} occurrences
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-1 text-sm text-gray-600">
|
||||
Classroom: <span className="text-blue-600 font-medium">{duplicate.classroom}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
<Modal isOpen={showMismatchesModal} onClose={() => setShowMismatchesModal(false)}>
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<FaExclamationTriangle className="w-5 h-5 text-orange-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900">Mismatched User Information</h2>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{state.userMismatches.map((mismatch) => (
|
||||
<div
|
||||
key={mismatch.email}
|
||||
className="relative rounded-lg border border-gray-200 bg-gray-50 p-4"
|
||||
>
|
||||
<div className="mb-2 font-medium text-gray-900">
|
||||
Email: {mismatch.email}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Rows: {mismatch.rows.join(', ')}
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{mismatch.mismatches.map((field) => (
|
||||
<div key={field.field} className="pl-4 border-l-2 border-orange-500">
|
||||
<div className="font-medium text-gray-700">{fieldMapper[field.field]}:</div>
|
||||
<div className="mt-1 text-sm text-gray-600">
|
||||
Values: {field.values.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -116,7 +116,7 @@ const CodeGenImportSummary: React.FC<Props> = ({ infos, parsedExcel, duplicateRo
|
||||
if (duplicates.length === 0) return null;
|
||||
return (
|
||||
<div key={field} className="ml-4 text-sm text-gray-600">
|
||||
<span className="text-gray-500">{field}:</span> rows {
|
||||
<span className="text-gray-500">{fieldMapper[field]}:</span> rows {
|
||||
duplicates.map(([_, rows]) => rows.join(', ')).join('; ')
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ const UserImportSummary: React.FC<Props> = ({ parsedExcel, newUsers, enlistedUse
|
||||
if (duplicates.length === 0) return null;
|
||||
return (
|
||||
<div key={field} className="ml-4 text-sm text-gray-600">
|
||||
<span className="text-gray-500">{field}:</span> rows {
|
||||
<span className="text-gray-500">{fieldMapper[field]}:</span> rows {
|
||||
duplicates.map(([_, rows]) => rows.join(', ')).join('; ')
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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