Files
Meeting-Mixer/js/data.js
T
ferdi2goandClaude Opus 4.8 065876ced4 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>
2026-06-03 12:20:41 +00:00

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));
}
}