// ----------------------------- // Projektbereich — Leistungsphasen // ----------------------------- // Bereich "Leistungsphasen" (Eriks Wahl "B"): die Phasenbalken des // Terminplans (Aufgaben mit t.lph, balkenPhase() in load.js) als eigener // Bereich statt als Zeilen zwischen den Aufgaben. Oben ein Ablaufbalken über // alle Phasen mit Heute-Linie, darunter eine Kachel je Phase: Fortschritt // (aus dem Wochenbericht, p.lph), Zeitraum (aus dem Balken), Aufwand // (Summe der Stunden ihrer Aufgaben) und die Aufgaben, die in ihrem // Zeitraum beginnen — dieselbe Zuordnung wie die Gliederung im Terminplan // (plan-build-gantt.js). // // Keine eigene Datenhaltung: Bearbeiten öffnet dasselbe Panel wie im // Terminplan (openTodoModal() im Phasen-Modus), Fortschritt bleibt im // Wochenbericht ("Ein Ort je Angabe"). // // renderPlanPhasen() wird von renderPlanGantt() (plan-gantt.js) und beim // Wechsel in den Bereich (showPlanSection(), plan-sections.js) aufgerufen. // aufgabenPhase: zu welcher Leistungsphase eine (gewöhnliche) Aufgabe gehört — // der Phasenbalken, in dessen Zeitraum sie beginnt. Dieselbe Regel wie die // Gliederung im Terminplan. Parameter: t (Aufgabe), todos (alle Aufgaben des // Plans). Rückgabe: Phasenname ('LPH 5') oder ''. function aufgabenPhase(t, todos){ if(!t || balkenPhase(t)) return ''; const s0 = parseDateISO(t.start); if(!s0) return ''; const k = (todos || []) .filter(b => balkenPhase(b) && b.start && b.end) .sort((a, b) => String(a.start).localeCompare(String(b.start))) .find(b => s0 >= parseDateISO(b.start) && s0 <= parseDateISO(b.end)); return k ? balkenPhase(k) : ''; } (function(){ const app = window.PlanApp; const LPH_NAMEN = { 'LPH 1': 'Grundlagenermittlung', 'LPH 2': 'Vorplanung', 'LPH 3': 'Entwurfsplanung', 'LPH 4': 'Genehmigungsplanung', 'LPH 5': 'Ausführungsplanung', 'LPH 6': 'Vorbereitung der Vergabe', 'LPH 7': 'Mitwirkung bei der Vergabe', 'LPH 8': 'Objektüberwachung', 'LPH 9': 'Objektbetreuung', 'W+M': 'Wartung und Monitoring' }; const kurzDatum = d => d ? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) : ''; const langDatum = d => d ? formatDateDE(d) : ''; // phasenDaten: je Phase alles, was Ablaufbalken und Kachel brauchen. // Rückgabe: Array, sortiert läuft → geplant → fertig, darin nach Beginn. function phasenDaten(){ const p = app.project; const plan = app.ensurePlanLocal(); const todos = Array.isArray(plan.todos) ? plan.todos : []; const beauftragt = Array.isArray(p.beauftragteLP) ? p.beauftragteLP : []; // Phasen: beauftragte plus alle, die einen Balken haben. const namen = LPHS.filter(l => beauftragt.includes(l) || todos.some(t => balkenPhase(t) === l)); const amtierend = (typeof lpActiveList === 'function' ? lpActiveList(p) : [])[0] || null; const laufend = typeof arbeitsPhase === 'function' ? arbeitsPhase(p, amtierend) : amtierend; return namen.map(l => { const idx = todos.findIndex(t => balkenPhase(t) === l); const balken = idx >= 0 ? todos[idx] : null; const start = balken ? parseDateISO(balken.start) : null; const ende = balken ? parseDateISO(balken.end) : null; const prozent = typeof fortschrittProzent === 'function' ? fortschrittProzent(p, l) : 0; const aufgaben = todos.filter(t => aufgabenPhase(t, todos) === l); // Aufwand = Summe der Stunden ihrer Aufgaben (dieselbe Quelle wie die // Auslastung, js/load/load.js), nicht mehr aus dem Honorar. const stunden = Math.round(aufgaben.reduce((s, t) => s + (Number(t.aufwand) || 0), 0) * 10) / 10; const status = prozent >= 100 ? 'fertig' : (l === laufend ? 'laeuft' : 'geplant'); return { l, idx, balken, start, ende, prozent, stunden, aufgaben, status, beauftragt: !beauftragt.length || beauftragt.includes(l) }; }).sort((a, b) => { const rang = { laeuft: 0, geplant: 1, fertig: 2 }; if(rang[a.status] !== rang[b.status]) return rang[a.status] - rang[b.status]; return LPHS.indexOf(a.l) - LPHS.indexOf(b.l); }); } // ablaufHtml: der schmale Balken über alle Phasen mit Zeitraum. Phasen // ohne Zeitraum fehlen hier (sie stehen als Kachel mit Hinweis darunter). function ablaufHtml(daten){ const mit = daten.filter(d => d.start && d.ende && d.ende >= d.start) .sort((a, b) => a.start - b.start); if(!mit.length) return ''; const von = mit[0].start.getTime(); const bis = Math.max(...mit.map(d => d.ende.getTime())); const spanne = Math.max(1, bis - von); const pos = t => Math.min(100, Math.max(0, (t - von) / spanne * 100)); const heute = new Date(); heute.setHours(0, 0, 0, 0); const heutePos = heute.getTime() >= von && heute.getTime() <= bis ? pos(heute.getTime()) : null; const segmente = mit.map(d => { const links = pos(d.start.getTime()); const breite = Math.max(1.5, pos(d.ende.getTime() + 86400000) - links); const titel = `${d.l}: ${langDatum(d.start)} – ${langDatum(d.ende)}, ${d.prozent} %`; return ``; }).join(''); const heuteHtml = heutePos === null ? '' : ``; const monat = d => d.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' }); return `