// ----------------------------- // 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 ? `${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) => `
${e.erfuellt ? '✓' : '✕'}
${esc(e.merkmal)}
${esc(e.begruendung)}
${e.screenshot ? `` : ''}
`).join(''); zielEl.innerHTML = `
${esc(dateiname)} geprüft mit ${esc(anbieterLabel)} · ${esc(zeit)}
${zeilen || 'Keine Merkmale geprüft.'}`; } // planCheckerZeigeScreenshot: füllt #pcShotModal (App.html) mit dem // Screenshot der Ergebniszeile index (aus planCheckerLetzteErgebnisse) und // blendet es ein. Parameter: index (Number). Keine Rückgabe. function planCheckerZeigeScreenshot(index){ const e = planCheckerLetzteErgebnisse[index]; if(!e || !e.screenshot) return; const img = document.getElementById('pcShotModalImg'); const titel = document.getElementById('pcShotModalTitel'); const modal = document.getElementById('pcShotModal'); if(img) img.src = e.screenshot; if(titel) titel.textContent = e.merkmal; if(modal) modal.style.display = 'flex'; } // planCheckerSchliesseScreenshot: blendet #pcShotModal wieder aus. Leert // zusätzlich img.src (statt die Data-URL im DOM zu belassen) - unnötig, das // Bild erneut im Speicher zu halten, solange der Dialog geschlossen ist. // Keine Parameter, keine Rückgabe. function planCheckerSchliesseScreenshot(){ const modal = document.getElementById('pcShotModal'); const img = document.getElementById('pcShotModalImg'); if(modal) modal.style.display = 'none'; if(img) img.src = ''; }