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

+464
View File
@@ -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">&#9656;</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.');
}
})();