// ----------------------------- // 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, project: versionen[i].project, 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 => { // Nur, wenn das Projekt in JENER Sitzung das Gewerk hatte. const k = (g in kommentareDesProjekts(v.project)) ? kommentarVon(v.project, g) : { status: '' }; 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) => `
  • ${esc(kurzKw(e.week.label))}
    ${e.html}
    ${esc(e.gewerk)}${e.autor ? ' · ' + esc(e.autor) : ''}
  • `).join(''); const mehr = weitere ? `
    ${weitere}
    ` : ''; el.innerHTML = `
    Verlauf ${verlauf.length === 0 ? 'in den geladenen Sitzungen kein früherer Kommentar' : verlauf.length === 1 ? '1 früherer Kommentar' : verlauf.length + ' frühere Kommentare'}
    ${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; bei mehreren je mit Überschrift, Linie und // Verfasser (kommentarAnzeigeHtml(), js/utils.js, Eriks Wahl „B“). const text = kom.letzter ? kommentarAnzeigeHtml(kom.letzter.project, null, true) : ''; 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 : {}; // Bei mehreren Gewerken steht der Verfasser schon je Gewerk im Text. const mehrere = kom.letzter && kommentarGewerkeMitText(kom.letzter.project) > 1; autorEl.innerHTML = !mehrere && 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. })();