- 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>
142 lines
4.7 KiB
JavaScript
142 lines
4.7 KiB
JavaScript
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);
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|