// ----------------------------- // Projektplan — buildGanttHtml-Erweiterung (TODOs & Meilensteine im GANTT) // ----------------------------- // Dieses Modul erweitert die in gantt.js definierte Funktion buildGanttHtml, // damit im Projektplan TODOs und Meilensteine als eigene Zeilen unter der // Projektzeile dargestellt werden. Mit der Option showTodos wird zuerst das // Basis-GANTT erzeugt und dann per DOM-Bearbeitung um die Zusatzzeilen // ergänzt. Die Abhängigkeitslinien zeichnet danach plan-gantt.js // (renderGanttDependencies), die Drag-/Resize- und Klick-Attribute der // Balken nutzen plan-drag.js und plan-modals.js. // // Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp // (plan-state.js). (function(){ const app = window.PlanApp; // Referenz auf die Original-Funktion aus gantt.js, die für das Basis-GANTT // weiterhin verwendet wird. const originalBuildGanttHtml = window.buildGanttHtml; // window.buildGanttHtml (überschrieben): Erweiterte Version der Funktion // aus gantt.js. Ohne Option showTodos wird unverändert die Original-Funktion // aufgerufen. Mit showTodos wird deren HTML per DOM-Bearbeitung um die // TODO- und Meilensteinzeilen aller übergebenen Projekte ergänzt und als // HTML-String zurückgegeben. Parameter: projects (Array), start/end (ISO- // Datum), options (Optionen inkl. editable und criticalPath). Rückgabe: // String mit dem kompletten GANTT-HTML. window.buildGanttHtml = function(projects, start, end, options = {}){ if(!options.showTodos) return originalBuildGanttHtml(projects, start, end, options); const baseHtml = originalBuildGanttHtml(projects, start, end, options); const temp = document.createElement('div'); temp.innerHTML = baseHtml; const chart = temp.querySelector('.gantt-chart'); if(!chart) return baseHtml; const totalDays = Math.max(1, daysBetween(start, end) + 1); const editable = options.editable; projects.forEach(p => { const plan = p.plan || {}; const todos = Array.isArray(plan.todos) ? plan.todos : []; const milestones = Array.isArray(plan.milestones) ? plan.milestones : []; // Projektzeile finden, unter der TODOs und Meilensteine eingehängt werden const projectRow = chart.querySelector('.gantt-row.gantt-project.highlighted') || Array.from(chart.querySelectorAll('.gantt-row.gantt-project')).find(r => { const label = r.querySelector('.gantt-label'); return label && label.textContent.includes(p.name); }); // buildTodoRow: baut die DOM-Zeile einer TODO-Aufgabe mit Label, ggf. // Abhängigkeits-Badge und (im Bearbeitungsmodus) verschiebbarem Balken // mit Resize-Griffen und Fortschrittsanzeige. Vergibt fehlende IDs beim // ersten Rendern. Parameter: todo (Aufgabenobjekt), tIdx (Index in der // Aufgabenliste). Rückgabe: Zeilen-DOM-Element. function buildTodoRow(todo, tIdx){ if(!todo.id) todo.id = uid(); const s = parseDateISO(todo.start), e = parseDateISO(todo.end); const row = document.createElement('div'); row.className = 'gantt-row gantt-todo' + (todo.done ? ' done' : ''); row.dataset.todoId = todo.id; row.dataset.todoIndex = tIdx; let barHtml = ''; if(s && e){ const barStart = s < start ? start : s; const barEnd = e > end ? end : e; if(barEnd >= start && barStart <= end){ const offset = daysBetween(start, barStart); const span = daysBetween(barStart, barEnd) + 1; const dragAttrs = editable ? ` data-gantt-type="todo" data-gantt-project="${esc(p.id)}" data-gantt-index="${tIdx}" data-gantt-start="${formatDateISO(s)}" data-gantt-end="${formatDateISO(e)}" draggable="false"` : ''; const handles = editable ? `` : ''; const isCritical = options.criticalPath && options.criticalPath.includes(todo.id); const progress = Math.max(0, Math.min(100, parseInt(todo.progress, 10) || 0)); barHtml = `
`; } } let depBadge = ''; if(Array.isArray(todo.dependsOn) && todo.dependsOn.length > 0){ // Vorgänger können TODOs oder Meilensteine (Prefix "m_") sein const preds = todo.dependsOn.map(id => { const t = todos.find(x => x.id === id); if(t) return { title: t.title || 'TODO', end: t.end }; const m = milestones.find(x => 'm_' + x.id === id); if(m) return { title: '🚩 ' + (m.name || 'Meilenstein'), end: m.date }; return null; }).filter(Boolean); if(preds.length > 0){ const isConflict = preds.some(pred => pred.end && todo.start && parseDateISO(todo.start) <= parseDateISO(pred.end)); const names = preds.map(pr => pr.title).join(', '); depBadge = `↳ ${preds.length === 1 ? esc(preds[0].title) : esc(preds.length + ' Vorgänger')}`; } } row.innerHTML = `