function generateId() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { const b = new Uint8Array(16); crypto.getRandomValues(b); b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; const h = [...b].map(x => x.toString(16).padStart(2, '0')); return `${h.slice(0,4).join('')}-${h.slice(4,6).join('')}-${h.slice(6,8).join('')}-${h.slice(8,10).join('')}-${h.slice(10,16).join('')}`; } return Date.now().toString(36) + Math.random().toString(36).slice(2, 10); } function parseSettings(raw) { const n = raw?.moderatorsPerDay; const valid = Number.isInteger(n) && n >= 1 && n <= 5; 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 { parsed = JSON.parse(json); } catch (e) { throw new Error('Ungültige JSON-Datei: ' + e.message); } if (typeof parsed !== 'object' || parsed === null) { throw new Error('Ungültiges Dateiformat: Objekt erwartet'); } return { employees: normalizeEmployees(parsed.employees), calendar: (parsed.calendar && typeof parsed.calendar === 'object') ? { holidays: Array.isArray(parsed.calendar.holidays) ? parsed.calendar.holidays : [], companyClosures: Array.isArray(parsed.calendar.companyClosures) ? parsed.calendar.companyClosures : [] } : { holidays: [], companyClosures: [] }, history: Array.isArray(parsed.history) ? parsed.history : [], settings: parseSettings(parsed.settings) }; } export function saveToJSON(state) { return JSON.stringify({ employees: state.employees, calendar: state.calendar, history: state.history, settings: state.settings }, null, 2); } export function createEmployee(name) { const trimmed = (name ?? '').trim(); if (!trimmed) throw new Error('Mitarbeitername darf nicht leer sein'); return { id: generateId(), name: trimmed, constraints: defaultConstraints() }; } export function removeEmployee(state, id) { state.employees = state.employees.filter(e => e.id !== id); // Also drop the employee's history entries so no orphaned "(unbekannt)" rows remain state.history = state.history.filter(h => h.employeeId !== id); } export function toggleHoliday(state, isoDate) { const arr = state.calendar.holidays; const idx = arr.indexOf(isoDate); if (idx >= 0) arr.splice(idx, 1); else arr.push(isoDate); } export function toggleClosure(state, isoDate) { const arr = state.calendar.companyClosures; const idx = arr.indexOf(isoDate); if (idx >= 0) arr.splice(idx, 1); else arr.push(isoDate); } // Adds entries to history, replacing any existing entries with the same (date, employeeId) export function addHistoryEntries(state, entries) { if (!entries?.length) return; const keys = new Set(entries.map(e => e.date + '|' + e.employeeId)); state.history = state.history.filter(h => !keys.has(h.date + '|' + h.employeeId)); state.history.push(...entries); state.history.sort((a, b) => a.date.localeCompare(b.date)); } export function removeHistoryEntry(state, isoDate, employeeId) { if (employeeId === undefined) { state.history = state.history.filter(h => h.date !== isoDate); } else { state.history = state.history.filter(h => !(h.date === isoDate && h.employeeId === employeeId)); } }