// -----------------------------
// Projektübersicht — Plan Checker (Bereich "Studenten")
// -----------------------------
// Eigenständiges Prüf-Werkzeug: lädt einen Bauplan als PDF hoch und lässt
// eine KI (ChatGPT oder Claude, Anbieter wählbar — exakt wie beim
// bestehenden Protokoll-Import, js/plan/plan-protokoll.js) ihn gegen eine
// 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 - 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
// das Ergebnis. Nach einer Korrektur am Plan lässt sich erneut hochladen und
// prüfen, ohne das vorherige Ergebnis aus den Augen zu verlieren.
//
// Server-Gegenstück: POST /plan-checker-pruefen (server.js) — Roh-Binärkörper
// wie beim Protokoll-Import, rendert die erste PDF-Seite serverseitig zu
// mehreren Bildern (Übersicht + hochaufgelöste Kachel-Ausschnitte,
// renderPdfAlsBildsatz()) und schickt die an die KI (Maßketten/Plankopf sind
// optische Merkmale, kein Fließtext). Die KI nennt zu jedem Merkmal
// zusätzlich den Index DESJENIGEN der gesendeten Bilder, auf dem der
// Nachweis am besten zu sehen ist (bildIndex) — genau dieses, bereits
// gerenderte Bild kommt unverändert als "screenshot"-Feld je Ergebnis
// zurück, s. renderPlanCheckerErgebnis() unten. (Ein erster Versuch mit
// einem Beschriftungsraster + nachträglichem Zuschnitt einer genannten
// Rasterzelle erwies sich als nicht gut genug und wurde durch diese
// einfachere Variante ersetzt.) Weder die PDF- noch die Bilddateien werden
// gespeichert — kein Dokumentenarchiv, ein Ad-hoc-Werkzeug.
//
// Läuft wie js/planung/planung.js als klassisches globales Skript (kein
// Modul/IIFE), geladen nach js/store.js/js/weeks.js, vor js/tabs-auth.js
// (das renderPlanChecker() beim Tab-Wechsel aufruft) — siehe App.html.
// planCheckerDatei: die aktuell gewählte PDF-Datei (File-Objekt) oder null.
let planCheckerDatei = null;
// planCheckerMerkmale: die zuletzt geladene Grundliste der Merkmale
// ({id, titel, prompt}), für die Vorschau links und als Grundlage der
// Ergebnisliste rechts (die Reihenfolge der Antwort folgt derselben Liste).
let planCheckerMerkmale = [];
// renderPlanChecker: Einstiegspunkt, von showTab('planchecker') gerufen.
// 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 = '' + gruppenNamen.map(g => {
const opts = projekte.filter(p => (p.group || 'Ohne Gruppe') === g)
.slice().sort((a, b) => a.name.localeCompare(b.name))
.map(p => ``).join('');
return opts ? `` : '';
}).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
// /plan-checker-merkmale, jeder angemeldete Nutzer darf lesen) und zeigt sie
// read-only als Vorschau, damit vor dem Prüfen klar ist, was geprüft wird.
// Keine Parameter, keine Rückgabe.
function ladePlanCheckerMerkmale(){
const el = document.getElementById('planCheckerMerkmaleListe');
if(el) el.innerHTML = 'Lade Merkmale…';
fetch(baseUrl() + '/plan-checker-merkmale')
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
.then(data => {
planCheckerMerkmale = Array.isArray(data.merkmale) ? data.merkmale : [];
if(!el) return;
el.innerHTML = planCheckerMerkmale.length === 0
? 'Noch kein Merkmal hinterlegt (siehe Admin → Plan-Checker-Merkmale).'
: planCheckerMerkmale.map(m => `
${esc(m.titel)}
`).join('');
})
.catch(err => {
console.error('Plan-Checker-Merkmale laden fehlgeschlagen:', err);
if(el) el.innerHTML = 'Merkmale konnten nicht geladen werden.';
});
}
// ladePlanCheckerAnbieter: baut die Anbieter-Auswahl aus GET /my-ai-settings
// auf — derselbe Weg wie beim Protokoll-Import-Dialog
// (oeffneProtokollImportDialog(), js/plan/plan-protokoll.js): der Hinweis
// "(kein Schlüssel hinterlegt)" bezieht sich auf den EIGENEN Schlüssel des
// angemeldeten Kontos. Keine Parameter, keine Rückgabe.
function ladePlanCheckerAnbieter(){
const sel = document.getElementById('planCheckerAnbieter');
if(!sel) return;
sel.innerHTML = ``;
fetch(baseUrl() + '/my-ai-settings')
.then(r => r.json())
.then(d => {
if(!d.ok) return;
sel.innerHTML = `
`;
})
.catch(err => console.error('KI-Einstellungen laden fehlgeschlagen:', err));
}
// ---------- Datei-Auswahl (Klick oder Ziehen-und-Ablegen) ----------
// planCheckerDateiGewaehlt: übernimmt eine gewählte/abgelegte Datei, prüft
// grob die Endung (die eigentliche Formatprüfung — PDF-Magic-Bytes — läuft
// serverseitig) und zeigt den Dateinamen in der Ablagefläche an.
// Parameter: file (File oder undefined). Keine Rückgabe.
function planCheckerDateiGewaehlt(file){
const fehlerEl = document.getElementById('planCheckerError');
if(fehlerEl){ fehlerEl.hidden = true; fehlerEl.textContent = ''; }
if(!file) return;
if(!/\.pdf$/i.test(file.name)){
if(fehlerEl){ fehlerEl.textContent = 'Bitte eine PDF-Datei auswählen.'; fehlerEl.hidden = false; }
planCheckerDatei = null;
return;
}
planCheckerDatei = file;
const nameEl = document.getElementById('planCheckerDateiname');
const textEl = document.getElementById('planCheckerDropText');
if(nameEl){ nameEl.textContent = file.name; nameEl.hidden = false; }
if(textEl) textEl.textContent = 'Andere Datei wählen';
}
function planCheckerDragOver(e){
e.preventDefault();
const drop = document.getElementById('planCheckerDrop');
if(drop) drop.classList.add('is-dragover');
}
function planCheckerDragLeave(e){
const drop = document.getElementById('planCheckerDrop');
if(drop) drop.classList.remove('is-dragover');
}
function planCheckerDrop(e){
e.preventDefault();
const drop = document.getElementById('planCheckerDrop');
if(drop) drop.classList.remove('is-dragover');
const file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
planCheckerDateiGewaehlt(file);
}
// ---------- Prüfen ----------
// planCheckerLadezustand: sperrt Eingaben/Knopf während der KI-Aufruf läuft
// — kann bei einem lokal laufenden Modell mehrere Minuten dauern (siehe
// KI_TIMEOUT_MS_LOKAL, server.js); ohne Rückmeldung wirkte der Bildschirm
// sonst hängengeblieben, und ein zweiter Klick hätte dieselbe Prüfung
// doppelt losgeschickt. Parameter: aktiv (Boolean). Keine Rückgabe.
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', 'planCheckerProjekt'].forEach(id => {
const el = document.getElementById(id);
if(el) el.disabled = aktiv;
});
}
// planCheckerPruefen: schickt die gewählte Datei als rohen Binär-Body an
// POST /plan-checker-pruefen (gleiches Vorgehen wie importiereProtokoll(),
// js/plan/plan-protokoll.js) und zeigt das Ergebnis rechts an. Keine
// Parameter, keine Rückgabe.
function planCheckerPruefen(){
const fehlerEl = document.getElementById('planCheckerError');
const zeigeFehler = msg => { if(fehlerEl){ fehlerEl.textContent = msg; fehlerEl.hidden = false; } };
if(fehlerEl){ fehlerEl.hidden = true; fehlerEl.textContent = ''; }
if(!planCheckerDatei) return zeigeFehler('Bitte zuerst eine PDF-Datei wählen.');
const anbieterEl = document.getElementById('planCheckerAnbieter');
const provider = anbieterEl ? anbieterEl.value : '';
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)
+ '&projektNr=' + encodeURIComponent(projekt ? (projekt.nr || '') : '')
+ '&projektName=' + encodeURIComponent(projekt ? projekt.name : '');
const datei = planCheckerDatei;
planCheckerLadezustand(true);
fetch(url, {
method: 'POST',
headers: { 'Content-Type': datei.type || 'application/pdf' },
body: datei
})
.then(r => r.json().then(d => ({ ok: r.ok, d })))
.then(({ ok, d }) => {
if(!ok || !d.ok) throw new Error((d && d.error) || 'Prüfung fehlgeschlagen.');
renderPlanCheckerErgebnis(datei.name, provider, d.ergebnisse || []);
})
.catch(err => {
console.error('Plan-Prüfung fehlgeschlagen:', err);
zeigeFehler(err.message || String(err));
})
.finally(() => planCheckerLadezustand(false));
}
// planCheckerLetzteErgebnisse: die zuletzt gerenderten Ergebniszeilen,
// gemerkt für planCheckerZeigeScreenshot() unten — ein Klick auf eine Zeile
// übergibt nur den Index, nicht die (potenziell sehr lange) Data-URL selbst
// als Inline-onclick-Parameter.
let planCheckerLetzteErgebnisse = [];
// renderPlanCheckerErgebnis: zeigt das Prüfergebnis in der rechten Spalte —
// Dateiname/Anbieter/Zeitpunkt als Kopfzeile, danach je Merkmal eine Zeile
// mit grünem/rotem Kreis, der Begründung der KI und (falls vorhanden) einem
// Screenshot — demjenigen der an die KI gesendeten Bilder (Übersicht oder
// eine Kachel), das die KI über bildIndex als besten Beleg für dieses
// Merkmal benannt hat (server.js, pruefePlanMitKI()). Parameter: dateiname
// (String), provider ('chatgpt'|'claude'), ergebnisse (Array {merkmal,
// erfuellt, begruendung, screenshot}, screenshot eine Data-URL oder null).
// Keine Rückgabe.
function renderPlanCheckerErgebnis(dateiname, provider, ergebnisse){
const leerEl = document.getElementById('planCheckerLeer');
const zielEl = document.getElementById('planCheckerErgebnis');
if(!zielEl) return;
if(leerEl) leerEl.hidden = true;
planCheckerLetzteErgebnisse = ergebnisse;
const anbieterLabel = provider === 'claude' ? 'Claude' : 'ChatGPT';
const zeit = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
// Der Screenshot öffnet NICHT als auf die data:-URL —
// Chrome blockiert das Öffnen einer data:-URL als Top-Level-Navigation in
// einem neuen Tab, der Klick blieb dadurch wirkungslos (Eriks Rückmeldung:
// "wenn ich auf das Bild im Protokoll öffne, wird es nicht in groß
// angezeigt"). Stattdessen ein Knopf, der #pcShotModal füllt und einblendet
// (planCheckerZeigeScreenshot(), App.html) — bleibt auf derselben Seite,
// keine Navigation nötig.
const zeilen = ergebnisse.map((e, i) => `