Plan Checker, Wochenbericht-Fokus-Modus und Wandtafel-Dashboard-Feinschliff
Plan Checker (neues Studenten-Werkzeug, Seitenleiste + Admin → Plan-Checker- Merkmale): lädt einen Bauplan als PDF hoch und lässt ChatGPT oder Claude ihn gegen eine gepflegte Merkmalsliste prüfen. PDF-Rendering (pdf.js + @napi-rs/canvas) berücksichtigt jetzt Ebenen (Optional Content Groups) und WASM-Decoder korrekt, sendet eine Übersicht plus mehrere hochaufgelöste Kachel-Ausschnitte statt eines einzelnen, zu groben Bilds, und beide Anbieter bekommen dasselbe Bildmaterial. Jedes Prüfergebnis kann auf eines der gesendeten Bilder als Beleg verweisen (bildIndex) — als Screenshot im Ergebnis anklickbar, öffnet vergrößert in einem Modal statt eines von Chrome blockierten target=_blank-Links auf eine data:-URL. KI-Modellname ist jetzt pro Konto überschreibbar (Einstellungen → Mein Konto) für Drittanbieter mit abweichendem Modell-Katalog. Wochenbericht: dichte Kartenansicht bekommt einen Fokus-Modus — nur der Kommentar liegt offen, Leistungsphasen und Beteiligte stecken in aufklappbaren Abschnitten statt dauerhaft nebeneinander zu stehen. Projektbereich: Terminplan-Auslastung rechnet jetzt exakt wie der Auslastungs-Tab/die Wandtafel (statt einer unabhängigen, tageweisen Schätzung); die LPH-Fortschrittsliste im Dashboard zeigt nur noch die beauftragten Leistungsphasen. Wandtafel: Dashboard-Kopf mit Kennzahlen-Reihe und Panel-Reihe neu strukturiert, in den Blautönen des Firmenlogos; dieselbe Palette wurde anschließend auf den gesamten App-weiten Dark Mode übernommen. Dateibereinigung: nicht mehr benötigte Test-Uploads/Backups entfernt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db63a29365
commit
6ba93b2a92
308 files changed
+64441
-743
No files matched your search
@@ -0,0 +1,261 @@
|
||||
// -----------------------------
|
||||
// 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.
|
||||
//
|
||||
// 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 und die KI-Anbieter-Auswahl neu — beide
|
||||
// können sich seit dem letzten Besuch geändert haben (Admin-Pflege bzw.
|
||||
// eigener Schlüssel unter Einstellungen → Mein Konto). Keine Parameter,
|
||||
// keine Rückgabe.
|
||||
function renderPlanChecker(){
|
||||
ladePlanCheckerMerkmale();
|
||||
ladePlanCheckerAnbieter();
|
||||
}
|
||||
|
||||
// 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 = '<span class="hint" style="margin:0">Lade Merkmale…</span>';
|
||||
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
|
||||
? '<span class="hint" style="margin:0">Noch kein Merkmal hinterlegt (siehe Admin → Plan-Checker-Merkmale).</span>'
|
||||
: planCheckerMerkmale.map(m => `<div class="pc-merkmal-chip">${esc(m.titel)}</div>`).join('');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Plan-Checker-Merkmale laden fehlgeschlagen:', err);
|
||||
if(el) el.innerHTML = '<span class="hint" style="margin:0">Merkmale konnten nicht geladen werden.</span>';
|
||||
});
|
||||
}
|
||||
|
||||
// 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 = `<option value="chatgpt">ChatGPT</option><option value="claude">Claude</option>`;
|
||||
fetch(baseUrl() + '/my-ai-settings')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if(!d.ok) return;
|
||||
sel.innerHTML = `
|
||||
<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(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'].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() : '';
|
||||
|
||||
const url = baseUrl() + '/plan-checker-pruefen'
|
||||
+ '?provider=' + encodeURIComponent(provider)
|
||||
+ '&zusatzMerkmal=' + encodeURIComponent(zusatz);
|
||||
|
||||
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 <a target="_blank"> 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) => `<div class="pc-result-row">
|
||||
<span class="pc-verdict ${e.erfuellt ? 'ok' : 'fail'}">${e.erfuellt ? '✓' : '✕'}</span>
|
||||
<div class="pc-result-text"><div class="titel">${esc(e.merkmal)}</div><div class="begruendung">${esc(e.begruendung)}</div></div>
|
||||
${e.screenshot ? `<button type="button" class="pc-result-shot" onclick="planCheckerZeigeScreenshot(${i})" title="Planausschnitt vergrößern">
|
||||
<img src="${e.screenshot}" alt="Planausschnitt zu ${esc(e.merkmal)}">
|
||||
</button>` : ''}
|
||||
</div>`).join('');
|
||||
|
||||
zielEl.innerHTML = `
|
||||
<div class="pc-result-head">
|
||||
<span class="filename">${esc(dateiname)}</span>
|
||||
<span class="meta">geprüft mit ${esc(anbieterLabel)} · ${esc(zeit)}</span>
|
||||
</div>
|
||||
${zeilen || '<span class="hint">Keine Merkmale geprüft.</span>'}`;
|
||||
}
|
||||
|
||||
// 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 = '';
|
||||
}
|
||||
Reference in new issue
Block a user