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>
1621 lines
58 KiB
JavaScript
1621 lines
58 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
}
|