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

+128 -45
View File
@@ -16,12 +16,14 @@
// Der Vergleich zur Vorwoche läuft über project_uid (siehe
// migrateAddProjectUid() in server.js) — die dauerhafte, wochenunabhängige
// Projekt-Identität, die project_id (wird bei jedem cloneWeek() neu
// vergeben) nicht bieten konnte. Die Bearbeitung der Projekte selbst
// (Kommentar-Editor, LPH, Beteiligte) läuft vollständig über die bestehenden
// Funktionen aus js/board/board.js (editLph/cycleAssign/
// renderCommentEditor/renderBoardDrawer/renderBoardRow) — dieses Modul
// ergänzt die Gruppierung/Diff-Logik, das Layout des Berichts und die
// Anwesenheitserfassung am Seitenfuß (renderAttendance(), weiter unten).
// vergeben) nicht bieten konnte. Die Bearbeitung von Kommentar und LPH läuft
// vollständig über die bestehenden Funktionen aus js/board/board.js
// (editLph/renderCommentEditor/renderBoardDrawer/renderBoardRow) — dieses
// Modul ergänzt die Gruppierung/Diff-Logik, das Layout des Berichts und die
// Anwesenheitserfassung am Seitenfuß (renderAttendance(), weiter unten). Die
// Beteiligten-Zuordnung zeigt dieser Tab seit ihrer Verlagerung in die
// Projekteinstellungen nur noch zum Lesen, mit Verweis dorthin
// (springZuEinstellungen(), js/gantt.js).
// Die Anwesenheit lag früher im Projektboard; sie gehört hierher, weil in
// diesem Tab die Sitzung abgearbeitet wird.
//
@@ -154,7 +156,10 @@ function absenceNoteFor(personId, weekDate){
}
// renderAttendance: baut den Anwesenheitsblock der aktuellen Sitzung.
// Aufgerufen aus renderBoard(). Keine Parameter, keine Rückgabe.
// Aufgerufen innerhalb dieser Datei (toggleAttendance(), setAllAttendance()
// sowie innerhalb/rund um renderWochenbericht()) — NICHT aus renderBoard(),
// die Anwesenheit ist allein eine Wochenbericht-Angelegenheit. Keine
// Parameter, keine Rückgabe.
function renderAttendance(){
const container = document.getElementById('weekAttendance');
if(!container) return;
@@ -246,9 +251,9 @@ function setAllAttendance(present){
// renderWochenbericht: baut den kompletten Wochenbericht-Tab für die eine
// offene Sitzung auf (currentWeek(), js/weeks.js) — Hinweis auf die Vorwoche,
// dann die drei Gruppen. Wird nach jedem Laden sowie nach jeder Änderung
// über editLph()/cycleAssign() (js/board/board.js) neu aufgerufen, damit ein
// Projekt sofort in die passende Gruppe wandert. Keine Parameter, keine
// Rückgabe.
// über editLph() (js/board/board.js) bzw. planCycleAssign()
// (js/plan/plan-settings.js) neu aufgerufen, damit ein Projekt sofort in die
// passende Gruppe wandert. Keine Parameter, keine Rückgabe.
// ---------- Tagesordnung: Zustand ----------
// berichtAuswahl: project_uid des Projekts, das gerade rechts im Detail
@@ -347,6 +352,8 @@ function aktuellesBerichtProjekt(w){
// Parameter: uid (project_uid). Keine Rückgabe.
function waehleBerichtProjekt(uid){
berichtAuswahl = uid;
vorwocheRadIndex = 0; // neues Projekt beginnt wieder bei der jüngsten Vorwoche
vorwocheRadScrollAkku = 0; // kein halb aufgelaufenes Scrollen vom vorherigen Projekt übernehmen
renderWochenbericht();
const detail = document.getElementById('berichtDetail');
if(detail) detail.scrollIntoView({ block: 'nearest' });
@@ -389,7 +396,13 @@ function besprochenUndWeiter(pid){
function setzeAlleBesprochen(wert){
if(guardWeekLocked()) return;
const w = currentWeek();
(w.projects || []).forEach(p => { p.besprochen = !!wert; });
// berichtProjekte(w) statt w.projects direkt: Der Knopf wirkt auf die
// gerade sichtbare, nach wochenberichtGewerk gefilterte Liste - sonst
// markierte "Alle abhaken" bei gewähltem Gewerk auch die Projekte der
// ANDEREN Gewerke als besprochen, die in dieser Besprechung nie zu sehen
// waren (widerspricht "Wochenbericht — Gewerke-Trennung": Vollständigkeit
// gilt innerhalb des gewählten Gewerks, nicht über alle hinweg).
berichtProjekte(w).forEach(p => { p.besprochen = !!wert; });
save();
renderWochenbericht();
}
@@ -576,13 +589,17 @@ function renderBerichtKopf(w, liste, prevWeek, locked){
</div>`;
}
// renderBerichtListe: die Tagesordnung als schmale Nummern-Leiste (Fokus-
// Modus, Eriks Wahl "C" aus dem Darstellungs-Vergleich) — jede Kachel trägt
// nur die Position in der Liste plus ggf. den Änderungs-Punkt, Nummer und
// Name stehen in einem Tooltip. Eigene Funktion, weil das Sprung-Feld sie
// allein neu zeichnet — das Detail und vor allem ein gerade getippter
// Kommentar dürfen dabei nicht verlorengehen. Keine Parameter, keine
// Rückgabe.
// renderBerichtListe: die Tagesordnung als Leiste mit Nummer, Gruppe UND
// Name direkt sichtbar (Entwurf "Aktenregal", Eriks Wahl aus dem zweiten
// Darstellungs-Vergleich — löst die vorherige schmale Icon-Leiste ab, die
// Nummer und Namen nur im Hover-Tooltip zeigte: "so sehe ich die ganze
// Tagesordnung auf einen Blick"). Gruppiert wie die Projektliste
// (GROUP_ORDER, js/globals.js), mit einer Zwischenüberschrift bei jedem
// Gruppenwechsel — die Positionsnummer zählt dabei über alle Gruppen hinweg
// weiter, sie ist die Position in der VOLLSTÄNDIGEN Tagesordnung. Eigene
// Funktion, weil das Sprung-Feld sie allein neu zeichnet — das Detail und
// vor allem ein gerade getippter Kommentar dürfen dabei nicht
// verlorengehen. Keine Parameter, keine Rückgabe.
function renderBerichtListe(){
const el = document.getElementById('berichtListe');
const w = currentWeek();
@@ -601,21 +618,21 @@ function renderBerichtListe(){
return;
}
let gruppeVorher = null;
el.innerHTML = liste.map(p => {
const info = classifyProjectChange(p, prevWeek);
const istAktiv = aktiv && aktiv.id === p.id;
const position = voll.findIndex(x => x.id === p.id) + 1;
const tooltip = (p.nr ? p.nr + ' · ' : '') + p.name;
// Eigene, schwebende Tooltip-Box (wie im freigegebenen Entwurf) statt
// des nativen Browser-Tooltips: Seit die Leiste nicht mehr selbst
// scrollt (kein overflow-y:auto mehr, siehe .bericht-rail in styles.css),
// gibt es auch keine abschneidende Overflow-Begrenzung mehr dafür.
return `<div class="bericht-rail-item${istAktiv ? ' is-aktiv' : ''}${p.besprochen ? ' is-ab' : ''}"
const gruppe = p.group || 'Ohne Gruppe';
const gruppenkopf = gruppe !== gruppeVorher ? `<div class="bericht-rail-gruppe">${esc(gruppe)}</div>` : '';
gruppeVorher = gruppe;
const name = (p.nr ? p.nr + ' · ' : '') + p.name;
return `${gruppenkopf}<div class="bericht-rail-item${istAktiv ? ' is-aktiv' : ''}${p.besprochen ? ' is-ab' : ''}"
role="button" tabindex="0" data-uid="${esc(p.projectUid || '')}"
aria-current="${istAktiv ? 'true' : 'false'}" aria-label="${esc(tooltip)}">
aria-current="${istAktiv ? 'true' : 'false'}">
<span class="bericht-rail-num">${String(position).padStart(2, '0')}</span>
${info.bucket === 'unchanged' ? '' : `<span class="bericht-rail-dot is-${esc(info.bucket)}" aria-hidden="true"></span>`}
<span class="bericht-rail-tooltip">${esc(tooltip)}</span>
<span class="bericht-rail-name">${esc(name)}</span>
</div>`;
}).join('');
@@ -653,7 +670,7 @@ function renderBerichtDetail(p, w, prevWeek, locked){
<h1 class="bericht-h1">${esc(p.name)}</h1>
<div class="bericht-sub">${subText}</div>
${fehlen.length ? `<span class="bericht-abwesend-hinweis">Fehlt heute: ${esc(fehlen.join(', '))} — eventuell vertagen.</span>` : ''}
${renderVorwochenzeile(p, prevWeek)}
${renderVorwocheRad(p, w)}
${renderBoardDrawer(p, allPeople, locked, w.label, BERICHT_OPTS)}
${locked ? '' : `<div class="bericht-detail-fuss">
<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')">
@@ -664,22 +681,79 @@ function renderBerichtDetail(p, w, prevWeek, locked){
</div>`}`;
}
// renderVorwochenzeile: der Kommentar der Vorwoche als eine Zeile ÜBER dem
// Eingabefeld. Die häufigste Frage im Meeting ist "was hatten wir letzte
// Woche vereinbart?" — der Verlauf beantwortet sie zwar, aber erst nach dem
// Aufklappen. Parameter: p, prevWeek. Rückgabe: HTML (leer ohne Vorwoche).
function renderVorwochenzeile(p, prevWeek){
if(!prevWeek || !p.projectUid) return '';
const vor = (prevWeek.projects || []).find(x => x.projectUid === p.projectUid);
if(!vor) return '';
const text = statusPlainText(vor.status);
if(!text) return '';
return `<div class="bericht-vorwoche">
<span class="bericht-vorwoche-label">Vorwoche · ${esc(prevWeek.label)}</span>
<span class="bericht-vorwoche-text">${esc(text)}</span>
// vorwocheRadIndex: Position im Kommentar-Verlauf des GERADE GEÖFFNETEN
// Projekts (0 = jüngste Vorwoche, höher = weiter zurück). Ein einzelner
// modulweiter Stand genügt, da immer nur ein Projekt im Fokus steht;
// waehleBerichtProjekt() setzt ihn beim Projektwechsel zurück.
let vorwocheRadIndex = 0;
// renderVorwocheRad: löst renderVorwochenzeile() ab, die nur die
// UNMITTELBARE Vorwoche zeigte. Eriks Rückmeldung zum Entwurfsvergleich: mit
// dem Mausrad (oder den beiden Pfeilen) durch MEHRERE vergangene Kommentare
// blättern können, nicht nur eine Vorwoche sehen. Nutzt
// projectStatusHistory() (js/utils.js) — denselben, bereits für den
// Verlaufs-Dialog geprüften Verlauf (nur Sitzungen mit tatsächlich
// geändertem Text, neueste zuerst), abzüglich der aktuellen Sitzung selbst
// (die steht ja bereits im Eingabefeld darunter). Parameter: p, w (aktuelle
// Sitzung). Rückgabe: HTML (leer ohne Verlauf).
function renderVorwocheRad(p, w){
if(!p.projectUid) return '';
const verlauf = projectStatusHistory(p.projectUid).filter(e => e.week.label !== w.label);
if(verlauf.length === 0) return '';
const i = Math.max(0, Math.min(verlauf.length - 1, vorwocheRadIndex));
const eintrag = verlauf[i];
const punkte = verlauf.map((_, idx) => `<span class="dot${idx === i ? ' aktiv' : ''}"></span>`).join('');
return `<div class="bericht-vorwoche-rad" title="Mit dem Mausrad durch den Verlauf blättern" onwheel="vorwocheRadScroll(event)">
<button type="button" class="bericht-vorwoche-nav" onclick="vorwocheRadSchritt(1)" ${i >= verlauf.length - 1 ? 'disabled' : ''} title="Weiter zurück">‹</button>
<div class="bericht-vorwoche-inhalt">
<span class="bericht-vorwoche-label">${esc(eintrag.week.label)}</span>
<span class="bericht-vorwoche-text">${esc(eintrag.text)}</span>
</div>
<button type="button" class="bericht-vorwoche-nav" onclick="vorwocheRadSchritt(-1)" ${i <= 0 ? 'disabled' : ''} title="Neuer">›</button>
<div class="bericht-vorwoche-dots">${punkte}</div>
</div>`;
}
// vorwocheRadSchritt: bewegt vorwocheRadIndex um delta (geklemmt auf den
// vorhandenen Verlauf) und zeichnet den Bericht neu — derselbe vollständige
// Neuaufbau, den auch editLph() auslöst; ein offener
// Kommentar-Editor übersteht das über dasselbe Fokus-Wiederherstellungsnetz
// (pendingCommentFocus, js/board/board.js), eine aufgeklappte Anwesenheit
// über berichtDisclosureState. Parameter: delta (1 = weiter zurück, -1 =
// neuer). Keine Rückgabe.
function vorwocheRadSchritt(delta){
vorwocheRadScrollAkku = 0; // ein gezielter Klick verwirft ein halb aufgelaufenes Scrollen
const w = currentWeek();
const p = aktuellesBerichtProjekt(w);
if(!w || !p) return;
const max = projectStatusHistory(p.projectUid).filter(e => e.week.label !== w.label).length - 1;
vorwocheRadIndex = Math.max(0, Math.min(max, vorwocheRadIndex + delta));
renderWochenbericht();
}
// vorwocheRadScrollAkku/-SCHWELLE: jeder einzelne wheel-Tick löste zunächst
// sofort einen Schritt aus — auf einem Trackpad erzeugt schon ein kurzes
// Antippen mehrere Ticks mit kleinem deltaY, wodurch ein einziges Scrollen
// mehrere Wochen auf einmal übersprang (Erik: "Empfindlichkeit beim Scrollen
// etwas verlangsamen"). Jetzt wird deltaY aufsummiert, bis die Schwelle
// überschritten ist — eine einzelne Mausrad-Rastung (meist ~100) überschreitet
// sie sofort und bleibt damit bei einem Schritt je Rastung, ein kurzes
// Trackpad-Antippen dagegen oft nicht. Erster Richtwert, bei Bedarf
// nachjustieren.
let vorwocheRadScrollAkku = 0;
const VORWOCHE_RAD_SCHWELLE = 50;
// vorwocheRadScroll: Mausrad-/Trackpad-Bedienung desselben Scrollrads —
// runter (deltaY positiv, aufsummiert) blättert weiter zurück, hoch blättert
// zu neueren Einträgen. e.preventDefault() verhindert, dass dieselbe Bewegung
// zusätzlich die Seite scrollt. Parameter: e (WheelEvent). Keine Rückgabe.
function vorwocheRadScroll(e){
e.preventDefault();
vorwocheRadScrollAkku += e.deltaY;
if(Math.abs(vorwocheRadScrollAkku) < VORWOCHE_RAD_SCHWELLE) return;
vorwocheRadSchritt(vorwocheRadScrollAkku > 0 ? 1 : -1);
}
// ---------- Aufgabe aus dem Kommentar ----------
//
// Beschlüsse fallen im Meeting und landen bisher als Fließtext im Kommentar —
@@ -872,10 +946,9 @@ function protokollAlsMail(){
// ---------- Tastatur ----------
//
// Wer im Meeting mitschreibt, hat keine Hand für die Maus frei. ↓/↑ wechseln
// das Projekt, Strg+↵ speichert den Kommentar und geht weiter. Die Tasten
// greifen nur im Wochenbericht und nicht, während in einem Eingabefeld
// getippt wird — mit einer Ausnahme: Strg+↵ im Kommentarfeld ist genau der
// Fall, für den es gedacht ist.
// das Projekt, Strg+↵ im Kommentarfeld speichert den Kommentar und geht
// weiter. Die Tasten greifen nur im Wochenbericht und nicht, während in
// einem Eingabefeld getippt wird.
document.addEventListener('keydown', function(e){
if(typeof currentTab === 'undefined' || currentTab !== 'wochenbericht') return;
const w = currentWeek();
@@ -888,12 +961,22 @@ document.addEventListener('keydown', function(e){
if((e.key === 'Enter') && (e.ctrlKey || e.metaKey)){
const feld = e.target && e.target.closest ? e.target.closest('.comment-field') : null;
// Strg+Enter nur abfangen, wenn der Fokus tatsächlich im Kommentarfeld
// liegt — sonst OHNE feld-Prüfung, z. B. in einem Leistungsphasen-
// Eingabefeld (.bericht-lph-input) oder im Titelfeld des "Aufgabe aus
// Kommentar"-Dialogs getippt, sprang die App sofort zum nächsten Projekt
// und baute die Liste neu auf — ein gerade eingetippter, noch nicht per
// onchange/blur gespeicherter Wert ging dabei kommentarlos verloren, und
// ein offen stehender Dialog bezog sich danach lautlos auf das falsche
// (inzwischen gewechselte) Projekt. Ohne feld bricht diese Weiche jetzt
// einfach ab und überlässt Strg+Enter dem normalen Feldverhalten.
if(!feld) return;
const aktiv = aktuellesBerichtProjekt(w);
if(!aktiv) return;
e.preventDefault();
// Der Kommentar speichert sich über den bestehenden Weg selbst; hier
// folgt nur der Sprung zum nächsten offenen Punkt.
if(feld && typeof saveBoardComment === 'function'){
if(typeof saveBoardComment === 'function'){
const knopf = feld.closest('.comment-block').querySelector('.comment-editor-foot button');
if(knopf) saveBoardComment(knopf);
}