Files
projektuebersicht/public/js/plan/plan-protokoll-todos.js
T
erikandClaude Opus 5.5 e291ca8438 Neues Design „Bauplan“, Dashboard, Notizen-Bereich, Seitenleiste
- Gestaltung „Bauplan“: ruhigere Farben, Schriftskala, Abstände im
  4-px-Raster, Kalenderwochen und Nummern in Mono-Ziffern, Glas beruhigt
- Dashboard „Meine Aufgaben“: Kopf mit Kalenderwoche, Band „Diese Woche“
  (Mo–Fr), je Projekt Meine Rolle / Team / Notizen; offene Termine blau
  hervorgehoben; Monatskalender unverändert
- Neuer Bereich „Notizen“ in der Seitenleiste: Notizen aller eigenen
  Projekte und eigene Notizen ohne Projekt, später einem Projekt zuordenbar
- Seitenleiste neu gegliedert (Projekte, Team, Notizen, Studenten,
  Einstellungen/Admin unten), Admin-Punkte in Personen/Vorgaben/System
- Reiter im Projektkopf wie die Seitenleiste hervorgehoben
- Behoben: „&…“ in Kommentaren wuchs bei jedem Speichern;
  bestehende Kommentare werden beim Start repariert
- Behoben: Lösch-Rückfrage bei Notizen unlesbar bzw. blieb stehen,
  Cursor im Notiztitel verdeckt, Dropdown im Dunkelmodus gekachelt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:07:32 +01:00

465 lines
24 KiB
JavaScript

// -----------------------------
// 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 || (p.erledigt ? 'erledigt' : 'offen')) === '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.');
}
})();