// ----------------------------- // Projektübersicht — Projektliste (Ablage-Logik) // ----------------------------- // Der eigene Tab "Projekte" ist entfallen. Er listete dieselben Projekte ein // zweites Mal auf und ließ dieselben Angaben — Nummer, Bezeichnung, Gruppe, // amtierende Leistungsphase, Schlagwörter, Aufwand — ein zweites Mal // bearbeiten. Diese Angaben haben ihren einen Ort jetzt im Projektbereich // (js/plan/plan-settings.js, js/plan/plan-info.js); die Liste selbst ist im // Projektboard aufgegangen (renderBoard(), js/board/board.js). // // Was hier bleibt, betrifft die LISTE, nicht die einzelnen Angaben: // // * delProjectFromList() — Projekt aus der Sitzung löschen // * die Drag-and-Drop-Handler + reorderProjects() — Reihenfolge innerhalb // einer Gruppe und Wechsel zwischen Gruppen. Das Board spannt dieselbe // Ablagefläche auf (.projects-sortable mit data-group), damit diese // Handler unverändert weiterarbeiten. // * getDragAfterElement() — Sortier-Hilfe, auch von js/people/people.js // genutzt // // editProjectField()/editProjectActiveLP()/addProjectTag()/removeProjectTag() // stehen weiterhin bereit: Der Beauftragungs-Dialog und die Auslastungs-Maske // rufen sie auf. renderProjects() rendert nur noch, wenn ein Element // #projectsList vorhanden ist — sonst übernimmt renderBoard(). // delProjectFromList: löscht ein Projekt (per Bestätigungsdialog) in der // Listen-Ansicht. Parameter `id`: Projekt-ID. Keine Rückgabe; bei gesperrter // Sitzung abgebrochen. function delProjectFromList(id){ if(guardWeekLocked()) return; if(!confirm("Projekt aus dieser Sitzung löschen?")) return; const w = currentWeek(); w.projects = w.projects.filter(p=>p.id!==id); save(); renderProjects(); renderBoard(); } // editProjectField: ändert ein Feld eines Projekts (z.B. Name oder Gruppe). // Parameter: `id` (Projekt-ID), `field` (Feldname), `val` (neuer Wert). // Beim Wechsel der Gruppe wird das Projekt ans Ende der Zielgruppe angehängt // (neue `order` = höchste Order der Zielgruppe + 1), damit es nicht anhand // seiner alten `order` in der Liste landet. Keine Rückgabe. function editProjectField(id,field,val){ if(guardWeekLocked()) return; const w = currentWeek(); const p = w.projects.find(x=>x.id===id); if(!p) return; if(field === 'group' && val && val !== p.group){ p.group = val; const targetProjects = w.projects.filter(x => x.id !== id && x.group === val); const maxOrder = targetProjects.reduce((m,x)=>Math.max(m, x.order || 0), -1); p.order = maxOrder + 1; } else { p[field] = val; } save(); renderProjects(); renderBoard(); } // editProjectActiveLP: setzt bzw. hebt die amtierende Leistungsphase eines // Projekts auf (`p.aktuelleLP`). Parameter: `id` (Projekt-ID), `value` // (LPH-Name oder leer). Aktualisiert Projekte, Board und Auslastung. function editProjectActiveLP(id, value){ if(guardWeekLocked()) return; const p = currentWeek().projects.find(x=>x.id===id); if(!p) return; if(value) p.aktuelleLP = [value]; else delete p.aktuelleLP; save(); renderProjects(); renderBoard(); renderLoad(); } // addProjectTag: fügt dem Projekt einen neuen Tag hinzu (getrimmt, nicht // leer, keine exakten Duplikate). Initialisiert `p.tags`, falls es fehlt // oder kein Array ist. Parameter: `projectId`, `tag` (roher Eingabetext). // Keine Rückgabe; bei gesperrter Sitzung abgebrochen. function addProjectTag(projectId, tag){ if(guardWeekLocked()) return; const val = String(tag || '').trim(); if(!val) return; const w = currentWeek(); const p = w.projects.find(x=>x.id===projectId); if(!p) return; if(!Array.isArray(p.tags)) p.tags = []; if(p.tags.includes(val)) return; p.tags.push(val); save(); renderProjects(); renderBoard(); } // removeProjectTag: entfernt einen Tag aus dem Projekt (exakter Textvergleich). // Parameter: `projectId`, `tag`. Keine Rückgabe; bei gesperrter Sitzung // abgebrochen. function removeProjectTag(projectId, tag){ if(guardWeekLocked()) return; const w = currentWeek(); const p = w.projects.find(x=>x.id===projectId); if(!p) return; p.tags = (Array.isArray(p.tags) ? p.tags : []).filter(t => t !== tag); save(); renderProjects(); renderBoard(); } // handleProjectTagKeydown: fügt bei Enter oder Komma im Tag-Eingabefeld den // aktuellen Feldinhalt als neuen Tag hinzu (das Eingabefeld wird ohnehin // beim anschließenden renderProjects() frisch/leer neu aufgebaut). Parameter: // `event` (KeyboardEvent), `projectId`. Keine Rückgabe. function handleProjectTagKeydown(event, projectId){ if(event.key !== 'Enter' && event.key !== ','){ return; } event.preventDefault(); addProjectTag(projectId, event.target.value); } // renderProjects: baut die HTML-Liste der Projekte auf, gruppiert nach `group` // und sortiert je Gruppe nach `order`. Jedes Projekt erhält Felder für Nr., // Name, Gruppe, aktive LP sowie Drag-and-Drop-Handler. Keine Parameter, keine // Rückgabe. function renderProjects(){ // Ohne #projectsList gibt es den eigenen Projekte-Bereich nicht mehr — die // Liste steht im Projektboard. Der Aufruf bleibt dennoch überall stehen // (renderAll(), editProjectField(), …), damit kein Pfad ins Leere greift. const listEl = document.getElementById('projectsList'); if(!listEl) return; const w = currentWeek(); if(!w) return; const locked = isWeekLocked(w); let html=""; const groups = {}; // Alle Standardgruppen vorbelegen: Ohne das entsteht für eine leer geräumte // Gruppe weder Überschrift noch Ablagefläche — ein Projekt ließ sich dann // nur noch über das Auswahlfeld zurückverschieben, und der Hinweis // "Keine Projekte." unten war toter Code. ['Aktive Projekte','Kleine Projekte','In Anbahnung','Allgemein'].forEach(g => { groups[g] = []; }); w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); }); Object.keys(groups).forEach(g=>{ groups[g].sort((a,b)=>(a.order||0)-(b.order||0)); }); sortGroups(Object.keys(groups)).forEach(g=>{ html += `
${esc(g)}
`; html += `
`; groups[g].forEach(p=>{ const tags = Array.isArray(p.tags) ? p.tags : []; html += `
${locked ? '' : `${icon('grip')}`}
${tags.map(tag => `${esc(tag)}${locked ? '' : ` ×`}`).join('')}
${locked ? '' : ``}
`; }); if(groups[g].length===0) html+=`
Keine Projekte — Projekt hierher ziehen.
`; html += `
`; }); listEl.innerHTML = html; } // Drag-and-Drop Handling für Projekte // draggedProject: die während eines Drag-and-Drop-Vorgangs gezogene // Projekt-Zeile (DOM-Element) bzw. null nach dem Loslassen. let draggedProject = null; // handleDragStart: startet das Ziehen einer Projekt-Zeile, merkt sich das // Element in `draggedProject` und verringert seine Deckkraft. function handleDragStart(event){ draggedProject = event.currentTarget; event.dataTransfer.effectAllowed = 'move'; event.currentTarget.style.opacity = '0.5'; } // handleDragOver: erlaubt das Ablegen während des Überfahrens eines Ziels // (verhindert Standardverhalten, setzt `dropEffect='move'`). function handleDragOver(event){ event.preventDefault(); event.dataTransfer.dropEffect = 'move'; } // handleDrop: legt die gezogene Projekt-Zeile ab. Der Handler hängt am // Gruppen-Container (`.projects-sortable`), sodass das Ablegen über der // gesamten Gruppe inklusive leerer Flächen möglich ist. Beim Wechsel zwischen // Gruppen (z. B. "Kleine Projekte" → "Aktive Projekte") wird das Projekt ans // Ende der Zielgruppe angehängt; innerhalb derselben Gruppe wird die Position // per `getDragAfterElement` relativ zur Mausposition bestimmt. Danach speichert // `reorderProjects()` die neue Reihenfolge. function handleDrop(event){ event.preventDefault(); if(!draggedProject) return; const sortable = event.currentTarget; const sourceSortable = draggedProject.parentElement; const isSameGroup = sourceSortable && sourceSortable.dataset && sortable.dataset && sourceSortable.dataset.group === sortable.dataset.group; if(isSameGroup){ // Die Ablagefläche der Projektliste enthält .board-row-Zeilen, der // Personen-/Projekte-Aufbau .person-list-item. getDragAfterElement() // braucht den passenden Selektor, sonst findet es keine Nachbarn und // hängt jedes Projekt ans Gruppenende. const sel = sortable.querySelector('.board-row') ? '.board-row:not([style*="opacity"])' : undefined; const afterElement = getDragAfterElement(sortable, event.clientY, sel); if(afterElement == null){ sortable.appendChild(draggedProject); } else { sortable.insertBefore(draggedProject, afterElement); } } else { // Gruppenwechsel: ans Ende der Zielgruppe anhängen sortable.appendChild(draggedProject); } reorderProjects(); } // handleDragEnd: stellt die Deckkraft der gezogenen Zeile wieder her und // setzt `draggedProject` zurück (Ende des Drag-Vorgangs). function handleDragEnd(event){ // Attribut entfernen statt auf 1 setzen: getDragAfterElement() schließt jedes // Element mit style="…opacity…" von der Positionsberechnung aus. Nach einem // abgebrochenen Ziehen (Loslassen außerhalb der Liste) blieb das Attribut // stehen, und die Zeile wurde beim nächsten Ablegen ignoriert — das Projekt // landete dann am Gruppenende statt an der gewünschten Stelle. event.currentTarget.style.removeProperty('opacity'); if(event.currentTarget.getAttribute('style') === '') event.currentTarget.removeAttribute('style'); draggedProject = null; } // getDragAfterElement: ermittelt das Element in `container`, vor dem die // gezogene Zeile bei Mausposition `y` abgelegt werden soll (Suche nach dem // Element mit der kleinsten negativen Distanz zur Mitte). Parameter: // `selector` = CSS-Selektor der sortierbaren Elemente (standardmäßig die // Projekt-Zeilen, steht aber auch der Personen-Liste für `.person-card` zur // Verfügung). Rückgabe: Ziel-Element oder undefined (→ ans Ende anfügen). // Wird auch von `js/people/people.js` für die Personen-Sortierung verwendet. function getDragAfterElement(container, y, selector){ const sel = selector || '.person-list-item:not([style*="opacity"])'; const draggableElements = [...container.querySelectorAll(sel)]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if(offset < 0 && offset > closest.offset){ return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } // reorderProjects: übernimmt die im DOM vorgefundene Reihenfolge der // Projekt-Zeilen in die Daten (je Gruppe `order`-Werte) und aktualisiert // Gruppe/Zuordnung pro Projekt. Keine Parameter, keine Rückgabe; bei // gesperrter Sitzung abgebrochen. function reorderProjects(){ if(guardWeekLocked()) return; const w = currentWeek(); const sortables = document.querySelectorAll('.projects-sortable'); sortables.forEach(sortable => { const group = sortable.dataset.group; const items = sortable.querySelectorAll('[data-project-id]'); items.forEach((item, index) => { const projectId = item.dataset.projectId; const project = w.projects.find(p => p.id === projectId); if(project){ project.order = index; project.group = group; } }); }); save(); // renderProjects() nur, solange der eigene Projekte-Bereich existiert; die // Projektliste im Board rendert renderBoard(). if(typeof renderProjects === 'function' && document.getElementById('projectsList')) renderProjects(); renderBoard(); }