// -----------------------------
// Projektübersicht — Projekte (Listen-Ansicht)
// -----------------------------
// Diese Datei rendert den Projekte-Tab (Tab "Projekte" in `Test.html`) als
// sortierbare Listenansicht: Projekte werden nach Gruppe ("Aktive Projekte",
// "Kleine Projekte" usw.) gruppiert, inline bearbeitbar und per
// Drag-and-Drop umsortiert. Sie wird von `Test.html` nach `js/people/people.js`
// (vor `js/load/load.js`) geladen und stellt globale Funktionen wie
// `renderProjects()`, `editProjectField()`, `editProjectActiveLP()` sowie die
// Drag-and-Drop-Handler und die Sortier-Hilfe `getDragAfterElement()` bereit.
// Letztere wird auch von `js/people/people.js` (Personen-Sortierung) genutzt;
// `renderBoard()` aus `js/board/board.js` wird nach Änderungen aktualisiert.
// 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();
}
// 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(){
const w = currentWeek();
if(!w) return;
const locked = isWeekLocked(w);
let html="";
const groups = {};
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 += ``;
if(groups[g].length===0) html+=`Keine Projekte.
`;
});
document.getElementById('projectsList').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){
const afterElement = getDragAfterElement(sortable, event.clientY);
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){
event.currentTarget.style.opacity = '1';
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();
renderBoard();
}