Files
erikandClaude Opus 5.5 4ac3d8d53d Kommentare je Gewerk klar getrennt, abgewählte Gewerke ausgeblendet
- Kommentare mehrerer Gewerke mit Überschrift und Linie: Gewerk in seiner
  Farbe, rechts Verfasser und Zeit, darunter der Text (Projekt-Dashboard,
  Projektliste, Wandtafel, Verlauf-Kacheln)
- Behoben: Im Projekt-Dashboard erschienen Kommentare von Gewerken, die das
  Projekt nicht mehr hat (z. B. HLS bei 21-4030); Anzeige und Verlauf zeigen
  nur noch Gewerke des Projekts
- Sitzungswechsel übernimmt nur noch Kommentare der Gewerke des Projekts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:08:20 +01:00

332 lines
17 KiB
JavaScript
Raw Permalink 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, 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) => `
<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 = weitere ? `<div class="plan-verlauf-mehr-zeile">${weitere}</div>` : '';
el.innerHTML = `
<div class="plan-verlauf-kopf">
<span class="plan-verlauf-titel">Verlauf</span>
<span class="plan-verlauf-anzahl">${verlauf.length === 0 ? 'in den geladenen Sitzungen kein früherer Kommentar' : verlauf.length === 1 ? '1 früherer Kommentar' : verlauf.length + ' frühere Kommentare'}</span>
</div>
${zeilen ? `<ol class="plan-verlauf-liste">${zeilen}</ol>` : ''}${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 || '<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 : {};
// 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
? `<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.
})();