// -----------------------------
// 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 `
`;
})
.catch(() => '');
}
// 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 = `
`;
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 = `${n} 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 '
Keine Aufgaben vorgeschlagen — die ausgewählten Punkte waren rein informativ.
';
}
// 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 `