const LOG_PREFIX = "[BulkUpload:Parser]"; type ParseLogPayload = Record; function log(stage: string, payload?: ParseLogPayload) { if (payload) { console.log(LOG_PREFIX, stage, payload); return; } console.log(LOG_PREFIX, stage); } export function logBulkUploadParseStart( mode: "new" | "update", fileName: string, ) { log("start", { mode, fileName }); } export function logBulkUploadWorkbook( mode: "new" | "update", sheetNames: string[], ) { log("workbook", { mode, sheetNames, sheetCount: sheetNames.length }); } export function logBulkUploadSheetParse( mode: "new" | "update", sheetName: string, details: ParseLogPayload, ) { log(`sheet:${sheetName}`, { mode, ...details }); } export function logBulkUploadValidation( mode: "new" | "update", details: ParseLogPayload, ) { log("validation", { mode, ...details }); } export function logBulkUploadParseSuccess( mode: "new" | "update", details: ParseLogPayload, ) { log("success", { mode, ...details }); } export function logBulkUploadParseError( mode: "new" | "update", error: unknown, details?: ParseLogPayload, ) { log("error", { mode, message: error instanceof Error ? error.message : String(error), ...details, }); if (error instanceof Error && error.stack) { console.error(LOG_PREFIX, "stack", error.stack); } } export function logBulkUploadParseAbort( mode: "new" | "update", reason: string, details?: ParseLogPayload, ) { log("abort", { mode, reason, ...details }); } export type DuplicateRow = { Username?: string; Email?: string; PhoneNumber?: string; Position?: string; ReportsTo?: string; __rowNumber?: number; }; function normalizeDuplicateKey( value: string, field: keyof DuplicateRow, ): string { const trimmed = value.trim(); if (field === "PhoneNumber") { return trimmed.replace(/\D/g, ""); } if (field === "Email" || field === "Username") { return trimmed.toLowerCase(); } return trimmed.toLowerCase(); } function findDuplicateValues( rows: T[], field: keyof T, ): Array<{ value: string; count: number; rows: T[] }> { const map = new Map(); for (const row of rows) { const raw = row[field]; const value = typeof raw === "string" ? raw.trim() : ""; if (!value) continue; const key = normalizeDuplicateKey(value, field as keyof DuplicateRow); if (!key) continue; if (!map.has(key)) map.set(key, []); map.get(key)!.push(row); } return [...map.entries()] .filter(([, matches]) => matches.length > 1) .map(([, matches]) => ({ value: String(matches[0][field] ?? "").trim(), count: matches.length, rows: matches, })); } export function collectBulkUploadDuplicateErrors( users: DuplicateRow[], ): string[] { const errors: string[] = []; const checks: Array<{ field: keyof DuplicateRow; label: string }> = [ { field: "Username", label: "Username" }, { field: "Email", label: "Email" }, { field: "PhoneNumber", label: "Phone number" }, ]; for (const { field, label } of checks) { const duplicates = findDuplicateValues(users, field); for (const duplicate of duplicates) { const rows = duplicate.rows .map((row) => typeof row.__rowNumber === "number" ? `row ${row.__rowNumber}` : String(row.Username ?? "unknown"), ) .join(", "); errors.push( `Duplicate ${label} "${duplicate.value}" found in ${rows}`, ); } } return errors; } export function logBulkUploadDuplicateCheck( mode: "new" | "update", positions: DuplicateRow[], users: DuplicateRow[], ) { const positionNameDups = findDuplicateValues(positions, "Position"); const usernameDups = findDuplicateValues(users, "Username"); const emailDups = findDuplicateValues(users, "Email"); const phoneDups = findDuplicateValues(users, "PhoneNumber"); const duplicateErrors = collectBulkUploadDuplicateErrors(users); const invalidReportsTo = positions.filter((row) => { const position = String(row.Position ?? "").trim(); const reportsTo = String(row.ReportsTo ?? "").trim(); return reportsTo && reportsTo === position; }); const userPositions = new Set( positions .map((row) => String(row.Position ?? "").trim()) .filter(Boolean), ); const missingPositionRefs = users.filter( (row) => row.Position && !userPositions.has(String(row.Position).trim()), ); log("duplicate-check", { mode, duplicateErrors, positionNameDuplicates: positionNameDups.map((item) => ({ value: item.value, count: item.count, })), usernameDuplicates: usernameDups.map((item) => ({ value: item.value, count: item.count, usernames: item.rows.map((row) => row.Username), })), emailDuplicates: emailDups.map((item) => ({ value: item.value, count: item.count, usernames: item.rows.map((row) => row.Username), })), phoneDuplicates: phoneDups.map((item) => ({ value: item.value, count: item.count, usernames: item.rows.map((row) => row.Username), })), selfReferencingReportsTo: invalidReportsTo.map((row) => row.Position), usersWithUnknownPosition: missingPositionRefs.map((row) => ({ username: row.Username, position: row.Position, })), }); }