docker.dateiverwaltung/frontend/static/js/app.js
data 21e1ffe9e2 Version 1.1: Dateimanager mit 3-Panel Layout
Neue Features:
- 3-Panel Dateimanager (Ordnerbaum, Dateiliste, Vorschau)
- Separates Vorschau-Fenster für zweiten Monitor
- Resize-Handles für flexible Panel-Größen (horizontal & vertikal)
- Vorschau-Panel ausblendbar wenn externes Fenster aktiv
- Natürliche Sortierung (Sonderzeichen → Zahlen → Buchstaben)
- PDF-Vorschau mit Fit-to-Page
- Email-Attachment Abruf erweitert

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:51:40 +01:00

1621 lines
58 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 =
`<li class="file-browser-item" style="color: var(--danger);">${data.error}</li>`;
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)} <span class="perm-badge">${permStr}</span>`;
let html = '';
// Parent directory
if (data.parent) {
html += `<li class="file-browser-item" onclick="ladeBrowserInhalt('${data.parent}')">
<span class="file-icon">📁</span> ..
</li>`;
}
// 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 += `<li class="file-browser-item ${permClass}" ondblclick="ladeBrowserInhalt('${entry.path}')" onclick="browserSelect(this, '${entry.path}')">
<span class="file-icon">📁</span> ${escapeHtml(entry.name)} <span class="perm-badge-small">${entryPermStr}</span>
</li>`;
}
if (data.entries.length === 0 && !data.parent) {
html = '<li class="file-browser-item">Keine Unterordner</li>';
}
document.getElementById('browser-list').innerHTML = html;
} catch (error) {
document.getElementById('browser-list').innerHTML =
`<li class="file-browser-item" style="color: var(--danger);">Fehler: ${error.message}</li>`;
}
}
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 = '<p class="empty-state">Keine Postfächer konfiguriert</p>';
return;
}
container.innerHTML = postfaecher.map(p => {
const istAktiv = aktiveAbrufe[p.id];
return `
<div class="config-item" id="postfach-${p.id}">
<div class="config-item-info">
<h4>${escapeHtml(p.name)} ${istAktiv ? '<span class="badge badge-warning">Läuft...</span>' : ''}</h4>
<small>${escapeHtml(p.email)}${escapeHtml(p.ziel_ordner)}</small>
</div>
<div class="config-item-actions">
${istAktiv
? `<button class="btn btn-sm btn-danger" onclick="postfachAbrufStoppen(${p.id})">Stoppen</button>`
: `<button class="btn btn-sm" onclick="postfachAbrufen(${p.id})">Abrufen</button>`
}
<button class="btn btn-sm" onclick="postfachBearbeiten(${p.id})">Bearbeiten</button>
<button class="btn btn-sm" onclick="postfachTesten(${p.id})">Testen</button>
<button class="btn btn-sm btn-danger" onclick="postfachLoeschen(${p.id})">×</button>
</div>
</div>
`}).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 = '<div class="log-entry info"><span>Verbinde...</span></div>';
// 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 = `<div class="log-entry info">
<span>Starte Abruf: ${escapeHtml(data.postfach)}</span>
<small>${data.bereits_verarbeitet} bereits verarbeitet</small>
</div>`;
break;
case 'info':
logContainer.innerHTML += `<div class="log-entry info">
<span>${escapeHtml(data.nachricht)}</span>
</div>`;
break;
case 'ordner':
currentOrdner = data.name;
logContainer.innerHTML += `<div class="log-entry info" id="ordner-status">
<span>📁 ${escapeHtml(data.name)}</span>
</div>`;
break;
case 'mails':
const ordnerStatus = document.getElementById('ordner-status');
if (ordnerStatus) {
ordnerStatus.innerHTML = `<span>📁 ${escapeHtml(data.ordner)}: ${data.anzahl} Mails</span>`;
ordnerStatus.id = ''; // ID entfernen für nächsten Ordner
}
break;
case 'datei':
dateiCount++;
logContainer.innerHTML += `<div class="log-entry success">
<span>✓ ${escapeHtml(data.original_name)}</span>
<small>${formatBytes(data.groesse)}</small>
</div>`;
// Scroll nach unten
logContainer.scrollTop = logContainer.scrollHeight;
break;
case 'skip':
logContainer.innerHTML += `<div class="log-entry" style="opacity:0.6;">
<span>⊘ ${escapeHtml(data.datei)}: ${data.grund}</span>
</div>`;
break;
case 'fehler':
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ ${escapeHtml(data.nachricht)}</span>
</div>`;
break;
case 'abgebrochen':
logContainer.innerHTML += `<div class="log-entry" style="color: var(--warning);">
<span>⚠ ${escapeHtml(data.nachricht)}</span>
</div>`;
break;
case 'fertig':
const msg = data.abgebrochen
? `⚠ Abgebrochen: ${data.anzahl} Dateien gespeichert`
: `✓ Fertig: ${data.anzahl} Dateien gespeichert`;
logContainer.innerHTML += `<div class="log-entry ${data.abgebrochen ? '' : 'success'}" style="font-weight:bold;">
<span>${msg}</span>
</div>`;
eventSource.close();
delete aktiveAbrufe[id];
ladePostfaecher();
break;
}
};
eventSource.onerror = (error) => {
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ Verbindung unterbrochen</span>
</div>`;
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 += `<div class="log-entry" style="color: var(--warning);">
<span>⚠ Stopp angefordert...</span>
</div>`;
}
} 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 = '<p class="empty-state">Keine neuen Attachments gefunden</p>';
return;
}
let html = '';
for (const r of result.ergebnisse) {
const status = r.fehler ? 'error' : 'success';
const icon = r.fehler ? '✗' : '✓';
html += `<div class="log-entry ${status}">
<span>${icon} ${escapeHtml(r.postfach)}: ${r.anzahl || 0} Dateien</span>
${r.fehler ? `<small>${escapeHtml(r.fehler)}</small>` : ''}
</div>`;
if (r.dateien) {
for (const d of r.dateien) {
html += `<div class="log-entry info">
<span style="padding-left: 1rem;">→ ${escapeHtml(d)}</span>
</div>`;
}
}
}
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 = '<p class="empty-state">Keine Ordner konfiguriert</p>';
return;
}
container.innerHTML = ordner.map(o => `
<div class="config-item">
<div class="config-item-info">
<h4>${escapeHtml(o.name)} ${o.rekursiv ? '<span class="badge badge-info">rekursiv</span>' : ''}</h4>
<small>${escapeHtml(o.pfad)}${escapeHtml(o.ziel_ordner)}</small>
<small style="display:block;">${(o.dateitypen || []).join(', ')}</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="ordnerScannen(${o.id})">Scannen</button>
<button class="btn btn-sm btn-danger" onclick="ordnerLoeschen(${o.id})">×</button>
</div>
</div>
`).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 = '<p class="empty-state">Keine Schnell-Regeln definiert</p>';
} else {
schnellContainer.innerHTML = schnellRegeln.map((r, index) => {
const typBadge = getTypRegelBadge(r.muster);
const istFallback = r.prioritaet >= 900;
const fallbackClass = istFallback ? 'fallback-regel' : '';
return `
<div class="config-item typ-regel ${fallbackClass}" data-regel-id="${r.id}">
<div class="config-item-info">
<h4>${escapeHtml(r.name)} <span class="badge badge-typ">Prio ${r.prioritaet}</span> ${typBadge}</h4>
<small>→ ${escapeHtml(r.unterordner || 'Zielordner')}</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="regelPrioritaetAendern(${r.id}, -10)" title="Höhere Priorität">▲</button>
<button class="btn btn-sm" onclick="regelPrioritaetAendern(${r.id}, 10)" title="Niedrigere Priorität">▼</button>
<button class="btn btn-sm btn-danger" onclick="regelLoeschen(${r.id})">×</button>
</div>
</div>
`}).join('');
}
// Fein-Regeln rendern (sortiert nach Priorität)
feinRegeln.sort((a, b) => a.prioritaet - b.prioritaet);
if (feinRegeln.length === 0) {
feinContainer.innerHTML = '<p class="empty-state">Keine Fein-Regeln definiert</p>';
} else {
feinContainer.innerHTML = feinRegeln.map((r, index) => `
<div class="config-item" data-regel-id="${r.id}">
<div class="config-item-info">
<h4>${escapeHtml(r.name)} <span class="badge badge-info">Prio ${r.prioritaet}</span></h4>
<small>${escapeHtml(r.schema)}</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="regelPrioritaetAendern(${r.id}, -10)" title="Höhere Priorität">▲</button>
<button class="btn btn-sm" onclick="regelPrioritaetAendern(${r.id}, 10)" title="Niedrigere Priorität">▼</button>
<button class="btn btn-sm" onclick="bearbeiteRegel(${r.id})">Bearbeiten</button>
<button class="btn btn-sm btn-danger" onclick="regelLoeschen(${r.id})">×</button>
</div>
</div>
`).join('');
}
}
function getTypRegelBadge(muster) {
const badges = [];
if (muster.ist_zugferd) badges.push('<span class="badge badge-success">ZUGFeRD</span>');
if (muster.ist_signiert) badges.push('<span class="badge badge-warning">Signiert</span>');
if (muster.ist_bild) badges.push('<span class="badge badge-info">Bild</span>');
if (muster.ist_pdf) badges.push('<span class="badge badge-info">PDF</span>');
if (muster.hat_text === false) badges.push('<span class="badge badge-secondary">Ohne Text</span>');
return badges.join(' ');
}
// ============ Schnell-Regeln UI ============
function zeigeSchnellRegelModal() {
// Dropdown befüllen
const select = document.getElementById('schnell-regel-typ');
select.innerHTML = '<option value="">-- Bitte wählen --</option>';
for (const typ of verfuegbareTypRegeln) {
select.innerHTML += `<option value="${typ.id}">${escapeHtml(typ.name)}</option>`;
}
// 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 = `<div class="log-entry info"><span>${modeText}</span></div>`;
// 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 ? '<span class="badge badge-warning">TEST</span> ' : '';
switch (data.type) {
case 'start':
const modeInfo = data.testmodus
? '🔍 TESTMODUS - Dateien werden nur analysiert, nicht verschoben!'
: 'Sortierung gestartet';
logContainer.innerHTML = `<div class="log-entry info">
<span>${modeInfo}: ${data.ordner} Ordner, ${data.regeln} Regeln</span>
</div>`;
break;
case 'ordner':
logContainer.innerHTML += `<div class="log-entry info">
<span>📁 ${escapeHtml(data.name)}</span>
<small>${escapeHtml(data.pfad)}</small>
</div>`;
break;
case 'dateien_gefunden':
logContainer.innerHTML += `<div class="log-entry info">
<span>${data.anzahl} Dateien gefunden</span>
</div>`;
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
? `<small style="display:block;opacity:0.7;">→ ${escapeHtml(data.ziel_ordner)}</small>`
: '';
logContainer.innerHTML += `<div class="log-entry ${statusClass}">
<span>${testBadge}${icon} ${escapeHtml(data.neuer_name || data.original)}</span>
${data.regel ? `<small>Regel: ${escapeHtml(data.regel)}</small>` : ''}
${data.fehler ? `<small>${escapeHtml(data.fehler)}</small>` : ''}
${zielInfo}
</div>`;
logContainer.scrollTop = logContainer.scrollHeight;
break;
case 'warnung':
logContainer.innerHTML += `<div class="log-entry" style="color: var(--warning);">
<span>⚠ ${escapeHtml(data.nachricht)}</span>
</div>`;
break;
case 'abgebrochen':
logContainer.innerHTML += `<div class="log-entry" style="color: var(--warning); font-weight: bold;">
<span>⚠ Sortierung abgebrochen</span>
</div>`;
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
? '<div class="log-entry" style="opacity:0.7;"><span>Keine Dateien wurden verschoben. Starte echte Sortierung wenn zufrieden.</span></div>'
: '';
logContainer.innerHTML += `<div class="log-entry ${data.testmodus ? 'info' : 'success'}" style="font-weight:bold;">
<span>${fertigText}</span>
</div>${fertigHint}`;
sortierungEventSource.close();
sortierungAktiv = false;
sortierungEventSource = null;
aktualisiereSortierungsButtons();
break;
}
};
sortierungEventSource.onerror = (error) => {
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ Verbindung unterbrochen</span>
</div>`;
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 += `<div class="log-entry" style="color: var(--warning);">
<span>⚠ Stopp angefordert...</span>
</div>`;
}
} 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 = '<p class="empty-state">Keine Dateien verarbeitet</p>';
return;
}
let html = `<div class="log-entry info">
<span>Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Fehler: ${result.fehler}</span>
</div>`;
for (const d of result.verarbeitet) {
const status = d.fehler ? 'error' : (d.zugferd ? 'info' : 'success');
const icon = d.fehler ? '✗' : (d.zugferd ? '🧾' : '✓');
html += `<div class="log-entry ${status}">
<span>${icon} ${escapeHtml(d.neuer_name || d.original)}</span>
${d.fehler ? `<small>${escapeHtml(d.fehler)}</small>` : ''}
</div>`;
}
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 = `
<div class="statistik-grid">
<div class="stat-item">
<span class="stat-label">Postfächer</span>
<span class="stat-value">${stats.postfaecher}</span>
</div>
<div class="stat-item">
<span class="stat-label">Quell-Ordner</span>
<span class="stat-value">${stats.quell_ordner}</span>
</div>
<div class="stat-item">
<span class="stat-label">Sortier-Regeln</span>
<span class="stat-value">${stats.regeln}</span>
</div>
<div class="stat-item">
<span class="stat-label">Verarbeitete Mails</span>
<span class="stat-value">${stats.verarbeitete_mails}</span>
</div>
<div class="stat-item">
<span class="stat-label">Verarbeitete Dateien</span>
<span class="stat-value">${stats.verarbeitete_dateien}</span>
</div>
</div>
`;
} catch (error) {
document.getElementById('statistik-inhalt').innerHTML = `<p class="error">Fehler: ${error.message}</p>`;
}
}
// ============ 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 = `
<h5>Automatisch extrahierte Felder:</h5>
<pre>${JSON.stringify(result.extrahiert, null, 2)}</pre>
<p style="color: var(--warning); font-size: 0.8rem; margin-top: 0.5rem;">
Falls Werte falsch sind, passe unten die Regex-Muster an!
</p>
`;
// 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 = `
<h5>Extrahierte Felder (mit deinen Regex):</h5>
<pre>${JSON.stringify(result.extrahiert, null, 2)}</pre>
${result.fehler ? `<p style="color: var(--danger);">Fehler: ${escapeHtml(result.fehler)}</p>` : ''}
`;
} 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 =
'<p class="empty-state">Keine Quell-Ordner konfiguriert</p>';
return;
}
// Ordner-Auswahl anzeigen
let html = '<div class="pdf-ordner-auswahl">';
html += '<label>Ordner auswählen:</label>';
html += '<select id="pdf-browser-ordner" onchange="ladePdfDateien()">';
html += '<option value="">-- Ordner wählen --</option>';
for (const o of ordner) {
html += `<option value="${o.id}">${escapeHtml(o.name)} (${escapeHtml(o.pfad)})</option>`;
}
html += '</select></div>';
html += '<div id="pdf-dateien-liste" class="pdf-dateien-liste"></div>';
document.getElementById('pdf-browser-liste').innerHTML = html;
} catch (error) {
document.getElementById('pdf-browser-liste').innerHTML =
`<p class="error">Fehler: ${escapeHtml(error.message)}</p>`;
}
}
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 = '<p class="empty-state">Keine Dateien im Ordner</p>';
return;
}
// Ordner-Daten für Pfad-Konstruktion
const ordner = pdfBrowserOrdner.find(o => o.id == ordnerId);
const basePfad = ordner?.pfad || '';
let html = `<p>${result.anzahl} Dateien gefunden:</p><ul class="pdf-file-list">`;
for (const datei of result.dateien) {
const fullPath = basePfad + (basePfad.endsWith('/') ? '' : '/') + datei;
html += `<li class="pdf-file-item" onclick="waehleServerPdf('${escapeHtml(fullPath)}')" title="${escapeHtml(fullPath)}">
<span class="file-icon">📄</span> ${escapeHtml(datei)}
</li>`;
}
html += '</ul>';
container.innerHTML = html;
} catch (error) {
container.innerHTML = `<p class="error">Fehler: ${escapeHtml(error.message)}</p>`;
}
}
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();
}
}