Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb

- 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>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-10-29 14:16:52 +01:00
1 parent f4fb3c550b
commit b7e0bd0d92
56 files changed
+12265 -2974

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
// -----------------------------
// 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');
};
})();