- 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>
332 lines
17 KiB
JavaScript
332 lines
17 KiB
JavaScript
// -----------------------------
|
||
// 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.
|
||
})();
|