Files
projektuebersicht/public/js/plan/plan-info.js
T
erikandClaude Opus 5.5 b7e0bd0d92 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>
2026-10-29 14:16:52 +01:00

335 lines
17 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.
// -----------------------------
// Projektbereich — Projektangaben (Anzeige und Stammdaten)
// -----------------------------
// Dieses Modul füllt die Projektangaben des Projektbereichs. Es unterscheidet
// dabei zwei Arten von Angaben, und das ist der Kern der Sache:
//
// * NUR ANZEIGE — Kommentar der Sitzung (#planStatusEdit) und Fortschritt
// je Leistungsphase (#planLphList). Beide entstehen in der wöchentlichen
// Besprechung und werden ausschließlich im Wochenbericht erfasst. Neben
// jeder von ihnen steht ein Verweis dorthin (setzeBerichtVerweis()), der
// hinüberwechselt UND dort dieses Projekt einstellt.
// * STAMMDATEN, hier pflegbar — Gruppe (#planGroupSelect) und amtierende
// Leistungsphase (#planActiveLpSelect), beide im Bereich "Einstellungen".
//
// Warum: Dieselben Angaben waren zuvor an DREI Stellen bearbeitbar —
// Projektboard, Wochenbericht und hier. Niemand wusste, welche davon die
// richtige ist, und zwei Personen konnten dasselbe Feld an verschiedenen
// Stellen ändern. Seitdem hat jede Angabe genau einen Ort, an dem sie
// entsteht, und überall sonst steht sie zum Lesen samt Weg dorthin. Die
// früheren Schreibfunktionen dieses Moduls (planStatusBlur(), setPlanLph(),
// setPlanAssign()) sind damit entfallen.
//
// Die Zuordnung der Beteiligten (p.assign) gehörte früher ebenfalls zu
// dieser "nur Anzeige"-Gruppe — auf Eriks ausdrücklichen Wunsch ist sie
// seitdem umgekehrt: kein reines Anzeigefeld mehr, sondern ein echtes
// Stammdatum, und zwar NUR HIER im Projektbereich editierbar (Bereich
// "Einstellungen", gerendert von js/plan/plan-settings.js —
// #planAssignMatrix liegt dort im Markup, nicht im Dashboard). Der
// Wochenbericht zeigt die Zuordnung seitdem nur noch zum Lesen, mit einem
// Verweis hierher.
//
// BEWUSST NICHT hier: Projektnummer, Bezeichnung, Schlagwörter, Honorar,
// beauftragte Leistungsphasen und die Beteiligten-Matrix. Sie sind
// Stammdaten und werden von js/plan/plan-settings.js gerendert (Honorar und
// beauftragte Phasen geschrieben von setHonorarBetrag()/toggleBeauftragteLp()
// aus js/plan/plan-honorar.js, die Beteiligten-Matrix von planCycleAssign()).
//
// renderPlanInfo() wird von
// renderPlanPage() (plan-state.js) nach jedem Laden aufgerufen; Änderungen
// speichern direkt über app.savePlan() und rendern danach nur das Infopanel
// neu (das GANTT bleibt unberührt).
//
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
// (plan-state.js).
(function(){
const app = window.PlanApp;
// PLAN_ACTIVE_LPS: Phasen, die als amtierende Leistungsphase wählbar sind —
// alle echten Leistungsphasen (inkl. LPH 9), aber ohne die Sammelkategorie
// "W+M". Entspricht AUFWAND_LPS in js/load/load.js, hier separat definiert,
// weil load.js auf der Projektplan-Seite bewusst nicht geladen wird (siehe
// Kopfkommentar in App.html). Bewusst per Filter statt per
// Index-Schnitt, damit eine spätere Ergänzung von LPHS nicht stillschweigend
// durchs Raster fällt.
const PLAN_ACTIVE_LPS = LPHS.filter(l => l !== 'W+M');
// planLpActiveList: liefert die (auf gültige Phasen gefilterte) amtierende
// Leistungsphase eines Projekts als Array — Kopie von lpActiveList() in
// js/load/load.js, das auf dieser Seite nicht geladen wird. Parameter: p
// (Projekt). Rückgabe: Array (leer, wenn keine gesetzt).
function planLpActiveList(p){
const a = p && p.aktuelleLP;
if(Array.isArray(a)) return a.filter(l => PLAN_ACTIVE_LPS.includes(l));
if(typeof a === 'string' && PLAN_ACTIVE_LPS.includes(a)) return [a];
return [];
}
// renderPlanInfo: baut das Projektinfo-Panel (Status-Editor, Gruppe,
// amtierende Leistungsphase, LPH-Prozentliste) aus app.project auf. Wird
// nach jedem Laden (renderPlanPage(), plan-state.js) sowie nach jeder
// Änderung hier selbst erneut aufgerufen. Keine Parameter, keine Rückgabe.
// ---------------------------------------------------------------------------
// Kommentar-Verlauf im Projekt-Dashboard
// ---------------------------------------------------------------------------
// planKommentarDaten: sucht über alle Sitzungen dieses Projekts (bis
// einschließlich der angezeigten) den jüngsten Kommentar und listet die
// früheren je Gewerk. Ein Eintrag im Verlauf entsteht nur, wenn sich der
// Text eines Gewerks gegenüber der Sitzung davor geändert hat — ein
// unverändert mitgeführter Kommentar stünde sonst jede Woche erneut da.
// Rückgabe: { letzter: {week, k, aktuell} | null, verlauf: [{week, gewerk, html, autor, at}] }
function planKommentarDaten(p, week){
const bisKey = week && week.label ? weekSortKey(week.label) : null;
let versionen = (typeof projectVersionsForUid === 'function' && p.projectUid)
? projectVersionsForUid(p.projectUid).filter(v => !bisKey || weekSortKey(v.week.label) <= bisKey)
: [];
// Angezeigte Sitzung immer mit dem lebenden Projektobjekt (Sandbox, oder
// ein Projekt, das in db.weeks noch nicht steht).
versionen = versionen.filter(v => !week || v.week.label !== week.label);
versionen.push({ week: week || { label: '' }, project: p });
let letzter = null;
for(let i = versionen.length - 1; i >= 0; i--){
const k = kommentarVon(versionen[i].project, null);
if(statusPlainText(k.status)){
letzter = { week: versionen[i].week, k, aktuell: i === versionen.length - 1 };
break;
}
}
const gewerke = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
const verlauf = [];
gewerke.forEach(g => {
let vorher = '';
versionen.forEach(v => {
const k = kommentarVon(v.project, g);
const text = statusPlainText(k.status);
if(text && text !== vorher && !(letzter && v.week === letzter.week)){
verlauf.push({ week: v.week, gewerk: g, html: sanitizeStatusHtml(k.status), autor: k.statusAuthor || '', at: k.statusAt || '' });
}
vorher = text;
});
});
// Derselbe Text in mehreren Gewerken derselben Sitzung (Altbestand: die
// Umstellung auf Kommentare je Gewerk hat ihn in jedes kopiert) wird ein
// Eintrag mit allen Gewerken, statt zweimal untereinander zu stehen.
const zusammen = [];
verlauf.forEach(e => {
const gleich = zusammen.find(z => z.week === e.week && statusPlainText(z.html) === statusPlainText(e.html));
if(gleich){ gleich.gewerk += ', ' + e.gewerk; if(!gleich.autor) gleich.autor = e.autor; }
else zusammen.push(Object.assign({}, e));
});
zusammen.sort((a, b) => weekSortKey(b.week.label).localeCompare(weekSortKey(a.week.label))
|| String(b.at).localeCompare(String(a.at)));
return { letzter, verlauf: zusammen };
}
// "KW 43 / 2026" -> "KW 43" (das Jahr nur, wenn es nicht das laufende ist)
function kurzKw(label){
const m = /^(KW\s*\d+)\s*\/\s*(\d{4})$/.exec(String(label || '').trim());
if(!m) return label || '';
return m[2] === String(new Date().getFullYear()) ? m[1] : m[1] + ' / ' + m[2];
}
const PLAN_VERLAUF_KURZ = 3;
let planVerlaufAlle = false;
let planVerlaufProjekt = null;
function renderPlanKommentarVerlauf(verlauf){
const el = document.getElementById('planStatusVerlauf');
if(!el) return;
const uid = app.project && (app.project.projectUid || app.project.id);
if(uid !== planVerlaufProjekt){ planVerlaufProjekt = uid; planVerlaufAlle = false; }
if(!verlauf.length){ el.hidden = true; el.innerHTML = ''; return; }
el.hidden = false;
const zeigen = planVerlaufAlle ? verlauf : verlauf.slice(0, PLAN_VERLAUF_KURZ);
const zeilen = zeigen.map((e, i) => `
<li class="plan-verlauf-eintrag${i === 0 ? ' ist-juengster' : ''}">
<span class="plan-verlauf-kw">${esc(kurzKw(e.week.label))}</span>
<span class="plan-verlauf-punkt" aria-hidden="true"></span>
<div class="plan-verlauf-inhalt">
<div class="plan-verlauf-text">${e.html}</div>
<div class="plan-verlauf-meta">${esc(e.gewerk)}${e.autor ? ' · ' + esc(e.autor) : ''}</div>
</div>
</li>`).join('');
const mehr = verlauf.length > PLAN_VERLAUF_KURZ
? `<button type="button" class="plan-verlauf-mehr" onclick="planVerlaufUmschalten()">${planVerlaufAlle ? 'Weniger anzeigen' : 'Alle ' + verlauf.length + ' anzeigen'}</button>`
: '';
el.innerHTML = `
<div class="plan-verlauf-kopf">
<span class="plan-verlauf-titel">Verlauf</span>
<span class="plan-verlauf-anzahl">${verlauf.length === 1 ? '1 früherer Kommentar' : verlauf.length + ' frühere Kommentare'}</span>
</div>
<ol class="plan-verlauf-liste">${zeilen}</ol>${mehr}`;
}
window.planVerlaufUmschalten = function(){
planVerlaufAlle = !planVerlaufAlle;
window.renderPlanInfo();
};
window.renderPlanInfo = function(){
const p = app.project;
const locked = app.stammGesperrt();
// Kommentar, Fortschritt und Zuordnung sind hier NUR ZU SEHEN.
//
// Sie entstehen in der wöchentlichen Besprechung und werden deshalb im
// Wochenbericht gepflegt — an genau einer Stelle. Zuvor waren dieselben
// drei Angaben an drei Orten bearbeitbar (Projektboard, Wochenbericht,
// Projektbereich); niemand wusste, welcher der richtige ist, und zwei
// Personen konnten dasselbe Feld an verschiedenen Stellen pflegen.
// Was hier bleibt, sind die Stammdaten des Projekts (Bereich
// "Einstellungen", siehe plan-settings.js).
// Gezeigt wird der ZULETZT GESCHRIEBENE Kommentar (Eriks Wunsch, Entwurf
// "A"): Hat die Sitzung noch keinen, der jüngste aus einer früheren — mit
// Hinweis, aus welcher. Darunter der Verlauf der früheren Kommentare.
const kom = planKommentarDaten(p, app.week);
const editEl = document.getElementById('planStatusEdit');
if(editEl){
editEl.className = 'status-edit is-ansicht';
editEl.contentEditable = 'false';
// Alle Gewerke zusammen, je mit Gewerk davor (kommentarVon(), js/utils.js).
const text = kom.letzter ? sanitizeStatusHtml(kom.letzter.k.status) : '';
editEl.innerHTML = text || '<span class="hint" style="margin:0">Zu diesem Projekt ist noch kein Kommentar erfasst.</span>';
}
const hinweisEl = document.getElementById('planStatusHinweis');
if(hinweisEl){
const aelter = kom.letzter && !kom.letzter.aktuell;
hinweisEl.hidden = !aelter;
hinweisEl.innerHTML = aelter
? `In dieser Sitzung noch kein Kommentar — angezeigt wird der letzte aus <b>${esc(kurzKw(kom.letzter.week.label))}</b>.`
: '';
}
// Wer den Kommentar wann geschrieben hat (jüngster Eintrag aller Gewerke).
const autorEl = document.getElementById('planStatusAutor');
if(autorEl){
const k = kom.letzter ? kom.letzter.k : {};
autorEl.innerHTML = k.status && k.statusAuthor
? `<span class="plan-autor-ava">${esc(initialsForName(k.statusAuthor))}</span>${esc(k.statusAuthor)}`
+ (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '')
: '';
}
renderPlanKommentarVerlauf(kom.verlauf);
const statusWrap = document.getElementById('planStatusWrap');
// Nicht mehr als gesperrt markieren: .locked-cell dimmt auf 55 % und legt im
// Dunkelmodus ein Grau darunter — der Kommentar war dadurch kaum lesbar.
// Dass er hier nur angezeigt wird, zeigen schon .is-ansicht und der
// Verweis "Im Wochenbericht bearbeiten".
if(statusWrap) statusWrap.classList.remove('locked-cell');
setzeBerichtVerweis('planStatusVerweis', locked);
// Gruppe — Stammdatum, bleibt hier pflegbar
const groupSel = document.getElementById('planGroupSelect');
if(groupSel){
groupSel.value = p.group || 'Allgemein';
groupSel.disabled = locked;
}
// Amtierende Leistungsphase — Stammdatum, bleibt hier pflegbar
const aktLp = planLpActiveList(p)[0] || '';
const lpSel = document.getElementById('planActiveLpSelect');
if(lpSel){
lpSel.innerHTML = '<option value="">Keine</option>' + PLAN_ACTIVE_LPS.map(l => `<option value="${l}" ${l===aktLp?'selected':''}>${l}</option>`).join('');
lpSel.disabled = locked;
}
// Fortschritt je Leistungsphase — nur Anzeige, ohne Eingabefeld.
// Zeigt nur die BEAUFTRAGTEN Leistungsphasen (p.beauftragteLP, siehe
// "Einstellungen"), genau wie die Wochenbericht-/Board-Karte
// (BOARD_OPTS.nurBeauftragteLph, js/board/board.js) — dieselbe Regel,
// hier nur fest statt als opt-in, da das Dashboard keine zweite Fassung
// mit allen Phasen kennt. Ist nichts beauftragt (ältere Bestands-
// projekte), bleibt es bei der vollständigen Liste, sonst stünde dort
// gar nichts. Vorher zeigte diese Liste IMMER alle Leistungsphasen,
// unabhängig von den Einstellungen — von Erik gemeldet.
const beauftragt = Array.isArray(p.beauftragteLP) ? p.beauftragteLP : [];
const sichtbareLphs = beauftragt.length > 0 ? LPHS.filter(l => beauftragt.includes(l)) : LPHS;
const lphList = document.getElementById('planLphList');
if(lphList){
if(!p.lph) p.lph = {};
lphList.innerHTML = sichtbareLphs.map(l => {
const v = (p.lph[l] !== undefined && p.lph[l] !== '') ? p.lph[l] : '';
const aktiv = l === aktLp;
const pct = v !== '' ? Math.min(100, parseFloat(v)||0) : 0;
return `<div class="board-lph-row is-ansicht${aktiv ? ' lp-aktiv' : ''}"${aktiv ? ' title="Amtierende Leistungsphase"' : ''}>
<span class="board-lph-label">${esc(l)}</span>
<div class="bar"><div style="width:${pct}%;background:${getProgressColor(pct)}"></div></div>
<span class="board-lph-wert">${v === '' ? '–' : esc(v) + ' %'}</span>
</div>`;
}).join('');
}
setzeBerichtVerweis('planLphVerweis', locked);
// Beteiligte: siehe js/plan/plan-settings.js (renderPlanSettings()) —
// Stammdatum, nicht mehr hier.
};
// setzeBerichtVerweis: füllt einen Hinweis-Platz mit dem Weg zur
// Bearbeitung. Der Verweis ist ein echter Sprung: Er wechselt in den
// Wochenbericht UND wählt dort dieses Projekt aus — ohne das müsste man es
// in einer Liste von dreißig wiederfinden.
// Parameter: elementId, locked. Keine Rückgabe.
function setzeBerichtVerweis(elementId, locked){
const el = document.getElementById(elementId);
if(!el) return;
if(locked){
el.innerHTML = '<span class="hint" style="margin:0">Die Sitzung ist gesperrt.</span>';
return;
}
const uid = esc(app.project && app.project.projectUid ? app.project.projectUid : '');
el.innerHTML = `<button type="button" class="plan-verweis" onclick="springZumWochenbericht('${uid}')">
Im Wochenbericht bearbeiten →
</button>`;
}
// springZumWochenbericht: wechselt in den Wochenbericht und stellt dort
// dieses Projekt ein. Beides zusammen, weil ein Sprung, der nur den Bereich
// wechselt, die eigentliche Arbeit — das Projekt wiederfinden — beim
// Nutzer ließe. Parameter: uid (project_uid). Keine Rückgabe.
window.springZumWochenbericht = function(uid){
if(typeof showTab !== 'function') return;
// Erst die Auswahl setzen, dann umschalten: showTab('wochenbericht')
// rendert den Bereich bereits, und der soll gleich das richtige Projekt
// zeigen — sonst blitzte kurz das erste Projekt der Liste auf.
// berichtAuswahl ist eine globale let-Bindung aus
// js/wochenbericht/wochenbericht.js (klassische Skripte, kein Modul).
if(uid) berichtAuswahl = uid;
showTab('wochenbericht');
};
// setPlanGroup: ändert die Gruppe des Projekts. Parameter: val (String).
// Keine Rückgabe; bei gesperrter Woche abgebrochen.
window.setPlanGroup = function(val){
if(app.stammGesperrt()) return;
app.project.group = val;
app.savePlan();
};
// setPlanActiveLp: setzt bzw. hebt die amtierende Leistungsphase des
// Projekts auf. Parameter: val (LPH-Name oder leer). Rendert das
// Infopanel neu, damit die Markierung in der LPH-Liste sofort mitzieht.
window.setPlanActiveLp = function(val){
if(app.stammGesperrt()) return;
if(val) app.project.aktuelleLP = [val];
else delete app.project.aktuelleLP;
app.savePlan();
window.renderPlanInfo();
// Abschnitt "Leistungsphasen" der Einstellungen: Kennzeichen "läuft"
// und die Kurzfassung in der Unternavigation nachziehen.
if(typeof renderPlanSettings === 'function') renderPlanSettings();
};
// Die Schreibfunktionen setPlanLph()/setPlanAssign()/planStatusBlur() sind
// entfallen. Fortschritt und Kommentar werden allein im Wochenbericht
// erfasst (siehe den Kopf dieser Datei); hier gab es damit keinen Aufrufer
// mehr. Die gleichnamigen Funktionen des Projektboards
// (editLph()/saveBoardComment() in js/board/board.js) sind davon
// unberührt — dort hängt die Erfassung des Wochenberichts dran. Die
// Zuordnung der Beteiligten (ehemals setPlanAssign()) läuft seit ihrer
// Verlagerung in die Projekteinstellungen über planCycleAssign()
// (js/plan/plan-settings.js) — ebenfalls kein Fall für dieses Modul.
})();