// ----------------------------- // 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; } // Phasenfilter der Aufgabenliste: '' = alle, 'LPH 5', oder '-' = ohne // Phase. Gilt nur für das Projekt, für das er gesetzt wurde. let filter = { projekt: null, phase: '' }; const filterPhase = () => filter.projekt === app.project ? filter.phase : ''; // setzeAufgabenFilter: von außen (Bereich Leistungsphasen) oder über die // Filterknöpfe. Parameter: phase ('' | 'LPH x' | '-'). window.setzeAufgabenFilter = function(phase){ filter = { projekt: app.project, phase: phase || '' }; window.renderPlanTasks(); }; // 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 alleTodos = Array.isArray(plan.todos) ? plan.todos : []; // Phasenbalken stehen im eigenen Bereich "Leistungsphasen" // (plan-phasen.js), nicht in der Aufgabenliste. idx bleibt der Index in // plan.todos, den die Dialoge erwarten. const eintraege = alleTodos.map((t, idx) => ({ t, idx, phase: aufgabenPhase(t, alleTodos) })) .filter(e => !balkenPhase(e.t)); const todos = eintraege.map(e => e.t); const fp = filterPhase(); const sichtbar = eintraege.filter(e => !fp || (fp === '-' ? !e.phase : e.phase === fp)); const filterEl = document.getElementById('planTodoFilter'); if(filterEl){ const phasen = LPHS.filter(l => eintraege.some(e => e.phase === l)); const knopf = (wert, text) => ``; filterEl.innerHTML = phasen.length ? 'Phase:' + knopf('', 'Alle') + phasen.map(l => knopf(l, l)).join('') + (eintraege.some(e => !e.phase) ? knopf('-', 'ohne Phase') : '') : ''; } 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 + '
' : sichtbar.length === 0 ? '
Keine Aufgaben in dieser Auswahl.
' : sichtbar.map(({ t, idx, phase }) => { 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')} ${t.notiz ? `${icon('fileText')}` : ''} ${phase ? `${esc(phase)}` : ''} ${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 && !m.erreicht; const msAction = locked ? '' : ` onclick="openMilestoneModal(${idx})" title="Zum Bearbeiten klicken"`; return `
${esc(m.name || 'Meilenstein')} ${m.erreicht ? 'erreicht · ' : ''}${esc(d ? formatDateDE(d) : 'ohne Datum')}
`; }).join(''); } const msCount = document.getElementById('planMilestoneCount'); if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.filter(m => m.erreicht).length} von ${milestones.length} erreicht`; }; })();