feat(import): harden workbook parser boundaries

This commit is contained in:
2026-03-31 22:48:30 +02:00
parent 3e8b1702bc
commit db50e2e555
20 changed files with 936 additions and 174 deletions
@@ -166,6 +166,25 @@ function getSlotHalfDayPart(slotLabel: string | null): "AFTERNOON" | "MORNING" |
return null;
}
function isPlanningSummaryRow(row: ReadonlyArray<WorksheetCellValue>): boolean {
if ((row[0] ?? null) !== null || (row[1] ?? null) !== null) {
return false;
}
const repeatedLabels = row
.slice(DISPO_EID_COLUMN - 1, 9)
.map((value) => normalizeNullableWorkbookValue(value))
.filter((value): value is string => value !== null);
if (repeatedLabels.length === 0) {
return false;
}
const normalizedLabels = new Set(repeatedLabels.map((value) => value.toLowerCase()));
const label = repeatedLabels[0] ?? null;
return normalizedLabels.size === 1 && label !== null && label.startsWith("(") && label.endsWith(")");
}
function buildPlanningColumns(rows: ReadonlyArray<ReadonlyArray<WorksheetCellValue>>) {
const columns: PlanningColumn[] = [];
const headerWidth = Math.max(rows[DISPO_DATE_ROW - 1]?.length ?? 0, rows[DISPO_SLOT_ROW - 1]?.length ?? 0);
@@ -483,6 +502,9 @@ export async function parseDispoPlanningWorkbook(
for (let rowNumber = DISPO_DATA_START_ROW; rowNumber <= rows.length; rowNumber += 1) {
const row = rows[rowNumber - 1] ?? [];
if (isPlanningSummaryRow(row)) {
continue;
}
const eid = normalizeNullableWorkbookValue(row[DISPO_EID_COLUMN - 1]);
if (!eid) {