Files
projektuebersicht/public/js/plan/plan-build-gantt.js
T

174 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------
// 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 ? `<span class="gantt-resize gantt-resize-left" data-resize="left"></span><span class="gantt-resize gantt-resize-right" data-resize="right"></span>` : '';
const isCritical = options.criticalPath && options.criticalPath.includes(todo.id);
const progress = Math.max(0, Math.min(100, parseInt(todo.progress, 10) || 0));
barHtml = `<div class="gantt-bar todo${todo.done ? ' done' : ''}${isCritical ? ' critical' : ''}"${dragAttrs} data-todo-id="${esc(todo.id)}" data-todo-depends="${esc((todo.dependsOn || []).join(','))}" style="left:${(offset/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%" title="${esc(todo.title || 'TODO')}: ${formatDateDE(s)}–${formatDateDE(e)}">${handles}<div class="gantt-progress" style="width:${progress}%"></div></div>`;
}
}
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 = `<span class="gantt-dep-badge${isConflict ? ' conflict' : ''}" title="Vorgänger: ${esc(names)}">↳ ${preds.length === 1 ? esc(preds[0].title) : esc(preds.length + ' Vorgänger')}</span>`;
}
}
row.innerHTML = `<div class="gantt-label" data-todo-id="${esc(todo.id)}" style="padding-left:24px;font-weight:400;color:var(--text-muted)" title="${esc(todo.title || 'TODO')}">
${editable ? '<span class="gantt-todo-drag">⋮⋮</span>' : ''}
<span class="gantt-todo-title"${editable ? ' onclick="openTodoModal(' + tIdx + ')" style="cursor:pointer"' : ''}>${esc(todo.title || 'TODO')}${depBadge}</span>
</div>
<div class="gantt-timeline">${barHtml}</div>`;
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 = `<div class="gantt-milestone"${dragAttrs} data-todo-id="m_${esc(m.id)}" data-todo-depends="${deps}" style="left:${left}" title="${esc(m.name)}: ${formatDateDE(d)}"></div>`;
labelStyle = 'left:' + left;
}
}
row.innerHTML = `<div class="gantt-label" style="padding-left:24px;font-weight:400;color:var(--text-muted)">
${editable ? '<span class="gantt-todo-drag">⋮⋮</span>' : ''}
<span class="gantt-milestone-title"${editable ? ' onclick="openMilestoneModal(' + mIdx + ')" style="cursor:pointer"' : ''}>${esc(m.name || 'Meilenstein')}</span>
</div>
<div class="gantt-timeline">${diamond}<div class="gantt-milestone-label" style="${labelStyle}">${esc(m.name || '')}</div></div>`;
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;
});
// 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;
};
})();