Beteiligte in die Projekteinstellungen, TODOs aus Protokoll, Bewegung

Beteiligten-Zuordnung verlagert: p.assign ist jetzt ein Stammdatum und
nur noch in den Projekteinstellungen editierbar (planCycleAssign()).
Wochenbericht und Projektboard zeigen sie nur noch an, mit Verweis
dorthin (springZuEinstellungen()). cycleAssign() ist damit entfallen.

Neu "TODOs aus Protokoll": ein externes Besprechungsprotokoll als PDF
wird per KI eingelesen und daraus in einem zweiten KI-Schritt Aufgaben
abgeleitet. Eigener Bereich in der Aufgaben-/GANTT-Leiste, bewusst
unabhaengig von den gespeicherten Protokollen - nichts davon wird
abgelegt. Drei Schritte in einem Dialog mit fester Fusszeile; die
Vorschlagsliste als kompakte, aufklappbare Zeilen, weil ein echter
Import leicht 20 Vorschlaege liefert.

Sichtbarkeit je Aufgabe im GANTT: neue Spalte plan_todos.show_in_gantt
(Vorgabe 1, idempotente Migration) und eine Checkbox im Aufgaben-Dialog.
Eine terminierte Aufgabe kann damit gezielt aus dem Terminplan
herausgehalten werden, bleibt sonst aber eine vollwertige Aufgabe.
plan-build-gantt.js und plan-drag.js pruefen dieselbe Bedingung, damit
Zeilenzahl und Umsortierung nicht auseinanderlaufen.

KI-Timeout dritte Stufe: ein abweichender, aber entfernter Endpunkt
(Drittanbieter-Proxy) bekommt 2 Minuten statt der 45 Sekunden der
echten Anbieter-API. Gemessen: die Punkte-Erkennung eines realen
Protokolls brauchte dort 39 Sekunden, lag also stets knapp unter dem
alten Limit und brach bei jeder Schwankung ab.

Bewegung: Hover- und Klick-Rueckmeldung als Token-Satz in :root,
angewandt auf die anklickbaren Zeilen, Schalter und Dialoge. Die
Akzentleiste ist ein inset-Schatten statt eines positionierten
Elements, damit Drag-and-Drop unberuehrt bleibt; prefers-reduced-motion
war bisher gar nicht vorhanden und kippt jetzt einfach die Tokens.

Plan Checker: optionaler Projektabgleich des Plankopfs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5 committed 2026-10-17 15:14:26 +02:00
1 parent 6ba93b2a92
commit 93505e0e48
17 files changed
+1883 -334

No files matched your search

