/** * Dateiverwaltung Frontend * Zwei getrennte Bereiche: Mail-Abruf und Datei-Sortierung */ // ============ API ============ async function api(endpoint, options = {}) { const response = await fetch(`/api${endpoint}`, { headers: { 'Content-Type': 'application/json', ...options.headers }, ...options }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || 'API Fehler'); } return response.json(); } // ============ Loading Overlay ============ function zeigeLoading(text = 'Wird geladen...') { document.getElementById('loading-text').textContent = text; document.getElementById('loading-overlay').classList.remove('hidden'); } function versteckeLoading() { document.getElementById('loading-overlay').classList.add('hidden'); } // ============ File Browser ============ let browserTargetInput = null; let browserCurrentPath = '/mnt'; function oeffneBrowser(inputId) { browserTargetInput = inputId; const currentValue = document.getElementById(inputId).value; browserCurrentPath = currentValue || '/mnt'; ladeBrowserInhalt(browserCurrentPath); document.getElementById('browser-modal').classList.remove('hidden'); } async function ladeBrowserInhalt(path) { try { const data = await api(`/browse?path=${encodeURIComponent(path)}`); if (data.error) { document.getElementById('browser-list').innerHTML = `
  • ${data.error}
  • `; return; } browserCurrentPath = data.current; // Berechtigungen des aktuellen Ordners anzeigen const permStr = getPermissionString(data.readable, data.writable); document.getElementById('browser-current-path').innerHTML = `${escapeHtml(data.current)} ${permStr}`; let html = ''; // Parent directory if (data.parent) { html += `
  • 📁 ..
  • `; } // Directories mit Berechtigungsanzeige for (const entry of data.entries) { const entryPermStr = getPermissionString(entry.readable, entry.writable); const permClass = !entry.readable ? 'perm-no-read' : (!entry.writable ? 'perm-no-write' : 'perm-ok'); html += `
  • 📁 ${escapeHtml(entry.name)} ${entryPermStr}
  • `; } if (data.entries.length === 0 && !data.parent) { html = '
  • Keine Unterordner
  • '; } document.getElementById('browser-list').innerHTML = html; } catch (error) { document.getElementById('browser-list').innerHTML = `
  • Fehler: ${error.message}
  • `; } } function getPermissionString(readable, writable) { if (readable && writable) return '✓ RW'; if (readable && !writable) return '⚠ R'; if (!readable) return '✗ ---'; return '?'; } function browserSelect(element, path) { document.querySelectorAll('.file-browser-item.selected').forEach(el => el.classList.remove('selected')); element.classList.add('selected'); browserCurrentPath = path; } function browserAuswahl() { if (browserTargetInput && browserCurrentPath) { document.getElementById(browserTargetInput).value = browserCurrentPath + '/'; } schliesseModal('browser-modal'); } // ============ Checkbox Helpers ============ function getCheckedTypes(groupId) { const checkboxes = document.querySelectorAll(`#${groupId} input[type="checkbox"]:checked`); return Array.from(checkboxes).map(cb => cb.value); } function setCheckedTypes(groupId, types) { const checkboxes = document.querySelectorAll(`#${groupId} input[type="checkbox"]`); checkboxes.forEach(cb => { cb.checked = types.includes(cb.value); }); } // ============ Init ============ document.addEventListener('DOMContentLoaded', () => { ladeTheme(); // Theme zuerst laden ladePostfaecher(); ladeOrdner(); ladeRegeln(); ladeTypRegeln(); // Typ-Regeln fĂŒr Schnell-Regeln Dropdown laden ladeOcrBackupEinstellungen(); // OCR-Backup Einstellungen laden }); // ============ Theme Management ============ function ladeTheme() { const gespeichertesTheme = localStorage.getItem('theme') || 'auto'; const select = document.getElementById('theme-select'); if (select) { select.value = gespeichertesTheme; } wendeThemeAn(gespeichertesTheme); } function wechsleTheme(theme) { localStorage.setItem('theme', theme); wendeThemeAn(theme); } function wendeThemeAn(theme) { const html = document.documentElement; if (theme === 'auto') { // System-PrĂ€ferenz nutzen html.removeAttribute('data-theme'); } else if (theme === 'dark') { // Original Dark Theme (kein data-theme = default CSS) html.removeAttribute('data-theme'); // Aber System-PrĂ€ferenz ĂŒberschreiben durch explizites Setzen html.setAttribute('data-theme', 'dark'); } else { // Breeze Themes html.setAttribute('data-theme', theme); } } // Original Dark Theme explizit definieren // (wird verwendet wenn "dark" gewĂ€hlt ist, auch bei Light System-PrĂ€ferenz) // ============ BEREICH 1: Mail-Abruf ============ async function ladePostfaecher() { try { const postfaecher = await api('/postfaecher'); renderPostfaecher(postfaecher); } catch (error) { console.error('Fehler:', error); } } let bearbeitetesPostfachId = null; // Aktive Abrufe tracken let aktiveAbrufe = {}; function renderPostfaecher(postfaecher) { const container = document.getElementById('postfaecher-liste'); if (!postfaecher || postfaecher.length === 0) { container.innerHTML = '

    Keine PostfÀcher konfiguriert

    '; return; } container.innerHTML = postfaecher.map(p => { const istAktiv = aktiveAbrufe[p.id]; return `

    ${escapeHtml(p.name)} ${istAktiv ? 'LĂ€uft...' : ''}

    ${escapeHtml(p.email)} → ${escapeHtml(p.ziel_ordner)}
    ${istAktiv ? `` : `` }
    `}).join(''); } function zeigePostfachModal(postfach = null) { bearbeitetesPostfachId = postfach?.id || null; document.getElementById('pf-name').value = postfach?.name || ''; document.getElementById('pf-server').value = postfach?.imap_server || ''; document.getElementById('pf-port').value = postfach?.imap_port || '993'; document.getElementById('pf-email').value = postfach?.email || ''; document.getElementById('pf-passwort').value = ''; // Passwort nicht vorausfĂŒllen document.getElementById('pf-ordner').value = postfach?.ordner || 'INBOX'; document.getElementById('pf-alle-ordner').value = postfach?.alle_ordner ? 'true' : 'false'; // Datum formatieren fĂŒr date input (YYYY-MM-DD) if (postfach?.ab_datum) { const d = new Date(postfach.ab_datum); document.getElementById('pf-ab-datum').value = d.toISOString().split('T')[0]; } else { document.getElementById('pf-ab-datum').value = ''; } document.getElementById('pf-ziel').value = postfach?.ziel_ordner || '/srv/http/dateiverwaltung/data/inbox/'; setCheckedTypes('pf-typen-gruppe', postfach?.erlaubte_typen || ['.pdf']); document.getElementById('pf-max-groesse').value = postfach?.max_groesse_mb || '25'; document.getElementById('postfach-modal').classList.remove('hidden'); } async function postfachBearbeiten(id) { try { const postfaecher = await api('/postfaecher'); const postfach = postfaecher.find(p => p.id === id); if (postfach) { zeigePostfachModal(postfach); } } catch (error) { alert('Fehler: ' + error.message); } } async function speicherePostfach() { const erlaubteTypen = getCheckedTypes('pf-typen-gruppe'); if (erlaubteTypen.length === 0) { alert('Bitte mindestens einen Dateityp auswĂ€hlen'); return; } // Datum konvertieren const abDatumValue = document.getElementById('pf-ab-datum').value; let abDatum = null; if (abDatumValue) { abDatum = new Date(abDatumValue).toISOString(); } const data = { name: document.getElementById('pf-name').value.trim(), imap_server: document.getElementById('pf-server').value.trim(), imap_port: parseInt(document.getElementById('pf-port').value), email: document.getElementById('pf-email').value.trim(), passwort: document.getElementById('pf-passwort').value, ordner: document.getElementById('pf-ordner').value.trim(), alle_ordner: document.getElementById('pf-alle-ordner').value === 'true', ab_datum: abDatum, ziel_ordner: document.getElementById('pf-ziel').value.trim(), erlaubte_typen: erlaubteTypen, max_groesse_mb: parseInt(document.getElementById('pf-max-groesse').value) }; if (!data.name || !data.imap_server || !data.email || !data.ziel_ordner) { alert('Bitte alle Pflichtfelder ausfĂŒllen'); return; } // Bei Bearbeitung: Passwort nur senden wenn eingegeben if (bearbeitetesPostfachId && !data.passwort) { delete data.passwort; } else if (!data.passwort) { alert('Passwort ist erforderlich'); return; } try { if (bearbeitetesPostfachId) { await api(`/postfaecher/${bearbeitetesPostfachId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await api('/postfaecher', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('postfach-modal'); ladePostfaecher(); } catch (error) { alert('Fehler: ' + error.message); } } async function postfachTesten(id) { try { const result = await api(`/postfaecher/${id}/test`, { method: 'POST' }); alert(result.erfolg ? 'Verbindung erfolgreich!' : 'Fehler: ' + result.nachricht); } catch (error) { alert('Fehler: ' + error.message); } } async function postfachAbrufen(id) { const logContainer = document.getElementById('abruf-log'); logContainer.innerHTML = '
    Verbinde...
    '; // Als aktiv markieren aktiveAbrufe[id] = true; ladePostfaecher(); // EventSource fĂŒr Server-Sent Events const eventSource = new EventSource(`/api/postfaecher/${id}/abrufen/stream`); aktiveAbrufe[id] = eventSource; // EventSource speichern fĂŒr Stopp let dateiCount = 0; let currentOrdner = ''; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'start': logContainer.innerHTML = `
    Starte Abruf: ${escapeHtml(data.postfach)} ${data.bereits_verarbeitet} bereits verarbeitet
    `; break; case 'info': logContainer.innerHTML += `
    ${escapeHtml(data.nachricht)}
    `; break; case 'ordner': currentOrdner = data.name; logContainer.innerHTML += `
    📁 ${escapeHtml(data.name)}
    `; break; case 'mails': const ordnerStatus = document.getElementById('ordner-status'); if (ordnerStatus) { ordnerStatus.innerHTML = `📁 ${escapeHtml(data.ordner)}: ${data.anzahl} Mails`; ordnerStatus.id = ''; // ID entfernen fĂŒr nĂ€chsten Ordner } break; case 'datei': dateiCount++; logContainer.innerHTML += `
    ✓ ${escapeHtml(data.original_name)} ${formatBytes(data.groesse)}
    `; // Scroll nach unten logContainer.scrollTop = logContainer.scrollHeight; break; case 'skip': logContainer.innerHTML += `
    ⊘ ${escapeHtml(data.datei)}: ${data.grund}
    `; break; case 'fehler': logContainer.innerHTML += `
    ✗ ${escapeHtml(data.nachricht)}
    `; break; case 'abgebrochen': logContainer.innerHTML += `
    ⚠ ${escapeHtml(data.nachricht)}
    `; break; case 'fertig': const msg = data.abgebrochen ? `⚠ Abgebrochen: ${data.anzahl} Dateien gespeichert` : `✓ Fertig: ${data.anzahl} Dateien gespeichert`; logContainer.innerHTML += `
    ${msg}
    `; eventSource.close(); delete aktiveAbrufe[id]; ladePostfaecher(); break; } }; eventSource.onerror = (error) => { logContainer.innerHTML += `
    ✗ Verbindung unterbrochen
    `; eventSource.close(); delete aktiveAbrufe[id]; ladePostfaecher(); }; } async function postfachAbrufStoppen(id) { try { const result = await api(`/postfaecher/${id}/abrufen/stoppen`, { method: 'POST' }); if (result.erfolg) { const logContainer = document.getElementById('abruf-log'); logContainer.innerHTML += `
    ⚠ Stopp angefordert...
    `; } } catch (error) { alert('Fehler: ' + error.message); } } function formatBytes(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } async function allePostfaecherAbrufen() { try { zeigeLoading('Rufe alle PostfÀcher ab...'); const result = await api('/postfaecher/abrufen-alle', { method: 'POST' }); zeigeAbrufLog(result); ladePostfaecher(); } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } } async function postfachLoeschen(id) { if (!confirm('Postfach wirklich löschen?')) return; try { await api(`/postfaecher/${id}`, { method: 'DELETE' }); ladePostfaecher(); } catch (error) { alert('Fehler: ' + error.message); } } function zeigeAbrufLog(result) { const container = document.getElementById('abruf-log'); if (!result.ergebnisse || result.ergebnisse.length === 0) { container.innerHTML = '

    Keine neuen Attachments gefunden

    '; return; } let html = ''; for (const r of result.ergebnisse) { const status = r.fehler ? 'error' : 'success'; const icon = r.fehler ? '✗' : '✓'; html += `
    ${icon} ${escapeHtml(r.postfach)}: ${r.anzahl || 0} Dateien ${r.fehler ? `${escapeHtml(r.fehler)}` : ''}
    `; if (r.dateien) { for (const d of r.dateien) { html += `
    → ${escapeHtml(d)}
    `; } } } container.innerHTML = html; } // ============ BEREICH 2: Datei-Sortierung ============ async function ladeOrdner() { try { const ordner = await api('/ordner'); renderOrdner(ordner); } catch (error) { console.error('Fehler:', error); } } function renderOrdner(ordner) { const container = document.getElementById('ordner-liste'); if (!ordner || ordner.length === 0) { container.innerHTML = '

    Keine Ordner konfiguriert

    '; return; } container.innerHTML = ordner.map(o => `

    ${escapeHtml(o.name)} ${o.rekursiv ? 'rekursiv' : ''}

    ${escapeHtml(o.pfad)} → ${escapeHtml(o.ziel_ordner)} ${(o.dateitypen || []).join(', ')}
    `).join(''); } function zeigeOrdnerModal() { document.getElementById('ord-name').value = ''; document.getElementById('ord-pfad').value = '/srv/http/dateiverwaltung/data/inbox/'; document.getElementById('ord-ziel').value = '/srv/http/dateiverwaltung/data/archiv/'; setCheckedTypes('ord-typen-gruppe', ['.pdf', '.jpg', '.jpeg', '.png', '.tiff']); document.getElementById('ord-rekursiv').value = 'true'; document.getElementById('ordner-modal').classList.remove('hidden'); } async function speichereOrdner() { const dateitypen = getCheckedTypes('ord-typen-gruppe'); if (dateitypen.length === 0) { alert('Bitte mindestens einen Dateityp auswĂ€hlen'); return; } const data = { name: document.getElementById('ord-name').value.trim(), pfad: document.getElementById('ord-pfad').value.trim(), ziel_ordner: document.getElementById('ord-ziel').value.trim(), rekursiv: document.getElementById('ord-rekursiv').value === 'true', dateitypen: dateitypen }; if (!data.name || !data.pfad || !data.ziel_ordner) { alert('Bitte alle Felder ausfĂŒllen'); return; } try { zeigeLoading('Speichere Ordner...'); await api('/ordner', { method: 'POST', body: JSON.stringify(data) }); schliesseModal('ordner-modal'); ladeOrdner(); } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } } async function ordnerLoeschen(id) { if (!confirm('Ordner wirklich löschen?')) return; try { await api(`/ordner/${id}`, { method: 'DELETE' }); ladeOrdner(); } catch (error) { alert('Fehler: ' + error.message); } } async function ordnerScannen(id) { try { const result = await api(`/ordner/${id}/scannen`); alert(`${result.anzahl} Dateien im Ordner gefunden`); } catch (error) { alert('Fehler: ' + error.message); } } // ============ Regeln ============ let editierteRegelId = null; // VerfĂŒgbare Typ-Regeln (vom Server geladen) let verfuegbareTypRegeln = []; async function ladeTypRegeln() { try { verfuegbareTypRegeln = await api('/typ-regeln'); } catch (error) { console.error('Fehler beim Laden der Typ-Regeln:', error); } } async function ladeRegeln() { try { const regeln = await api('/regeln'); renderRegeln(regeln); } catch (error) { console.error('Fehler:', error); } } function renderRegeln(regeln) { const schnellContainer = document.getElementById('schnell-regeln-liste'); const feinContainer = document.getElementById('regeln-liste'); // Regeln aufteilen: Typ-basierte (Schnell) vs. Inhalt-basierte (Fein) const schnellRegeln = []; const feinRegeln = []; for (const r of (regeln || [])) { const muster = r.muster || {}; // Schnell-Regel wenn sie Typ-basierte Muster hat (ohne Keywords/Text-Match) const istTypRegel = ( ('ist_zugferd' in muster || 'ist_signiert' in muster || 'ist_bild' in muster || 'ist_pdf' in muster || 'hat_text' in muster || 'dateityp_ist' in muster) && !muster.keywords && !muster.text_match && !muster.text_match_any && !muster.text_regex ); if (istTypRegel) { schnellRegeln.push(r); } else { feinRegeln.push(r); } } // Schnell-Regeln rendern (sortiert nach PrioritĂ€t) schnellRegeln.sort((a, b) => a.prioritaet - b.prioritaet); if (schnellRegeln.length === 0) { schnellContainer.innerHTML = '

    Keine Schnell-Regeln definiert

    '; } else { schnellContainer.innerHTML = schnellRegeln.map((r, index) => { const typBadge = getTypRegelBadge(r.muster); const istFallback = r.prioritaet >= 900; const fallbackClass = istFallback ? 'fallback-regel' : ''; return `

    ${escapeHtml(r.name)} Prio ${r.prioritaet} ${typBadge}

    → ${escapeHtml(r.unterordner || 'Zielordner')}
    `}).join(''); } // Fein-Regeln rendern (sortiert nach PrioritÀt) feinRegeln.sort((a, b) => a.prioritaet - b.prioritaet); if (feinRegeln.length === 0) { feinContainer.innerHTML = '

    Keine Fein-Regeln definiert

    '; } else { feinContainer.innerHTML = feinRegeln.map((r, index) => `

    ${escapeHtml(r.name)} Prio ${r.prioritaet}

    ${escapeHtml(r.schema)}
    `).join(''); } } function getTypRegelBadge(muster) { const badges = []; if (muster.ist_zugferd) badges.push('ZUGFeRD'); if (muster.ist_signiert) badges.push('Signiert'); if (muster.ist_bild) badges.push('Bild'); if (muster.ist_pdf) badges.push('PDF'); if (muster.hat_text === false) badges.push('Ohne Text'); return badges.join(' '); } // ============ Schnell-Regeln UI ============ function zeigeSchnellRegelModal() { // Dropdown befĂŒllen const select = document.getElementById('schnell-regel-typ'); select.innerHTML = ''; for (const typ of verfuegbareTypRegeln) { select.innerHTML += ``; } // Details verstecken document.getElementById('schnell-regel-details').classList.add('hidden'); document.getElementById('schnell-regel-speichern-btn').disabled = true; document.getElementById('schnell-regel-modal').classList.remove('hidden'); } function schnellRegelTypGeaendert() { const typId = document.getElementById('schnell-regel-typ').value; const detailsDiv = document.getElementById('schnell-regel-details'); const speichernBtn = document.getElementById('schnell-regel-speichern-btn'); if (!typId) { detailsDiv.classList.add('hidden'); speichernBtn.disabled = true; return; } const typ = verfuegbareTypRegeln.find(t => t.id === typId); if (!typ) return; // Details anzeigen document.getElementById('schnell-regel-name').textContent = typ.name; document.getElementById('schnell-regel-beschreibung').textContent = typ.beschreibung; document.getElementById('schnell-regel-muster').textContent = JSON.stringify(typ.muster); document.getElementById('schnell-regel-unterordner').value = typ.unterordner || ''; document.getElementById('schnell-regel-prioritaet').value = typ.prioritaet || 10; detailsDiv.classList.remove('hidden'); speichernBtn.disabled = false; } async function speichereSchnellRegel() { const typId = document.getElementById('schnell-regel-typ').value; const unterordner = document.getElementById('schnell-regel-unterordner').value.trim(); const prioritaet = parseInt(document.getElementById('schnell-regel-prioritaet').value) || 10; if (!typId) { alert('Bitte Regel-Typ auswĂ€hlen'); return; } try { zeigeLoading('Erstelle Schnell-Regel...'); await api('/regeln/typ', { method: 'POST', body: JSON.stringify({ typ_id: typId, unterordner: unterordner || null, prioritaet: prioritaet }) }); schliesseModal('schnell-regel-modal'); ladeRegeln(); } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } } // ============ PrioritĂ€ts-Verwaltung ============ async function regelPrioritaetAendern(regelId, delta) { try { // Aktuelle Regeln holen um PrioritĂ€t zu finden const regeln = await api('/regeln'); const regel = regeln.find(r => r.id === regelId); if (!regel) return; const neuePrio = Math.max(1, regel.prioritaet + delta); await api(`/regeln/${regelId}/prioritaet`, { method: 'PUT', body: JSON.stringify({ prioritaet: neuePrio }) }); ladeRegeln(); } catch (error) { alert('Fehler: ' + error.message); } } // ============ Datei-Browser fĂŒr Regel-Unterordner ============ let regelBrowserQuellOrdner = null; async function oeffneBrowserFuerRegel() { // Wenn Quell-Ordner konfiguriert sind, deren Ziel-Ordner als Basis nehmen try { const ordner = await api('/ordner'); if (ordner.length > 0) { // Ersten konfigurierten Ziel-Ordner als Startpunkt browserCurrentPath = ordner[0].ziel_ordner || '/mnt'; regelBrowserQuellOrdner = ordner[0]; } else { browserCurrentPath = '/mnt'; } browserTargetInput = 'regel-unterordner'; ladeBrowserInhalt(browserCurrentPath); document.getElementById('browser-modal').classList.remove('hidden'); } catch (error) { browserCurrentPath = '/mnt'; browserTargetInput = 'regel-unterordner'; ladeBrowserInhalt(browserCurrentPath); document.getElementById('browser-modal').classList.remove('hidden'); } } function zeigeRegelModal(regel = null) { editierteRegelId = regel?.id || null; document.getElementById('regel-modal-title').textContent = regel ? 'Regel bearbeiten' : 'Regel hinzufĂŒgen'; document.getElementById('regel-name').value = regel?.name || ''; document.getElementById('regel-prioritaet').value = regel?.prioritaet || 100; document.getElementById('regel-muster').value = JSON.stringify(regel?.muster || {"text_match_any": [], "text_match": []}, null, 2); document.getElementById('regel-extraktion').value = JSON.stringify(regel?.extraktion || {}, null, 2); document.getElementById('regel-schema').value = regel?.schema || '{datum} - Dokument.pdf'; document.getElementById('regel-unterordner').value = regel?.unterordner || ''; document.getElementById('regel-test-text').value = ''; document.getElementById('regel-test-ergebnis').classList.add('hidden'); document.getElementById('regel-modal').classList.remove('hidden'); } async function bearbeiteRegel(id) { try { const regeln = await api('/regeln'); const regel = regeln.find(r => r.id === id); if (regel) zeigeRegelModal(regel); } catch (error) { alert('Fehler: ' + error.message); } } async function speichereRegel() { let muster, extraktion; try { muster = JSON.parse(document.getElementById('regel-muster').value); } catch (e) { alert('UngĂŒltiges JSON im Muster-Feld'); return; } try { extraktion = JSON.parse(document.getElementById('regel-extraktion').value); } catch (e) { alert('UngĂŒltiges JSON im Extraktion-Feld'); return; } const data = { name: document.getElementById('regel-name').value.trim(), prioritaet: parseInt(document.getElementById('regel-prioritaet').value), muster, extraktion, schema: document.getElementById('regel-schema').value.trim(), unterordner: document.getElementById('regel-unterordner').value.trim() || null }; if (!data.name) { alert('Bitte einen Namen eingeben'); return; } try { if (editierteRegelId) { await api(`/regeln/${editierteRegelId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await api('/regeln', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('regel-modal'); ladeRegeln(); } catch (error) { alert('Fehler: ' + error.message); } } async function regelLoeschen(id) { if (!confirm('Regel wirklich löschen?')) return; try { await api(`/regeln/${id}`, { method: 'DELETE' }); ladeRegeln(); } catch (error) { alert('Fehler: ' + error.message); } } async function testeRegel() { const text = document.getElementById('regel-test-text').value; if (!text) { alert('Bitte Testtext eingeben'); return; } let muster, extraktion; try { muster = JSON.parse(document.getElementById('regel-muster').value); extraktion = JSON.parse(document.getElementById('regel-extraktion').value); } catch (e) { alert('UngĂŒltiges JSON'); return; } const regel = { name: 'Test', muster, extraktion, schema: document.getElementById('regel-schema').value.trim() }; try { const result = await api('/regeln/test', { method: 'POST', body: JSON.stringify({ regel, text }) }); const container = document.getElementById('regel-test-ergebnis'); container.classList.remove('hidden', 'success', 'error'); if (result.passt) { container.classList.add('success'); container.textContent = `✓ Regel passt!\n\nExtrahiert:\n${JSON.stringify(result.extrahiert, null, 2)}\n\nDateiname:\n${result.dateiname}`; } else { container.classList.add('error'); container.textContent = '✗ Regel passt nicht'; } } catch (error) { alert('Fehler: ' + error.message); } } // ============ Sortierung starten ============ let sortierungAktiv = false; let sortierungEventSource = null; // ============ OCR-Backup Einstellungen ============ function toggleOcrBackup() { const checkbox = document.getElementById('ocr-backup-aktiv'); const ordnerGruppe = document.getElementById('ocr-backup-ordner-gruppe'); if (checkbox.checked) { ordnerGruppe.classList.remove('hidden'); } else { ordnerGruppe.classList.add('hidden'); } // Einstellung im localStorage speichern localStorage.setItem('ocr-backup-aktiv', checkbox.checked); localStorage.setItem('ocr-backup-ordner', document.getElementById('ocr-backup-ordner').value); } function ladeOcrBackupEinstellungen() { const aktiv = localStorage.getItem('ocr-backup-aktiv') === 'true'; const ordner = localStorage.getItem('ocr-backup-ordner') || ''; const checkbox = document.getElementById('ocr-backup-aktiv'); const input = document.getElementById('ocr-backup-ordner'); const gruppe = document.getElementById('ocr-backup-ordner-gruppe'); if (checkbox) { checkbox.checked = aktiv; if (aktiv) { gruppe.classList.remove('hidden'); } } if (input) { input.value = ordner; // Event-Listener fĂŒr Änderungen input.addEventListener('change', () => { localStorage.setItem('ocr-backup-ordner', input.value); }); } } function oeffneBrowserFuerOcrBackup() { browserCurrentPath = '/mnt'; browserTargetInput = 'ocr-backup-ordner'; ladeBrowserInhalt(browserCurrentPath); document.getElementById('browser-modal').classList.remove('hidden'); } async function sortierungStarten(testmodus = false) { const logContainer = document.getElementById('sortierung-log'); const modeText = testmodus ? 'Starte Testlauf (keine Änderungen)...' : 'Starte Sortierung...'; logContainer.innerHTML = `
    ${modeText}
    `; // Button aktualisieren sortierungAktiv = true; aktualisiereSortierungsButtons(); // URL mit optionalem Testmodus und Backup-Ordner bauen let url = '/api/sortierung/stream?'; const params = []; if (testmodus) params.push('testmodus=true'); // OCR-Backup-Ordner hinzufĂŒgen wenn aktiviert const backupAktiv = document.getElementById('ocr-backup-aktiv')?.checked; const backupOrdner = document.getElementById('ocr-backup-ordner')?.value?.trim(); if (backupAktiv && backupOrdner) { params.push(`ocr_backup_ordner=${encodeURIComponent(backupOrdner)}`); } url += params.join('&'); sortierungEventSource = new EventSource(url); let stats = { gesamt: 0, sortiert: 0, zugferd: 0, fehler: 0 }; sortierungEventSource.onmessage = (event) => { const data = JSON.parse(event.data); const testBadge = data.testmodus ? 'TEST ' : ''; switch (data.type) { case 'start': const modeInfo = data.testmodus ? '🔍 TESTMODUS - Dateien werden nur analysiert, nicht verschoben!' : 'Sortierung gestartet'; logContainer.innerHTML = `
    ${modeInfo}: ${data.ordner} Ordner, ${data.regeln} Regeln
    `; break; case 'ordner': logContainer.innerHTML += `
    📁 ${escapeHtml(data.name)} ${escapeHtml(data.pfad)}
    `; break; case 'dateien_gefunden': logContainer.innerHTML += `
    ${data.anzahl} Dateien gefunden
    `; break; case 'datei': stats.gesamt++; let icon, statusClass, statusText; if (data.status === 'sortiert') { stats.sortiert++; icon = data.testmodus ? '→' : '✓'; statusClass = data.testmodus ? 'info' : 'success'; statusText = data.testmodus ? 'wĂŒrde sortiert' : 'sortiert'; } else if (data.status === 'zugferd') { stats.zugferd++; icon = 'đŸ§Ÿ'; statusClass = 'info'; statusText = data.testmodus ? 'ZUGFeRD erkannt' : 'ZUGFeRD'; } else if (data.status === 'keine_regel') { stats.fehler++; icon = '?'; statusClass = ''; statusText = 'keine Regel'; } else { stats.fehler++; icon = '✗'; statusClass = 'error'; statusText = 'Fehler'; } // Zielordner im Testmodus anzeigen const zielInfo = data.testmodus && data.ziel_ordner ? `→ ${escapeHtml(data.ziel_ordner)}` : ''; logContainer.innerHTML += `
    ${testBadge}${icon} ${escapeHtml(data.neuer_name || data.original)} ${data.regel ? `Regel: ${escapeHtml(data.regel)}` : ''} ${data.fehler ? `${escapeHtml(data.fehler)}` : ''} ${zielInfo}
    `; logContainer.scrollTop = logContainer.scrollHeight; break; case 'warnung': logContainer.innerHTML += `
    ⚠ ${escapeHtml(data.nachricht)}
    `; break; case 'abgebrochen': logContainer.innerHTML += `
    ⚠ Sortierung abgebrochen
    `; break; case 'fertig': const fertigText = data.testmodus ? `🔍 Testlauf fertig: ${data.sortiert} wĂŒrden sortiert, ${data.zugferd} ZUGFeRD, ${data.fehler} ohne Regel` : `✓ Fertig: ${data.sortiert} sortiert, ${data.zugferd} ZUGFeRD, ${data.fehler} ohne Regel`; const fertigHint = data.testmodus ? '
    Keine Dateien wurden verschoben. Starte echte Sortierung wenn zufrieden.
    ' : ''; logContainer.innerHTML += `
    ${fertigText}
    ${fertigHint}`; sortierungEventSource.close(); sortierungAktiv = false; sortierungEventSource = null; aktualisiereSortierungsButtons(); break; } }; sortierungEventSource.onerror = (error) => { logContainer.innerHTML += `
    ✗ Verbindung unterbrochen
    `; sortierungEventSource.close(); sortierungAktiv = false; sortierungEventSource = null; aktualisiereSortierungsButtons(); }; } async function sortierungStoppen() { try { const result = await api('/sortierung/stoppen', { method: 'POST' }); if (result.erfolg) { const logContainer = document.getElementById('sortierung-log'); logContainer.innerHTML += `
    ⚠ Stopp angefordert...
    `; } } catch (error) { alert('Fehler: ' + error.message); } } function aktualisiereSortierungsButtons() { const startBtn = document.getElementById('sortierung-start-btn'); const testBtn = document.getElementById('sortierung-test-btn'); const stoppBtn = document.getElementById('sortierung-stopp-btn'); if (startBtn && stoppBtn) { if (sortierungAktiv) { startBtn.classList.add('hidden'); if (testBtn) testBtn.classList.add('hidden'); stoppBtn.classList.remove('hidden'); } else { startBtn.classList.remove('hidden'); if (testBtn) testBtn.classList.remove('hidden'); stoppBtn.classList.add('hidden'); } } } function zeigeSortierungLog(result) { const container = document.getElementById('sortierung-log'); if (!result.verarbeitet || result.verarbeitet.length === 0) { container.innerHTML = '

    Keine Dateien verarbeitet

    '; return; } let html = `
    Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Fehler: ${result.fehler}
    `; for (const d of result.verarbeitet) { const status = d.fehler ? 'error' : (d.zugferd ? 'info' : 'success'); const icon = d.fehler ? '✗' : (d.zugferd ? 'đŸ§Ÿ' : '✓'); html += `
    ${icon} ${escapeHtml(d.neuer_name || d.original)} ${d.fehler ? `${escapeHtml(d.fehler)}` : ''}
    `; } container.innerHTML = html; } // ============ Utilities ============ function schliesseModal(id) { document.getElementById(id).classList.add('hidden'); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } document.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { e.target.classList.add('hidden'); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal:not(.hidden)').forEach(m => m.classList.add('hidden')); } }); // ============ Datenbank-Management ============ async function dbZuruecksetzen() { if (!confirm('Datenbank wirklich zurĂŒcksetzen?\n\nDies löscht alle EintrĂ€ge ĂŒber verarbeitete Mails und Dateien.\nPostfĂ€cher, Ordner und Regeln bleiben erhalten.\n\nBeim nĂ€chsten Abruf werden alle Mails erneut verarbeitet!')) { return; } try { zeigeLoading('Setze Datenbank zurĂŒck...'); const result = await api('/db/reset', { method: 'POST' }); if (result.erfolg) { alert(`Datenbank zurĂŒckgesetzt!\n\nGelöscht:\n- ${result.geloescht.mails} verarbeitete Mails\n- ${result.geloescht.dateien} verarbeitete Dateien`); } else { alert('Fehler: ' + result.nachricht); } } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } } async function zeigeStatistik() { document.getElementById('statistik-modal').classList.remove('hidden'); document.getElementById('statistik-inhalt').innerHTML = 'Wird geladen...'; try { const stats = await api('/db/statistik'); document.getElementById('statistik-inhalt').innerHTML = `
    PostfÀcher ${stats.postfaecher}
    Quell-Ordner ${stats.quell_ordner}
    Sortier-Regeln ${stats.regeln}
    Verarbeitete Mails ${stats.verarbeitete_mails}
    Verarbeitete Dateien ${stats.verarbeitete_dateien}
    `; } catch (error) { document.getElementById('statistik-inhalt').innerHTML = `

    Fehler: ${error.message}

    `; } } // ============ Regel-Hilfe ============ function zeigeRegelHilfe() { document.getElementById('hilfe-modal').classList.remove('hidden'); document.getElementById('hilfe-text').value = ''; document.getElementById('hilfe-ergebnis').classList.add('hidden'); } // Speichert das letzte Analyse-Ergebnis let letzteAnalyse = null; // Standard-Regex-Muster fĂŒr hĂ€ufige Felder const STANDARD_REGEX = { datum: [ { label: 'DD.MM.YYYY', regex: '(\\d{2}\\.\\d{2}\\.\\d{4})' }, { label: 'Rechnungsdatum: DD.MM.YYYY', regex: 'Rechnungsdatum[:\\s]*(\\d{2}\\.\\d{2}\\.\\d{4})' }, { label: 'Datum: DD.MM.YYYY', regex: 'Datum[:\\s]*(\\d{2}\\.\\d{2}\\.\\d{4})' }, { label: 'YYYY-MM-DD', regex: '(\\d{4}-\\d{2}-\\d{2})' } ], betrag: [ { label: 'Gesamtbetrag: X,XX', regex: 'Gesamtbetrag[:\\s]*([\\d.,]+)' }, { label: 'Summe: X,XX', regex: 'Summe[:\\s]*([\\d.,]+)' }, { label: 'Rechnungsbetrag: X,XX', regex: 'Rechnungsbetrag[:\\s]*([\\d.,]+)' }, { label: 'Total: X,XX', regex: 'Total[:\\s]*([\\d.,]+)' }, { label: 'Brutto: X,XX', regex: 'Brutto[:\\s]*([\\d.,]+)' }, { label: 'Netto: X,XX', regex: 'Netto[:\\s]*([\\d.,]+)' }, { label: 'EUR X,XX (letzter)', regex: 'EUR\\s*([\\d.,]+)(?!.*EUR\\s*[\\d.,]+)' } ], nummer: [ { label: 'Rechnungsnummer: XXX', regex: 'Rechnungsnummer[:\\s]*(\\S+)' }, { label: 'Rechnung Nr. XXX', regex: 'Rechnung\\s*(?:Nr\\.?|Nummer)?[:\\s]*(\\S+)' }, { label: 'Belegnummer: XXX', regex: 'Belegnummer[:\\s]*(\\S+)' }, { label: 'Bestellnummer: XXX', regex: 'Bestellnummer[:\\s]*(\\S+)' }, { label: 'Dokumentnummer: XXX', regex: 'Dokumentnummer[:\\s]*(\\S+)' } ] }; async function analysiereText() { const text = document.getElementById('hilfe-text').value.trim(); if (!text) { alert('Bitte Text eingeben oder Datei hochladen'); return; } try { zeigeLoading('Analysiere Text...'); const result = await api('/extraktion/test', { method: 'POST', body: JSON.stringify({ text: text }) }); letzteAnalyse = result; const container = document.getElementById('hilfe-analyse'); container.innerHTML = `
    Automatisch extrahierte Felder:
    ${JSON.stringify(result.extrahiert, null, 2)}

    Falls Werte falsch sind, passe unten die Regex-Muster an!

    `; // Felder vorausfĂŒllen wenn gefunden document.getElementById('hilfe-firma').value = result.extrahiert.firma || ''; // Standard-Regex vorausfĂŒllen basierend auf erkannten Werten if (result.extrahiert.datum) { // PrĂŒfen welches Muster gepasst haben könnte document.getElementById('hilfe-datum-regex').value = STANDARD_REGEX.datum[0].regex; } else { document.getElementById('hilfe-datum-regex').value = STANDARD_REGEX.datum[0].regex; } if (result.extrahiert.betrag) { document.getElementById('hilfe-betrag-regex').value = STANDARD_REGEX.betrag[0].regex; } else { document.getElementById('hilfe-betrag-regex').value = STANDARD_REGEX.betrag[0].regex; } if (result.extrahiert.nummer) { document.getElementById('hilfe-nummer-regex').value = STANDARD_REGEX.nummer[0].regex; } else { document.getElementById('hilfe-nummer-regex').value = STANDARD_REGEX.nummer[0].regex; } // Keywords aus ersten erkennbaren Wörtern const words = text.split(/\s+/).filter(w => w.length > 4 && /^[a-zA-ZĂ€Ă¶ĂŒĂ„Ă–ĂœĂŸ]+$/.test(w)); document.getElementById('hilfe-keywords').value = words.slice(0, 3).join(', ').toLowerCase(); document.getElementById('hilfe-ergebnis').classList.remove('hidden'); document.getElementById('hilfe-regel-vorschau').classList.add('hidden'); } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } } async function testeMitRegex() { const text = document.getElementById('hilfe-text').value.trim(); if (!text) { alert('Bitte erst Text eingeben'); return; } const firma = document.getElementById('hilfe-firma').value.trim(); const datumRegex = document.getElementById('hilfe-datum-regex').value.trim(); const betragRegex = document.getElementById('hilfe-betrag-regex').value.trim(); const nummerRegex = document.getElementById('hilfe-nummer-regex').value.trim(); try { zeigeLoading('Teste Regex...'); // Custom Regex an Backend senden const result = await api('/extraktion/test-custom', { method: 'POST', body: JSON.stringify({ text: text, firma: firma, datum_regex: datumRegex, betrag_regex: betragRegex, nummer_regex: nummerRegex }) }); letzteAnalyse = result; const container = document.getElementById('hilfe-analyse'); container.innerHTML = `
    Extrahierte Felder (mit deinen Regex):
    ${JSON.stringify(result.extrahiert, null, 2)}
    ${result.fehler ? `

    Fehler: ${escapeHtml(result.fehler)}

    ` : ''} `; } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } } function erstelleRegelAusHilfe() { const firma = document.getElementById('hilfe-firma').value.trim(); const datumRegex = document.getElementById('hilfe-datum-regex').value.trim(); const betragRegex = document.getElementById('hilfe-betrag-regex').value.trim(); const nummerRegex = document.getElementById('hilfe-nummer-regex').value.trim(); const keywords = document.getElementById('hilfe-keywords').value.trim(); if (!keywords) { alert('Bitte mindestens Keywords fĂŒr die Erkennung eingeben'); return; } // Regel-Objekt bauen const regel = { name: firma ? `${firma} Rechnung` : 'Neue Regel', prioritaet: 50, muster: { text_match: keywords.split(',').map(k => k.trim().toLowerCase()).filter(k => k) }, extraktion: {}, schema: '{datum} - Rechnung - {firma} - {nummer} - {betrag} EUR.pdf', unterordner: firma ? firma.toLowerCase().replace(/[^a-z0-9]/g, '_') : null }; // Extraktion hinzufĂŒgen if (firma) { regel.extraktion.firma = { wert: firma }; } if (datumRegex) { regel.extraktion.datum = { regex: datumRegex }; } if (betragRegex) { regel.extraktion.betrag = { regex: betragRegex, typ: 'betrag' }; } if (nummerRegex) { regel.extraktion.nummer = { regex: nummerRegex }; } // Vorschau anzeigen const jsonStr = JSON.stringify(regel, null, 2); document.getElementById('hilfe-regel-json').textContent = jsonStr; document.getElementById('hilfe-regel-vorschau').classList.remove('hidden'); // In Regel-Modal ĂŒbernehmen Button if (confirm('Regel ĂŒbernehmen und im Editor öffnen?')) { // Modal schließen schliesseModal('hilfe-modal'); // Regel-Modal öffnen und befĂŒllen editierteRegelId = null; document.getElementById('regel-modal-title').textContent = 'Regel hinzufĂŒgen'; document.getElementById('regel-name').value = regel.name; document.getElementById('regel-prioritaet').value = regel.prioritaet; document.getElementById('regel-muster').value = JSON.stringify(regel.muster, null, 2); document.getElementById('regel-extraktion').value = JSON.stringify(regel.extraktion, null, 2); document.getElementById('regel-schema').value = regel.schema; document.getElementById('regel-unterordner').value = regel.unterordner || ''; document.getElementById('regel-test-text').value = document.getElementById('hilfe-text').value; document.getElementById('regel-test-ergebnis').classList.add('hidden'); document.getElementById('regel-modal').classList.remove('hidden'); } } async function ladeHilfeDatei(input) { const file = input.files[0]; if (!file) return; if (file.type === 'text/plain') { // TXT Datei direkt lesen const reader = new FileReader(); reader.onload = (e) => { document.getElementById('hilfe-text').value = e.target.result; }; reader.readAsText(file); } else if (file.type === 'application/pdf') { // PDF serverseitig verarbeiten try { zeigeLoading('Extrahiere Text aus PDF...'); const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/extraktion/upload-pdf', { method: 'POST', body: formData }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || 'PDF-Verarbeitung fehlgeschlagen'); } const result = await response.json(); document.getElementById('hilfe-text').value = result.text || ''; if (result.ocr_durchgefuehrt) { alert('Text wurde per OCR extrahiert (Bild-PDF erkannt)'); } // Automatisch analysieren nach Upload versteckeLoading(); await analysiereText(); return; // Loading wird in analysiereText() gehandelt } catch (error) { alert('Fehler beim PDF-Upload: ' + error.message); } finally { versteckeLoading(); } } // Input zurĂŒcksetzen fĂŒr erneuten Upload input.value = ''; } // Regex-Preset aus Dropdown ĂŒbernehmen function setzeRegexPreset(typ) { const select = document.getElementById(`hilfe-${typ}-preset`); const input = document.getElementById(`hilfe-${typ}-regex`); if (select.value) { input.value = select.value; } select.value = ''; // Reset dropdown } // ============ Datei-Browser fĂŒr PDFs aus Quell-Ordnern ============ let pdfBrowserOrdner = []; async function zeigePdfBrowser() { document.getElementById('pdf-browser-modal').classList.remove('hidden'); document.getElementById('pdf-browser-liste').innerHTML = 'Lade Ordner...'; try { // Quell-Ordner laden const ordner = await api('/ordner'); pdfBrowserOrdner = ordner; if (ordner.length === 0) { document.getElementById('pdf-browser-liste').innerHTML = '

    Keine Quell-Ordner konfiguriert

    '; return; } // Ordner-Auswahl anzeigen let html = '
    '; html += ''; html += '
    '; html += '
    '; document.getElementById('pdf-browser-liste').innerHTML = html; } catch (error) { document.getElementById('pdf-browser-liste').innerHTML = `

    Fehler: ${escapeHtml(error.message)}

    `; } } async function ladePdfDateien() { const ordnerId = document.getElementById('pdf-browser-ordner').value; const container = document.getElementById('pdf-dateien-liste'); if (!ordnerId) { container.innerHTML = ''; return; } container.innerHTML = 'Scanne Ordner...'; try { const result = await api(`/ordner/${ordnerId}/scannen`); if (!result.dateien || result.dateien.length === 0) { container.innerHTML = '

    Keine Dateien im Ordner

    '; return; } // Ordner-Daten fĂŒr Pfad-Konstruktion const ordner = pdfBrowserOrdner.find(o => o.id == ordnerId); const basePfad = ordner?.pfad || ''; let html = `

    ${result.anzahl} Dateien gefunden:

    '; container.innerHTML = html; } catch (error) { container.innerHTML = `

    Fehler: ${escapeHtml(error.message)}

    `; } } async function waehleServerPdf(pfad) { try { zeigeLoading('Extrahiere Text aus PDF...'); const response = await api('/extraktion/from-file', { method: 'POST', body: JSON.stringify({ pfad: pfad }) }); document.getElementById('hilfe-text').value = response.text || ''; schliesseModal('pdf-browser-modal'); if (response.ocr_durchgefuehrt) { alert('Text wurde per OCR extrahiert (Bild-PDF erkannt)'); } // Automatisch analysieren nach Auswahl versteckeLoading(); await analysiereText(); return; } catch (error) { alert('Fehler: ' + error.message); } finally { versteckeLoading(); } }