// ----------------------------- // 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 `
Ablauf
${segmente}${heuteHtml}
${esc(monat(mit[0].start))}${heutePos === null ? '' : `heute, ${esc(kurzDatum(heute))}`}${esc(monat(new Date(bis)))}
`; } // kachelHtml: eine Kachel je Phase. function kachelHtml(d, locked){ const statusText = { laeuft: 'läuft', geplant: 'geplant', fertig: 'fertig' }[d.status]; const erledigt = d.aufgaben.filter(t => t.done).length; const aufgabenText = d.aufgaben.length ? `${erledigt} von ${d.aufgaben.length} erledigt` : 'keine'; const zeitraum = d.start && d.ende ? `${d.start.getFullYear() === d.ende.getFullYear() ? kurzDatum(d.start) : langDatum(d.start)} – ${langDatum(d.ende)}` : 'kein Zeitraum'; const bearbeiten = locked ? '' : (d.balken ? `` : ``); const ansehen = d.aufgaben.length ? `` : ''; return `
${d.prozent} %
${esc(d.l)} ${esc(LPH_NAMEN[d.l] || '')}
${statusText}
${d.beauftragt ? '' : '
nicht beauftragt
'}
Zeitraum${zeitraum} Aufwand${d.stunden ? d.stunden.toLocaleString('de-DE') + ' h' : 'keine Stunden'} Aufgaben${aufgabenText}
${bearbeiten || ansehen ? `
${bearbeiten}${ansehen}
` : ''}
`; } // renderPlanPhasen: baut den Bereich neu auf. Keine Parameter. window.renderPlanPhasen = function(){ const el = document.getElementById('planPhasenWrap'); if(!el || !app.project) return; const locked = app.isLocked(); const daten = phasenDaten(); if(!daten.length){ el.innerHTML = `
${locked ? 'Für dieses Projekt sind keine Leistungsphasen hinterlegt.' : 'Noch keine Leistungsphase. Unter „Einstellungen“ beauftragen oder über „+ Leistungsphase“ bzw. „Terminplan aus Honorar“ anlegen.'}
`; return; } const summeH = daten.reduce((s, d) => s + d.stunden, 0); const enden = daten.filter(d => d.ende).map(d => d.ende.getTime()); const laufend = daten.find(d => d.status === 'laeuft'); const teile = [daten.filter(d => d.beauftragt).length + ' beauftragt']; if(laufend) teile.push('läuft: ' + laufend.l); if(summeH) teile.push(summeH.toLocaleString('de-DE') + ' h in Aufgaben'); if(enden.length) teile.push('Ende voraussichtlich ' + langDatum(new Date(Math.max(...enden)))); el.innerHTML = `
${esc(teile.join(' · '))}Fortschritt aus dem Wochenbericht
${ablaufHtml(daten)}
${daten.map(d => kachelHtml(d, locked)).join('')}
`; }; // phasenKachelZeigen: Klick auf ein Segment im Ablauf — Kachel ins Bild // holen und kurz hervorheben. window.phasenKachelZeigen = function(l){ const k = document.querySelector(`#planPhasenWrap .phasen-kachel[data-phase="${CSS.escape(l)}"]`); if(!k) return; k.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); k.classList.remove('ist-markiert'); void k.offsetWidth; k.classList.add('ist-markiert'); }; // phasenAufgabenZeigen: in den Bereich Aufgaben, gefiltert auf diese Phase. window.phasenAufgabenZeigen = function(l){ if(typeof setzeAufgabenFilter === 'function') setzeAufgabenFilter(l); showPlanSection('aufgaben'); }; })();