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:
1 parent
6ba93b2a92
commit
93505e0e48
17 files changed
+1883
-334
No files matched your search
+77
-74
@@ -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
|
||||
|
||||
@@ -323,6 +323,19 @@ function openProjectPlan(pid){
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// springZuEinstellungen: wie openProjectPlan(), landet aber gezielt im
|
||||
// Bereich "Einstellungen" statt im Dashboard — der Verweis aus Board/
|
||||
// Wochenbericht zur jetzt dort editierbaren Beteiligten-Matrix
|
||||
// (planCycleAssign(), js/plan/plan-settings.js). Der Hash muss VOR
|
||||
// showTab('plan') gesetzt werden: dessen eigener updateRouteUrl() behält
|
||||
// einen bereits vorhandenen location.hash bei (dasselbe Prinzip wie bei
|
||||
// wandSpringeZuProjekt(), js/wand/wand.js). Parameter: pid (Projekt-ID der
|
||||
// aktuellen Sitzung). Kein Rückgabewert.
|
||||
function springZuEinstellungen(pid){
|
||||
location.hash = 'einstellungen';
|
||||
openProjectPlan(pid);
|
||||
}
|
||||
|
||||
// closePlanPage: Gegenstück zu openProjectPlan() für den "Schließen"-Knopf
|
||||
// in der Kopfzeile des Projektbereichs. Früher ging es hier in der
|
||||
// Browser-Historie zurück, weil der Projektbereich ein eigenes Dokument war.
|
||||
|
||||
@@ -281,7 +281,16 @@
|
||||
// Terminplan nicht mit leeren Zeilen ohne Balken zustellen. Nicht
|
||||
// jede Aufgabe aus "Aufgaben" landet also im GANTT, wohl aber jede
|
||||
// Aufgabe aus dem GANTT in "Aufgaben" (dieselbe Liste, siehe oben).
|
||||
if(todo.start && todo.end){
|
||||
// UND showInGantt darf nicht ausdrücklich false sein — die Checkbox
|
||||
// "Im Terminplan anzeigen" im Aufgaben-Dialog (plan-modals.js) blendet
|
||||
// eine terminierte Aufgabe trotz gültigem Zeitraum gezielt aus dem
|
||||
// GANTT aus (Erik: ein einzelner "TODOs aus Protokoll"-Import,
|
||||
// plan-protokoll-todos.js, legt leicht 15-25 datierte Aufgaben auf
|
||||
// einmal an, die sonst alle zwingend als Balken erschienen). Fehlt
|
||||
// das Feld (ältere Aufgaben vor dieser Spalte), gilt weiterhin
|
||||
// sichtbar — !== false statt === true, damit kein Bestandsverhalten
|
||||
// kippt.
|
||||
if(todo.start && todo.end && todo.showInGantt !== false){
|
||||
const todoRow = buildTodoRow(todo, tIdx);
|
||||
insertAfter.parentNode.insertBefore(todoRow, insertAfter.nextSibling);
|
||||
insertAfter = todoRow;
|
||||
|
||||
@@ -113,11 +113,17 @@
|
||||
const plan = app.ensurePlanLocal();
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
|
||||
// Nur terminierte Aufgaben (Start UND Ende gesetzt) haben überhaupt eine
|
||||
// eigene GANTT-Zeile (siehe plan-build-gantt.js) und damit einen
|
||||
// Ziehgriff — Aufgaben ohne Termin bleiben an ihrer Stelle
|
||||
// in plan.todos stehen, sie können hier gar nicht bewegt worden sein.
|
||||
const scheduledIds = new Set(todos.filter(t => t.start && t.end).map(t => t.id));
|
||||
// Nur terminierte Aufgaben (Start UND Ende gesetzt) MIT showInGantt !==
|
||||
// false haben überhaupt eine eigene GANTT-Zeile (exakt dieselbe
|
||||
// Bedingung wie in plan-build-gantt.js) und damit einen Ziehgriff —
|
||||
// Aufgaben ohne Termin oder bewusst aus dem GANTT ausgeblendete bleiben
|
||||
// an ihrer Stelle in plan.todos stehen, sie können hier gar nicht bewegt
|
||||
// worden sein. Ohne diesen zweiten Teil der Bedingung würde scheduledIds
|
||||
// mehr Einträge zählen als tatsächlich .gantt-todo-Zeilen im DOM stehen,
|
||||
// und der Sicherheitscheck unten (todoRowIds.length !== scheduledIds.size)
|
||||
// würde JEDE Umsortierung stillschweigend verwerfen, sobald auch nur eine
|
||||
// Aufgabe ausgeblendet ist.
|
||||
const scheduledIds = new Set(todos.filter(t => t.start && t.end && t.showInGantt !== false).map(t => t.id));
|
||||
|
||||
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
|
||||
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)];
|
||||
|
||||
+27
-33
@@ -4,16 +4,15 @@
|
||||
// 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), Fortschritt je
|
||||
// Leistungsphase (#planLphList) und die Zuordnung der Beteiligten
|
||||
// (#planAssignMatrix). Diese drei entstehen in der wöchentlichen
|
||||
// * 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 drei Angaben waren zuvor an DREI Stellen bearbeitbar —
|
||||
// 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
|
||||
@@ -21,11 +20,20 @@
|
||||
// früheren Schreibfunktionen dieses Moduls (planStatusBlur(), setPlanLph(),
|
||||
// setPlanAssign()) sind damit entfallen.
|
||||
//
|
||||
// BEWUSST NICHT hier: Projektnummer, Bezeichnung, Schlagwörter, Honorar und
|
||||
// beauftragte Leistungsphasen. Sie sind ebenfalls Stammdaten, werden aber von
|
||||
// js/plan/plan-settings.js gerendert (Honorar und beauftragte Phasen
|
||||
// geschrieben von setHonorarBetrag()/toggleBeauftragteLp() aus
|
||||
// js/plan/plan-honorar.js).
|
||||
// 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
|
||||
@@ -128,25 +136,8 @@
|
||||
}).join('');
|
||||
}
|
||||
setzeBerichtVerweis('planLphVerweis', locked);
|
||||
|
||||
// Beteiligte — nur Anzeige. Nicht zugeordnete Personen erscheinen
|
||||
// weiterhin blass, damit erkennbar bleibt, wer überhaupt zur Auswahl
|
||||
// stünde; anklickbar sind sie hier aber nicht mehr.
|
||||
const assignMatrix = document.getElementById('planAssignMatrix');
|
||||
if(assignMatrix){
|
||||
const grouped = peopleByRole(app.week);
|
||||
if(!p.assign) p.assign = {};
|
||||
assignMatrix.innerHTML = ROLES.map(r => {
|
||||
const entries = grouped[r] || [];
|
||||
const chips = entries.length ? entries.map(entry => {
|
||||
const state = p.assign[assignKey(entry.person.id, r)] || 0;
|
||||
const title = (entry.person.fullName || entry.person.name) + ' · ' + r;
|
||||
return `<span class="board-avatar ${app.planRoleClass(r)} state-${state} is-ansicht" title="${esc(title)}">${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 ${app.planRoleClass(r)}">${esc(r)}</span><div class="board-role-chips">${chips}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
setzeBerichtVerweis('planAssignVerweis', locked);
|
||||
// Beteiligte: siehe js/plan/plan-settings.js (renderPlanSettings()) —
|
||||
// Stammdatum, nicht mehr hier.
|
||||
};
|
||||
|
||||
// setzeBerichtVerweis: füllt einen Hinweis-Platz mit dem Weg zur
|
||||
@@ -202,9 +193,12 @@
|
||||
};
|
||||
|
||||
// Die Schreibfunktionen setPlanLph()/setPlanAssign()/planStatusBlur() sind
|
||||
// entfallen. Fortschritt, Zuordnung 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()/cycleAssign()/saveBoardComment() in js/board/board.js) sind
|
||||
// davon unberührt — dort hängt die Erfassung des Wochenberichts dran.
|
||||
// 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.
|
||||
})();
|
||||
@@ -397,6 +397,14 @@
|
||||
}
|
||||
refreshTodoDuration();
|
||||
|
||||
// Im Terminplan anzeigen: Vorgabe sichtbar (!== false, nicht === true —
|
||||
// eine Aufgabe ohne das Feld, z. B. aus einem alten Bestand, gilt
|
||||
// weiterhin als sichtbar, kein Bestandsverhalten kippt rückwirkend).
|
||||
// Bei "Speichern & Neu" übernimmt pre.showInGantt den zuletzt gesetzten
|
||||
// Wert (dieselben Rahmendaten wie Zeitraum/Zuständige, s. saveTodoEdit()).
|
||||
const showInGanttEl = document.getElementById('todoEditShowInGantt');
|
||||
if(showInGanttEl) showInGanttEl.checked = isNew ? (pre.showInGantt !== undefined ? pre.showInGantt : true) : currentTodo.showInGantt !== false;
|
||||
|
||||
const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
|
||||
todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50;
|
||||
applyTodoStatus(todoStatusFrom(currentTodo), storedProgress);
|
||||
@@ -563,11 +571,14 @@
|
||||
const assigneesDiv = document.getElementById('todoEditAssignees');
|
||||
const assignees = assigneesDiv ? Array.from(assigneesDiv.querySelectorAll('.person-chip[aria-pressed="true"]')).map(btn => btn.dataset.personId) : [];
|
||||
|
||||
const showInGanttEl = document.getElementById('todoEditShowInGantt');
|
||||
const showInGantt = showInGanttEl ? showInGanttEl.checked : true;
|
||||
|
||||
const plan = app.ensurePlanLocal();
|
||||
if(!Array.isArray(plan.todos)) plan.todos = [];
|
||||
|
||||
if(app.currentTodoIndex === null){
|
||||
plan.todos.push({ id: uid(), title, start, end, done, progress, dependsOn, assignees });
|
||||
plan.todos.push({ id: uid(), title, start, end, done, progress, dependsOn, assignees, showInGantt });
|
||||
} else if(plan.todos[app.currentTodoIndex]){
|
||||
const t = plan.todos[app.currentTodoIndex];
|
||||
t.title = title;
|
||||
@@ -577,6 +588,7 @@
|
||||
t.progress = progress;
|
||||
t.dependsOn = dependsOn;
|
||||
t.assignees = assignees;
|
||||
t.showInGantt = showInGantt;
|
||||
}
|
||||
app.savePlan();
|
||||
// Erst schließen (setzt den Bearbeitungsindex zurück), dann ggf. direkt
|
||||
@@ -584,7 +596,7 @@
|
||||
todoModalSnapshot = todoFormSnapshot();
|
||||
closeTodoModal();
|
||||
renderPlanGantt();
|
||||
if(andNew) openTodoModal(null, { start, end, assignees });
|
||||
if(andNew) openTodoModal(null, { start, end, assignees, showInGantt });
|
||||
};
|
||||
|
||||
// deleteTodoEdit: löscht nach Bestätigung die aktuell bearbeitete Aufgabe,
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
// -----------------------------
|
||||
// Projektplan — "TODOs aus Protokoll" (externes PDF per KI)
|
||||
// -----------------------------
|
||||
// Eigenständige Funktion im Bereich "Aufgaben" (+ GANTT-Toolbar, gleichrangig
|
||||
// neben "+ Vorlage"/"+ Terminplan"): ein EXTERNES Besprechungsprotokoll im
|
||||
// PDF-Format wird per KI eingelesen, und daraus werden — in einem zweiten,
|
||||
// eigenen KI-Schritt — Aufgaben für dieses Projekt abgeleitet. Bewusst
|
||||
// KOMPLETT unabhängig von js/plan/plan-protokoll.js: nichts hier liest aus
|
||||
// oder schreibt in project_protokolle/protokoll_punkte, nichts wird auf dem
|
||||
// Server dauerhaft abgelegt (weder das PDF noch die erkannten Punkte) — ein
|
||||
// reines Ad-hoc-Werkzeug wie der Plan Checker, kein Dokumentenarchiv. Wer das
|
||||
// externe Protokoll dauerhaft als Projektunterlage ablegen will, nutzt den
|
||||
// normalen Protokoll-Import (Bereich "Protokolle").
|
||||
//
|
||||
// Ein Dialog (#protokollTodosModal), DREI Schritte, feste Fußzeile
|
||||
// (Zurück/Abbrechen/Weiter) — Entwurfsrichtung "A — Schritt-Dialog" aus dem
|
||||
// Entwurfsvergleich (Taste-Skill), von Erik gewählt: ein sichtbarer
|
||||
// Fortschrittsbalken (#protokollTodosSteps) und derselbe Knopf-Platz über
|
||||
// alle Schritte hinweg, statt je Phase ein eigenes Knopf-Set. Nur der
|
||||
// Inhalt zwischen Fortschrittsbalken und Fußzeile (#protokollTodosBody)
|
||||
// wird je Schritt neu gerendert; Titel/Untertitel/Fortschritt/Fußzeile
|
||||
// bleiben derselbe Rahmen. Der rechte Fußzeilen-Knopf ("Weiter") führt
|
||||
// zugleich die KI-Aufrufe aus — ein Klick macht Schritt und Netzwerkaufruf
|
||||
// in einem:
|
||||
// 0. Hochladen — PDF + KI-Anbieter wählen → "Einlesen" ruft POST
|
||||
// /plan-todos-aus-protokoll/einlesen auf (server.js, nutzt dieselbe
|
||||
// Erkennung wie der Protokoll-Import: erkennePunkteMitKI()/
|
||||
// erkennePunkteAusText()).
|
||||
// 1. Punkte wählen — die erkannten Punkte als Checkliste (alle offenen
|
||||
// standardmäßig angehakt) → "Aufgaben ableiten" ruft POST
|
||||
// /plan-todos-aus-protokoll/aufgaben-vorschlag mit genau diesen
|
||||
// Punkten im Body (kein erneuter Server-Lookup, nichts ist
|
||||
// persistiert).
|
||||
// 2. Aufgaben bestätigen — die Vorschläge als kompakte Zeilenliste
|
||||
// (Entwurfsrichtung "B — Kompakte Zeilen" aus einem zweiten
|
||||
// Taste-Skill-Vergleich, nur für diesen Schritt; siehe
|
||||
// renderVorschlaegeHtml()): Titel/Termin-Abzeichen/Zuständig-Kürzel auf
|
||||
// einen Blick, Beschreibung/Bearbeitung erst nach Antippen der Zeile —
|
||||
// ein echter Testlauf lieferte bereits 19 Vorschläge, als ausführliche
|
||||
// Karten wäre das nur noch Scrollen gewesen. "N Aufgaben einfügen"
|
||||
// erzeugt normale plan_todos MIT Start- UND Enddatum (sonst keine
|
||||
// GANTT-Zeile, siehe CLAUDE.md) und ruft danach fitPlanGanttRange() wie
|
||||
// applyTemplate(), damit die neuen Balken im sichtbaren GANTT-Zeitraum
|
||||
// liegen.
|
||||
// "Zurück" springt ohne erneuten Serveraufruf auf den vorherigen Schritt
|
||||
// zurück (die Daten bleiben im Modul-Zustand); einzige Ausnahme: ein
|
||||
// Datei-Eingabefeld lässt sich aus Sicherheitsgründen nicht per Skript
|
||||
// erneut vorbelegen — wer von Schritt 1 zurück zu Schritt 0 geht, muss die
|
||||
// Datei bei Bedarf erneut auswählen.
|
||||
//
|
||||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||||
// (plan-state.js).
|
||||
|
||||
(function(){
|
||||
const app = window.PlanApp;
|
||||
|
||||
// Modul-Zustand — lebt nur, während der Dialog offen ist; ein erneutes
|
||||
// Öffnen startet immer wieder bei Schritt 0.
|
||||
let step = 0;
|
||||
let erkanntePunkte = [];
|
||||
let punkteAuswahl = {}; // Index in erkanntePunkte -> angehakt (Boolean), überlebt Zurück/Weiter
|
||||
let gewaehlterAnbieter = '';
|
||||
let ausgewaehltePunkte = []; // die Punkte, die tatsächlich an die KI gingen (für die Quelle-Referenz in Schritt 2)
|
||||
let vorschlaege = [];
|
||||
|
||||
const STEP_TITEL = ['Externes Protokoll einlesen', 'Punkte wählen', 'Aufgaben bestätigen'];
|
||||
|
||||
// openProtokollTodosModal: öffnet den Dialog bei Schritt 0. Bei gesperrter
|
||||
// Sitzung gar nicht erst geöffnet (wie bei den anderen Bearbeitungs-
|
||||
// Dialogen, siehe plan-modals.js).
|
||||
window.openProtokollTodosModal = function(){
|
||||
if(app.isLocked()) return;
|
||||
step = 0;
|
||||
erkanntePunkte = [];
|
||||
punkteAuswahl = {};
|
||||
gewaehlterAnbieter = '';
|
||||
ausgewaehltePunkte = [];
|
||||
vorschlaege = [];
|
||||
renderStep();
|
||||
document.getElementById('protokollTodosModal').style.display = 'flex';
|
||||
};
|
||||
|
||||
window.closeProtokollTodosModal = function(){
|
||||
document.getElementById('protokollTodosModal').style.display = 'none';
|
||||
};
|
||||
|
||||
function zeigeFehler(msg){
|
||||
const el = document.getElementById('protokollTodosError');
|
||||
if(el){ el.textContent = msg; el.hidden = false; }
|
||||
}
|
||||
function verbergeFehler(){
|
||||
const el = document.getElementById('protokollTodosError');
|
||||
if(el){ el.textContent = ''; el.hidden = true; }
|
||||
}
|
||||
// zeigeFallbackHinweis: eigener, bernsteinfarbener Hinweis für einen
|
||||
// GLÜCKTEN, aber degradierten Durchlauf (KI nicht erreichbar, auf die
|
||||
// einfache Übernahme zurückgefallen) — bewusst NICHT dieselbe rote
|
||||
// Fehlermeldung wie zeigeFehler() oben, dieselbe Unterscheidung wie der
|
||||
// Protokoll-Import (.protokoll-ki-fallback-banner, plan-protokoll.js):
|
||||
// ein Fallback ist ein akzeptiertes Ergebnis, kein Fehlschlag.
|
||||
function zeigeFallbackHinweis(msg){
|
||||
const el = document.getElementById('protokollTodosFallback');
|
||||
if(el){ el.textContent = '⚠️ ' + msg; el.hidden = false; }
|
||||
}
|
||||
function verbergeFallbackHinweis(){
|
||||
const el = document.getElementById('protokollTodosFallback');
|
||||
if(el){ el.textContent = ''; el.hidden = true; }
|
||||
}
|
||||
|
||||
// setzeLadezustand: sperrt während eines laufenden KI-Aufrufs den
|
||||
// gesamten Dialog (Fußzeile inklusive, damit kein zweiter Aufruf
|
||||
// überlappt) — ohne das gäbe es während eines länger laufenden Aufrufs
|
||||
// (bis zu KI_TIMEOUT_MS_LOKAL = 30 Minuten bei einem eigenen Server,
|
||||
// server.js) keinerlei Rückmeldung, derselbe Grund wie bei
|
||||
// setzeProtokollImportLadezustand() in plan-protokoll.js.
|
||||
function setzeLadezustand(aktiv, text){
|
||||
const modal = document.getElementById('protokollTodosModal');
|
||||
if(modal) modal.querySelectorAll('input, select, button').forEach(el => { el.disabled = aktiv; });
|
||||
const ladeEl = document.getElementById('protokollTodosLoading');
|
||||
if(ladeEl) ladeEl.hidden = !aktiv;
|
||||
const textEl = document.getElementById('protokollTodosLoadingText');
|
||||
if(textEl && text) textEl.textContent = text;
|
||||
// setzeLadezustand(false) würde sonst auch "Zurück" auf Schritt 0
|
||||
// wieder aktivieren — das macht erst renderStep() richtig.
|
||||
if(!aktiv) renderStep();
|
||||
}
|
||||
|
||||
// anbieterOptionenHtml: baut die Options-Liste des KI-Anbieter-Felds aus
|
||||
// GET /my-ai-settings — derselbe Aufbau wie #protokollImportErkennung in
|
||||
// plan-protokoll.js.
|
||||
function anbieterOptionenHtml(){
|
||||
return fetch(baseUrl() + '/my-ai-settings')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if(!d.ok) throw new Error('nicht ok');
|
||||
return `<option value="chatgpt">ChatGPT${d.chatgpt && d.chatgpt.configured ? '' : ' (kein Schlüssel hinterlegt)'}</option>
|
||||
<option value="claude">Claude${d.claude && d.claude.configured ? '' : ' (kein Schlüssel hinterlegt)'}</option>`;
|
||||
})
|
||||
.catch(() => '<option value="chatgpt">ChatGPT</option><option value="claude">Claude</option>');
|
||||
}
|
||||
|
||||
// renderStep: zeichnet Titel, Fortschrittsbalken, Fußzeilen-Beschriftung
|
||||
// und den schrittspezifischen Inhalt. Macht keinen Netzwerkaufruf — das
|
||||
// übernimmt protokollTodosWeiter(). Keine Parameter, keine Rückgabe.
|
||||
function renderStep(){
|
||||
verbergeFehler();
|
||||
verbergeFallbackHinweis();
|
||||
document.getElementById('protokollTodosTitle').textContent = STEP_TITEL[step];
|
||||
document.querySelectorAll('#protokollTodosSteps .seg').forEach(s => {
|
||||
s.classList.toggle('done', +s.dataset.i <= step);
|
||||
});
|
||||
document.getElementById('protokollTodosBackBtn').disabled = (step === 0);
|
||||
|
||||
const primaryBtn = document.getElementById('protokollTodosPrimaryBtn');
|
||||
const subtitleEl = document.getElementById('protokollTodosSubtitle');
|
||||
const body = document.getElementById('protokollTodosBody');
|
||||
|
||||
if(step === 0){
|
||||
subtitleEl.textContent = 'PDF hochladen, KI-Anbieter wählen.';
|
||||
primaryBtn.textContent = 'Einlesen';
|
||||
body.innerHTML = `
|
||||
<div class="form-row">
|
||||
<label for="protokollTodosFile">Externes Protokoll (PDF)</label>
|
||||
<input type="file" id="protokollTodosFile" accept=".pdf">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="protokollTodosAnbieter">KI-Anbieter</label>
|
||||
<select id="protokollTodosAnbieter"><option value="chatgpt">ChatGPT</option><option value="claude">Claude</option></select>
|
||||
</div>`;
|
||||
anbieterOptionenHtml().then(html => {
|
||||
const sel = document.getElementById('protokollTodosAnbieter');
|
||||
if(sel){ sel.innerHTML = html; if(gewaehlterAnbieter) sel.value = gewaehlterAnbieter; }
|
||||
});
|
||||
} else if(step === 1){
|
||||
subtitleEl.textContent = erkanntePunkte.length + ' Punkte erkannt — offene sind vorausgewählt.';
|
||||
primaryBtn.textContent = 'Aufgaben ableiten';
|
||||
body.innerHTML = `
|
||||
<div class="todo-depends-list" id="protokollTodosPunkteListe" style="max-height:280px">
|
||||
${erkanntePunkte.map((p, i) => {
|
||||
const checked = punkteAuswahl[i] !== undefined ? punkteAuswahl[i] : (p.status === 'offen');
|
||||
punkteAuswahl[i] = checked;
|
||||
return `<label class="todo-depends-item">
|
||||
<input type="checkbox" value="${i}" ${checked ? 'checked' : ''}>
|
||||
<span style="flex:1">
|
||||
<b>${esc(p.thema || 'Ohne Thema')}</b>
|
||||
${p.text ? `<br><span class="hint" style="margin:0">${esc(String(p.text).slice(0, 160))}</span>` : ''}
|
||||
</span>
|
||||
<span class="hint" style="margin:0">${esc(p.zustaendig || '')}${p.termin ? ' · ' + esc(formatDateDE(parseDateISO(p.termin)) || p.termin) : ''}</span>
|
||||
</label>`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
document.getElementById('protokollTodosPunkteListe').querySelectorAll('input[type=checkbox]').forEach(cb => {
|
||||
cb.addEventListener('change', () => { punkteAuswahl[cb.value] = cb.checked; });
|
||||
});
|
||||
} else {
|
||||
subtitleEl.textContent = vorschlaege.length + ' Vorschläge aus den gewählten Punkten.';
|
||||
body.innerHTML = renderVorschlaegeHtml();
|
||||
const liste = document.getElementById('protokollTodosVorschlaege');
|
||||
if(liste){
|
||||
// Abwahl blendet die Zeile ab (statt sie zu entfernen) — dieselbe
|
||||
// "noch da, aber erkennbar nicht dabei"-Behandlung wie bei den
|
||||
// Punkten in Schritt 1.
|
||||
liste.addEventListener('change', e => {
|
||||
if(!e.target.classList.contains('pt-vorschlag-check')) return;
|
||||
e.target.closest('.protokoll-todo-vorschlag').classList.toggle('off', !e.target.checked);
|
||||
aktualisierePrimaerKnopf();
|
||||
});
|
||||
// Klick auf die Zeile klappt Beschreibung/Termin/Zuständige auf —
|
||||
// außer der Klick galt der Checkbox oder dem Titel-Eingabefeld
|
||||
// selbst, die sollen nicht zugleich die Zeile zu-/aufklappen.
|
||||
liste.addEventListener('click', e => {
|
||||
if(e.target.closest('.pt-vorschlag-check, .pt-vorschlag-titel')) return;
|
||||
const main = e.target.closest('.pt-row-main');
|
||||
if(main) main.closest('.protokoll-todo-vorschlag').classList.toggle('open');
|
||||
});
|
||||
}
|
||||
aktualisierePrimaerKnopf();
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisierePrimaerKnopf(){
|
||||
const checks = document.querySelectorAll('.pt-vorschlag-check');
|
||||
const n = checks.length ? Array.from(checks).filter(c => c.checked).length : vorschlaege.length;
|
||||
const btn = document.getElementById('protokollTodosPrimaryBtn');
|
||||
btn.textContent = n + ' Aufgabe' + (n === 1 ? '' : 'n') + ' einfügen';
|
||||
btn.disabled = n === 0;
|
||||
const selCount = document.getElementById('protokollTodosSelCount');
|
||||
if(selCount) selCount.innerHTML = `<b>${n}</b> von ${vorschlaege.length} ausgewählt`;
|
||||
}
|
||||
|
||||
// protokollTodosAuswahlAlle: markiert alle Vorschlagszeilen auf einen
|
||||
// Schlag an oder ab ("Alle"/"Keine" neben der Auswahl-Zählung) — bei 19
|
||||
// Vorschlägen (im echten Testlauf beobachtet) sonst 19 Einzelklicks.
|
||||
// Parameter: alle (Boolean). Keine Rückgabe.
|
||||
window.protokollTodosAuswahlAlle = function(alle){
|
||||
document.querySelectorAll('#protokollTodosVorschlaege .pt-vorschlag-check').forEach(cb => {
|
||||
cb.checked = alle;
|
||||
cb.closest('.protokoll-todo-vorschlag').classList.toggle('off', !alle);
|
||||
});
|
||||
aktualisierePrimaerKnopf();
|
||||
};
|
||||
|
||||
// matchePersonenVonName: einfacher Namensabgleich zwischen einem
|
||||
// KI-Textvorschlag ("zustaendig" aus dem externen Protokoll — oft eine
|
||||
// externe Firma, kein Teammitglied) und den diesem Projekt zugeordneten
|
||||
// Personen. Case-insensitive Teilstring auf Name/vollem Namen; kein
|
||||
// Treffer ist der Normalfall und bleibt als reiner Text sichtbar.
|
||||
function matchePersonenVonName(text, kandidaten){
|
||||
const t = String(text || '').trim().toLowerCase();
|
||||
if(!t) return [];
|
||||
const treffer = new Set();
|
||||
kandidaten.forEach(k => {
|
||||
const name = String(k.person.name || '').toLowerCase();
|
||||
const voll = String(k.person.fullName || '').toLowerCase();
|
||||
if((name && t.includes(name)) || (voll && t.includes(voll))) treffer.add(k.person.id);
|
||||
});
|
||||
return Array.from(treffer);
|
||||
}
|
||||
|
||||
// renderVorschlaegeHtml: die Vorschlagsliste aus Entwurfsrichtung
|
||||
// "B — Kompakte Zeilen" (Taste-Skill-Vergleich) — eine Zeile je Vorschlag
|
||||
// (Titel, Termin-Abzeichen, Zuständig-Kürzel), Beschreibung/Termin-Feld/
|
||||
// Zuständigen-Auswahl erst nach Antippen der Zeile sichtbar. Gewählt, weil
|
||||
// ein echter Testlauf bereits 19 Vorschläge aus einem Protokoll lieferte —
|
||||
// als ausführliche Karte wäre das ganze Fenster voll Scrollen gewesen,
|
||||
// als Zeilenliste passt die komplette Übersicht auf einen Blick, ohne
|
||||
// Termin/Zuständig ganz zu verstecken (beide stehen als Abzeichen schon
|
||||
// in der eingeklappten Zeile). Eine Auswahl-Zeile oben
|
||||
// ("N von M ausgewählt" + Alle/Keine) erspart bei vielen Vorschlägen das
|
||||
// einzelne Abwählen.
|
||||
function renderVorschlaegeHtml(){
|
||||
if(vorschlaege.length === 0){
|
||||
return '<p class="hint">Keine Aufgaben vorgeschlagen — die ausgewählten Punkte waren rein informativ.</p>';
|
||||
}
|
||||
// Kandidaten für die Zuständigen-Auswahl: die diesem Projekt bereits
|
||||
// zugeordneten Personen (projectAssignees(), js/gantt.js), einmalig
|
||||
// entdoppelt (eine Person kann in mehreren Rollen zugeordnet sein).
|
||||
const kandidaten = [];
|
||||
const gesehen = new Set();
|
||||
projectAssignees(app.project, app.week).forEach(k => {
|
||||
if(!gesehen.has(k.person.id)){ gesehen.add(k.person.id); kandidaten.push(k); }
|
||||
});
|
||||
|
||||
const zeilen = vorschlaege.map((v, idx) => {
|
||||
const quelle = (v.quellRefs || []).map(r => ausgewaehltePunkte[r] && ausgewaehltePunkte[r].thema).filter(Boolean).join(', ');
|
||||
const vorausgewaehlt = matchePersonenVonName(v.zustaendigVorschlag, kandidaten);
|
||||
const erstePerson = vorausgewaehlt.length ? kandidaten.find(k => k.person.id === vorausgewaehlt[0]) : null;
|
||||
return `<div class="protokoll-todo-vorschlag" data-idx="${idx}">
|
||||
<div class="pt-row-main">
|
||||
<input type="checkbox" class="pt-vorschlag-check" checked>
|
||||
<input type="text" class="pt-vorschlag-titel" value="${esc(v.titel)}">
|
||||
<span class="pt-row-badges">
|
||||
${v.faelligkeit ? `<span class="pt-badge-date">${esc(formatDateDE(parseDateISO(v.faelligkeit)) || v.faelligkeit)}</span>` : ''}
|
||||
${erstePerson ? `<span class="pt-badge-person" title="${esc(erstePerson.person.fullName || erstePerson.person.name)}">${esc(erstePerson.person.name || initialsForName(erstePerson.person.fullName || '') || '?')}</span>` : ''}
|
||||
</span>
|
||||
<span class="pt-chevron">▸</span>
|
||||
</div>
|
||||
<div class="pt-row-detail">
|
||||
${v.beschreibung ? `<div class="hint protokoll-todo-vorschlag-beschreibung">${esc(v.beschreibung)}</div>` : ''}
|
||||
<div class="protokoll-todo-vorschlag-felder">
|
||||
<label class="form-row" style="margin-bottom:6px">
|
||||
<span>Fällig am</span>
|
||||
<input type="date" class="pt-vorschlag-termin" value="${esc(v.faelligkeit)}" style="max-width:180px">
|
||||
</label>
|
||||
<!-- Dieselben .person-chip/.person-chip-ini-Chips mit
|
||||
aria-pressed und toggleTodoAssignee() (js/plan/plan-modals.js)
|
||||
wie im echten TODO-Dialog ("Zuständige") — die erste Fassung
|
||||
hier nutzte stattdessen eine rohe, unformatierte Checkbox
|
||||
neben dem Kürzel, was neben den übrigen, bewusst gestalteten
|
||||
Zeilen schlicht unfertig aussah (Eriks Rückmeldung). -->
|
||||
<div class="person-chips">
|
||||
${kandidaten.length ? kandidaten.map(k => {
|
||||
const kuerzel = k.person.name || initialsForName(k.person.fullName || '') || '?';
|
||||
const voll = k.person.fullName || k.person.name || '';
|
||||
return `<button type="button" class="person-chip pt-vorschlag-person" data-person-id="${esc(k.person.id)}"
|
||||
aria-pressed="${vorausgewaehlt.includes(k.person.id)}" onclick="toggleTodoAssignee(this)">
|
||||
<span class="person-chip-ini">${esc(kuerzel)}</span>${esc(voll)}
|
||||
</button>`;
|
||||
}).join('') : '<span class="hint" style="margin:0">Niemand diesem Projekt zugeordnet.</span>'}
|
||||
</div>
|
||||
${!vorausgewaehlt.length && v.zustaendigVorschlag ? `<span class="hint" style="margin:0">KI-Vorschlag „${esc(v.zustaendigVorschlag)}“ — kein passendes Teammitglied gefunden.</span>` : ''}
|
||||
${quelle ? `<span class="hint" style="margin:0">aus: ${esc(quelle)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="pt-sel-bar">
|
||||
<span class="pt-sel-count" id="protokollTodosSelCount"></span>
|
||||
<span class="pt-sel-actions">
|
||||
<button type="button" onclick="protokollTodosAuswahlAlle(true)">Alle</button> ·
|
||||
<button type="button" onclick="protokollTodosAuswahlAlle(false)">Keine</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="todo-depends-list" id="protokollTodosVorschlaege" style="max-height:360px">${zeilen}</div>`;
|
||||
}
|
||||
|
||||
// protokollTodosZurueck: ein Schritt zurück, ohne erneuten Serveraufruf —
|
||||
// alle Daten bleiben im Modul-Zustand. Keine Parameter, keine Rückgabe.
|
||||
window.protokollTodosZurueck = function(){
|
||||
if(step === 0) return;
|
||||
step--;
|
||||
renderStep();
|
||||
};
|
||||
|
||||
// protokollTodosWeiter: der rechte Fußzeilen-Knopf führt Schritt UND
|
||||
// Netzwerkaufruf in einem aus — "Einlesen"/"Aufgaben ableiten" lösen den
|
||||
// jeweils nächsten KI-Schritt aus, der letzte Klick übernimmt die
|
||||
// Aufgaben. Keine Parameter, keine Rückgabe.
|
||||
window.protokollTodosWeiter = function(){
|
||||
if(step === 0) return schrittEinlesen();
|
||||
if(step === 1) return schrittAbleiten();
|
||||
return schrittUebernehmen();
|
||||
};
|
||||
|
||||
function schrittEinlesen(){
|
||||
verbergeFehler();
|
||||
const fileInput = document.getElementById('protokollTodosFile');
|
||||
const file = fileInput && fileInput.files && fileInput.files[0];
|
||||
if(!file) return zeigeFehler('Bitte eine PDF-Datei auswählen.');
|
||||
gewaehlterAnbieter = (document.getElementById('protokollTodosAnbieter') || {}).value || '';
|
||||
|
||||
setzeLadezustand(true, 'Wird eingelesen … bei KI-Erkennung über einen eigenen Server kann dies bis zu 30 Minuten dauern.');
|
||||
fetch(baseUrl() + '/plan-todos-aus-protokoll/einlesen?anbieter=' + encodeURIComponent(gewaehlterAnbieter), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': file.type || 'application/pdf' },
|
||||
body: file
|
||||
})
|
||||
.then(r => r.json().then(d => ({ ok: r.ok, d })))
|
||||
.then(({ ok, d }) => {
|
||||
if(!ok || !d.ok) throw new Error((d && d.error) || 'Einlesen fehlgeschlagen.');
|
||||
erkanntePunkte = Array.isArray(d.punkte) ? d.punkte : [];
|
||||
punkteAuswahl = {};
|
||||
if(erkanntePunkte.length === 0){ setzeLadezustand(false); zeigeFehler('Es wurden keine Punkte erkannt.'); return; }
|
||||
step = 1;
|
||||
setzeLadezustand(false);
|
||||
if(d.erkennungsart !== 'chatgpt' && d.erkennungsart !== 'claude' && d.erkennungshinweis){
|
||||
zeigeFallbackHinweis('KI-Erkennung nicht verfügbar (' + d.erkennungshinweis + ') — Punkte per Textmuster erkannt.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('TODOs aus Protokoll (Einlesen) fehlgeschlagen:', err);
|
||||
setzeLadezustand(false);
|
||||
zeigeFehler(err.message || String(err));
|
||||
});
|
||||
}
|
||||
|
||||
function schrittAbleiten(){
|
||||
verbergeFehler();
|
||||
const listEl = document.getElementById('protokollTodosPunkteListe');
|
||||
const checkedIdx = listEl
|
||||
? Array.from(listEl.querySelectorAll('input[type="checkbox"]:checked')).map(cb => parseInt(cb.value, 10))
|
||||
: [];
|
||||
if(checkedIdx.length === 0) return zeigeFehler('Bitte mindestens einen Punkt auswählen.');
|
||||
|
||||
ausgewaehltePunkte = checkedIdx.map(i => erkanntePunkte[i]);
|
||||
setzeLadezustand(true, 'Aufgaben werden abgeleitet …');
|
||||
fetch(baseUrl() + '/plan-todos-aus-protokoll/aufgaben-vorschlag', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify({ anbieter: gewaehlterAnbieter, punkte: ausgewaehltePunkte })
|
||||
})
|
||||
.then(r => r.json().then(d => ({ ok: r.ok, d })))
|
||||
.then(({ ok, d }) => {
|
||||
if(!ok || !d.ok) throw new Error((d && d.error) || 'Ableiten fehlgeschlagen.');
|
||||
vorschlaege = Array.isArray(d.vorschlaege) ? d.vorschlaege : [];
|
||||
step = 2;
|
||||
setzeLadezustand(false);
|
||||
if(d.erkennungsart === 'direkt' && d.erkennungshinweis){
|
||||
zeigeFallbackHinweis('KI-Ableitung nicht verfügbar (' + d.erkennungshinweis + ') — Punkte direkt übernommen, ohne Zusammenfassung.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('TODOs aus Protokoll (Ableiten) fehlgeschlagen:', err);
|
||||
setzeLadezustand(false);
|
||||
zeigeFehler(err.message || String(err));
|
||||
});
|
||||
}
|
||||
|
||||
// schrittUebernehmen: legt für jede angehakte Vorschlagszeile einen
|
||||
// normalen plan_todo-Eintrag an — IMMER mit gesetztem Start UND Ende,
|
||||
// sonst bleibt die Aufgabe ein reiner Checklisten-Eintrag ohne GANTT-Zeile
|
||||
// (CLAUDE.md: "Ein TODO ist nur dann eine GANTT-Zeile, wenn es BEIDE Daten
|
||||
// hat"). Gleiche Form wie applyTemplate() (plan-templates.js) und
|
||||
// saveTodoAusKommentar() (wochenbericht.js). Kein Netzwerkaufruf hier
|
||||
// selbst — app.savePlan() übernimmt das Speichern wie überall sonst im
|
||||
// Projektbereich.
|
||||
function schrittUebernehmen(){
|
||||
if(app.isLocked()) return;
|
||||
const rows = document.querySelectorAll('#protokollTodosVorschlaege .protokoll-todo-vorschlag');
|
||||
const plan = app.ensurePlanLocal();
|
||||
const heute = formatDateISO(new Date());
|
||||
let anzahl = 0;
|
||||
rows.forEach(row => {
|
||||
const check = row.querySelector('.pt-vorschlag-check');
|
||||
if(!check || !check.checked) return;
|
||||
const titel = ((row.querySelector('.pt-vorschlag-titel') || {}).value || '').trim();
|
||||
if(!titel) return;
|
||||
const faelligkeit = (row.querySelector('.pt-vorschlag-termin') || {}).value || '';
|
||||
const personen = Array.from(row.querySelectorAll('.pt-vorschlag-person[aria-pressed="true"]')).map(el => el.dataset.personId);
|
||||
plan.todos.push({
|
||||
id: uid(),
|
||||
title: titel,
|
||||
start: heute,
|
||||
end: faelligkeit || heute,
|
||||
done: false,
|
||||
progress: 0,
|
||||
dependsOn: [],
|
||||
assignees: personen,
|
||||
});
|
||||
anzahl++;
|
||||
});
|
||||
|
||||
closeProtokollTodosModal();
|
||||
if(anzahl === 0) return;
|
||||
app.savePlan();
|
||||
// Wie applyTemplate(): der sichtbare GANTT-Zeitraum muss die neuen
|
||||
// Balken einschließen, sonst erscheinen sie zwar in der Aufgabenliste,
|
||||
// aber außerhalb des aktuellen Zooms unsichtbar im Terminplan.
|
||||
fitPlanGanttRange();
|
||||
app.showToast(anzahl + ' Aufgabe(n) aus Protokoll angelegt.');
|
||||
}
|
||||
})();
|
||||
@@ -13,7 +13,7 @@
|
||||
// gespeichert von renderPlanInfo()/setPlanGroup()/setPlanActiveLp() in
|
||||
// js/plan/plan-info.js — dieses Modul rührt sie nicht an)
|
||||
// * Honorar (netto) und die BEAUFTRAGTEN LEISTUNGSPHASEN
|
||||
// * Beteiligte (Rollen-/Personen-Matrix, ebenfalls aus plan-info.js)
|
||||
// * Beteiligte (Rollen-/Personen-Matrix, #planAssignMatrix)
|
||||
//
|
||||
// Honorar und beauftragte Leistungsphasen standen früher ausschließlich im
|
||||
// Dialog "Terminplan aus Honorar" (js/plan/plan-honorar.js). Sie sind
|
||||
@@ -24,9 +24,21 @@
|
||||
// aus plan-honorar.js und rendern einander nach jeder Änderung neu, sodass
|
||||
// sie nicht auseinanderlaufen können.
|
||||
//
|
||||
// Die Beteiligten-Matrix war früher ein reines Anzeigefeld, gefüllt von
|
||||
// plan-info.js, mit einem Verweis in den Wochenbericht — dort entstand die
|
||||
// Zuordnung. Auf Eriks ausdrücklichen Wunsch ("In den Einstellungen der
|
||||
// Projekte will ich auch die Projektbeteiligten einstellen können") ist das
|
||||
// umgekehrt: Zuordnung ist jetzt ein Stammdatum wie Honorar, und zwar NUR
|
||||
// HIER editierbar — der Wochenbericht zeigt sie seitdem nur noch zum Lesen
|
||||
// (board.js, renderBoardDrawer()) mit einem Verweis hierher. planCycleAssign()
|
||||
// unten ersetzt für diesen Zweck cycleAssign() (js/board/board.js): jene
|
||||
// Funktion sucht das Projekt über currentWeek() und fände es bei einem
|
||||
// Tiefenlink auf eine andere Sitzung oder einem Planung-Sandbox-Plan dort
|
||||
// nicht — dieselbe Falle, vor der planOpenAufwand() unten warnt.
|
||||
//
|
||||
// Gespeichert wird über den normalen Weg app.savePlan() (die Felder sind Teil
|
||||
// des Wochen-JSON, Tabellen projects.honorar bzw. project_beauftragte_lph) —
|
||||
// es gibt dafür keinen eigenen Endpunkt.
|
||||
// des Wochen-JSON, Tabellen projects.honorar/project_assign bzw.
|
||||
// project_beauftragte_lph) — es gibt dafür keinen eigenen Endpunkt.
|
||||
//
|
||||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||||
// (plan-state.js).
|
||||
@@ -151,6 +163,47 @@
|
||||
${esc(l)}
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
// Beteiligte — Stammdatum, hier editierbar (siehe Kopfkommentar). Nicht
|
||||
// zugeordnete Personen erscheinen blass, damit erkennbar bleibt, wer zur
|
||||
// Auswahl stünde; ein Klick schaltet den Zustand durch (planCycleAssign()).
|
||||
const assignMatrix = document.getElementById('planAssignMatrix');
|
||||
if(assignMatrix){
|
||||
const grouped = peopleByRole(app.week);
|
||||
if(!p.assign) p.assign = {};
|
||||
assignMatrix.innerHTML = ROLES.map(r => {
|
||||
const entries = grouped[r] || [];
|
||||
const chips = entries.length ? entries.map(entry => {
|
||||
const state = p.assign[assignKey(entry.person.id, r)] || 0;
|
||||
const click = locked ? '' : `onclick="planCycleAssign('${entry.person.id}','${r}')"`;
|
||||
const title = (entry.person.fullName || entry.person.name) + ' · ' + r;
|
||||
return `<span class="board-avatar ${app.planRoleClass(r)} state-${state}${locked ? ' locked-cell' : ''}" ${click} title="${esc(title)}">${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 ${app.planRoleClass(r)}">${esc(r)}</span><div class="board-role-chips">${chips}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
};
|
||||
|
||||
// planCycleAssign: wechselt die Zuordnung einer Person zu DIESEM Projekt
|
||||
// zyklisch durch 0=keine → 1=Haupt → 2=optional → 0 (dieselbe Zustandsfolge
|
||||
// wie cycleAssign() in js/board/board.js, hier aber auf app.project statt
|
||||
// currentWeek() — siehe Kopfkommentar). Parameter: personId, role. Keine
|
||||
// Rückgabe.
|
||||
window.planCycleAssign = function(personId, role){
|
||||
const p = app.project;
|
||||
if(!p || app.isLocked()) return;
|
||||
if(!p.assign) p.assign = {};
|
||||
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;
|
||||
app.savePlan();
|
||||
renderPlanSettings();
|
||||
// Dieselbe Zuordnung erscheint im Projektboard und im Wochenbericht
|
||||
// (dort jetzt nur noch zum Lesen) — beide sollen sofort nachziehen,
|
||||
// nicht erst beim nächsten vollständigen Neuladen.
|
||||
if(typeof renderBoard === 'function') renderBoard();
|
||||
if(typeof renderWochenbericht === 'function') renderWochenbericht();
|
||||
};
|
||||
|
||||
// setPlanProjNr: ändert die Projektnummer.
|
||||
|
||||
@@ -249,8 +249,8 @@ window.PlanApp = {
|
||||
// Sitzungen nicht möglich. Die Liste enthält die Knöpfe beider Bereiche:
|
||||
// Terminplan (…Btn) und Aufgaben (…BtnList), siehe App.html.
|
||||
const locked = isLocked();
|
||||
['addMilestoneBtn', 'addTodoBtn', 'templateBtn', 'honorarBtn', 'autoScheduleBtn',
|
||||
'addTodoBtnList', 'addMilestoneBtnList', 'templateBtnList', 'honorarBtnList', 'autoScheduleBtnList']
|
||||
['addMilestoneBtn', 'addTodoBtn', 'templateBtn', 'honorarBtn', 'protokollTodosBtn', 'autoScheduleBtn',
|
||||
'addTodoBtnList', 'addMilestoneBtnList', 'templateBtnList', 'honorarBtnList', 'protokollTodosBtnList', 'autoScheduleBtnList']
|
||||
.forEach(id => {
|
||||
const btn = document.getElementById(id);
|
||||
if(btn) btn.style.display = locked ? 'none' : '';
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
// admin-gepflegte Liste von Merkmalen prüfen (z. B. "Maßketten vorhanden",
|
||||
// siehe Admin → Plan-Checker-Merkmale, js/admin/admin.js). Zusätzlich lässt
|
||||
// sich je Prüfung EIN weiteres Merkmal ergänzen, das nur für diese eine
|
||||
// Prüfung gilt und nicht gespeichert wird.
|
||||
// Prüfung gilt und nicht gespeichert wird - und optional ein Projekt der
|
||||
// aktuellen Sitzung wählen: Dann kommt serverseitig automatisch ein
|
||||
// weiteres, ebenso nicht gespeichertes Merkmal dazu, das den Plankopf gegen
|
||||
// Projektnummer/Bezeichnung dieses Projekts abgleicht (ladePlanCheckerProjekte()/
|
||||
// planCheckerPruefen() unten, server.js POST /plan-checker-pruefen).
|
||||
//
|
||||
// Darstellung "Arbeitsfläche" (Eriks Wahl "B" aus dem Design-Vergleich):
|
||||
// zwei Spalten dauerhaft nebeneinander — links Upload/Konfiguration, rechts
|
||||
@@ -41,13 +45,42 @@ let planCheckerDatei = null;
|
||||
let planCheckerMerkmale = [];
|
||||
|
||||
// renderPlanChecker: Einstiegspunkt, von showTab('planchecker') gerufen.
|
||||
// Lädt die Merkmale-Grundliste und die KI-Anbieter-Auswahl neu — beide
|
||||
// können sich seit dem letzten Besuch geändert haben (Admin-Pflege bzw.
|
||||
// eigener Schlüssel unter Einstellungen → Mein Konto). Keine Parameter,
|
||||
// keine Rückgabe.
|
||||
// Lädt die Merkmale-Grundliste, die KI-Anbieter-Auswahl und die Projektliste
|
||||
// neu — alle drei können sich seit dem letzten Besuch geändert haben
|
||||
// (Admin-Pflege, eigener Schlüssel unter Einstellungen → Mein Konto, bzw.
|
||||
// ein inzwischen angelegtes/umbenanntes Projekt). Keine Parameter, keine
|
||||
// Rückgabe.
|
||||
function renderPlanChecker(){
|
||||
ladePlanCheckerMerkmale();
|
||||
ladePlanCheckerAnbieter();
|
||||
ladePlanCheckerProjekte();
|
||||
}
|
||||
|
||||
// ladePlanCheckerProjekte: füllt die optionale Projekt-Auswahl aus der
|
||||
// bereits geladenen aktuellen Sitzung (currentWeek().projects, js/weeks.js)
|
||||
// — kein eigener Server-Aufruf nötig, die Liste liegt dem Client ohnehin
|
||||
// schon vor. Gruppiert wie die Projektliste selbst (GROUP_ORDER/sortGroups(),
|
||||
// js/globals.js bzw. js/utils.js). Der Options-Wert ist die Projekt-ID
|
||||
// (stabil innerhalb der Sitzung) statt Nummer/Name direkt zu kodieren — bei
|
||||
// der Prüfung wird das Projekt darüber erneut in currentWeek().projects
|
||||
// nachgeschlagen (planCheckerPruefen() unten). Keine Parameter, keine
|
||||
// Rückgabe.
|
||||
function ladePlanCheckerProjekte(){
|
||||
const sel = document.getElementById('planCheckerProjekt');
|
||||
if(!sel) return;
|
||||
const w = typeof currentWeek === 'function' ? currentWeek() : null;
|
||||
const projekte = (w && Array.isArray(w.projects)) ? w.projects : [];
|
||||
const bisher = sel.value;
|
||||
const gruppenNamen = sortGroups([...new Set(projekte.map(p => p.group || 'Ohne Gruppe'))]);
|
||||
sel.innerHTML = '<option value="">Kein Projekt wählen</option>' + gruppenNamen.map(g => {
|
||||
const opts = projekte.filter(p => (p.group || 'Ohne Gruppe') === g)
|
||||
.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(p => `<option value="${esc(p.id)}">${esc(p.nr ? p.nr + ' · ' : '')}${esc(p.name)}</option>`).join('');
|
||||
return opts ? `<optgroup label="${esc(g)}">${opts}</optgroup>` : '';
|
||||
}).join('');
|
||||
// Auswahl erhalten, falls sie nach dem Neuladen noch existiert (z. B. nach
|
||||
// erneutem Tab-Wechsel innerhalb derselben Sitzung).
|
||||
if(bisher && [...sel.options].some(o => o.value === bisher)) sel.value = bisher;
|
||||
}
|
||||
|
||||
// ladePlanCheckerMerkmale: holt die admin-gepflegte Grundliste (GET
|
||||
@@ -143,7 +176,7 @@ function planCheckerDrop(e){
|
||||
function planCheckerLadezustand(aktiv){
|
||||
const btn = document.getElementById('planCheckerPruefenBtn');
|
||||
if(btn){ btn.disabled = aktiv; btn.textContent = aktiv ? 'Wird geprüft…' : 'Plan prüfen'; }
|
||||
['planCheckerFile', 'planCheckerZusatz', 'planCheckerAnbieter'].forEach(id => {
|
||||
['planCheckerFile', 'planCheckerZusatz', 'planCheckerAnbieter', 'planCheckerProjekt'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if(el) el.disabled = aktiv;
|
||||
});
|
||||
@@ -164,9 +197,20 @@ function planCheckerPruefen(){
|
||||
const zusatzEl = document.getElementById('planCheckerZusatz');
|
||||
const zusatz = zusatzEl ? zusatzEl.value.trim() : '';
|
||||
|
||||
// Projekt-Abgleich: die Auswahl trägt nur die Projekt-ID (s.
|
||||
// ladePlanCheckerProjekte() oben) - Nummer/Bezeichnung werden hier erneut
|
||||
// aus currentWeek().projects nachgeschlagen, statt sie schon beim Aufbau
|
||||
// der Auswahl im value zu kodieren.
|
||||
const projektEl = document.getElementById('planCheckerProjekt');
|
||||
const projektId = projektEl ? projektEl.value : '';
|
||||
const w = typeof currentWeek === 'function' ? currentWeek() : null;
|
||||
const projekt = projektId && w ? (w.projects || []).find(p => p.id === projektId) : null;
|
||||
|
||||
const url = baseUrl() + '/plan-checker-pruefen'
|
||||
+ '?provider=' + encodeURIComponent(provider)
|
||||
+ '&zusatzMerkmal=' + encodeURIComponent(zusatz);
|
||||
+ '&zusatzMerkmal=' + encodeURIComponent(zusatz)
|
||||
+ '&projektNr=' + encodeURIComponent(projekt ? (projekt.nr || '') : '')
|
||||
+ '&projektName=' + encodeURIComponent(projekt ? projekt.name : '');
|
||||
|
||||
const datei = planCheckerDatei;
|
||||
planCheckerLadezustand(true);
|
||||
|
||||
@@ -726,7 +726,7 @@ function wizardAnlegen(){
|
||||
};
|
||||
|
||||
// Zuordnungen: Zustand 1 = Hauptverantwortung (dieselbe Bedeutung wie in
|
||||
// cycleAssign(), js/board/board.js).
|
||||
// planCycleAssign(), js/plan/plan-settings.js).
|
||||
ROLES.forEach(r => {
|
||||
wizard.team[r].forEach(id => { pr.assign[assignKey(id, r)] = 1; });
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user