// ----------------------------- // Projektbereich — Aufgaben- und Meilensteinliste // ----------------------------- // Bereich "Aufgaben" des Projektbereichs (siehe js/plan/plan-sections.js): // dieselben Daten wie im GANTT, nur als Liste — für alle, die Aufgaben lieber // abarbeiten als sie zu terminieren. Es gibt hier bewusst KEINEN eigenen // Datenbestand und keine eigenen Dialoge: eine Zeile öffnet denselben Dialog // wie ein Klick im GANTT (openTodoModal/openMilestoneModal aus // js/plan/plan-modals.js), das Häkchen ruft toggleTodoDone() auf, und neue // Aufgaben entstehen über dieselben Schaltflächen wie im Terminplan // (addTodo, addPlanMilestone, openTemplateModal, openHonorarModal). // // Die Reihenfolge entspricht der Reihenfolge im GANTT (plan.todos bzw. // plan.milestones), damit der Index in der Liste und der im Dialog // erwartete Index übereinstimmen und beide Ansichten dasselbe Bild zeigen. // // renderPlanTasks() wird am Ende von renderPlanGantt() (plan-gantt.js) // mitgerendert — das ist die zentrale Neu-Render-Funktion, die alle // Änderungen an Aufgaben und Meilensteinen ohnehin aufrufen. // // Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp // (plan-state.js). (function(){ const app = window.PlanApp; // todoStatus: leitet den Status einer Aufgabe aus done/progress ab — // dieselbe Dreiteilung wie das Status-Segment im TODO-Dialog // (offen/arbeit/fertig, siehe plan-modals.js). Parameter: t (TODO). // Rückgabe: 'offen' | 'arbeit' | 'fertig'. function todoStatus(t){ if(t.done) return 'fertig'; return (Number(t.progress) || 0) > 0 ? 'arbeit' : 'offen'; } // personById: sucht eine Person der aktuellen Sitzung anhand ihrer ID. // Rückgabe: Personenobjekt oder null. function personById(id){ return (app.week && Array.isArray(app.week.people) ? app.week.people : []).find(p => p.id === id) || null; } // assigneeAvatars: baut den überlappenden Avatar-Stapel der Zuständigen // einer Aufgabe (dieselben Kreise wie im Projektboard, .board-avatar in // styles.css). Parameter: ids (Array von Person-IDs). Rückgabe: HTML-String. function assigneeAvatars(ids){ const chips = (Array.isArray(ids) ? ids : []).map(id => { const person = personById(id); if(!person) return ''; const role = Array.isArray(person.role) && person.role.length ? person.role[0] : ''; const title = (person.fullName || person.name) + (role ? ' · ' + role : ''); return `${esc(person.name)}`; }).join(''); return chips ? `${chips}` : ''; } // dateRangeText: formatiert den Zeitraum einer Aufgabe als "01.02.2026 – // 05.02.2026" (bzw. nur ein Datum, wenn Start und Ende gleich sind). // Parameter: t (TODO). Rückgabe: String (leer ohne Termine). function dateRangeText(t){ const s = parseDateISO(t.start); const e = parseDateISO(t.end); if(!s && !e) return ''; if(s && e) return formatDateDE(s) === formatDateDE(e) ? formatDateDE(s) : formatDateDE(s) + ' – ' + formatDateDE(e); return formatDateDE(s || e); } // isOverdue: true, wenn eine noch offene Aufgabe ihren Endtermin // überschritten hat. Parameter: t (TODO). Rückgabe: Boolescher Wert. function isOverdue(t){ if(t.done) return false; const e = parseDateISO(t.end); if(!e) return false; const today = new Date(); today.setHours(0, 0, 0, 0); return e < today; } // renderPlanTasks: baut die Aufgaben- und die Meilensteinliste des Bereichs // "Aufgaben" neu auf und schreibt die Zähler in die Kartenüberschriften. // Keine Parameter, keine Rückgabe. window.renderPlanTasks = function(){ if(!app.project) return; const plan = app.ensurePlanLocal(); const locked = app.isLocked(); // ---- Aufgaben ---- const todoEl = document.getElementById('planTodoList'); const todos = Array.isArray(plan.todos) ? plan.todos : []; if(todoEl){ // In gesperrten Sitzungen sind die Anlege-Schaltflächen ausgeblendet // (renderPlanPage(), plan-state.js) — dann darf der Leertext auch nicht // auf sie verweisen. const leerTextTodos = locked ? 'Für diese Sitzung sind keine Aufgaben hinterlegt.' : 'Noch keine Aufgaben. Über „+ Aufgabe“, „+ Vorlage“ oder „+ Terminplan“ anlegen.'; todoEl.innerHTML = todos.length === 0 ? '
' + leerTextTodos + '
' : todos.map((t, idx) => { const status = todoStatus(t); const range = dateRangeText(t); const overdue = isOverdue(t); const del = locked ? '' : ``; const check = locked ? '' : ``; const progress = (!t.done && (Number(t.progress) || 0) > 0) ? `${Math.round(Number(t.progress) || 0)} %` : ''; // In gesperrten Sitzungen weder Klick-Handler noch Einladung zum // Bearbeiten: openTodoModal() bricht dort sowieso wortlos ab. const rowAction = locked ? '' : ` onclick="openTodoModal(${idx})" title="Zum Bearbeiten klicken"`; return `
${check} ${esc(t.title || 'Aufgabe')} ${assigneeAvatars(t.assignees)} ${progress} ${range ? esc(range) : 'ohne Termin'} ${del}
`; }).join(''); } const todoCount = document.getElementById('planTodoCount'); if(todoCount){ const offen = todos.filter(t => !t.done).length; todoCount.textContent = todos.length === 0 ? '' : `${offen} von ${todos.length} offen`; } // ---- Meilensteine ---- const msEl = document.getElementById('planMilestoneList'); const milestones = Array.isArray(plan.milestones) ? plan.milestones : []; if(msEl){ msEl.innerHTML = milestones.length === 0 ? '
' + (locked ? 'Für diese Sitzung sind keine Meilensteine hinterlegt.' : 'Noch keine Meilensteine. Über „+ Meilenstein“ anlegen.') + '
' : milestones.map((m, idx) => { const d = parseDateISO(m.date); const today = new Date(); today.setHours(0, 0, 0, 0); const overdue = d && d < today; const msAction = locked ? '' : ` onclick="openMilestoneModal(${idx})" title="Zum Bearbeiten klicken"`; return `
${esc(m.name || 'Meilenstein')} ${esc(d ? formatDateDE(d) : 'ohne Datum')}
`; }).join(''); } const msCount = document.getElementById('planMilestoneCount'); if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.length} gesamt`; }; })();