feat: employees tab with constraint editor (DOM-based, XSS-safe)

This commit is contained in:
Ferdinand
2026-04-08 13:18:48 +02:00
parent 72a2af3a74
commit d61dcbe6b2

264
js/employees.js Normal file
View File

@@ -0,0 +1,264 @@
import { state } from './app.js';
import { createEmployee, removeEmployee } from './data.js';
import { readEmployeeNames } from './excel.js';
const WD_LABELS = ['Mo','Di','Mi','Do','Fr'];
const WD_VALUES = [1,2,3,4,5];
export function renderEmployees() {
const container = document.getElementById('tab-employees');
// Build static shell
const shell = document.createElement('div');
const title = document.createElement('h2');
title.className = 'section-title';
title.textContent = 'Mitarbeiter';
shell.appendChild(title);
const row = document.createElement('div');
row.className = 'row';
const nameInput = document.createElement('input');
nameInput.id = 'emp-name-input';
nameInput.type = 'text';
nameInput.placeholder = 'Name eingeben...';
nameInput.style.width = '220px';
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'primary';
addBtn.textContent = 'Hinzufügen';
const excelBtn = document.createElement('button');
excelBtn.type = 'button';
excelBtn.textContent = 'Excel importieren';
const fileInput = document.createElement('input');
fileInput.id = 'emp-excel-input';
fileInput.type = 'file';
fileInput.accept = '.xlsx,.xls';
fileInput.style.display = 'none';
row.append(nameInput, addBtn, excelBtn, fileInput);
const list = document.createElement('div');
list.id = 'employee-list';
shell.append(row, list);
container.replaceChildren(shell);
refreshList();
addBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
if (!name) return;
try {
state.employees.push(createEmployee(name));
} catch {
return;
}
nameInput.value = '';
refreshList();
});
nameInput.addEventListener('keydown', e => { if (e.key === 'Enter') addBtn.click(); });
excelBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
const buf = await file.arrayBuffer();
const names = readEmployeeNames(buf);
for (const name of names) {
if (!state.employees.some(emp => emp.name === name)) {
try { state.employees.push(createEmployee(name)); } catch { /* skip invalid */ }
}
}
e.target.value = '';
refreshList();
});
}
function refreshList() {
const list = document.getElementById('employee-list');
list.replaceChildren();
if (state.employees.length === 0) {
const msg = document.createElement('p');
msg.style.cssText = 'color:#9ca3af;margin-top:16px';
msg.textContent = 'Noch keine Mitarbeiter hinzugefügt.';
list.appendChild(msg);
return;
}
for (const emp of state.employees) {
const item = document.createElement('div');
item.className = 'employee-item';
const nameSpan = document.createElement('span');
nameSpan.className = 'employee-name';
nameSpan.textContent = emp.name; // textContent: safe, no XSS
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.textContent = 'Einschränkungen';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'danger';
removeBtn.textContent = 'Entfernen';
item.append(nameSpan, editBtn, removeBtn);
list.appendChild(item);
const editorDiv = document.createElement('div');
editorDiv.id = 'editor-' + emp.id;
editorDiv.style.display = 'none';
list.appendChild(editorDiv);
removeBtn.addEventListener('click', () => {
removeEmployee(state, emp.id);
refreshList();
});
editBtn.addEventListener('click', () => {
if (editorDiv.style.display === 'none') {
renderConstraintEditor(emp.id);
editorDiv.style.display = 'block';
} else {
editorDiv.style.display = 'none';
}
});
}
}
function renderConstraintEditor(empId) {
const emp = state.employees.find(e => e.id === empId);
if (!emp) return;
const c = emp.constraints;
const el = document.getElementById('editor-' + empId);
el.replaceChildren();
const form = document.createElement('div');
form.className = 'constraint-form';
// Helper: day toggle group
function makeDayGroup(label, constraintKey) {
const row = document.createElement('div');
row.className = 'constraint-row';
const lbl = document.createElement('label');
lbl.textContent = label;
const group = document.createElement('div');
group.className = 'day-toggle-group';
for (let i = 0; i < 5; i++) {
const day = WD_VALUES[i];
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'day-toggle' + (c[constraintKey].includes(day) ? ' active' : '');
btn.textContent = WD_LABELS[i];
btn.addEventListener('click', () => {
const arr = emp.constraints[constraintKey];
const idx = arr.indexOf(day);
if (idx >= 0) arr.splice(idx, 1); else arr.push(day);
btn.classList.toggle('active');
});
group.appendChild(btn);
}
row.append(lbl, group);
return row;
}
// Helper: number input
function makeNumberInput(label, constraintKey, placeholder) {
const row = document.createElement('div');
row.className = 'constraint-row';
const lbl = document.createElement('label');
lbl.textContent = label;
const input = document.createElement('input');
input.type = 'number';
input.min = '0';
input.style.width = '120px';
input.placeholder = placeholder;
input.value = c[constraintKey] != null ? c[constraintKey] : '';
input.addEventListener('change', () => {
const val = input.value.trim();
emp.constraints[constraintKey] = val === '' ? null : parseInt(val, 10);
});
row.append(lbl, input);
return row;
}
form.appendChild(makeDayGroup('Nie verfügbar', 'neverDays'));
form.appendChild(makeDayGroup('Niedrige Priorität / Home-Office', 'lowPriorityDays'));
form.appendChild(makeDayGroup('Nur an diesen Tagen (leer = alle)', 'onlyDays'));
form.appendChild(makeNumberInput('Max. Einsätze pro Monat', 'maxPerMonth', 'Kein Limit'));
form.appendChild(makeNumberInput('Mindestabstand (Tage)', 'minGapDays', '0'));
// Vacation section
const vacRow = document.createElement('div');
vacRow.className = 'constraint-row';
const vacLabel = document.createElement('label');
vacLabel.textContent = 'Urlaub';
const vacContainer = document.createElement('div');
vacContainer.id = 'vacations-' + empId;
const addVacBtn = document.createElement('button');
addVacBtn.type = 'button';
addVacBtn.textContent = '+ Urlaub hinzufügen';
addVacBtn.style.marginTop = '6px';
vacRow.append(vacLabel, vacContainer, addVacBtn);
form.appendChild(vacRow);
el.appendChild(form);
renderVacationList(empId);
addVacBtn.addEventListener('click', () => {
emp.constraints.vacations.push({ from: '', to: '' });
renderVacationList(empId);
});
}
function renderVacationList(empId) {
const emp = state.employees.find(e => e.id === empId);
const container = document.getElementById('vacations-' + empId);
container.replaceChildren();
if (emp.constraints.vacations.length === 0) {
const msg = document.createElement('span');
msg.style.cssText = 'color:#9ca3af;font-size:12px';
msg.textContent = 'Kein Urlaub eingetragen';
container.appendChild(msg);
return;
}
emp.constraints.vacations.forEach((v, i) => {
const row = document.createElement('div');
row.className = 'row';
row.style.marginBottom = '4px';
const fromInput = document.createElement('input');
fromInput.type = 'date';
fromInput.value = v.from;
fromInput.addEventListener('change', () => { emp.constraints.vacations[i].from = fromInput.value; });
const sep = document.createElement('span');
sep.textContent = 'bis';
const toInput = document.createElement('input');
toInput.type = 'date';
toInput.value = v.to;
toInput.addEventListener('change', () => { emp.constraints.vacations[i].to = toInput.value; });
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.textContent = '✕';
delBtn.addEventListener('click', () => {
emp.constraints.vacations.splice(i, 1);
renderVacationList(empId);
});
row.append(fromInput, sep, toInput, delBtn);
container.appendChild(row);
});
}