feat: employees tab with constraint editor (DOM-based, XSS-safe)
This commit is contained in:
264
js/employees.js
Normal file
264
js/employees.js
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user