265 lines
8.2 KiB
JavaScript
265 lines
8.2 KiB
JavaScript
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);
|
|
});
|
|
}
|