+77 -74
View File
@@ -9,17 +9,20 @@
// Detailpanel auf (voller Status-Editor, vollständige Rollen-Zuordnung, alle
// LPH-Balken). Sie wird von `App.html` nach `js/weeks.js` (vor
// `js/people/people.js`) geladen und stellt globale Funktionen wie
// `renderBoard()`, `toggleBoardRow()`, `cycleAssign()`,
// `renderBoard()`, `toggleBoardRow()`,
// und `editLph()` bereit. Sie nutzt `currentWeek()`/
// `isWeekLocked()` aus `js/weeks.js`, `lpActiveList()` aus `js/load/load.js`,
// `assignKey()`, `statusFocus()`, `sanitizeStatusHtml()` und
// `getProgressColor()` aus `js/utils.js` sowie `openProjectPlan()` aus
// `js/gantt.js` — navigiert bewusst im selben Tab (kein window.open() mehr),
// ein Klick auf den Rest der Zeile klappt stattdessen nur das Detailpanel
// auf/zu (toggleBoardRow()). editLph()/cycleAssign() rufen zusätzlich
// auf/zu (toggleBoardRow()). editLph() ruft zusätzlich
// renderWochenbericht() (js/wochenbericht/wochenbericht.js) auf, da dessen
// Karten dieselben renderBoardDrawer()-Bedienelemente nutzen, aber in einem
// eigenen DOM-Container (#wochenberichtContent) liegen.
// eigenen DOM-Container (#wochenberichtContent) liegen. Die Zuordnung der
// Beteiligten (p.assign) ist hier und im Wochenbericht nur noch Anzeige;
// editierbar ist sie allein über planCycleAssign() in
// js/plan/plan-settings.js — siehe renderBoardDrawer()s Kopfkommentar.
// boardRowOpen: UI-Zustand, welche Projekt-Zeile ihr Detailpanel aufgeklappt
// darstellt (Key = Projekt-ID, Wert = boolean). Reine Anzeige-Steuerung, lebt
@@ -28,17 +31,20 @@
// persistiert — jede Sitzung startet mit eingeklappten Zeilen.
let boardRowOpen = {};
// berichtDisclosureState: merkt sich je Abschnitt (Leistungsphasen, Beteiligte,
// Anwesenheit), ob er im Wochenbericht (Fokus-Modus — Eriks Wahl "C" aus dem
// Darstellungs-Vergleich: renderBoardDrawer()s dichte Fassung weiter unten +
// renderAttendance() in js/wochenbericht/wochenbericht.js) gerade auf- oder
// zugeklappt ist. Ohne dieses Gedächtnis klappte jeder Abschnitt bei jedem
// Neu-Rendern wieder auf seinen Vorgabewert zurück — und JEDE Änderung in der
// Schublade (eine Zuordnung, ein LPH-Wert) baut den ganzen Bericht neu auf.
// 'toggle' bubbelt nicht, ein Lauscher in der Capture-Phase sieht das
// Ereignis trotzdem auf dem Weg nach unten (gleiches Muster wie der
// berichtDisclosureState: merkt sich, ob die Anwesenheit im Wochenbericht
// (renderAttendance(), js/wochenbericht/wochenbericht.js) gerade auf- oder
// zugeklappt ist. Ursprünglich auch für Leistungsphasen/Beteiligte gedacht
// (damals beide ebenfalls Aufklapper, Fokus-Modus "C"); seit Eriks
// Rückmeldung zum Entwurfsvergleich ("Aktenregal") sind beide wieder
// dauerhaft sichtbar (renderBoardDrawer()s dichte Fassung weiter unten) und
// brauchen dieses Gedächtnis nicht mehr — nur die Anwesenheit bleibt ein
// Aufklapper. Ohne dieses Gedächtnis klappte sie bei jedem Neu-Rendern wieder
// auf ihren Vorgabewert zurück — und JEDE Änderung im Bericht (eine
// Zuordnung, ein LPH-Wert, ein Schritt im Vorwoche-Scrollrad) baut den ganzen
// Bericht neu auf. 'toggle' bubbelt nicht, ein Lauscher in der Capture-Phase
// sieht das Ereignis trotzdem auf dem Weg nach unten (gleiches Muster wie der
// scroll-Lauscher der Wandtafel).
const berichtDisclosureState = { lph: true, beteiligte: false, anwesenheit: false };
const berichtDisclosureState = { anwesenheit: false };
document.addEventListener('toggle', function(e){
const key = e.target && e.target.dataset && e.target.dataset.berichtAbschnitt;
if(key) berichtDisclosureState[key] = e.target.open;
@@ -467,21 +473,37 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
// bei der vollständigen Liste, sonst stünde dort gar nichts.
// opts.nurAnsicht: die Karte zeigt, sie erfasst nicht.
//
// Kommentar, Fortschritt je Leistungsphase und die Zuordnung der
// Beteiligten entstehen in der wöchentlichen Besprechung und werden deshalb
// allein im Wochenbericht gepflegt. Zuvor waren dieselben drei Angaben an
// drei Stellen bearbeitbar (Projektboard, Wochenbericht, Projektbereich) —
// niemand wusste, welche die richtige ist, und zwei Personen konnten
// dasselbe Feld an verschiedenen Stellen ändern. Dieselbe Karte, dieselben
// Werte, nur ohne Eingabefelder und mit einem Verweis dorthin, wo sie
// gepflegt werden. Eine gesperrte Sitzung wirkt weiterhin zusätzlich.
// Kommentar und Fortschritt je Leistungsphase entstehen in der
// wöchentlichen Besprechung und werden deshalb allein im Wochenbericht
// gepflegt. Zuvor waren dieselben Angaben an drei Stellen bearbeitbar
// (Projektboard, Wochenbericht, Projektbereich) — niemand wusste, welche
// die richtige ist, und zwei Personen konnten dasselbe Feld an
// verschiedenen Stellen ändern. Dieselbe Karte, dieselben Werte, nur ohne
// Eingabefelder und mit einem Verweis dorthin, wo sie gepflegt werden.
// Eine gesperrte Sitzung wirkt weiterhin zusätzlich.
//
// Die Zuordnung der Beteiligten (p.assign, roleRows unten) gehörte früher
// zu dieser Gruppe, ist aber auf Eriks Wunsch umgezogen: ein Stammdatum,
// nur noch in den Projekteinstellungen editierbar
// (planCycleAssign(), js/plan/plan-settings.js). Hier — sowohl im
// Projektboard als auch im Wochenbericht — ist sie deshalb IMMER nur
// Anzeige, unabhängig von opts.nurAnsicht; cycleAssign() ist damit ohne
// Aufrufer und entfallen.
const ansicht = !!(opts && opts.nurAnsicht);
const starr = ansicht || locked;
const beauftragt = Array.isArray(p.beauftragteLP) ? p.beauftragteLP : [];
const nurBeauftragte = !!(opts && opts.nurBeauftragteLph) && beauftragt.length > 0;
const sichtbareLphs = nurBeauftragte ? LPHS.filter(l => beauftragt.includes(l)) : LPHS;
const aktLp = lpActiveList(p)[0];
// Die amtierende Phase (aktLp) IMMER mit anzeigen, auch wenn sie nicht in
// beauftragteLP steht: aktLp kommt unabhängig von beauftragteLP aus
// lpActiveList() (js/load/load.js) - beide Felder können auseinanderlaufen
// (z. B. ein Projekt auf LP 8, aber nur LP 1/2/3/6 als beauftragt
// gepflegt). Ohne diese Ergänzung behauptete die Kopfzeile dieses
// Abschnitts ("LP 8 aktiv"), während LP 8 in der aufgeklappten Liste
// darunter gar nicht auftauchte und ihr Fortschritt dort nicht pflegbar
// war - gefunden beim Live-Test mit echten Projektdaten.
const sichtbareLphs = nurBeauftragte ? LPHS.filter(l => beauftragt.includes(l) || l === aktLp) : LPHS;
// lphLeiste: die Leistungsphasen nebeneinander statt gestapelt — seit
// Variante A (Eriks Wahl aus dem Layout-Vergleich) die EINE Darstellung für
@@ -510,55 +532,48 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
const entries = allPeople.filter(e=>e.role===r);
const chips = entries.length ? entries.map(entry=>{
const state = p.assign[assignKey(entry.person.id, entry.role)] || 0;
const click = starr ? '' : `onclick="cycleAssign('${p.id}','${entry.person.id}','${entry.role}')"`;
return `<span class="board-avatar ${roleClass(entry.role)} state-${state}${ansicht ? ' is-ansicht' : ''}${locked ? ' locked-cell' : ''}" ${click} title="${esc(entry.person.fullName || entry.person.name)}">${esc(entry.person.name)}</span>`;
return `<span class="board-avatar ${roleClass(entry.role)} state-${state} is-ansicht${locked ? ' locked-cell' : ''}" title="${esc(entry.person.fullName || entry.person.name)}">${esc(entry.person.name)}</span>`;
}).join('') : `<span class="hint" style="margin:0">Niemand hinterlegt</span>`;
return `<div class="board-role-row"><span class="board-role-label ${roleClass(r)}">${esc(r)}</span><div class="board-role-chips">${chips}</div></div>`;
}).join('');
// Dichte Fassung (Wochenbericht): erst zwei, dann drei Spalten nebeneinander
// — bei 29 Projekten mit allem gleichzeitig sichtbar kam von Erik die
// Rückmeldung "zu überladen". Jetzt (Fokus-Modus, Eriks Wahl "C" aus dem
// Darstellungs-Vergleich) liegt nur der Kommentar offen, Leistungsphasen
// und Beteiligte stecken in aufklappbaren Abschnitten untereinander.
// Dichte Fassung (Wochenbericht), zweite Fassung (Eriks Rückmeldung zum
// Entwurfsvergleich "Aktenregal"): Leistungsphasen und Beteiligte stehen
// jetzt dauerhaft sichtbar nebeneinander statt hinter Aufklappern verborgen
// — "die Beteiligten sollen mit angezeigt werden". Die erste Fokus-Modus-
// Fassung hatte beide hinter <details> versteckt (Reaktion auf "zu
// überladen" bei allem gleichzeitig sichtbar); diesmal bleiben beide
// bewusst KOMPAKT, damit sie nebeneinander Platz finden, ohne den Kommentar
// ("im Vergleich zu groß") zu überragen: Leistungsphasen nutzen dieselbe
// Kästchen-Leiste wie das Projektboard (lphLeiste, oben in dieser Funktion
// berechnet — keine zweite, eigene LPH-Darstellung mehr für den
// Wochenbericht), Beteiligte ihre ohnehin schon kompakten Rollenzeilen
// (roleRows, s. u.).
//
// Die Beteiligten behalten ihre Zeile je Rolle — anders als im Entwurf, wo
// sie zu einer flachen Reihe zusammengefasst waren: Ein Kürzel kann in
// mehreren Rollen vorkommen, und cycleAssign() braucht die Rolle. Ohne sie
// wäre beim Klicken nicht erkennbar, welche Zuordnung man durchschaltet.
// mehreren Rollen vorkommen, und ohne die Zeile wäre nicht erkennbar, in
// welcher Rolle genau jemand zugeordnet ist (dieselbe Zeilenform nutzt die
// jetzt editierbare Matrix der Projekteinstellungen, planCycleAssign(),
// js/plan/plan-settings.js).
if(opts && opts.dicht){
// Fokus-Modus (Eriks Wahl "C" aus dem Darstellungs-Vergleich): nur der
// Kommentar liegt offen, Leistungsphasen und Beteiligte stecken in
// aufklappbaren Abschnitten (<details>, Zustand in berichtDisclosureState)
// statt dauerhaft nebeneinander zu stehen. Die Leistungsphasen stehen hier
// als Balkenzeilen (bericht-lph-row) statt als Kästchen-Leiste
// (board-lph-leiste bleibt die Darstellung des Projektboards, unverändert).
const lphBarRows = sichtbareLphs.map(l=>{
const v = (p.lph[l]!==undefined && p.lph[l]!="") ? p.lph[l] : "";
const aktiv = l === aktLp;
const inputAttr = starr ? 'readonly tabindex="-1"' : `onchange="editLph('${p.id}','${l}',this.value)"`;
const pct = v !== "" ? Math.min(100, parseFloat(v)||0) : 0;
return `<div class="bericht-lph-row${aktiv ? ' is-aktiv' : ''}">
<span class="bericht-lph-label">${esc(l.replace('LPH ', 'LP '))}</span>
<span class="bericht-lph-track"><i style="width:${pct}%"></i></span>
<span class="bericht-lph-pct"><input type="text" class="bericht-lph-input${locked ? ' locked-cell' : ''}" value="${esc(v)}" ${inputAttr} placeholder="-">%</span>
</div>`;
}).join('');
const lphInfo = aktLp
? esc(aktLp.replace('LPH ', 'LP ')) + ' aktiv' + (p.lph[aktLp] !== undefined && p.lph[aktLp] !== '' ? ' · ' + esc(p.lph[aktLp]) + '%' : '')
: 'Keine aktive Leistungsphase';
const zugeordnetAnzahl = allPeople.filter(e => (p.assign[assignKey(e.person.id, e.role)] || 0) > 0).length;
: 'keine aktiv';
return `<div class="board-drawer board-drawer-dicht">
${renderCommentEditor(p, weekLabel, locked, true)}
<details class="bericht-disclosure" data-bericht-abschnitt="lph" ${berichtDisclosureState.lph ? 'open' : ''}>
<summary><span>${nurBeauftragte ? 'Beauftragte Leistungsphasen' : 'Leistungsphasen'}</span><span class="count">${lphInfo}</span><span class="chev">${icon('chevronRight')}</span></summary>
<div class="bericht-disclosure-body">${lphBarRows}</div>
</details>
<details class="bericht-disclosure" data-bericht-abschnitt="beteiligte" ${berichtDisclosureState.beteiligte ? 'open' : ''}>
<summary><span>Beteiligte</span><span class="count">${zugeordnetAnzahl} zugeordnet</span><span class="chev">${icon('chevronRight')}</span></summary>
<div class="bericht-disclosure-body"><div class="board-role-mini">${roleRows}</div></div>
</details>
<div class="bericht-meta-grid">
<div>
<div class="bericht-meta-label">${nurBeauftragte ? 'Beauftragte Leistungsphasen' : 'Leistungsphasen'} · ${lphInfo}</div>
${lphLeiste ? `<div class="board-lph-leiste">${lphLeiste}</div>` : '<span class="hint" style="margin:0">Keine gültige Leistungsphase hinterlegt — bitte in den Projekteinstellungen prüfen.</span>'}
</div>
<div>
<div class="bericht-meta-label">Beteiligte</div>
<div class="board-role-mini">${roleRows}</div>
${!locked ? `<button type="button" class="plan-verweis" style="margin-top:4px" onclick="springZuEinstellungen('${p.id}')">In den Projekteinstellungen bearbeiten →</button>` : ''}
</div>
</div>
${typeof renderProjektVerlauf === 'function' ? renderProjektVerlauf(p, weekLabel, locked) : ''}
</div>`;
}
@@ -582,6 +597,7 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
<div class="drawer-col drawer-col-people">
<h4>Beteiligte</h4>
<div class="board-role-mini">${roleRows}</div>
${!locked ? `<button type="button" class="plan-verweis" style="margin-top:4px" onclick="springZuEinstellungen('${p.id}')">In den Projekteinstellungen bearbeiten →</button>` : ''}
</div>
</div>
${ansicht ? renderBerichtVerweis(p, locked) : ''}
@@ -605,7 +621,7 @@ function renderBerichtVerweis(p, locked){
if(locked) return '<div class="drawer-verweis"><span class="hint" style="margin:0">Die Sitzung ist gesperrt.</span></div>';
const uid = esc(p.projectUid || '');
return `<div class="drawer-verweis">
<span class="hint" style="margin:0">Kommentar, Fortschritt und Zuordnung werden in der Besprechung erfasst.</span>
<span class="hint" style="margin:0">Kommentar und Fortschritt werden in der Besprechung erfasst.</span>
<button type="button" class="plan-verweis" onclick="springZumWochenbericht('${uid}')">Im Wochenbericht bearbeiten →</button>
</div>`;
}
@@ -673,20 +689,6 @@ function renderBoard(){
container.innerHTML = html;
}
// cycleAssign: wechselt die Zuweisung einer Person zu einem Projekt zyklisch
// durch die Zustände 0=keine → 1=Haupt → 2=optional → 0. Parameter `pid`
// (Projekt-ID), `personId`, `role`; bei Zustand 0 wird der Eintrag gelöscht.
// Keine Rückgabe.
function cycleAssign(pid, personId, role){
if(guardWeekLocked()) return;
const p = currentWeek().projects.find(x=>x.id===pid);
const key = assignKey(personId, role);
const cur = p.assign[key]||0;
const next = (cur+1)%3;
if(next===0) delete p.assign[key]; else p.assign[key]=next;
save(); renderBoard(); renderWochenbericht();
}
// editLph: setzt oder entfernt den Prozentwert einer Leistungsphase für ein
// Projekt. Parameter: `pid` (Projekt-ID), `lph` (Leistungsphasen-Schlüssel),
// `val` (Eingabewert). Leere/`%`-Werte werden bereinigt; keine Rückgabe.
@@ -696,7 +698,8 @@ function editLph(pid,lph,val){
val = val.trim();
if(val==="") delete p.lph[lph];
else p.lph[lph]=val.replace('%','');
save(); renderBoard(); renderWochenbericht();
save(); renderBoard();
if(typeof renderWochenbericht === 'function') renderWochenbericht();
}
// HINWEIS: delProj() ist entfallen — seit der neuen Zeilendarstellung des