// ----------------------------- // 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 = `
${handles}
`; } } 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 = `
${editable ? '⋮⋮' : ''} ${esc(todo.title || 'TODO')}${depBadge}
${barHtml}
`; return row; } // buildMilestoneRow: baut die DOM-Zeile eines Meilensteins mit Rauten-Symbol // an der Datumsposition, Label und ggf. Datumskennzeichnung. Parameter: // m (Meilensteinobjekt), mIdx (Index in der Meilensteinliste). Rückgabe: // Zeilen-DOM-Element. function buildMilestoneRow(m, mIdx){ const d = parseDateISO(m.date); const row = document.createElement('div'); row.className = 'gantt-row gantt-milestone-row' + (m.position ? ' positioned' : ''); row.dataset.milestoneId = m.id; row.dataset.milestoneIndex = mIdx; let diamond = '', labelStyle = 'display:none'; if(d){ if(d >= start && d <= end){ const offset = daysBetween(start, d); const left = (offset / totalDays * 100).toFixed(4) + '%'; const dragAttrs = editable ? ` data-gantt-type="milestone" data-gantt-project="${esc(p.id)}" data-gantt-index="${mIdx}" data-gantt-date="${formatDateISO(d)}" draggable="false"` : ''; const deps = esc((m.dependsOn || []).join(',')); diamond = `
`; labelStyle = 'left:' + left; } } row.innerHTML = `
${editable ? '⋮⋮' : ''} ${esc(m.name || 'Meilenstein')}
${diamond}
${esc(m.name || '')}
`; return row; } if(!projectRow) return; let insertAfter = projectRow; const insertedMilestoneIds = new Set(); todos.forEach((todo, tIdx) => { // Meilensteine, die VOR dieser Aufgabe positioniert sind milestones.filter(m => m.position === 'before:' + todo.id).forEach(m => { const mIdx = milestones.indexOf(m); const row = buildMilestoneRow(m, mIdx); insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling); insertAfter = row; insertedMilestoneIds.add(m.id); }); // Aufgabenzeile const todoRow = buildTodoRow(todo, tIdx); insertAfter.parentNode.insertBefore(todoRow, insertAfter.nextSibling); insertAfter = todoRow; // Meilensteine, die NACH dieser Aufgabe positioniert sind milestones.filter(m => m.position === 'after:' + todo.id).forEach(m => { const mIdx = milestones.indexOf(m); const row = buildMilestoneRow(m, mIdx); insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling); insertAfter = row; insertedMilestoneIds.add(m.id); }); }); // Positionierte Meilensteine, deren referenzierte Aufgabe nicht mehr existiert, // am Ende der Aufgabenliste anhängen, damit sie nicht verloren gehen. milestones.filter(m => m.position && !insertedMilestoneIds.has(m.id)).forEach(m => { const mIdx = milestones.indexOf(m); const row = buildMilestoneRow(m, mIdx); insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling); insertAfter = row; }); }); return temp.innerHTML; }; })();