- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
465 lines
24 KiB
JavaScript
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">▸</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.');
|
|
}
|
|
})();
|