mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-04 05:33:39 +00:00
204 lines
5.5 KiB
TypeScript
204 lines
5.5 KiB
TypeScript
const LOG_PREFIX = "[BulkUpload:Parser]";
|
|
|
|
type ParseLogPayload = Record<string, unknown>;
|
|
|
|
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<T extends DuplicateRow>(
|
|
rows: T[],
|
|
field: keyof T,
|
|
): Array<{ value: string; count: number; rows: T[] }> {
|
|
const map = new Map<string, T[]>();
|
|
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,
|
|
})),
|
|
});
|
|
}
|