// -----------------------------
// 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];
}
function renderPlanKommentarVerlauf(verlauf){
const el = document.getElementById('planStatusVerlauf');
if(!el) return;
// Gezeigt wird, was geladen ist — beim Start die drei Sitzungen vor der
// aktuellen; „Weitere laden“ holt je fünf ältere (weitereLadenKnopf(),
// js/store.js). Die Sandbox hat keine früheren Fassungen.
const weitere = app.isSandboxPlan() ? '' : weitereLadenKnopf();
if(!verlauf.length && !weitere){ el.hidden = true; el.innerHTML = ''; return; }
el.hidden = false;
const zeigen = verlauf;
const zeilen = zeigen.map((e, i) => `
${zeilen ? `${zeilen}` : ''}${mehr}`;
}
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 || 'Zu diesem Projekt ist noch kein Kommentar erfasst.';
}
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 ${esc(kurzKw(kom.letzter.week.label))}.`
: '';
}
// 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
? `${esc(initialsForName(k.statusAuthor))}${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 = '' + PLAN_ACTIVE_LPS.map(l => ``).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 `
${esc(l)}
${v === '' ? '–' : esc(v) + ' %'}
`;
}).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 = 'Die Sitzung ist gesperrt.';
return;
}
const uid = esc(app.project && app.project.projectUid ? app.project.projectUid : '');
el.innerHTML = ``;
}
// 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.
})();