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>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-02 22:08:20 +01:00
1 parent e291ca8438
commit 4ac3d8d53d
7 files changed
+95 -24

No files matched your search

+25
View File
@@ -4871,6 +4871,31 @@ Kommentare nach dreimaligem Bereinigen unverändert). `entitiesReparieren()`
`project_kommentare.status` und `protokoll_punkte.text` aller Sitzungen über `project_kommentare.status` und `protokoll_punkte.text` aller Sitzungen über
`entitiesEntstapeln()` zurück. `entitiesEntstapeln()` zurück.
## Behobener Fehler: Kommentare abgewählter Gewerke (11/2026)
Eriks Meldung zu 21-4030: Im Projekt-Dashboard stand ein HLS-Kommentar,
obwohl das Projekt seit KW 31 nur noch ELT hat. Ursache: Die einmalige
Umstellung `kommentareInAktuelleSitzungUebernehmen()` hatte je Gewerk den
jüngsten früheren Kommentar übernommen, ohne die aktuellen Gewerke des
Projekts zu prüfen; der Sitzungswechsel trug ihn weiter. Jetzt:
`kommentareDesProjekts()` (js/utils.js) — die Gesamtanzeige
(`kommentarVon(p, null)`, `kommentarAnzeigeHtml()`) und der Verlauf im
Projekt-Dashboard (`planKommentarDaten()`, je Sitzung mit DEREN Gewerken)
zeigen nur Gewerke des Projekts; `ensureCurrentWeekRollover()` übernimmt nur
Kommentare dieser Gewerke. Gelöscht wird nichts — die verirrte Kopie fällt
beim nächsten Sitzungswechsel aus der aktuellen Sitzung heraus.
**Darstellung mehrerer Gewerke „B – Überschrift mit Linie“** (Eriks Wahl
aus dem Artifact „Kommentare je Gewerk“): `kommentarAnzeigeHtml(p, null,
mitMeta)` zeichnet je Gewerk eine Kopfzeile (Gewerk in seiner Farbe
`--gewerk-elt/-hls/-arch`, feine Linie, mit `mitMeta` Verfasser und Zeit)
und darunter den Text; bei nur einem Gewerk wie bisher nur den Text.
Genutzt von der Kachel „Kommentar der Sitzung“ im Projekt-Dashboard (dort
entfällt dann die gemeinsame Verfasserzeile, `kommentarGewerkeMitText()`),
der Kommentaransicht der Projektliste/Wandtafel (`renderKommentarAnsicht()`)
und den Verlauf-Kacheln (ohne Verfasser). Stile `.kommentar-gewerke`,
`.kommentar-kopf` in `styles.css`.
## Behobene Fehler (Testlauf 10/2026, fünf Test-Agents) ## Behobene Fehler (Testlauf 10/2026, fünf Test-Agents)
- **Gesperrte Sitzungen sind für Nicht-Admins auch inhaltlich unveränderlich**: - **Gesperrte Sitzungen sind für Nicht-Admins auch inhaltlich unveränderlich**:
+4 -4
View File
@@ -38,7 +38,7 @@
<link rel="icon" type="image/png" href="/logo.png"> <link rel="icon" type="image/png" href="/logo.png">
<link rel="alternate icon" href="/logo.ico"> <link rel="alternate icon" href="/logo.ico">
<link rel="apple-touch-icon" href="/logo.png"> <link rel="apple-touch-icon" href="/logo.png">
<link rel="stylesheet" href="/styles.css?v=122"> <link rel="stylesheet" href="/styles.css?v=123">
<link rel="stylesheet" href="/plan-styles.css?v=56"> <link rel="stylesheet" href="/plan-styles.css?v=56">
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit <!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen, der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
@@ -1954,7 +1954,7 @@
</div> </div>
<script src="/js/globals.js?v=10"></script> <script src="/js/globals.js?v=10"></script>
<script src="/js/utils.js?v=31"></script> <script src="/js/utils.js?v=33"></script>
<script src="/js/icons.js?v=2"></script> <script src="/js/icons.js?v=2"></script>
<script src="/js/ui.js?v=9"></script> <script src="/js/ui.js?v=9"></script>
<script src="/js/date-picker.js?v=6"></script> <script src="/js/date-picker.js?v=6"></script>
@@ -1962,7 +1962,7 @@
<script src="/js/auth.js?v=11"></script> <script src="/js/auth.js?v=11"></script>
<script src="/js/store.js?v=17"></script> <script src="/js/store.js?v=17"></script>
<script src="/js/weeks.js?v=12"></script> <script src="/js/weeks.js?v=12"></script>
<script src="/js/board/board.js?v=41"></script> <script src="/js/board/board.js?v=42"></script>
<script src="/js/wochenbericht/wochenbericht.js?v=29"></script> <script src="/js/wochenbericht/wochenbericht.js?v=29"></script>
<script src="/js/people/people.js?v=8"></script> <script src="/js/people/people.js?v=8"></script>
<script src="/js/planung/planung.js?v=3"></script> <script src="/js/planung/planung.js?v=3"></script>
@@ -2001,7 +2001,7 @@
umhuellt. --> umhuellt. -->
<script src="/js/plan/plan-state.js?v=25"></script> <script src="/js/plan/plan-state.js?v=25"></script>
<script src="/js/plan/plan-sections.js?v=12"></script> <script src="/js/plan/plan-sections.js?v=12"></script>
<script src="/js/plan/plan-info.js?v=15"></script> <script src="/js/plan/plan-info.js?v=17"></script>
<script src="/js/plan/plan-history.js?v=4"></script> <script src="/js/plan/plan-history.js?v=4"></script>
<script src="/js/plan/plan-build-gantt.js?v=11"></script> <script src="/js/plan/plan-build-gantt.js?v=11"></script>
<script src="/js/plan/plan-gantt.js?v=23"></script> <script src="/js/plan/plan-gantt.js?v=23"></script>
+1 -1
View File
@@ -748,7 +748,7 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
// renderKommentarAnsicht: der Kommentar der laufenden Sitzung, nur zu lesen. // renderKommentarAnsicht: der Kommentar der laufenden Sitzung, nur zu lesen.
// Parameter: p (Projekt), locked. Rückgabe: HTML-String. // Parameter: p (Projekt), locked. Rückgabe: HTML-String.
function renderKommentarAnsicht(p, locked){ function renderKommentarAnsicht(p, locked){
const text = sanitizeStatusHtml(kommentarVon(p, kommentarGewerk()).status || ''); const text = kommentarAnzeigeHtml(p, kommentarGewerk(), true);
return `<div class="status-edit is-ansicht locked-cell">${text || return `<div class="status-edit is-ansicht locked-cell">${text ||
'<span class="hint" style="margin:0">Für diese Sitzung ist noch kein Kommentar erfasst.</span>'}</div>`; '<span class="hint" style="margin:0">Für diese Sitzung ist noch kein Kommentar erfasst.</span>'}</div>`;
} }
+9 -5
View File
@@ -93,7 +93,7 @@
for(let i = versionen.length - 1; i >= 0; i--){ for(let i = versionen.length - 1; i >= 0; i--){
const k = kommentarVon(versionen[i].project, null); const k = kommentarVon(versionen[i].project, null);
if(statusPlainText(k.status)){ if(statusPlainText(k.status)){
letzter = { week: versionen[i].week, k, aktuell: i === versionen.length - 1 }; letzter = { week: versionen[i].week, k, project: versionen[i].project, aktuell: i === versionen.length - 1 };
break; break;
} }
} }
@@ -103,7 +103,8 @@
gewerke.forEach(g => { gewerke.forEach(g => {
let vorher = ''; let vorher = '';
versionen.forEach(v => { versionen.forEach(v => {
const k = kommentarVon(v.project, g); // 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); const text = statusPlainText(k.status);
if(text && text !== vorher && !(letzter && v.week === letzter.week)){ 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 || '' }); verlauf.push({ week: v.week, gewerk: g, html: sanitizeStatusHtml(k.status), autor: k.statusAuthor || '', at: k.statusAt || '' });
@@ -184,8 +185,9 @@
if(editEl){ if(editEl){
editEl.className = 'status-edit is-ansicht'; editEl.className = 'status-edit is-ansicht';
editEl.contentEditable = 'false'; editEl.contentEditable = 'false';
// Alle Gewerke zusammen, je mit Gewerk davor (kommentarVon(), js/utils.js). // Alle Gewerke zusammen; bei mehreren je mit Überschrift, Linie und
const text = kom.letzter ? sanitizeStatusHtml(kom.letzter.k.status) : ''; // 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>'; editEl.innerHTML = text || '<span class="hint" style="margin:0">Zu diesem Projekt ist noch kein Kommentar erfasst.</span>';
} }
const hinweisEl = document.getElementById('planStatusHinweis'); const hinweisEl = document.getElementById('planStatusHinweis');
@@ -200,7 +202,9 @@
const autorEl = document.getElementById('planStatusAutor'); const autorEl = document.getElementById('planStatusAutor');
if(autorEl){ if(autorEl){
const k = kom.letzter ? kom.letzter.k : {}; const k = kom.letzter ? kom.letzter.k : {};
autorEl.innerHTML = k.status && k.statusAuthor // 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)}` ? `<span class="plan-autor-ava">${esc(initialsForName(k.statusAuthor))}</span>${esc(k.statusAuthor)}`
+ (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '') + (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '')
: ''; : '';
+39 -8
View File
@@ -385,8 +385,21 @@ function kommentarGewerk(){
// Verfasser/Zeit des jüngsten, besprochen nur, wenn ALLE Gewerke des // Verfasser/Zeit des jüngsten, besprochen nur, wenn ALLE Gewerke des
// Projekts es abgehakt haben. Parameter: p (Projekt), g (Gewerk oder // Projekts es abgehakt haben. Parameter: p (Projekt), g (Gewerk oder
// null/undefined). Rückgabe: { status, statusAuthor, statusAt, besprochen }. // null/undefined). Rückgabe: { status, statusAuthor, statusAt, besprochen }.
function kommentarVon(p, g){ // kommentareDesProjekts: die Kommentare nur der Gewerke, die das Projekt hat.
// Ein Kommentar eines Gewerks, das dem Projekt nicht (mehr) gehört — etwa aus
// einer Zeit, als es noch HLS hatte —, gehört nicht in die Gesamtanzeige
// (Eriks Meldung zu 21-4030). Ohne gepflegte Gewerke: alle. Rückgabe: Objekt.
function kommentareDesProjekts(p){
const alle = (p && p.kommentare) || {}; const alle = (p && p.kommentare) || {};
const gewerke = Array.isArray(p && p.gewerke) ? p.gewerke : [];
if(!gewerke.length) return alle;
const aus = {};
Object.keys(alle).forEach(k => { if(gewerke.includes(k)) aus[k] = alle[k]; });
return aus;
}
function kommentarVon(p, g){
const alle = g ? ((p && p.kommentare) || {}) : kommentareDesProjekts(p);
if(g){ if(g){
const e = alle[g] || {}; const e = alle[g] || {};
return { status: e.status || '', statusAuthor: e.statusAuthor || '', statusAt: e.statusAt || '', besprochen: !!e.besprochen }; return { status: e.status || '', statusAuthor: e.statusAuthor || '', statusAt: e.statusAt || '', besprochen: !!e.besprochen };
@@ -408,19 +421,37 @@ function kommentarVon(p, g){
} }
// kommentarAnzeigeHtml: der Kommentar zur ANZEIGE, bereinigt. Mit Gewerk g // kommentarAnzeigeHtml: der Kommentar zur ANZEIGE, bereinigt. Mit Gewerk g
// nur dessen Text; ohne Gewerk die Texte aller Gewerke, bei mehreren je mit // nur dessen Text; ohne Gewerk die Texte aller Gewerke des Projekts. Bei
// dem Gewerk als eigener, dezenter Beschriftung (.kommentar-gewerk) — // mehreren Gewerken Darstellung „B – Überschrift mit Linie“ (Eriks Wahl):
// Systemtext, nicht Teil des Kommentars (anders als die fette "ELT:"-Zeile, // je Gewerk eine Kopfzeile mit dem Gewerk in seiner Farbe, einer feinen Linie
// die kommentarVon() für Klartext-Vorschauen einfügt). Rückgabe: HTML ('' ohne Text). // und (mitMeta) Verfasser und Zeit, darunter der Text — Systemtext, nicht
function kommentarAnzeigeHtml(p, g){ // Teil des Kommentars (anders als die fette "ELT:"-Zeile, die kommentarVon()
// für Klartext-Vorschauen einfügt). Parameter: p, g, mitMeta.
// Rückgabe: HTML ('' ohne Text).
function kommentarAnzeigeHtml(p, g, mitMeta){
if(g) return sanitizeStatusHtml(kommentarVon(p, g).status || ''); if(g) return sanitizeStatusHtml(kommentarVon(p, g).status || '');
const alle = (p && p.kommentare) || {}; const alle = kommentareDesProjekts(p);
const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH']; const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); }); Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); });
const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== ''); const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== '');
if(mitText.length === 0) return ''; if(mitText.length === 0) return '';
if(mitText.length === 1) return sanitizeStatusHtml(alle[mitText[0]].status); if(mitText.length === 1) return sanitizeStatusHtml(alle[mitText[0]].status);
return mitText.map(k => `<div class="kommentar-gewerk">${esc(k)}</div><div>${sanitizeStatusHtml(alle[k].status)}</div>`).join(''); return '<div class="kommentar-gewerke">' + mitText.map(k => {
const e = alle[k];
const zeit = e.statusAt && typeof formatCommentStamp === 'function' ? formatCommentStamp(e.statusAt) : '';
const meta = mitMeta ? [e.statusAuthor, zeit].filter(Boolean).join(' · ') : '';
return '<div class="kommentar-block">'
+ '<div class="kommentar-kopf"><span class="kommentar-gewerk gewerk-' + esc(k.toLowerCase()) + '">' + esc(k) + '</span>'
+ '<span class="kommentar-linie"></span>' + (meta ? '<span class="kommentar-meta">' + esc(meta) + '</span>' : '') + '</div>'
+ '<div class="kommentar-text">' + sanitizeStatusHtml(e.status) + '</div></div>';
}).join('') + '</div>';
}
// kommentarGewerkeMitText: wie viele Gewerke des Projekts einen Kommentar
// haben (für die Frage, ob eine gemeinsame Verfasserzeile Sinn ergibt).
function kommentarGewerkeMitText(p){
const alle = kommentareDesProjekts(p);
return Object.keys(alle).filter(k => alle[k] && statusPlainText(alle[k].status) !== '').length;
} }
// kommentarSetzen: ändert den Kommentar-Eintrag eines Gewerks (nur die in // kommentarSetzen: ändert den Kommentar-Eintrag eines Gewerks (nur die in
+13 -6
View File
@@ -4188,12 +4188,19 @@ tr.is-menu-open { background: var(--surface-alt); }
min-height: 3em; min-height: 3em;
} }
.timeline-text p { margin: 0; } .timeline-text p { margin: 0; }
/* Gewerk über dem Kommentar: Systemtext, nicht Teil des Kommentars. */ /* Kommentare mehrerer Gewerke, Darstellung „B – Überschrift mit Linie“
.kommentar-gewerk { (Eriks Wahl): Gewerk in seiner Farbe, feine Linie, Verfasser und Zeit,
font-size: 11px; font-weight: 600; letter-spacing: 0; darunter der Text. Systemtext, nicht Teil des Kommentars. */
color: var(--text-subtle); margin-top: 4px; .kommentar-gewerke { display: flex; flex-direction: column; gap: 16px; }
} .kommentar-block { display: flex; flex-direction: column; gap: 4px; }
.kommentar-gewerk:first-child { margin-top: 0; } .kommentar-kopf { display: flex; align-items: center; gap: 12px; }
.kommentar-gewerk { flex: none; font-size: 12px; font-weight: 500; letter-spacing: 0; color: var(--text-muted); }
.kommentar-gewerk.gewerk-elt { color: var(--gewerk-elt); }
.kommentar-gewerk.gewerk-hls { color: var(--gewerk-hls); }
.kommentar-gewerk.gewerk-arch { color: var(--gewerk-arch); }
.kommentar-linie { flex: 1; height: 1px; background: var(--line-soft); min-width: 16px; }
.kommentar-meta { flex: none; font-size: 12px; font-weight: 400; color: var(--text-subtle); white-space: nowrap; }
:root { --gewerk-elt: var(--accent); --gewerk-hls: var(--green); --gewerk-arch: var(--amber); }
.timeline-leer { color: var(--text-muted); font-style: normal; } .timeline-leer { color: var(--text-muted); font-style: normal; }
.timeline-lph-list { display: flex; flex-direction: column; gap: 1px; } .timeline-lph-list { display: flex; flex-direction: column; gap: 1px; }
+4
View File
@@ -2956,8 +2956,12 @@ function ensureCurrentWeekRollover(){
// zur Sitzung und beginnt leer. // zur Sitzung und beginnt leer.
const vorher = p.kommentare || {}; const vorher = p.kommentare || {};
p.kommentare = {}; p.kommentare = {};
// Nur Gewerke, die das Projekt hat — sonst lebte ein Kommentar eines
// längst abgewählten Gewerks Woche für Woche weiter (21-4030).
const gewerke = Array.isArray(p.gewerke) ? p.gewerke : [];
Object.keys(vorher).forEach(g => { Object.keys(vorher).forEach(g => {
const k = vorher[g] || {}; const k = vorher[g] || {};
if(gewerke.length && !gewerke.includes(g)) return;
if(!String(k.status || '').trim()) return; if(!String(k.status || '').trim()) return;
p.kommentare[g] = { status: k.status, statusAuthor: k.statusAuthor || '', p.kommentare[g] = { status: k.status, statusAuthor: k.statusAuthor || '',
statusAt: k.statusAt || '', besprochen: false }; statusAt: k.statusAt || '', besprochen: false };