// ----------------------------- // Projektübersicht — Personen-Verwaltung (global pro Abteilung) // ----------------------------- // Diese Datei rendert den Personen-Tab (Tab "Projektbeteiligte" in `Test.html`) // und verwaltet die globale Personenliste pro Abteilung: Anlegen, Bearbeiten, // Löschen, Rollen-Auswahl, Drag-and-Drop-Sortierung sowie Urlaub/Abwesenheiten. // Sie wird von `Test.html` nach `js/board/board.js` (vor `js/projects/projects.js`) geladen // und stellt globale Funktionen wie `addPerson()`, `delPerson()`, // `editPerson()`, `renderPeople()` und `addAbsence()` bereit. Genutzt wird sie // u.a. von `js/board/board.js` (`renderBoard`), `js/gantt.js` und // `js/plan/plan-modals.js`; die Sortier-Hilfe `getDragAfterElement()` liegt in // `js/projects/projects.js`. // addPerson: fügt eine neue Person der globalen Liste hinzu und // synchronisiert sie in die aktuelle Sitzung. function addPerson(){ if(guardWeekLocked()) return; const w = currentWeek(); if(!w) return; const name = document.getElementById('newPersonName').value.trim(); const fullName = document.getElementById('newPersonFullName').value.trim(); const email = document.getElementById('newPersonEmail').value.trim(); if(!name){ alert("Kürzel eingeben."); return; } const roles = [...document.querySelectorAll('input[name="newPersonRoles"]:checked')].map(el=>el.value); if(roles.length===0){ alert("Bitte mindestens eine Rolle auswählen."); return; } const person = {id:personIdForName(name), name, fullName:fullName||name, email:email, role:roles}; // Globale Liste aktualisieren if(!Array.isArray(db.globalPeople[currentDepartment])) db.globalPeople[currentDepartment] = []; const existing = db.globalPeople[currentDepartment].find(p => p.id === person.id); if(existing){ alert('Eine Person mit diesem Kürzel existiert bereits.'); return; } db.globalPeople[currentDepartment].push(person); // Aktuelle Sitzung aktualisieren if(!Array.isArray(w.people)) w.people = []; if(!w.people.find(p => p.id === person.id)){ w.people.push(JSON.parse(JSON.stringify(person))); } document.getElementById('newPersonName').value=''; document.getElementById('newPersonFullName').value=''; document.getElementById('newPersonEmail').value=''; document.querySelectorAll('input[name="newPersonRoles"]').forEach(el=>el.checked=false); save(); renderPeople(); renderBoard(); } // delPerson: entfernt eine Person aus der globalen Liste. Bereits in // historischen Sitzungen verwendete Personen bleiben dort sichtbar, da // jede Sitzung ihre eigene Kopie der Personendaten speichert. function delPerson(id){ if(!confirm("Person global löschen? Sie bleibt in bereits existierenden Sitzungen sichtbar, wird aber in neuen Sitzungen nicht mehr angeboten.")) return; // Aus globaler Liste entfernen if(Array.isArray(db.globalPeople[currentDepartment])){ db.globalPeople[currentDepartment] = db.globalPeople[currentDepartment].filter(p=>p.id!==id); } // Aus aktueller Sitzung entfernen (nur wenn nicht in Zuordnungen genutzt) const w = currentWeek(); if(w){ const inUse = w.projects.some(p => Object.keys(p.assign).some(k => k.startsWith(id + '|'))); if(inUse){ alert('Die Person ist in der aktuellen Sitzung noch Projekten zugeordnet. Bitte zuerst die Zuordnungen entfernen.'); return; } w.people = (w.people || []).filter(p=>p.id!==id); } save(); renderPeople(); renderBoard(); } // editPerson: ändert Name/Vollname/Rolle sowohl global als auch in der // aktuellen Sitzung. Historische Sitzungen bleiben unverändert. // Bei Feld `name` wird die ID migriert (`personIdForName`) und die // Zuordnungen/Anwesenheiten werden umgeschrieben. Parameter: `id` // (Personen-ID), `field` (Feldname), `val` (neuer Wert). Keine Rückgabe. function editPerson(id,field,val){ if(guardWeekLocked()) return; const w = currentWeek(); if(!w) return; // Global aktualisieren const globalPeople = db.globalPeople[currentDepartment] || []; const globalPerson = globalPeople.find(p => p.id === id); if(field === 'name'){ const newName = val.trim(); if(!newName) return; const newId = personIdForName(newName); if(newId !== id && globalPeople.some(p => p.id === newId)){ alert('Dieses Kürzel wird bereits verwendet.'); return; } if(globalPerson){ globalPerson.name = newName; globalPerson.id = newId; } // Aktuelle Sitzung aktualisieren und Zuordnungen migrieren const localPerson = (w.people || []).find(p => p.id === id); if(localPerson){ localPerson.name = newName; localPerson.id = newId; } w.projects.forEach(p => { const newAssign = {}; Object.keys(p.assign).forEach(k => { const [oldId, role] = k.split('|'); if(oldId === id){ newAssign[assignKey(newId, role)] = p.assign[k]; } else { newAssign[k] = p.assign[k]; } }); p.assign = newAssign; if(p.plan && Array.isArray(p.plan.people)){ p.plan.people = p.plan.people.map(pid => pid === id ? newId : pid); } }); if(w.attendance){ if(w.attendance[id]){ w.attendance[newId] = true; delete w.attendance[id]; } } save(); renderPeople(); renderBoard(); return; } if(field === 'role'){ const roles = Array.isArray(val) ? [...new Set(val)] : val ? [val] : []; if(globalPerson) globalPerson.role = roles; const localPerson = (w.people || []).find(p => p.id === id); if(localPerson) localPerson.role = roles; } else { if(globalPerson) globalPerson[field] = val; const localPerson = (w.people || []).find(p => p.id === id); if(localPerson) localPerson[field] = val; } save(); renderPeople(); renderBoard(); } // togglePersonRole: fügt eine Rolle (`role`) zur Person `id` hinzu bzw. entfernt // sie (je nach Checkbox-Zustand `checked`), sowohl global als auch in der // aktuellen Sitzung. Keine Rückgabe. function togglePersonRole(id, role, checked){ if(guardWeekLocked()) return; const w = currentWeek(); if(!w) return; const globalPeople = db.globalPeople[currentDepartment] || []; const globalPerson = globalPeople.find(p => p.id === id); const localPerson = (w.people || []).find(p => p.id === id); function updateRoles(person){ person.role = Array.isArray(person.role) ? [...person.role] : person.role ? [person.role] : []; if(checked){ if(!person.role.includes(role)) person.role.push(role); } else { person.role = person.role.filter(r=>r!==role); } } if(globalPerson) updateRoles(globalPerson); if(localPerson) updateRoles(localPerson); save(); renderPeople(); renderBoard(); } // renderPeople: baut die HTML-Liste der Personen für den Personen-Tab auf // (Karten mit Bearbeitungsfeldern, Rollen-Checkboxen, aufklappbaren // Abwesenheiten und Drag-Handle). Bei gesperrter Sitzung wird das // Abwesenheits-Formular ausgeblendet. Keine Parameter, keine Rückgabe. function renderPeople(){ const people = db.globalPeople[currentDepartment] || []; const locked = isWeekLocked(currentWeek()); let html = `
`; people.forEach(p=>{ const isOpen = personAbsenceOpen[p.id]; html += `
`; html += `
⋮⋮ ${isOpen ? '▼' : '▶'}
${ROLES.map(rr=>``).join('')}
`; // Aufklappbare Abwesenheiten html += `
`; const absences = p.absences || []; if(absences.length > 0){ html += `
`; absences.forEach(a => { html += `
${esc(a.type || 'Abwesenheit')}: ${formatDateDE(parseDateISO(a.start))}–${formatDateDE(parseDateISO(a.end))} ${a.note ? `${esc(a.note)}` : ''} ${locked ? '' : ``}
`; }); html += `
`; } if(!locked){ html += `
`; } html += `
`; }); html += `
`; if(people.length===0) html = '
Keine Personen vorhanden.
'; document.getElementById('peopleList').innerHTML=html; } // personAbsenceOpen: UI-Zustand, welche Personen-Karte ihre Abwesenheiten // aufgeklappt darstellt (Key = Personen-ID, Wert = boolean). let personAbsenceOpen = {}; // togglePersonAbsences: klappt die Abwesenheits-Liste einer Person auf bzw. zu // (Umschalten des Eintrags in `personAbsenceOpen`) und rendert neu. function togglePersonAbsences(personId){ personAbsenceOpen[personId] = !personAbsenceOpen[personId]; renderPeople(); } // draggedPerson: die während eines Drag-and-Drop-Vorgangs verschobene // Personen-Karte (DOM-Element) bzw. null nach dem Loslassen. let draggedPerson = null; // getPersonCard: liefert die nächstgelegene Personen-Karte (`.person-card`). // Parameter `el`: Event-Ziel-Element. Rückgabe: Karten-Element oder null. function getPersonCard(el){ return el && el.closest ? el.closest('.person-card') : null; } // handlePersonDragStart: startet das Ziehen einer Personen-Karte, merkt sich // diese in `draggedPerson` und verringert ihre Deckkraft. Rückgabe: keine. function handlePersonDragStart(event){ draggedPerson = getPersonCard(event.target); if(!draggedPerson) return; event.dataTransfer.effectAllowed = 'move'; draggedPerson.style.opacity = '0.5'; } // handlePersonDragOver: erlaubt das Ablegen während des Überfahrens // (verhindert den Standard-Drop-Typ und setzt `dropEffect='move'`). function handlePersonDragOver(event){ event.preventDefault(); event.dataTransfer.dropEffect = 'move'; } // handlePersonDrop: legt die gezogene Personen-Karte an der Zielposition // innerhalb `.people-sortable` ab (per `getDragAfterElement`) und speichert // die neue Reihenfolge über `reorderPeople()`. function handlePersonDrop(event){ event.preventDefault(); const targetCard = getPersonCard(event.target); if(!draggedPerson || draggedPerson === targetCard) return; const sortable = document.querySelector('.people-sortable'); if(!sortable) return; const afterElement = getDragAfterElement(sortable, event.clientY); if(afterElement == null){ sortable.appendChild(draggedPerson); } else { sortable.insertBefore(draggedPerson, afterElement); } reorderPeople(); } // handlePersonDragEnd: stellt die Deckkraft der Karte wieder her und setzt // `draggedPerson` zurück (Ende des Drag-Vorgangs). function handlePersonDragEnd(event){ const card = getPersonCard(event.target); if(card) card.style.opacity = '1'; draggedPerson = null; } // reorderPeople: übernimmt die im DOM vorgefundene Reihenfolge der // Personen-Karten in die globale Personenliste der Abteilung und speichert sie. // Keine Parameter, keine Rückgabe; abgebrochen bei gesperrter Sitzung. function reorderPeople(){ if(guardWeekLocked()) return; const sortable = document.querySelector('.people-sortable'); if(!sortable) return; const people = db.globalPeople[currentDepartment] || []; if(people.length === 0) return; const newPeople = []; [...sortable.querySelectorAll('[data-person-id]')].forEach(item => { const person = people.find(p => p.id === item.dataset.personId); if(person) newPeople.push(person); }); if(newPeople.length === people.length){ db.globalPeople[currentDepartment] = newPeople; save(); } } // ---------- Urlaub / Abwesenheiten ---------- // addAbsence: legt für eine Person eine neue Abwesenheit an, die aus dem // Bereichspicker (Start-/Enddatum), Typ und Notiz des Formulars gelesen wird. // Prüft Datumsfolge, sortiert die Liste nach Startdatum und speichert. // Parameter `personId`: Personen-ID. Keine Rückgabe. function addAbsence(personId){ if(guardWeekLocked()) return; const people = db.globalPeople[currentDepartment] || []; const person = people.find(p => p.id === personId); if(!person) return; const start = document.getElementById('absenceStart_' + personId).value; const end = document.getElementById('absenceEnd_' + personId).value; const type = document.getElementById('absenceType_' + personId).value; const note = document.getElementById('absenceNote_' + personId).value.trim(); if(!start || !end){ alert('Bitte Start- und Enddatum angeben.'); return; } if(parseDateISO(end) < parseDateISO(start)){ alert('Enddatum liegt vor Startdatum.'); return; } if(!Array.isArray(person.absences)) person.absences = []; person.absences.push({ id: uid(), start, end, type, note }); person.absences.sort((a,b) => parseDateISO(a.start) - parseDateISO(b.start)); save(); renderPeople(); if(currentTab === 'gantt') renderGantt(); } // deleteAbsence: entfernt eine Abwesenheit (`absenceId`) aus der Liste der // Person (`personId`), speichert und rendert neu. Keine Rückgabe. function deleteAbsence(personId, absenceId){ if(guardWeekLocked()) return; const people = db.globalPeople[currentDepartment] || []; const person = people.find(p => p.id === personId); if(!person || !person.absences) return; person.absences = person.absences.filter(a => a.id !== absenceId); save(); renderPeople(); if(currentTab === 'gantt') renderGantt(); }