fix(import): validate/normalize imported data, guard Excel I/O (#3)

- loadFromJSON and the localStorage startup load now run employees through
  normalizeEmployees(): fills missing constraints, drops unusable rows,
  de-duplicates ids, and discards invalid vacation ranges — preventing
  later crashes (e.g. c.neverDays on undefined) and same-day double-books.
- Excel import is wrapped in try/catch with a typeof XLSX guard and user
  feedback; the Excel export button gets the same guard.
- createEmployee reuses the shared defaultConstraints() factory.

Adds normalizeEmployees tests (defaults, dropping, dedup, vacations) and a
loadFromJSON normalization test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Opus 4.8 committed 2026-06-03 12:20:41 +00:00
1 parent e851bb2833
commit 065876ced4
5 files changed
+118 -18

No files matched your search

+48 -9
View File
@@ -19,6 +19,52 @@ function parseSettings(raw) {
return { moderatorsPerDay: valid ? n : 1 };
}
function defaultConstraints() {
return {
neverDays: [],
lowPriorityDays: [],
onlyDays: [],
maxPerMonth: null,
minGapDays: 14,
vacations: []
};
}
// Coerce an arbitrary constraints object into a valid, fully-populated one.
function normalizeConstraints(c) {
const raw = (c && typeof c === 'object') ? c : {};
const arr = v => (Array.isArray(v) ? v : []);
return {
neverDays: arr(raw.neverDays),
lowPriorityDays: arr(raw.lowPriorityDays),
onlyDays: arr(raw.onlyDays),
maxPerMonth: Number.isFinite(raw.maxPerMonth) ? raw.maxPerMonth : null,
minGapDays: Number.isFinite(raw.minGapDays) ? raw.minGapDays : 14,
vacations: arr(raw.vacations).filter(v =>
v && typeof v.from === 'string' && typeof v.to === 'string' && v.from <= v.to
)
};
}
// Validate/repair an imported employee list: drop unusable rows, fill missing
// constraints, and deduplicate ids so downstream code never crashes or
// double-books. Returns a fresh, safe array.
export function normalizeEmployees(rawList) {
if (!Array.isArray(rawList)) return [];
const seen = new Set();
const out = [];
for (const e of rawList) {
if (!e || typeof e !== 'object') continue;
const name = String(e.name ?? '').trim();
if (!name) continue; // unusable without a name
let id = (typeof e.id === 'string' && e.id) ? e.id : generateId();
if (seen.has(id)) id = generateId(); // resolve duplicate ids
seen.add(id);
out.push({ id, name, constraints: normalizeConstraints(e.constraints) });
}
return out;
}
export function loadFromJSON(json) {
let parsed;
try {
@@ -30,7 +76,7 @@ export function loadFromJSON(json) {
throw new Error('Ungültiges Dateiformat: Objekt erwartet');
}
return {
employees: Array.isArray(parsed.employees) ? parsed.employees : [],
employees: normalizeEmployees(parsed.employees),
calendar: (parsed.calendar && typeof parsed.calendar === 'object')
? {
holidays: Array.isArray(parsed.calendar.holidays) ? parsed.calendar.holidays : [],
@@ -57,14 +103,7 @@ export function createEmployee(name) {
return {
id: generateId(),
name: trimmed,
constraints: {
neverDays: [],
lowPriorityDays: [],
onlyDays: [],
maxPerMonth: null,
minGapDays: 14,
vacations: []
}
constraints: defaultConstraints()
};
}