von single Page hin zur mehreren Seiten geändert

This commit is contained in:
erik committed 2026-08-14 08:26:46 +02:00
1 parent 8c94fc91f6
commit 31c4bd47cd
36 files changed
+207 -97

No files matched your search

+349
View File
@@ -0,0 +1,349 @@
// -----------------------------
// 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 = `<div class="people-sortable">`;
people.forEach(p=>{
const isOpen = personAbsenceOpen[p.id];
html += `<div class="person-card${isOpen ? ' open' : ''}" data-person-id="${p.id}">`;
html += `<div class="person-header" onclick="togglePersonAbsences('${p.id}')">
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px" onclick="event.stopPropagation()" ondragstart="handlePersonDragStart(event)" draggable="true">⋮⋮</span>
<span class="person-toggle">${isOpen ? '▼' : '▶'}</span>
<input type="text" value="${esc(p.name)}" onclick="event.stopPropagation()" onchange="editPerson('${p.id}','name',this.value)" style="width:80px;" placeholder="Kürzel">
<input type="text" value="${esc(p.fullName||p.name)}" onclick="event.stopPropagation()" onchange="editPerson('${p.id}','fullName',this.value)" style="width:180px;" placeholder="Voller Name">
<input type="text" value="${esc(p.email)}" onclick="event.stopPropagation()" onchange="editPerson('${p.id}','email',this.value)" style="width:180px;" placeholder="E-Mail">
<div class="role-checkboxes" onclick="event.stopPropagation()">${ROLES.map(rr=>`<label><input type="checkbox" onchange="togglePersonRole('${p.id}','${rr}',this.checked)" value="${rr}" ${p.role.includes(rr)?'checked':''}> ${rr}</label>`).join('')}</div>
<button class="danger small" onclick="event.stopPropagation(); delPerson('${p.id}')">Löschen</button>
</div>`;
// Aufklappbare Abwesenheiten
html += `<div class="person-absences" style="display:${isOpen ? 'block' : 'none'}">`;
const absences = p.absences || [];
if(absences.length > 0){
html += `<div class="absence-list">`;
absences.forEach(a => {
html += `<div class="absence-chip">
<span>${esc(a.type || 'Abwesenheit')}: ${formatDateDE(parseDateISO(a.start))}–${formatDateDE(parseDateISO(a.end))}</span>
${a.note ? `<small>${esc(a.note)}</small>` : ''}
${locked ? '' : `<button class="danger small" onclick="deleteAbsence('${p.id}','${a.id}')">×</button>`}
</div>`;
});
html += `</div>`;
}
if(!locked){
html += `<div class="absence-form">
<input type="text" id="absenceRange_${p.id}" class="range-picker-display" placeholder="Zeitraum wählen…" readonly
onclick="event.stopPropagation(); openRangePicker({ anchor: this, startInputId: 'absenceStart_${p.id}', endInputId: 'absenceEnd_${p.id}', displayId: 'absenceRange_${p.id}' })">
<input type="hidden" id="absenceStart_${p.id}">
<input type="hidden" id="absenceEnd_${p.id}">
<select id="absenceType_${p.id}">
<option>Urlaub</option>
<option>Krank</option>
<option>Fortbildung</option>
<option>Sonstiges</option>
</select>
<input type="text" id="absenceNote_${p.id}" placeholder="Notiz (optional)" style="width:140px">
<button class="secondary small" onclick="addAbsence('${p.id}')">+ Abwesenheit</button>
</div>`;
}
html += `</div></div>`;
});
html += `</div>`;
if(people.length===0) html = '<div class="hint">Keine Personen vorhanden.</div>';
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();
}