- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
9.9 KiB
JavaScript
181 lines
9.9 KiB
JavaScript
// -----------------------------
|
||
// 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 `<button type="button" class="phasen-ablauf-seg is-${d.status}" style="left:${links}%;width:${breite}%;--fort:${d.prozent}%"
|
||
title="${esc(titel)}" onclick="phasenKachelZeigen('${esc(d.l)}')"><span>${esc(breite > 6 ? d.l : d.l.replace('LPH ', ''))}</span></button>`;
|
||
}).join('');
|
||
const heuteHtml = heutePos === null ? '' : `<span class="phasen-ablauf-heute" style="left:${heutePos}%" title="heute"></span>`;
|
||
const monat = d => d.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' });
|
||
return `<div class="plan-panel phasen-ablauf">
|
||
<div class="phasen-kopf">Ablauf</div>
|
||
<div class="phasen-ablauf-spur">${segmente}${heuteHtml}</div>
|
||
<div class="phasen-ablauf-achse"><span>${esc(monat(mit[0].start))}</span>${heutePos === null ? '' : `<span class="phasen-ablauf-heute-text" style="left:${heutePos}%">heute, ${esc(kurzDatum(heute))}</span>`}<span>${esc(monat(new Date(bis)))}</span></div>
|
||
</div>`;
|
||
}
|
||
|
||
// 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)}`
|
||
: '<span class="phasen-leer">kein Zeitraum</span>';
|
||
const bearbeiten = locked ? ''
|
||
: (d.balken
|
||
? `<button class="secondary small" onclick="openTodoModal(${d.idx})">Bearbeiten</button>`
|
||
: `<button class="secondary small" onclick="addPlanPhase('${esc(d.l)}')">Zeitraum festlegen</button>`);
|
||
const ansehen = d.aufgaben.length
|
||
? `<button class="secondary small" onclick="phasenAufgabenZeigen('${esc(d.l)}')">Aufgaben ansehen</button>` : '';
|
||
return `<div class="phasen-kachel is-${d.status}" data-phase="${esc(d.l)}">
|
||
<div class="phasen-kachel-kopf">
|
||
<div class="phasen-ring" style="--fort:${d.prozent}%"><span>${d.prozent} %</span></div>
|
||
<div class="phasen-kachel-name">
|
||
<b>${esc(d.l)}</b>
|
||
<span>${esc(LPH_NAMEN[d.l] || '')}</span>
|
||
</div>
|
||
<span class="phasen-chip is-${d.status}">${statusText}</span>
|
||
</div>
|
||
${d.beauftragt ? '' : '<div class="phasen-hinweis">nicht beauftragt</div>'}
|
||
<div class="phasen-werte">
|
||
<span><small>Zeitraum</small><b>${zeitraum}</b></span>
|
||
<span><small>Aufwand</small><b>${d.stunden ? d.stunden.toLocaleString('de-DE') + ' h' : '<span class="phasen-leer">keine Stunden</span>'}</b></span>
|
||
<span><small>Aufgaben</small><b>${aufgabenText}</b></span>
|
||
</div>
|
||
${bearbeiten || ansehen ? `<div class="phasen-knoepfe">${bearbeiten}${ansehen}</div>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
// 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 = `<div class="plan-panel"><div class="plan-task-empty">${locked
|
||
? 'Für dieses Projekt sind keine Leistungsphasen hinterlegt.'
|
||
: 'Noch keine Leistungsphase. Unter „Einstellungen“ beauftragen oder über „+ Leistungsphase“ bzw. „Terminplan aus Honorar“ anlegen.'}</div></div>`;
|
||
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 = `<div class="phasen-zusammenfassung">${esc(teile.join(' · '))}<span>Fortschritt aus dem Wochenbericht</span></div>
|
||
${ablaufHtml(daten)}
|
||
<div class="phasen-raster">${daten.map(d => kachelHtml(d, locked)).join('')}</div>`;
|
||
};
|
||
|
||
// 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');
|
||
};
|
||
})();
|