/** * 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(() => ({})); // Besseres Fehler-Handling für Pydantic Validation Errors let errorMsg = 'API Fehler'; if (error.detail) { if (Array.isArray(error.detail)) { // Pydantic Validation Error Format errorMsg = error.detail.map(e => `${e.loc?.join('.')}: ${e.msg}`).join('\n'); } else if (typeof error.detail === 'object') { errorMsg = JSON.stringify(error.detail); } else { errorMsg = error.detail; } } throw new Error(errorMsg); } 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'); } // ============ Dialog System (ersetzt alert/confirm) ============ let dialogResolve = null; const DIALOG_ICONS = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️', question: '❓' }; /** * Zeigt einen Dialog an (ersetzt alert/confirm) * @param {Object} options - Dialog-Optionen * @param {string} options.title - Dialog-Titel * @param {string} options.message - Nachricht * @param {string} options.type - Typ: success, error, warning, info, question * @param {boolean} options.showCancel - Abbrechen-Button anzeigen (für Confirm) * @param {string} options.okText - Text für OK-Button * @param {string} options.cancelText - Text für Abbrechen-Button * @returns {Promise} - true wenn OK, false wenn Abbrechen */ function showDialog(options = {}) { const { title = 'Hinweis', message = '', type = 'info', showCancel = false, okText = 'OK', cancelText = 'Abbrechen' } = options; return new Promise((resolve) => { dialogResolve = resolve; document.getElementById('dialog-title').textContent = title; document.getElementById('dialog-message').textContent = message; const iconEl = document.getElementById('dialog-icon'); iconEl.textContent = DIALOG_ICONS[type] || DIALOG_ICONS.info; iconEl.className = 'dialog-icon ' + type; document.getElementById('dialog-ok-btn').textContent = okText; document.getElementById('dialog-cancel-btn').textContent = cancelText; document.getElementById('dialog-cancel-btn').style.display = showCancel ? '' : 'none'; document.getElementById('dialog-modal').classList.remove('hidden'); }); } /** * Schließt den Dialog * @param {boolean} result - Ergebnis (true = OK, false = Abbrechen) */ function dialogSchliessen(result) { document.getElementById('dialog-modal').classList.add('hidden'); if (dialogResolve) { dialogResolve(result); dialogResolve = null; } } /** * Zeigt eine Benachrichtigung an (ersetzt alert) */ function showAlert(message, type = 'info', title = null) { const titles = { success: 'Erfolg', error: 'Fehler', warning: 'Warnung', info: 'Hinweis' }; return showDialog({ title: title || titles[type] || 'Hinweis', message: message, type: type, showCancel: false, okText: 'OK' }); } /** * Zeigt eine Bestätigung an (ersetzt confirm) */ function showConfirm(message, title = 'Bestätigung') { return showDialog({ title: title, message: message, type: 'question', showCancel: true, okText: 'Ja', cancelText: 'Nein' }); } // ============ File Browser ============ let browserTargetInput = null; let browserCurrentPath = '/'; function oeffneBrowser(inputId) { browserTargetInput = inputId; const currentValue = document.getElementById(inputId).value; // Entferne trailing slash für die Pfadnavigation browserCurrentPath = (currentValue || '/').replace(/\/+$/, '') || '/'; ladeBrowserInhalt(browserCurrentPath); document.getElementById('browser-modal').classList.remove('hidden'); } // Navigiert zum Pfad aus dem Eingabefeld function navigiereToPfad() { const input = document.getElementById('browser-path-input'); const path = input.value.trim() || '/'; ladeBrowserInhalt(path); } // Findet den nächsten existierenden Elternordner function getParentPath(path) { if (!path || path === '/') return null; const parts = path.replace(/\/+$/, '').split('/'); parts.pop(); return parts.length === 0 ? '/' : parts.join('/') || '/'; } async function ladeBrowserInhalt(path, versuchtesPfade = []) { try { const data = await api(`/browse?path=${encodeURIComponent(path)}`); if (data.error) { // Versuche Elternordner wenn dieser Pfad nicht existiert const parentPath = getParentPath(path); // Verhindere Endlosschleifen if (parentPath && !versuchtesPfade.includes(parentPath)) { versuchtesPfade.push(path); console.log(`Pfad "${path}" existiert nicht, versuche "${parentPath}"`); return ladeBrowserInhalt(parentPath, versuchtesPfade); } // Fallback zu Root wenn nichts funktioniert if (path !== '/') { console.log(`Fallback zu Root-Verzeichnis`); return ladeBrowserInhalt('/', []); } // Nur anzeigen wenn wirklich nichts geht document.getElementById('browser-list').innerHTML = `
  • ${data.error}
  • `; return; } browserCurrentPath = data.current; document.getElementById('browser-path-input').value = data.current; let html = ''; // Parent directory if (data.parent) { html += `
  • 📁 ..
  • `; } // Directories for (const entry of data.entries) { html += `
  • 📁 ${entry.name}
  • `; } if (data.entries.length === 0 && !data.parent) { html = '
  • Keine Unterordner
  • '; } document.getElementById('browser-list').innerHTML = html; } catch (error) { // Bei Netzwerk-/API-Fehler auch Elternordner versuchen const parentPath = getParentPath(path); if (parentPath && !versuchtesPfade.includes(parentPath) && path !== '/') { versuchtesPfade.push(path); console.log(`API-Fehler bei "${path}", versuche "${parentPath}"`); return ladeBrowserInhalt(parentPath, versuchtesPfade); } document.getElementById('browser-list').innerHTML = `
  • Fehler: ${error.message}
  • `; } } 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); }); } // ============ Theme System ============ function ladeGespeichertesTheme() { const gespeichertesTheme = localStorage.getItem('dateiverwaltung-theme') || 'dark'; setzeTheme(gespeichertesTheme, false); } function setzeTheme(theme, speichern = true) { document.documentElement.setAttribute('data-theme', theme); // Aktiven Button markieren document.querySelectorAll('.theme-option').forEach(btn => { btn.classList.toggle('active', btn.dataset.theme === theme); }); if (speichern) { localStorage.setItem('dateiverwaltung-theme', theme); } } function zeigeEinstellungenModal() { // Aktuelles Theme markieren const aktuellesTheme = localStorage.getItem('dateiverwaltung-theme') || 'dark'; document.querySelectorAll('.theme-option').forEach(btn => { btn.classList.toggle('active', btn.dataset.theme === aktuellesTheme); }); document.getElementById('einstellungen-modal').classList.remove('hidden'); } // ============ Debug Log ============ function zeigeLogModal() { document.getElementById('log-modal').classList.remove('hidden'); ladeLog(); } async function ladeLog() { const container = document.getElementById('log-container'); const filter = document.getElementById('log-filter').value; try { const logs = await api(`/logs${filter ? '?level=' + filter : ''}`); if (!logs || logs.length === 0) { container.innerHTML = '

    Keine Log-Einträge

    '; return; } container.innerHTML = logs.map(log => { const levelClass = log.level === 'ERROR' ? 'error' : log.level === 'WARNING' ? 'warning' : 'info'; return `
    ${log.zeit} ${log.level} ${escapeHtml(log.nachricht)}
    `; }).join(''); // Nach unten scrollen container.scrollTop = container.scrollHeight; } catch (error) { container.innerHTML = `

    Fehler: ${error.message}

    `; } } async function leereLog() { if (!await showConfirm('Log wirklich leeren?')) return; try { await api('/logs', { method: 'DELETE' }); ladeLog(); } catch (error) { showAlert(error.message, 'error'); } } // ============ Init ============ document.addEventListener('DOMContentLoaded', () => { ladeGespeichertesTheme(); ladePostfaecher(); ladeOrdner(); ladeRegeln(); ladeZeitplaene(); ladeStatus(); // Gespeicherten Tab wiederherstellen const gespeicherterTab = localStorage.getItem('aktiver-tab'); if (gespeicherterTab) { wechsleTab(gespeicherterTab); } // Event-Listener für Dateityp-Checkboxen im Postfach-Modal const pfTypenGruppe = document.getElementById('pf-typen-gruppe'); if (pfTypenGruppe) { pfTypenGruppe.addEventListener('change', () => { const container = document.getElementById('pf-groessen-filter'); if (container.classList.contains('visible')) { updateGroessenFilterTable(); } }); } }); // ============ BEREICH 1: Mail-Abruf ============ async function ladePostfaecher() { try { const postfaecher = await api('/postfaecher'); renderPostfaecher(postfaecher); } catch (error) { console.error('Fehler:', error); } } let bearbeitetesPostfachId = null; function renderPostfaecher(postfaecher) { const container = document.getElementById('postfaecher-liste'); if (!postfaecher || postfaecher.length === 0) { container.innerHTML = '

    Keine Postfächer konfiguriert

    '; return; } container.innerHTML = postfaecher.map(p => { const letzterAbruf = p.letzter_abruf ? formatDatum(p.letzter_abruf) : 'Nie'; return `

    ${escapeHtml(p.name)}

    ${escapeHtml(p.email)} → ${truncatePath(p.ziel_ordner)} Letzter Abruf: ${letzterAbruf} (${p.letzte_anzahl || 0} Dateien)
    `}).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'; 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('pf-min-groesse').value = postfach?.min_groesse_kb || '10'; // ab_datum: ISO-String in date-Input-Format (YYYY-MM-DD) const abDatum = postfach?.ab_datum ? postfach.ab_datum.split('T')[0] : ''; document.getElementById('pf-ab-datum').value = abDatum; // Größenfilter pro Dateityp initialisieren renderGroessenFilter(postfach?.groessen_filter || {}); document.getElementById('postfach-modal').classList.remove('hidden'); } // ============ Größenfilter pro Dateityp ============ function toggleGroessenFilter() { const container = document.getElementById('pf-groessen-filter'); container.classList.toggle('visible'); if (container.classList.contains('visible')) { // Tabelle aktualisieren basierend auf ausgewählten Dateitypen updateGroessenFilterTable(); } } function updateGroessenFilterTable() { const selectedTypes = getCheckedTypes('pf-typen-gruppe'); const container = document.getElementById('pf-groessen-filter'); const defaultMin = parseInt(document.getElementById('pf-min-groesse').value) || 10; const defaultMax = parseInt(document.getElementById('pf-max-groesse').value) || 25; // Bestehende Werte sammeln const existingValues = {}; container.querySelectorAll('.groessen-filter-row').forEach(row => { const typ = row.dataset.typ; const minInput = row.querySelector('.groessen-min'); const maxInput = row.querySelector('.groessen-max'); if (minInput && maxInput) { existingValues[typ] = { min_kb: parseInt(minInput.value) || defaultMin, max_mb: parseInt(maxInput.value) || defaultMax }; } }); if (selectedTypes.length === 0) { container.innerHTML = '

    Zuerst Dateitypen auswählen

    '; return; } let html = ` `; for (const typ of selectedTypes) { const existing = existingValues[typ] || { min_kb: defaultMin, max_mb: defaultMax }; html += ` `; } html += '
    Dateityp Min (KB) Max (MB)
    ${typ}
    '; container.innerHTML = html; } function renderGroessenFilter(groessenFilter) { const container = document.getElementById('pf-groessen-filter'); container.classList.remove('visible'); // Wenn Werte vorhanden, Tabelle aufbauen if (groessenFilter && Object.keys(groessenFilter).length > 0) { const selectedTypes = getCheckedTypes('pf-typen-gruppe'); const defaultMin = parseInt(document.getElementById('pf-min-groesse').value) || 10; const defaultMax = parseInt(document.getElementById('pf-max-groesse').value) || 25; let html = ` `; for (const typ of selectedTypes) { const filter = groessenFilter[typ] || { min_kb: defaultMin, max_mb: defaultMax }; html += ` `; } html += '
    Dateityp Min (KB) Max (MB)
    ${typ}
    '; container.innerHTML = html; } else { container.innerHTML = ''; } } function getGroessenFilter() { const container = document.getElementById('pf-groessen-filter'); const rows = container.querySelectorAll('.groessen-filter-row'); const defaultMin = parseInt(document.getElementById('pf-min-groesse').value) || 10; const defaultMax = parseInt(document.getElementById('pf-max-groesse').value) || 25; const filter = {}; rows.forEach(row => { const typ = row.dataset.typ; const minKb = parseInt(row.querySelector('.groessen-min')?.value); const maxMb = parseInt(row.querySelector('.groessen-max')?.value); // Nur speichern wenn unterschiedlich vom Default if ((minKb && minKb !== defaultMin) || (maxMb && maxMb !== defaultMax)) { filter[typ] = {}; if (minKb && minKb !== defaultMin) filter[typ].min_kb = minKb; if (maxMb && maxMb !== defaultMax) filter[typ].max_mb = maxMb; } }); return Object.keys(filter).length > 0 ? filter : null; } async function postfachBearbeiten(id) { try { const postfaecher = await api('/postfaecher'); const postfach = postfaecher.find(p => p.id === id); if (postfach) { zeigePostfachModal(postfach); } } catch (error) { showAlert(error.message, 'error'); } } async function speicherePostfach() { const erlaubteTypen = getCheckedTypes('pf-typen-gruppe'); if (erlaubteTypen.length === 0) { showAlert('Bitte mindestens einen Dateityp auswählen', 'warning'); return; } const abDatumValue = document.getElementById('pf-ab-datum').value; 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', ziel_ordner: document.getElementById('pf-ziel').value.trim(), erlaubte_typen: erlaubteTypen, max_groesse_mb: parseInt(document.getElementById('pf-max-groesse').value), min_groesse_kb: parseInt(document.getElementById('pf-min-groesse').value), ab_datum: abDatumValue ? abDatumValue + 'T00:00:00' : null, groessen_filter: getGroessenFilter() }; if (!data.name || !data.imap_server || !data.email || !data.ziel_ordner) { showAlert('Bitte alle Pflichtfelder ausfüllen', 'warning'); return; } // Bei Bearbeitung: Passwort nur senden wenn eingegeben if (bearbeitetesPostfachId && !data.passwort) { delete data.passwort; } else if (!data.passwort) { showAlert('Passwort ist erforderlich', 'warning'); 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) { showAlert(error.message, 'error'); } } async function postfachTesten(id) { try { const result = await api(`/postfaecher/${id}/test`, { method: 'POST' }); showAlert(result.erfolg ? 'Verbindung erfolgreich!' : result.nachricht, result.erfolg ? 'success' : 'error'); } catch (error) { showAlert(error.message, 'error'); } } async function postfachAbrufen(id) { const logContainer = document.getElementById('abruf-log'); logContainer.innerHTML = '
    Verbinde...
    '; // EventSource für Server-Sent Events const eventSource = new EventSource(`/api/postfaecher/${id}/abrufen/stream`); let dateiCount = 0; let currentOrdner = ''; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'start': logContainer.innerHTML = `
    Starte Abruf: ${escapeHtml(data.postfach)} ${data.bereits_verarbeitet} bereits verarbeitet
    `; break; case 'info': logContainer.innerHTML += `
    ${escapeHtml(data.nachricht)}
    `; break; case 'ordner': currentOrdner = data.name; logContainer.innerHTML += `
    📁 ${escapeHtml(data.name)}
    `; break; case 'mails': const ordnerStatus = document.getElementById('ordner-status'); if (ordnerStatus) { ordnerStatus.innerHTML = `📁 ${escapeHtml(data.ordner)}: ${data.anzahl} Mails`; ordnerStatus.id = ''; // ID entfernen für nächsten Ordner } break; case 'datei': dateiCount++; logContainer.innerHTML += `
    ✓ ${escapeHtml(data.original_name)} ${formatBytes(data.groesse)}
    `; // Scroll nach unten logContainer.scrollTop = logContainer.scrollHeight; break; case 'skip': logContainer.innerHTML += `
    ⊘ ${escapeHtml(data.datei)}: ${data.grund}
    `; break; case 'fehler': logContainer.innerHTML += `
    ✗ ${escapeHtml(data.nachricht)}
    `; break; case 'fertig': logContainer.innerHTML += `
    ✓ Fertig: ${data.anzahl} Dateien gespeichert
    `; eventSource.close(); ladePostfaecher(); break; } }; eventSource.onerror = (error) => { logContainer.innerHTML += `
    ✗ Verbindung unterbrochen
    `; eventSource.close(); }; } 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() { const container = document.getElementById('abruf-log'); container.innerHTML = '
    Starte Abruf...
    '; try { const response = await fetch('/api/postfaecher/abrufen-alle/stream'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let currentPostfach = ''; while (true) { const {done, value} = await reader.read(); if (done) break; const text = decoder.decode(value); const lines = text.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const event = JSON.parse(line.slice(6)); renderStreamEvent(container, event); } catch (e) {} } } } ladePostfaecher(); ladeStatus(); } catch (error) { container.innerHTML += `
    Fehler: ${escapeHtml(error.message)}
    `; } } function renderStreamEvent(container, event) { let html = ''; switch (event.type) { case 'init': html = `
    📧 ${event.anzahl_postfaecher} Postfächer werden abgerufen...
    `; break; case 'postfach_start': html = `
    📬 ${escapeHtml(event.name)} (${event.bereits_verarbeitet} bereits verarbeitet)
    `; break; case 'ordner': html = `
    📁 Ordner: ${escapeHtml(event.name)}
    `; break; case 'mails': html = `
    ${event.anzahl} Mails gefunden
    `; break; case 'datei': html = `
    ✓ ${escapeHtml(event.datei)} (${formatBytes(event.groesse)})
    `; break; case 'skip': html = `
    → ${escapeHtml(event.datei)} - ${event.grund}
    `; break; case 'postfach_done': html = `
    ✓ ${escapeHtml(event.name)}: ${event.anzahl} Dateien
    `; break; case 'postfach_error': html = `
    ✗ ${escapeHtml(event.name)}: ${escapeHtml(event.fehler)}
    `; break; case 'done': html = `
    Fertig!
    `; break; } if (html) { container.innerHTML += html; container.scrollTop = container.scrollHeight; } } async function postfachLoeschen(id) { if (!await showConfirm('Postfach wirklich löschen?')) return; try { await api(`/postfaecher/${id}`, { method: 'DELETE' }); ladePostfaecher(); } catch (error) { showAlert(error.message, 'error'); } } function zeigeAbrufLog(result) { const container = document.getElementById('abruf-log'); if (!result.ergebnisse || result.ergebnisse.length === 0) { container.innerHTML = '

    Keine neuen Attachments gefunden

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

    Keine Ordner konfiguriert

    '; return; } container.innerHTML = ordner.map(o => { const aktivClass = o.aktiv ? '' : 'opacity: 0.5;'; const aktivBadge = o.aktiv ? 'Aktiv' : 'Inaktiv'; const letzte = o.letzte_verarbeitung ? formatDatum(o.letzte_verarbeitung) : 'Nie'; return `

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

    ${truncatePath(o.pfad)} → ${truncatePath(o.ziel_ordner)} ${(o.dateitypen || []).join(', ')} | Letzte: ${letzte} (${o.letzte_anzahl || 0} Dateien)
    `}).join(''); } let bearbeitetesOrdnerId = null; function zeigeOrdnerModal(ordner = null) { bearbeitetesOrdnerId = ordner?.id || null; document.getElementById('ordner-modal-title').textContent = ordner ? 'Grobsortierung bearbeiten' : 'Grobsortierung hinzufügen'; document.getElementById('ord-name').value = ordner?.name || ''; document.getElementById('ord-pfad').value = ordner?.pfad || '/mnt/user/'; document.getElementById('ord-ziel').value = ordner?.ziel_ordner || '/mnt/user/'; setCheckedTypes('ord-typen-gruppe', ordner?.dateitypen || ['.pdf', '.jpg', '.jpeg', '.png', '.tiff']); document.getElementById('ord-rekursiv').value = ordner?.rekursiv !== false ? 'true' : 'false'; document.getElementById('ord-zugferd-sep').checked = ordner?.zugferd_behandlung === 'separieren' || !ordner; document.getElementById('ord-signiert-sep').checked = ordner?.signiert_behandlung === 'separieren'; document.getElementById('ord-ocr').checked = ordner?.ocr_aktivieren !== false; document.getElementById('ord-original-sichern').value = ordner?.original_sichern || ''; // Sortier-Modus const modus = ordner?.direkt_verschieben ? 'direkt' : 'regeln'; document.querySelector(`input[name="ord-modus"][value="${modus}"]`).checked = true; document.getElementById('ordner-modal').classList.remove('hidden'); } async function ordnerBearbeiten(id) { try { const ordnerListe = await api('/ordner'); const ordner = ordnerListe.find(o => o.id === id); if (ordner) { zeigeOrdnerModal(ordner); } } catch (error) { showAlert(error.message, 'error'); } } async function speichereOrdner() { const dateitypen = getCheckedTypes('ord-typen-gruppe'); if (dateitypen.length === 0) { showAlert('Bitte mindestens einen Dateityp auswählen', 'warning'); return; } // NEU: Sortier-Modus auslesen const modusRadio = document.querySelector('input[name="ord-modus"]:checked'); const direktVerschieben = modusRadio?.value === 'direkt'; 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, zugferd_behandlung: document.getElementById('ord-zugferd-sep').checked ? 'separieren' : 'normal', signiert_behandlung: document.getElementById('ord-signiert-sep').checked ? 'separieren' : 'normal', direkt_verschieben: direktVerschieben, ocr_aktivieren: document.getElementById('ord-ocr').checked, original_sichern: document.getElementById('ord-original-sichern').value.trim() || null }; if (!data.name || !data.pfad || !data.ziel_ordner) { showAlert('Bitte alle Felder ausfüllen', 'warning'); return; } try { zeigeLoading('Speichere Ordner...'); if (bearbeitetesOrdnerId) { await api(`/ordner/${bearbeitetesOrdnerId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await api('/ordner', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('ordner-modal'); ladeOrdner(); } catch (error) { showAlert(error.message, 'error'); } finally { versteckeLoading(); } } async function ordnerAktivieren(id) { try { await api(`/ordner/${id}/aktivieren`, { method: 'POST' }); ladeOrdner(); } catch (error) { showAlert(error.message, 'error'); } } async function ordnerLoeschen(id) { if (!await showConfirm('Ordner wirklich löschen?')) return; try { await api(`/ordner/${id}`, { method: 'DELETE' }); ladeOrdner(); } catch (error) { showAlert(error.message, 'error'); } } async function kopiereOrdner(id) { if (!await showConfirm('Grobsortierung kopieren?')) return; try { const result = await api(`/ordner/${id}/kopieren`, { method: 'POST' }); showAlert(`Grobsortierung kopiert: "${result.name}"`, 'success'); ladeOrdner(); } catch (error) { showAlert('Fehler beim Kopieren: ' + error.message, 'error'); } } async function ordnerVorschau(id) { try { const result = await api(`/ordner/${id}/scannen`); let msg = `${result.anzahl} Dateien gefunden`; if (result.dateien && result.dateien.length > 0) { msg += `:\n\n${result.dateien.slice(0, 10).join('\n')}`; if (result.anzahl > 10) { msg += `\n... und ${result.anzahl - 10} weitere`; } } showAlert(msg, 'info', 'Ordner-Vorschau'); } catch (error) { showAlert(error.message, 'error'); } } async function ordnerVerarbeiten(id) { if (!await showConfirm('Dateien jetzt verarbeiten und sortieren?')) return; const logContainer = document.getElementById('grobsortierung-log'); logContainer.innerHTML = '
    Verarbeite...
    '; try { debugLog('Verarbeite Ordner...', 'info'); const result = await api(`/ordner/${id}/verarbeiten`, { method: 'POST' }); // Ergebnis in der Mitte anzeigen let html = `
    Verarbeitung abgeschlossen
    Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Keine Regel: ${result.keine_regel || 0} | Fehler: ${result.fehler}
    `; // Details der verarbeiteten Dateien anzeigen if (result.verarbeitet && result.verarbeitet.length > 0) { result.verarbeitet.forEach(d => { const klasse = d.status === 'sortiert' || d.status === 'direkt_verschoben' ? 'success' : (d.status === 'fehler' ? 'error' : 'info'); const icon = d.status === 'sortiert' || d.status === 'direkt_verschoben' ? '✓' : (d.status === 'fehler' ? '✗' : 'ℹ'); html += `
    ${icon} ${escapeHtml(d.original)} → ${escapeHtml(d.neuer_name || d.status)}
    `; }); } logContainer.innerHTML = html; debugLog('Verarbeitung abgeschlossen', 'success'); } catch (error) { logContainer.innerHTML = `
    Fehler: ${escapeHtml(error.message)}
    `; debugLog('Fehler: ' + error.message, 'error'); } } // ============ Regeln ============ let editierteRegelId = null; // Natürliche Sortierung (Zahlen korrekt: 1, 2, 10, 100 statt 1, 10, 100, 2) function natuerlicheSortierung(a, b) { return a.localeCompare(b, 'de', { numeric: true, sensitivity: 'base' }); } // Cache für Ordner (für Filter-Dropdown) let ordnerCache = []; async function ladeOrdnerFuerFilter() { try { const ordner = await api('/ordner'); ordnerCache = ordner; const filterSelect = document.getElementById('regeln-ordner-filter'); if (!filterSelect) return; // Gespeicherten Filter wiederherstellen const gespeichert = localStorage.getItem('regeln-ordner-filter'); // Bestehende Optionen nach "Ohne Zuordnung" entfernen while (filterSelect.options.length > 2) { filterSelect.remove(2); } // Ordner hinzufügen ordner.forEach(o => { const option = document.createElement('option'); option.value = o.id; option.textContent = o.name; filterSelect.appendChild(option); }); // Gespeicherten Wert setzen if (gespeichert) { filterSelect.value = gespeichert; } } catch (error) { console.error('Fehler beim Laden der Ordner für Filter:', error); } } async function ladeRegeln() { try { const regeln = await api('/regeln'); // Ordner-Filter Dropdown beim ersten Mal befüllen const filterSelect = document.getElementById('regeln-ordner-filter'); if (filterSelect && filterSelect.options.length <= 2) { await ladeOrdnerFuerFilter(); } // Filter anwenden if (filterSelect) { localStorage.setItem('regeln-ordner-filter', filterSelect.value); } const filter = filterSelect?.value || 'alle'; let gefilterteRegeln = regeln; if (filter === 'ohne') { // Nur Regeln ohne Ordner-Zuweisung gefilterteRegeln = regeln.filter(r => !r.ordner_ids || r.ordner_ids.length === 0); } else if (filter !== 'alle') { // Nach spezifischem Ordner filtern const ordnerId = parseInt(filter); gefilterteRegeln = regeln.filter(r => r.ordner_ids && r.ordner_ids.includes(ordnerId)); } // Sortierung anwenden (und in localStorage speichern) const sortSelect = document.getElementById('regeln-sortierung'); if (sortSelect) { // Gespeicherte Sortierung wiederherstellen const gespeichert = localStorage.getItem('regeln-sortierung'); if (gespeichert && sortSelect.value !== gespeichert) { sortSelect.value = gespeichert; } // Aktuelle Sortierung speichern localStorage.setItem('regeln-sortierung', sortSelect.value); } const sortierung = sortSelect?.value || 'name_asc'; gefilterteRegeln.sort((a, b) => { switch (sortierung) { case 'name_asc': return natuerlicheSortierung(a.name || '', b.name || ''); case 'name_desc': return natuerlicheSortierung(b.name || '', a.name || ''); case 'prio_asc': return (a.prioritaet || 0) - (b.prioritaet || 0); case 'prio_desc': return (b.prioritaet || 0) - (a.prioritaet || 0); default: return 0; } }); renderRegeln(gefilterteRegeln); } catch (error) { console.error('Fehler:', error); } } function renderRegeln(regeln) { const container = document.getElementById('regeln-liste'); if (!regeln || regeln.length === 0) { container.innerHTML = '

    Keine Regeln definiert

    '; return; } container.innerHTML = regeln.map(r => { const aktivClass = r.aktiv ? '' : 'opacity: 0.5;'; const aktivBadge = r.aktiv ? 'Aktiv' : 'Inaktiv'; // Warnung wenn keine Ordner zugeordnet const ohneOrdner = !r.ordner_ids || r.ordner_ids.length === 0; const ohneOrdnerBadge = ohneOrdner ? '⚠ Ohne Ordner' : ''; const ohneOrdnerStyle = ohneOrdner ? 'border-left: 3px solid var(--warning);' : ''; // Ordner-Namen anzeigen let ordnerInfo = ''; if (r.ordner_ids && r.ordner_ids.length > 0 && ordnerCache.length > 0) { const ordnerNamen = r.ordner_ids .map(id => ordnerCache.find(o => o.id === id)?.name || `ID ${id}`) .join(', '); ordnerInfo = `
    📁 ${escapeHtml(ordnerNamen)}`; } return `

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

    ${escapeHtml(r.schema)}${ordnerInfo}
    `}).join(''); } async function kopiereRegel(id) { if (!await showConfirm('Regel kopieren?')) return; try { const result = await api(`/regeln/${id}/kopieren`, { method: 'POST' }); showAlert(`Regel kopiert: "${result.name}"`, 'success'); ladeRegeln(); } catch (error) { showAlert('Fehler beim Kopieren: ' + error.message, 'error'); } } // ============ Regeln Import/Export ============ async function exportiereRegeln() { try { const result = await api('/regeln/export'); const json = JSON.stringify(result.regeln, null, 2); // Download als Datei const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `sortierregeln_${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showAlert(`${result.anzahl} Regeln exportiert`, 'success'); } catch (error) { showAlert('Fehler beim Export: ' + error.message, 'error'); } } async function importiereRegeln() { // Datei-Input erstellen const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const regeln = JSON.parse(text); // Prüfen ob es ein Array oder ein Objekt mit "regeln" ist const regelListe = Array.isArray(regeln) ? regeln : regeln.regeln; if (!regelListe || !Array.isArray(regelListe)) { showAlert('Ungültiges Format: Array von Regeln erwartet', 'error'); return; } // Import-Modus fragen const modus = await new Promise(resolve => { const modal = document.createElement('div'); modal.className = 'modal'; modal.innerHTML = ` `; modal.resolve = (value) => { modal.remove(); resolve(value); }; document.body.appendChild(modal); }); if (!modus) return; if (modus === 'ersetzen') { if (!await showConfirm('Alle bestehenden Regeln werden gelöscht. Fortfahren?')) { return; } } const result = await api('/regeln/import', { method: 'POST', body: JSON.stringify({ regeln: regelListe, modus }) }); showAlert( `Import: ${result.importiert} neu, ${result.aktualisiert} aktualisiert, ${result.uebersprungen} übersprungen`, 'success' ); ladeRegeln(); } catch (error) { showAlert('Fehler beim Import: ' + error.message, 'error'); } }; input.click(); } // ============ Regel-Modal (NEU) ============ let alleOrdner = []; // Cache für Ordner-Liste // Toggle Ziel-Ordner Feld basierend auf "Nur umbenennen" Checkbox function toggleZielOrdnerGruppe() { const nurUmbenennen = document.getElementById('regel-nur-umbenennen').checked; const zielGruppe = document.getElementById('ziel-ordner-gruppe'); if (zielGruppe) { zielGruppe.style.display = nurUmbenennen ? 'none' : 'block'; } } async function zeigeRegelModal(regel = null) { editierteRegelId = regel?.id || null; document.getElementById('regel-modal-title').textContent = regel ? 'Regel bearbeiten' : 'Regel hinzufügen'; // Grundeinstellungen document.getElementById('regel-name').value = regel?.name || ''; document.getElementById('regel-prioritaet').value = regel?.prioritaet || 100; document.getElementById('regel-ist-fallback').checked = regel?.ist_fallback || false; document.getElementById('regel-nur-umbenennen').checked = regel?.nur_umbenennen || false; document.getElementById('regel-ziel-ordner').value = regel?.ziel_ordner || ''; toggleZielOrdnerGruppe(); // Ziel-Ordner Feld ein/ausblenden document.getElementById('regel-schema').value = regel?.schema || '{datum} - Rechnung - {firma} - {nummer} - {betrag} EUR.pdf'; document.getElementById('regel-unterordner').value = regel?.unterordner || ''; // Erkennungsmuster aus JSON extrahieren const muster = regel?.muster || {}; document.getElementById('regel-keywords').value = muster.keywords || ''; document.getElementById('regel-keywords-nicht').value = muster.keywords_nicht || ''; document.getElementById('regel-auch-dateiname').checked = muster.auch_dateiname || false; document.getElementById('regel-text-regex').value = muster.text_regex || ''; // Extraktion-Tabelle befüllen const extraktion = regel?.extraktion || {}; befuelleExtraktionTabelle(extraktion); // Ordner-Checkboxen laden await ladeOrdnerCheckboxen(editierteRegelId); // Test-Bereich zurücksetzen document.getElementById('regel-test-text').value = ''; document.getElementById('regel-test-ergebnis').classList.add('hidden'); document.getElementById('test-datei-name').textContent = ''; const displayEl = document.getElementById('regel-test-text-display'); if (displayEl) { displayEl.innerHTML = '

    PDF hochladen um Text anzuzeigen

    '; } document.getElementById('regel-modal').classList.remove('hidden'); } function befuelleExtraktionTabelle(extraktion) { const tbody = document.getElementById('extraktion-tbody'); tbody.innerHTML = ''; // Standard-Felder mit Beispiel-Regex const standardFelder = [ {name: 'datum', beispiel: 'Datum[:\\s]*(\\d{1,2}\\.\\d{1,2}\\.\\d{4})'}, {name: 'nummer', beispiel: 'Rechnungs-?Nr\\.?[:\\s]*(\\S+)\nBeleg-?Nr[:\\s]*(\\S+)'}, {name: 'betrag', beispiel: 'Gesamt[:\\s]*([\\d.,]+)\\s*€'}, {name: 'firma', beispiel: 'Auto = aus Absender/Text'} ]; const vorhandeneFelder = new Set(standardFelder.map(f => f.name)); // Zuerst Standard-Felder mit Beispielen als Placeholder for (const {name, beispiel} of standardFelder) { const config = extraktion[name]; fuegeExtraktionsZeileHinzuMitBeispiel(name, config, false, beispiel); } // Dann zusätzliche Felder for (const [feld, config] of Object.entries(extraktion)) { if (!vorhandeneFelder.has(feld)) { fuegeExtraktionsZeileHinzu(feld, config, true); } } } function fuegeExtraktionsZeileHinzuMitBeispiel(feld, config, removable, beispielPlaceholder) { const tbody = document.getElementById('extraktion-tbody'); const row = document.createElement('tr'); row.className = 'extraktion-row'; const istWert = config && 'wert' in config; const istRegex = config && 'regex' in config; let wert = ''; if (istWert) { wert = config.wert; } else if (istRegex) { wert = Array.isArray(config.regex) ? config.regex.join('\n') : config.regex; } // Auswahl-Modus (max/min/first/last) const auswahl = config?.auswahl || 'first'; // Placeholder: Beispiel oder Standard let placeholder = beispielPlaceholder || 'Regex-Muster mit (Gruppe)'; if (istWert) { placeholder = 'Fester Wert eingeben'; } row.innerHTML = ` ${removable ? '' : ''} `; tbody.appendChild(row); } // Beispiel-Placeholders für verschiedene Feldtypen const REGEX_BEISPIELE = { 'datum': 'Datum[:\\s]*(\\d{1,2}\\.\\d{1,2}\\.\\d{4})', 'nummer': 'Rechnungs-?Nr\\.?[:\\s]*(\\S+)', 'betrag': 'Gesamt[:\\s]*([\\d.,]+)\\s*€', 'firma': 'z.B. Sonepar|ACME GmbH', 'default': 'Muster[:\\s]*(\\S+)' }; function fuegeExtraktionsZeileHinzu(feld = '', config = null, removable = true, istBeispiel = false) { const tbody = document.getElementById('extraktion-tbody'); const row = document.createElement('tr'); row.className = 'extraktion-row' + (istBeispiel ? ' beispiel' : ''); const istWert = config && 'wert' in config; const istRegex = config && 'regex' in config; // Bei mehreren Regex-Patterns: Array zu Zeilenumbrüchen let wert = ''; if (istWert) { wert = config.wert; } else if (istRegex) { wert = Array.isArray(config.regex) ? config.regex.join('\n') : config.regex; } // Auswahl-Modus (max/min/first/last) const auswahl = config?.auswahl || 'first'; // Placeholder basierend auf Feldname let placeholder = 'Regex-Muster mit (Gruppe)'; if (feld) { placeholder = REGEX_BEISPIELE[feld.toLowerCase()] || REGEX_BEISPIELE['default']; } if (istWert) { placeholder = 'Fester Wert eingeben'; } row.innerHTML = ` ${removable ? '' : ''} `; tbody.appendChild(row); } function updateExtPlaceholder(selectEl) { const row = selectEl.closest('tr'); const textarea = row.querySelector('.ext-wert'); const typ = selectEl.value; if (typ === 'wert') { textarea.placeholder = 'Fester Wert eingeben'; } else if (typ === 'regex') { textarea.placeholder = 'Regex-Muster mit (Gruppe)\nZeile 2 = Alternative'; } else { textarea.placeholder = 'Leer = globale Extraktoren'; } } function fuegeExtraktionsFeldHinzu() { fuegeExtraktionsZeileHinzu('', null, true); } function fuegeBeispieleHinzu() { // Beispiele für neue Regeln const tbody = document.getElementById('extraktion-tbody'); if (tbody.children.length === 0) { // Beispiel 1: Einfache Regex fuegeExtraktionsZeileHinzu('nummer', {regex: 'Rechnungs-?Nr\\.?[:\\s]*(\\S+)'}, true, true); // Beispiel 2: Mehrere Regex-Alternativen fuegeExtraktionsZeileHinzu('betrag', {regex: ['Gesamt[:\\s]*([\\d.,]+)\\s*€', 'Summe[:\\s]*([\\d.,]+)']}, true, true); } } function sammleExtraktionAusTabelle() { const extraktion = {}; const rows = document.querySelectorAll('#extraktion-tbody .extraktion-row'); for (const row of rows) { const feld = row.querySelector('.ext-feld').value.trim(); const typ = row.querySelector('.ext-typ').value; const wertElement = row.querySelector('.ext-wert'); const wert = wertElement ? wertElement.value.trim() : ''; const auswahlElement = row.querySelector('.ext-auswahl'); const auswahl = auswahlElement ? auswahlElement.value : 'first'; if (!feld) continue; if (typ === 'wert' && wert) { extraktion[feld] = { wert: wert }; } else if (typ === 'regex' && wert) { // Mehrere Zeilen = Mehrere Regex-Alternativen const zeilen = wert.split('\n').map(z => z.trim()).filter(z => z); if (zeilen.length === 1) { extraktion[feld] = { regex: zeilen[0] }; } else if (zeilen.length > 1) { extraktion[feld] = { regex: zeilen }; } // Auswahl hinzufügen wenn nicht "first" (Standard) if (auswahl && auswahl !== 'first' && extraktion[feld]) { extraktion[feld].auswahl = auswahl; } } // 'auto' = nichts eintragen, globale Extraktoren werden genutzt } return extraktion; } // Cache für freie Ordner der aktuellen Regel let aktuelleFreieOrdner = []; async function ladeOrdnerCheckboxen(regelId) { const container = document.getElementById('regel-ordner-liste'); const freieOrdnerContainer = document.getElementById('regel-freie-ordner'); try { // Alle Ordner laden alleOrdner = await api('/ordner'); // Zugewiesene Ordner und freie Ordner laden (wenn Regel existiert) let zugewieseneIds = []; aktuelleFreieOrdner = []; if (regelId) { const result = await api(`/regeln/${regelId}/ordner`); zugewieseneIds = result.ordner_ids || []; aktuelleFreieOrdner = result.freie_ordner || []; } // Ziel-Ordner aus Grobsortierung (dedupliziert) const zielOrdnerMap = new Map(); for (const ordner of alleOrdner) { if (!zielOrdnerMap.has(ordner.ziel_ordner)) { zielOrdnerMap.set(ordner.ziel_ordner, { id: ordner.id, name: ordner.name, ziel_ordner: ordner.ziel_ordner }); } } if (zielOrdnerMap.size === 0) { container.innerHTML = '

    Keine Grobsortierung vorhanden. Bitte zuerst Ordner anlegen.

    '; } else { container.innerHTML = Array.from(zielOrdnerMap.values()).map(ordner => ` `).join(''); } // Freie Ordner anzeigen renderFreieOrdner(); } catch (error) { container.innerHTML = `

    Fehler: ${error.message}

    `; } } function renderFreieOrdner() { const container = document.getElementById('regel-freie-ordner'); if (!container) return; if (aktuelleFreieOrdner.length === 0) { container.innerHTML = ''; return; } container.innerHTML = aktuelleFreieOrdner.map((pfad, index) => `
    ${escapeHtml(pfad)}
    `).join(''); } function fuegeFreienOrdnerHinzu() { const input = document.getElementById('regel-neuer-ordner'); const pfad = input.value.trim(); if (!pfad) { showAlert('Bitte einen Ordner-Pfad eingeben oder per Filebrowser wählen', 'warning'); return; } // Prüfen ob bereits vorhanden if (aktuelleFreieOrdner.includes(pfad)) { showAlert('Dieser Ordner ist bereits hinzugefügt', 'warning'); return; } aktuelleFreieOrdner.push(pfad); input.value = ''; renderFreieOrdner(); } function entferneFreienOrdner(index) { aktuelleFreieOrdner.splice(index, 1); renderFreieOrdner(); } function sammleZugewieseneOrdner() { const checkboxen = document.querySelectorAll('#regel-ordner-liste input[type="checkbox"]:checked'); return Array.from(checkboxen).map(cb => parseInt(cb.value)); } async function bearbeiteRegel(id) { try { const regeln = await api('/regeln'); const regel = regeln.find(r => r.id === id); if (regel) zeigeRegelModal(regel); } catch (error) { showAlert(error.message, 'error'); } } async function speichereRegel() { // Muster aus UI-Feldern zusammenbauen const muster = {}; const keywords = document.getElementById('regel-keywords').value.trim(); const keywordsNicht = document.getElementById('regel-keywords-nicht').value.trim(); const auchDateiname = document.getElementById('regel-auch-dateiname').checked; const textRegex = document.getElementById('regel-text-regex').value.trim(); if (keywords) muster.keywords = keywords; if (keywordsNicht) muster.keywords_nicht = keywordsNicht; if (auchDateiname) muster.auch_dateiname = true; if (textRegex) muster.text_regex = textRegex; // Extraktion aus Tabelle sammeln const extraktion = sammleExtraktionAusTabelle(); // Versteckte Felder für Kompatibilität aktualisieren document.getElementById('regel-muster').value = JSON.stringify(muster); document.getElementById('regel-extraktion').value = JSON.stringify(extraktion); const data = { name: document.getElementById('regel-name').value.trim(), prioritaet: parseInt(document.getElementById('regel-prioritaet').value), muster, extraktion, nur_umbenennen: document.getElementById('regel-nur-umbenennen').checked, ziel_ordner: document.getElementById('regel-ziel-ordner').value.trim() || null, schema: document.getElementById('regel-schema').value.trim(), unterordner: document.getElementById('regel-unterordner').value.trim() || null, ist_fallback: document.getElementById('regel-ist-fallback').checked }; if (!data.name) { showAlert('Bitte einen Namen eingeben', 'warning'); return; } try { let regelId = editierteRegelId; if (regelId) { await api(`/regeln/${regelId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { const result = await api('/regeln', { method: 'POST', body: JSON.stringify(data) }); regelId = result.id; } // Ordner-Zuweisungen speichern (inkl. freie Ordner) const zugewieseneOrdner = sammleZugewieseneOrdner(); await api(`/regeln/${regelId}/ordner`, { method: 'PUT', body: JSON.stringify({ ordner_ids: zugewieseneOrdner, freie_ordner: aktuelleFreieOrdner }) }); schliesseModal('regel-modal'); ladeRegeln(); } catch (error) { showAlert(error.message, 'error'); } } async function regelAktivieren(id) { try { await api(`/regeln/${id}/aktivieren`, { method: 'POST' }); ladeRegeln(); } catch (error) { showAlert(error.message, 'error'); } } async function regelLoeschen(id) { if (!await showConfirm('Regel wirklich löschen?')) return; try { await api(`/regeln/${id}`, { method: 'DELETE' }); ladeRegeln(); } catch (error) { showAlert(error.message, 'error'); } } // PDF für Regel-Test hochladen let testPdfDatei = null; async function ladeTestPDF() { const input = document.getElementById('regel-test-datei'); if (!input.files || !input.files[0]) return; testPdfDatei = input.files[0]; document.getElementById('test-datei-name').textContent = `📄 ${testPdfDatei.name}`; // PDF-Text extrahieren const formData = new FormData(); formData.append('datei', testPdfDatei); try { zeigeLoading('Extrahiere PDF-Text...'); const response = await fetch('/api/pdf/extrahieren', { method: 'POST', body: formData }); if (!response.ok) { throw new Error('Fehler beim Extrahieren'); } const result = await response.json(); // Text in beide Elemente (verstecktes textarea + sichtbares div) document.getElementById('regel-test-text').value = result.text; // Text im Display-Bereich anzeigen (mit kompakter Info-Zeile) const displayEl = document.getElementById('regel-test-text-display'); let badges = []; if (result.seiten) badges.push(`${result.seiten}S`); if (result.ist_zugferd) badges.push('ZUGFeRD'); if (result.ist_signiert) badges.push('Signiert'); if (result.ocr_durchgefuehrt) badges.push('OCR'); const infoHtml = badges.length > 0 ? `${badges.join(' · ')}
    ` : ''; displayEl.innerHTML = infoHtml + `
    ${escapeHtml(result.text)}
    `; // Automatisch testen testeRegelLive(); } catch (error) { showAlert(error.message, 'error'); } finally { versteckeLoading(); } } async function testeRegelLive() { console.log('testeRegelLive() gestartet'); const textElement = document.getElementById('regel-test-text'); const text = textElement ? textElement.value : ''; if (!text) { // Kein Alert - einfach nichts tun wenn kein Text return; } // Muster aus UI-Feldern sammeln const muster = {}; const keywordsStr = document.getElementById('regel-keywords')?.value.trim(); const keywordsNicht = document.getElementById('regel-keywords-nicht')?.value.trim(); const textRegex = document.getElementById('regel-text-regex')?.value.trim(); if (keywordsStr) muster.keywords = keywordsStr; if (keywordsNicht) muster.keywords_nicht = keywordsNicht; if (textRegex) muster.text_regex = textRegex; // Extraktion aus Tabelle sammeln const extraktion = sammleExtraktionAusTabelle(); const regel = { name: 'Test', muster, extraktion, schema: document.getElementById('regel-schema').value.trim() || '{datum} - Dokument.pdf' }; 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'); // Status Box mit Keyword-Info const statusDiv = document.getElementById('test-status'); let statusHtml = ''; if (result.passt) { statusDiv.className = 'test-status-box success'; statusHtml = '✓ Regel passt!'; } else { statusDiv.className = 'test-status-box error'; statusHtml = '✗ Regel passt nicht'; } // Keyword-Matching anzeigen if (keywordsStr) { const keywords = keywordsStr.split(',').map(k => k.trim()).filter(k => k); const gefunden = keywords.filter(kw => text.toLowerCase().includes(kw.toLowerCase())); const nichtGefunden = keywords.filter(kw => !text.toLowerCase().includes(kw.toLowerCase())); if (gefunden.length > 0 || nichtGefunden.length > 0) { statusHtml += '
    '; if (gefunden.length > 0) { statusHtml += `✓ ${gefunden.join(', ')}`; } if (nichtGefunden.length > 0) { statusHtml += ` ✗ ${nichtGefunden.join(', ')}`; } statusHtml += '
    '; } } statusDiv.innerHTML = statusHtml; // Extrahierte Felder anzeigen const extrahiertDiv = document.getElementById('test-extrahiert'); if (result.extrahiert && Object.keys(result.extrahiert).length > 0) { let html = ''; for (const [key, value] of Object.entries(result.extrahiert)) { html += `
    {${key}} ${escapeHtml(String(value))}
    `; } extrahiertDiv.innerHTML = html; } else { extrahiertDiv.innerHTML = 'Keine Felder extrahiert'; } // Vorgeschlagener Dateiname const dateinameDiv = document.getElementById('test-dateiname'); if (result.dateiname) { dateinameDiv.innerHTML = `📁 ${escapeHtml(result.dateiname)}`; dateinameDiv.style.display = 'block'; } else { dateinameDiv.style.display = 'none'; } // Text-Highlighting im PDF-Display highlightPdfText(text, muster, result.extrahiert || {}); } catch (error) { console.error('testeRegelLive Fehler:', error); } } // Text-Highlighting für Keywords und extrahierte Werte function highlightPdfText(text, muster, extrahiert) { const contentEl = document.getElementById('pdf-text-content'); if (!contentEl) return; let html = escapeHtml(text); // Keywords highlighten (grün) if (muster.keywords) { const keywords = typeof muster.keywords === 'string' ? muster.keywords.split(',').map(k => k.trim()) : muster.keywords; keywords.forEach(kw => { if (kw) { const regex = new RegExp(`(${escapeRegex(kw)})`, 'gi'); html = html.replace(regex, '$1'); } }); } // Extrahierte Werte highlighten (orange) if (extrahiert) { Object.values(extrahiert).forEach(wert => { if (wert && typeof wert === 'string' && wert.length > 2) { const regex = new RegExp(`(${escapeRegex(wert)})`, 'g'); html = html.replace(regex, '$1'); } }); } contentEl.innerHTML = html; } // Hilfsfunktion: Regex-Sonderzeichen escapen function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // ============ Regex-Helfer: Aus Markierung Regex erstellen ============ function regexAusMarkierung() { // Markierten Text aus dem PDF-Content-Bereich holen const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (!selectedText) { showAlert('Bitte zuerst einen Text im PDF-Inhalt markieren', 'warning'); return; } // Kontext vor und nach der Markierung holen (wenn möglich) const pdfContent = document.getElementById('pdf-text-content'); if (!pdfContent) { showAlert('Bitte zuerst eine PDF laden', 'warning'); return; } const fullText = pdfContent.textContent || ''; const pos = fullText.indexOf(selectedText); let regexVorschlag = ''; let beschreibung = ''; // Prüfen ob es ein Muster ist (Datum, Betrag, Nummer, Label+Nummer) if (/^\d{1,2}[.\/]\d{1,2}[.\/]\d{2,4}$/.test(selectedText)) { // Datum erkannt regexVorschlag = erstelleDatumRegex(fullText, selectedText, pos); beschreibung = 'Datum erkannt'; } else if (/^[\d.,]+\s*(€|EUR)?$/.test(selectedText)) { // Betrag erkannt regexVorschlag = erstelleBetragRegex(fullText, selectedText, pos); beschreibung = 'Betrag erkannt'; } else if (/^[A-Z0-9][\w\-\/]+$/i.test(selectedText)) { // Nummer/ID erkannt (nur Buchstaben/Zahlen ohne Leerzeichen) regexVorschlag = erstelleNummerRegex(fullText, selectedText, pos); beschreibung = 'Nummer/ID erkannt'; } else if (/^[A-Za-zäöüÄÖÜß]+[:\s.\-#]+\d+$/i.test(selectedText)) { // Label + Nummer erkannt (z.B. "Rechnung 2493150", "Invoice: 12345") regexVorschlag = erstelleLabelNummerRegex(selectedText); beschreibung = 'Label + Nummer erkannt'; } else if (/^[A-Za-zäöüÄÖÜß]+[:\s.\-#]+[\w\-\/]+$/i.test(selectedText)) { // Label + ID erkannt (z.B. "Beleg RE-2024-001") regexVorschlag = erstelleLabelIdRegex(selectedText); beschreibung = 'Label + ID erkannt'; } else { // Allgemeiner Text - Kontext-basiertes Regex regexVorschlag = erstelleKontextRegex(fullText, selectedText, pos); beschreibung = 'Text mit Kontext'; } // Ergebnis anzeigen zeigeRegexHelferErgebnis(selectedText, regexVorschlag, beschreibung); } function erstelleDatumRegex(fullText, selected, pos) { // Kontext vor dem Datum finden (z.B. "Rechnungsdatum:", "Datum:") const kontextVorher = fullText.substring(Math.max(0, pos - 30), pos); const labelMatch = kontextVorher.match(/(\w+[-\s]?\w*)[:\s]*$/); if (labelMatch) { const label = labelMatch[1].trim(); return `${escapeRegex(label)}[:\\s]*(\\d{1,2}[./]\\d{1,2}[./]\\d{2,4})`; } return '(\\d{1,2}[./]\\d{1,2}[./]\\d{2,4})'; } function erstelleBetragRegex(fullText, selected, pos) { const kontextVorher = fullText.substring(Math.max(0, pos - 30), pos); const labelMatch = kontextVorher.match(/(\w+[-\s]?\w*)[:\s]*$/); if (labelMatch) { const label = labelMatch[1].trim(); return `${escapeRegex(label)}[:\\s]*([\\d.,]+)\\s*€?`; } return '([\\d.,]+)\\s*€'; } function erstelleNummerRegex(fullText, selected, pos) { const kontextVorher = fullText.substring(Math.max(0, pos - 40), pos); const labelMatch = kontextVorher.match(/([A-Za-zäöüÄÖÜß]+(?:[-\s]?[A-Za-zäöüÄÖÜß]+)?)[:\s#]*$/); if (labelMatch) { const label = labelMatch[1].trim(); return `${escapeRegex(label)}[:\\s#]*([A-Z0-9][\\w\\-/]+)`; } return '([A-Z0-9][\\w\\-/]+)'; } function erstelleLabelNummerRegex(selected) { // Für Texte wie "Rechnung 2493150", "Invoice: 12345", "Beleg-Nr. 789" // Splittet in Label und Nummer, erstellt Regex das die Nummer captured const match = selected.match(/^([A-Za-zäöüÄÖÜß]+)[:\s.\-#]+(\d+)$/i); if (match) { const label = match[1]; return `${escapeRegex(label)}[:\\s.\\-#]*(\\d+)`; } // Fallback: ganzer Text escaped mit Gruppe return `(${escapeRegex(selected)})`; } function erstelleLabelIdRegex(selected) { // Für Texte wie "Beleg RE-2024-001", "Order ABC123" // Splittet in Label und ID, erstellt Regex das die ID captured const match = selected.match(/^([A-Za-zäöüÄÖÜß]+)[:\s.\-#]+([\w\-\/]+)$/i); if (match) { const label = match[1]; return `${escapeRegex(label)}[:\\s.\\-#]*([\\w\\-/]+)`; } // Fallback: ganzer Text escaped mit Gruppe return `(${escapeRegex(selected)})`; } function erstelleKontextRegex(fullText, selected, pos) { // Kontext vor dem Text holen (max 50 Zeichen) const kontextVorher = fullText.substring(Math.max(0, pos - 50), pos); // Versuche ein Label zu finden (z.B. "Rechnungsnummer:", "Rechnung Nr.", "Beleg:") // Suche nach Wort(en) gefolgt von : oder Leerzeichen direkt vor dem markierten Text const labelMatch = kontextVorher.match(/([A-Za-zäöüÄÖÜß]+(?:[-\s]?[A-Za-zäöüÄÖÜß]+)?)\s*[:\s#.]*\s*$/); if (labelMatch) { const label = labelMatch[1].trim(); if (label.length >= 2) { // Label gefunden - baue Regex: Label gefolgt von : oder Whitespace, dann der Wert in einer Gruppe return `${escapeRegex(label)}[:\\s#.]*\\s*(${escapeRegex(selected)})`; } } // Kein brauchbares Label gefunden - nimm die letzten 1-2 Wörter als Kontext const worte = kontextVorher.trim().split(/\s+/).filter(w => w.length > 0); if (worte.length >= 1) { // Nimm das letzte Wort als Kontext (escapen, dann \\s+ anhängen) const letztesWort = worte[worte.length - 1]; if (letztesWort.length >= 2 && /[A-Za-zäöüÄÖÜß]/.test(letztesWort)) { return `${escapeRegex(letztesWort)}\\s+(${escapeRegex(selected)})`; } } // Fallback: nur den markierten Text escapen return `(${escapeRegex(selected)})`; } async function zeigeRegexHelferErgebnis(originalText, regex, beschreibung) { // Modal oder Inline-Anzeige const container = document.getElementById('regex-helfer-ergebnis'); if (container) { container.classList.remove('hidden'); container.innerHTML = `
    ${beschreibung}
    Markierter Text: ${escapeHtml(originalText)}
    Regex-Vorschlag:
    `; } else { // Fallback: Dialog + Clipboard if (await showConfirm(`${beschreibung}\n\nRegex-Vorschlag:\n${regex}\n\nIn Zwischenablage kopieren?`, 'Regex-Vorschlag')) { navigator.clipboard.writeText(regex).then(() => { showAlert('Regex in Zwischenablage kopiert!', 'success'); }); } } } function kopieRegelRegex() { const input = document.getElementById('regex-vorschlag-input'); if (input) { navigator.clipboard.writeText(input.value).then(() => { // Kurzes Feedback const btn = event.target; const originalText = btn.textContent; btn.textContent = '✓ Kopiert!'; setTimeout(() => btn.textContent = originalText, 1500); }); } } // Alte Funktion für Kompatibilität async function testeRegel() { console.log('testeRegel() aufgerufen -> testeRegelLive()'); testeRegelLive(); } // ============ Regel-Assistent ============ const REGEX_VORLAGEN = { datum: { auto: null, // Nutzt globale Extraktoren rechnungsdatum: "Rechnungsdatum[:\\s]*(\\d{2}[./]\\d{2}[./]\\d{4})", datum: "Datum[:\\s]*(\\d{2}[./]\\d{2}[./]\\d{4})", beliebig: "(\\d{2}[./]\\d{2}[./]\\d{4})" }, betrag: { auto: null, gesamtbetrag: "Gesamtbetrag[:\\s]*([\\d.,]+)", summe: "Summe[:\\s]*([\\d.,]+)", brutto: "Brutto[:\\s]*([\\d.,]+)" }, nummer: { auto: null, rechnungsnummer: "Rechnungsnummer[:\\s#]*([A-Z0-9][\\w\\-/]+)", belegnr: "Beleg-?Nr\\.?[:\\s#]*([A-Z0-9][\\w\\-/]+)", invoice: "Invoice[:\\s#]*([A-Z0-9][\\w\\-/]+)" } }; function zeigeRegelAssistent() { // Modal öffnen document.getElementById('assistent-modal').classList.remove('hidden'); // Event-Listener für Live-Vorschau ['ass-keywords', 'ass-firma', 'ass-datum-typ', 'ass-betrag-typ', 'ass-nummer-typ', 'ass-schema', 'ass-unterordner'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', aktualisiereAssistentVorschau); if (el) el.addEventListener('change', aktualisiereAssistentVorschau); }); ['ass-datum-aktiv', 'ass-betrag-aktiv', 'ass-nummer-aktiv'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('change', aktualisiereAssistentVorschau); }); aktualisiereAssistentVorschau(); } function aktualisiereAssistentVorschau() { const vorschauDiv = document.getElementById('ass-vorschau'); const regel = baueRegelAusAssistent(); let html = ''; // Keywords const keywords = document.getElementById('ass-keywords').value.trim(); if (keywords) { html += `
    Erkennung: Dokument muss "${keywords}" enthalten
    `; } else { html += `
    Erkennung: Passt auf ALLE Dateien (keine Keywords)
    `; } // Firma const firma = document.getElementById('ass-firma').value.trim(); if (firma) { html += `
    Firma: ${escapeHtml(firma)}
    `; } // Felder html += `
    Extrahiere: `; const felder = []; if (document.getElementById('ass-datum-aktiv').checked) felder.push('📅 Datum'); if (document.getElementById('ass-betrag-aktiv').checked) felder.push('💰 Betrag'); if (document.getElementById('ass-nummer-aktiv').checked) felder.push('🔢 Nummer'); html += felder.join(', ') || 'nichts'; html += '
    '; // Beispiel-Dateiname const schema = document.getElementById('ass-schema').value; let beispiel = schema .replace('{datum}', '2024-01-15') .replace('{firma}', firma || 'Firma') .replace('{nummer}', 'RE-12345') .replace('{betrag}', '199,99'); html += `
    Beispiel-Dateiname:
    ${escapeHtml(beispiel)}
    `; vorschauDiv.innerHTML = html; } function baueRegelAusAssistent() { const keywords = document.getElementById('ass-keywords').value.trim(); const firma = document.getElementById('ass-firma').value.trim(); const schema = document.getElementById('ass-schema').value; const unterordner = document.getElementById('ass-unterordner').value.trim(); // Muster const muster = {}; if (keywords) { muster.keywords = keywords; } // Extraktion const extraktion = {}; // Firma if (firma) { extraktion.firma = { wert: firma }; } // Datum if (document.getElementById('ass-datum-aktiv').checked) { const datumTyp = document.getElementById('ass-datum-typ').value; if (datumTyp !== 'auto' && REGEX_VORLAGEN.datum[datumTyp]) { extraktion.datum = { regex: REGEX_VORLAGEN.datum[datumTyp] }; } // Bei "auto" wird nichts eingetragen - globale Extraktoren werden genutzt } // Betrag if (document.getElementById('ass-betrag-aktiv').checked) { const betragTyp = document.getElementById('ass-betrag-typ').value; if (betragTyp !== 'auto' && REGEX_VORLAGEN.betrag[betragTyp]) { extraktion.betrag = { regex: REGEX_VORLAGEN.betrag[betragTyp], typ: "betrag" }; } } // Nummer if (document.getElementById('ass-nummer-aktiv').checked) { const nummerTyp = document.getElementById('ass-nummer-typ').value; if (nummerTyp !== 'auto' && REGEX_VORLAGEN.nummer[nummerTyp]) { extraktion.nummer = { regex: REGEX_VORLAGEN.nummer[nummerTyp] }; } } return { muster, extraktion, schema, unterordner }; } function assistentUebernehmen() { const regel = baueRegelAusAssistent(); // In die Regel-Felder übernehmen 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 || ''; // Regelname vorschlagen wenn leer const nameField = document.getElementById('regel-name'); if (!nameField.value.trim()) { const firma = document.getElementById('ass-firma').value.trim(); const keywords = document.getElementById('ass-keywords').value.trim(); if (firma) { nameField.value = firma + ' Rechnung'; } else if (keywords) { nameField.value = keywords.split(',')[0].trim() + ' Rechnung'; } } schliesseModal('assistent-modal'); // Wenn PDF-Text vorhanden, automatisch testen if (document.getElementById('regel-test-text').value.trim()) { setTimeout(() => testeRegelLive(), 300); } } // ============ Auto-Regex Generator ============ let letzteAutoRegexErgebnis = null; async function autoRegexGenerieren() { const input = document.getElementById('regel-test-datei'); if (!input.files || !input.files[0]) { showAlert('Bitte zuerst eine PDF-Datei hochladen', 'warning'); return; } const formData = new FormData(); formData.append('datei', input.files[0]); try { zeigeLoading('Analysiere PDF...'); const response = await fetch('/api/pdf/auto-regex', { method: 'POST', body: formData }); if (!response.ok) { throw new Error('Fehler bei der Analyse'); } const result = await response.json(); letzteAutoRegexErgebnis = result; // Ergebnis anzeigen const container = document.getElementById('auto-regex-ergebnis'); const felderDiv = document.getElementById('auto-regex-felder'); const keywordsDiv = document.getElementById('auto-regex-keywords'); container.classList.remove('hidden'); // Erkannte Felder anzeigen if (result.erkannte_felder && result.erkannte_felder.length > 0) { let html = ''; html += ''; for (const feld of result.erkannte_felder) { const feldName = { 'datum': '📅 Datum', 'betrag': '💰 Betrag', 'nummer': '🔢 Nummer', 'firma': '🏢 Firma' }[feld.feld] || feld.feld; html += ``; } html += '
    FeldWertKontext
    ${feldName} ${escapeHtml(feld.extrahiert || feld.wert)} ${escapeHtml(feld.kontext)}
    '; felderDiv.innerHTML = html; } else { felderDiv.innerHTML = '

    Keine Felder automatisch erkannt

    '; } // Keywords anzeigen if (result.gefundene_keywords && result.gefundene_keywords.length > 0) { keywordsDiv.innerHTML = `
    🏷️ Keywords: ${result.gefundene_keywords.join(', ')}
    `; } else { keywordsDiv.innerHTML = ''; } // Text auch anzeigen if (result.text_vorschau) { document.getElementById('regel-test-text').value = result.text_vorschau; } // DIREKT die Regel-Felder ausfüllen (nicht nur anzeigen) wendeAutoRegexAn(); } catch (error) { showAlert(error.message, 'error'); } finally { versteckeLoading(); } } function wendeAutoRegexAn() { if (!letzteAutoRegexErgebnis || !letzteAutoRegexErgebnis.regel_vorschlag) { // Stille Rückkehr wenn keine Ergebnisse (wird automatisch aufgerufen) return; } const vorschlag = letzteAutoRegexErgebnis.regel_vorschlag; const erkannteFelder = letzteAutoRegexErgebnis.erkannte_felder || []; // Muster immer übernehmen (auch wenn leer = passt auf alles) const muster = vorschlag.muster || {}; document.getElementById('regel-muster').value = JSON.stringify(muster, null, 2); // Extraktion immer übernehmen const extraktion = vorschlag.extraktion || {}; document.getElementById('regel-extraktion').value = JSON.stringify(extraktion, null, 2); // Schema übernehmen if (vorschlag.schema) { document.getElementById('regel-schema').value = vorschlag.schema; } // Auch Assistenten-Felder vorausfüllen (für spätere Nutzung) if (letzteAutoRegexErgebnis.gefundene_keywords) { document.getElementById('ass-keywords').value = letzteAutoRegexErgebnis.gefundene_keywords.join(', '); } // Firma aus erkannten Feldern const firmaFeld = erkannteFelder.find(f => f.feld === 'firma'); if (firmaFeld) { document.getElementById('ass-firma').value = firmaFeld.wert; } // Regelname vorschlagen const nameField = document.getElementById('regel-name'); if (!nameField.value.trim() && firmaFeld) { nameField.value = firmaFeld.wert + ' Rechnung'; } // Hinweis anzeigen const container = document.getElementById('auto-regex-ergebnis'); if (container) { container.classList.remove('hidden'); container.innerHTML = `
    ✓ Erkannte Muster wurden übernommen!
    Die Felder wurden automatisch ausgefüllt.
    `; } // Auto-Test starten setTimeout(() => testeRegelLive(), 500); } // ============ Sortierung starten ============ async function sortierungStarten() { const container = document.getElementById('sortierung-log'); container.innerHTML = '

    Sortierung läuft...

    '; try { const result = await api('/sortierung/starten', { method: 'POST' }); zeigeSortierungLog(result); } catch (error) { container.innerHTML = `
    ✗ Fehler: ${escapeHtml(error.message)}
    `; } } function zeigeSortierungLog(result) { const container = document.getElementById('sortierung-log'); if (!result.verarbeitet || result.verarbeitet.length === 0) { container.innerHTML = '

    Keine Dateien verarbeitet

    '; return; } let html = `
    Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Fehler: ${result.fehler}
    `; for (const d of result.verarbeitet) { const status = d.fehler ? 'error' : (d.zugferd ? 'info' : 'success'); const icon = d.fehler ? '✗' : (d.zugferd ? '🧾' : '✓'); html += `
    ${icon} ${escapeHtml(d.neuer_name || d.original)} ${d.fehler ? `${escapeHtml(d.fehler)}` : ''}
    `; } container.innerHTML = html; } // ============ BEREICH 3: Zeitpläne ============ let editierterZeitplanId = null; async function ladeZeitplaene() { try { const result = await api('/zeitplaene'); renderZeitplaene(result.zeitplaene || []); } catch (error) { console.error('Fehler:', error); } } function renderZeitplaene(zeitplaene) { const container = document.getElementById('zeitplaene-liste'); if (!zeitplaene || zeitplaene.length === 0) { container.innerHTML = '

    Keine Zeitpläne konfiguriert

    '; return; } container.innerHTML = zeitplaene.map(zp => { const statusClass = zp.letzter_status === 'erfolg' ? 'success' : (zp.letzter_status === 'fehler' ? 'error' : ''); const aktivClass = zp.aktiv ? '' : 'opacity: 0.5;'; const typIcon = zp.typ === 'mail_abruf' ? '📧' : (zp.typ === 'grobsortierung' ? '📁' : '📋'); const naechste = zp.naechste_ausfuehrung ? formatDatum(zp.naechste_ausfuehrung) : '-'; const letzte = zp.letzte_ausfuehrung ? formatDatum(zp.letzte_ausfuehrung) : 'Noch nie'; return `

    ${typIcon} ${escapeHtml(zp.name)} ${zp.aktiv ? 'Aktiv' : 'Inaktiv'} ${zp.intervall}

    Nächste: ${naechste} | Letzte: ${letzte} ${zp.letzter_status ? `Status: ${zp.letzter_status}${zp.letzte_meldung ? ' - ' + escapeHtml(zp.letzte_meldung.substring(0, 80)) : ''}` : ''}
    `}).join(''); } async function ladeStatus() { try { const result = await api('/status/uebersicht'); renderStatusUebersicht(result); } catch (error) { console.error('Fehler:', error); } } function renderStatusUebersicht(status) { const container = document.getElementById('status-uebersicht'); let html = '
    '; // Postfächer html += '

    📧 Postfächer

    '; if (status.postfaecher && status.postfaecher.length > 0) { for (const p of status.postfaecher) { const aktiv = p.aktiv ? '🟢' : '⚪'; const letzte = p.letzter_abruf ? formatDatum(p.letzter_abruf) : 'Nie'; html += `
    ${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)
    `; } } else { html += '
    Keine Postfächer
    '; } html += '
    '; // Grobsortierung html += '

    📁 Grobsortierung

    '; if (status.quell_ordner && status.quell_ordner.length > 0) { for (const o of status.quell_ordner) { const aktiv = o.aktiv ? '🟢' : '⚪'; html += `
    ${aktiv} ${escapeHtml(o.name)}
    `; } } else { html += '
    Keine Ordner
    '; } html += '
    '; // Scheduler html += '

    ⏰ Scheduler

    '; const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Läuft' : '🔴 Gestoppt'; html += `
    ${schedulerStatus}
    `; html += '
    '; html += '
    '; container.innerHTML = html; } function formatDatum(isoString) { if (!isoString) return '-'; const d = new Date(isoString); // Explizite Formatierung mit führenden Nullen const tag = String(d.getDate()).padStart(2, '0'); const monat = String(d.getMonth() + 1).padStart(2, '0'); const jahr = d.getFullYear(); const stunde = String(d.getHours()).padStart(2, '0'); const minute = String(d.getMinutes()).padStart(2, '0'); return `${tag}.${monat}.${jahr} ${stunde}:${minute}`; } async function zeigeZeitplanModal(zeitplan = null) { editierterZeitplanId = zeitplan?.id || null; document.getElementById('zeitplan-modal-title').textContent = zeitplan ? 'Zeitplan bearbeiten' : 'Zeitplan hinzufügen'; document.getElementById('zp-name').value = zeitplan?.name || ''; document.getElementById('zp-typ').value = zeitplan?.typ || 'mail_abruf'; document.getElementById('zp-intervall').value = zeitplan?.intervall || 'täglich'; document.getElementById('zp-stunde').value = zeitplan?.stunde ?? 6; document.getElementById('zp-minute').value = zeitplan?.minute ?? 0; document.getElementById('zp-wochentag').value = zeitplan?.wochentag ?? 0; document.getElementById('zp-monatstag').value = zeitplan?.monatstag ?? 1; // Postfächer, Ordner und Regeln laden für Dropdowns await ladeZeitplanOptionen(); document.getElementById('zp-postfach').value = zeitplan?.postfach_id || ''; document.getElementById('zp-ordner').value = zeitplan?.quell_ordner_id || ''; document.getElementById('zp-regel').value = zeitplan?.regel_id || ''; zeitplanTypChanged(); zeitplanIntervallChanged(); document.getElementById('zeitplan-modal').classList.remove('hidden'); } async function ladeZeitplanOptionen() { try { // Postfächer laden const postfaecher = await api('/postfaecher'); const pfSelect = document.getElementById('zp-postfach'); pfSelect.innerHTML = '' + postfaecher.map(p => ``).join(''); // Ordner laden const ordner = await api('/ordner'); const ordSelect = document.getElementById('zp-ordner'); ordSelect.innerHTML = '' + ordner.map(o => ``).join(''); // Regeln laden const regeln = await api('/regeln'); const regelSelect = document.getElementById('zp-regel'); regelSelect.innerHTML = '' + regeln.map(r => ``).join(''); } catch (error) { console.error('Fehler:', error); } } function zeitplanTypChanged() { const typ = document.getElementById('zp-typ').value; document.getElementById('zp-postfach-gruppe').classList.toggle('hidden', typ !== 'mail_abruf'); document.getElementById('zp-ordner-gruppe').classList.toggle('hidden', typ !== 'grobsortierung'); document.getElementById('zp-regel-gruppe').classList.toggle('hidden', typ !== 'sortierregeln'); } function zeitplanIntervallChanged() { const intervall = document.getElementById('zp-intervall').value; document.getElementById('zp-zeit-gruppe').classList.toggle('hidden', intervall === 'stündlich'); document.getElementById('zp-wochentag-gruppe').classList.toggle('hidden', intervall !== 'wöchentlich'); document.getElementById('zp-monatstag-gruppe').classList.toggle('hidden', intervall !== 'monatlich'); // Info-Text aktualisieren const info = document.getElementById('zp-intervall-info'); if (info) { const infos = { 'stündlich': 'Wird jede Stunde zur angegebenen Minute ausgeführt', 'täglich': 'Wird jeden Tag einmal zur angegebenen Uhrzeit ausgeführt', 'wöchentlich': 'Wird einmal pro Woche am angegebenen Tag und Uhrzeit ausgeführt', 'monatlich': 'Wird einmal pro Monat am angegebenen Tag und Uhrzeit ausgeführt' }; info.textContent = infos[intervall] || ''; } } async function speichereZeitplan() { const data = { name: document.getElementById('zp-name').value.trim(), typ: document.getElementById('zp-typ').value, intervall: document.getElementById('zp-intervall').value, stunde: parseInt(document.getElementById('zp-stunde').value) || 0, minute: parseInt(document.getElementById('zp-minute').value) || 0, wochentag: document.getElementById('zp-intervall').value === 'wöchentlich' ? parseInt(document.getElementById('zp-wochentag').value) : null, monatstag: document.getElementById('zp-intervall').value === 'monatlich' ? parseInt(document.getElementById('zp-monatstag').value) : null }; // Optionale IDs const postfachId = document.getElementById('zp-postfach').value; const ordnerId = document.getElementById('zp-ordner').value; const regelId = document.getElementById('zp-regel').value; if (data.typ === 'mail_abruf' && postfachId) { data.postfach_id = parseInt(postfachId); } if (data.typ === 'grobsortierung' && ordnerId) { data.quell_ordner_id = parseInt(ordnerId); } if (data.typ === 'sortierregeln' && regelId) { data.regel_id = parseInt(regelId); } if (!data.name) { showAlert('Bitte einen Namen eingeben', 'warning'); return; } try { if (editierterZeitplanId) { await api(`/zeitplaene/${editierterZeitplanId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await api('/zeitplaene', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('zeitplan-modal'); ladeZeitplaene(); ladeStatus(); } catch (error) { showAlert(error.message, 'error'); } } async function zeitplanAktivieren(id) { try { await api(`/zeitplaene/${id}/aktivieren`, { method: 'POST' }); ladeZeitplaene(); } catch (error) { showAlert(error.message, 'error'); } } async function zeitplanAusfuehren(id) { try { zeigeLoading('Führe Zeitplan aus...'); const result = await api(`/zeitplaene/${id}/ausfuehren`, { method: 'POST' }); showAlert(result.meldung || 'Ausgeführt', 'success', 'Zeitplan ausgeführt'); ladeZeitplaene(); ladeStatus(); } catch (error) { showAlert(error.message, 'error'); } finally { versteckeLoading(); } } async function zeitplanBearbeiten(id) { try { const zeitplan = await api(`/zeitplaene/${id}`); zeigeZeitplanModal(zeitplan); } catch (error) { showAlert('Fehler beim Laden: ' + error.message, 'error'); } } async function zeitplanLoeschen(id) { if (!await showConfirm('Zeitplan wirklich löschen?')) return; try { await api(`/zeitplaene/${id}`, { method: 'DELETE' }); ladeZeitplaene(); } catch (error) { showAlert(error.message, 'error'); } } // ============ 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; } function truncatePath(path, maxLength = 35) { if (!path) return ''; const escaped = escapeHtml(path); if (path.length <= maxLength) return escaped; // Pfad kürzen: Anfang ... Ende const start = path.substring(0, 15); const end = path.substring(path.length - 17); return `${escapeHtml(start)}...${escapeHtml(end)}`; } // Modal nur über X-Button oder Abbrechen schließen, nicht durch Klick auf Hintergrund // (entfernt: Klick auf Modal-Hintergrund schließt nicht mehr) document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal:not(.hidden)').forEach(m => m.classList.add('hidden')); } }); // ============ Tab Navigation ============ function wechsleTab(tabName) { // Alle Tabs deaktivieren document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); // Gewählten Tab aktivieren document.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active'); document.getElementById(`tab-${tabName}`).classList.add('active'); // Tab in localStorage speichern localStorage.setItem('aktiver-tab', tabName); // Tab-spezifische Daten laden switch(tabName) { case 'mailabruf': ladePostfaecher(); ladeZeitplaeneNachTyp('mail_abruf', 'zeitplaene-mailabruf'); break; case 'grobsortierung': ladeOrdner(); ladeZeitplaeneNachTyp('grobsortierung', 'zeitplaene-grobsortierung'); break; case 'feinsortierung': ladeRegeln(); ladeZeitplaeneNachTyp('sortierregeln', 'zeitplaene-feinsortierung'); break; case 'dbbackup': ladeDbServer(); ladeDatenbanken(); ladeBackups(); ladeZeitplaeneNachTyp('db_backup', 'zeitplaene-dbbackup'); break; } ladeStatus(); debugLog(`Tab gewechselt: ${tabName}`); } // ============ Debug Log Header ============ let lastDebugMessage = ''; function debugLog(message, type = 'info') { lastDebugMessage = message; const textEl = document.getElementById('debug-log-text'); if (textEl) { textEl.textContent = message; textEl.className = 'debug-log-text ' + type; } console.log(`[DEBUG] ${message}`); } // ============ Zeitpläne nach Typ laden ============ async function ladeZeitplaeneNachTyp(typ, containerId) { try { const zeitplaene = await api('/zeitplaene'); const gefiltert = zeitplaene.filter(z => z.typ === typ); const container = document.getElementById(containerId); if (gefiltert.length === 0) { container.innerHTML = '

    Keine Zeitpläne

    '; return; } container.innerHTML = gefiltert.map(z => `

    ${escapeHtml(z.name)} ${z.aktiv ? '✓' : ''}

    ${z.intervall} ${z.stunde != null ? `um ${z.stunde}:${String(z.minute || 0).padStart(2, '0')}` : ''}
    `).join(''); } catch (error) { console.error('Fehler beim Laden der Zeitpläne:', error); } } // ============ Grobsortierung / Alle Ordner verarbeiten ============ async function alleOrdnerVerarbeiten() { const logContainer = document.getElementById('grobsortierung-log'); logContainer.innerHTML = '
    Starte Grobsortierung...
    '; try { debugLog('Starte Grobsortierung mit Live-Updates...', 'info'); // Streaming-Endpoint verwenden für Live-Updates const response = await fetch('/api/grobsortierung/stream'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let zusammenfassungDiv = null; let gesamt = 0, sortiert = 0, fehler = 0; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); switch (data.type) { case 'start': logContainer.innerHTML = `
    Starte Grobsortierung... ${data.ordner_count} Ordner, ${data.gesamt} Dateien
    `; zusammenfassungDiv = document.createElement('div'); zusammenfassungDiv.className = 'log-entry success'; zusammenfassungDiv.style.cssText = 'position: sticky; top: 0; background: var(--bg-secondary); border-bottom: 1px solid var(--border); z-index: 1;'; zusammenfassungDiv.innerHTML = `Gesamt: 0 | Sortiert: 0 | Fehler: 0`; logContainer.appendChild(zusammenfassungDiv); break; case 'ordner': logContainer.innerHTML += `
    📁 ${escapeHtml(data.ordner)} (${data.dateien} Dateien)
    `; break; case 'datei_fertig': sortiert++; gesamt++; let text = escapeHtml(data.original || ''); if (data.neuer_name) { text += ` → ${escapeHtml(data.neuer_name)}`; } if (data.regel) { text += ` [${escapeHtml(data.regel)}]`; } logContainer.innerHTML += `
    ✓ ${text}
    `; if (zusammenfassungDiv) { zusammenfassungDiv.innerHTML = `Gesamt: ${gesamt} | Sortiert: ${sortiert} | Fehler: ${fehler}`; } break; case 'datei_keine_regel': gesamt++; logContainer.innerHTML += `
    ⚠ ${escapeHtml(data.original)} - keine passende Regel
    `; if (zusammenfassungDiv) { zusammenfassungDiv.innerHTML = `Gesamt: ${gesamt} | Sortiert: ${sortiert} | Fehler: ${fehler}`; } break; case 'datei_fehler': case 'ordner_fehler': fehler++; gesamt++; logContainer.innerHTML += `
    ✗ ${escapeHtml(data.original || data.ordner)} (${escapeHtml(data.fehler || 'Fehler')})
    `; if (zusammenfassungDiv) { zusammenfassungDiv.innerHTML = `Gesamt: ${gesamt} | Sortiert: ${sortiert} | Fehler: ${fehler}`; } break; case 'fertig': logContainer.innerHTML += `
    ✓ Grobsortierung abgeschlossen
    `; break; } logContainer.scrollTop = logContainer.scrollHeight; } catch (parseError) { console.error('SSE Parse-Fehler:', parseError, line); } } } } if (gesamt === 0) { logContainer.innerHTML = `
    Keine Dateien zur Verarbeitung gefunden
    `; } debugLog('Grobsortierung abgeschlossen', 'success'); } catch (error) { logContainer.innerHTML = `
    Fehler: ${escapeHtml(error.message)}
    `; debugLog('Fehler: ' + error.message, 'error'); } } // ============ Feinsortierung starten ============ async function feinsortierungStarten() { const logContainer = document.getElementById('sortierung-log'); logContainer.innerHTML = '
    Starte Feinsortierung...
    '; try { debugLog('Starte Feinsortierung mit Live-Updates...', 'info'); // Streaming-Endpoint verwenden für Live-Updates const response = await fetch('/api/sortierung/starten/stream'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let zusammenfassungDiv = null; let gesamt = 0, sortiert = 0, zugferd = 0, fehler = 0; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // Unvollständige Zeile behalten for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); switch (data.type) { case 'start': logContainer.innerHTML = `
    Starte Feinsortierung... ${data.ordner_count} Ordner, ${data.gesamt} Dateien
    `; // Zusammenfassungs-Div am Anfang erstellen zusammenfassungDiv = document.createElement('div'); zusammenfassungDiv.className = 'log-entry success'; zusammenfassungDiv.style.cssText = 'position: sticky; top: 0; background: var(--bg-secondary); border-bottom: 1px solid var(--border); z-index: 1;'; zusammenfassungDiv.innerHTML = `Gesamt: 0 | Sortiert: 0 | ZUGFeRD: 0 | Fehler: 0`; logContainer.appendChild(zusammenfassungDiv); break; case 'ordner': logContainer.innerHTML += `
    📁 ${escapeHtml(data.ordner)} (${data.dateien} Dateien)
    `; break; case 'datei_start': // Optional: Aktuelle Datei anzeigen break; case 'datei_fertig': sortiert++; if (data.zugferd) zugferd++; gesamt++; const hatRegel = data.regel; const icon = hatRegel ? '✓' : 'ℹ'; let text = escapeHtml(data.original || ''); if (data.neuer_name) { text += ` → ${escapeHtml(data.neuer_name)}`; } if (data.regel) { text += ` [${escapeHtml(data.regel)}]`; } if (data.zugferd) { text += ` (ZUGFeRD)`; } logContainer.innerHTML += `
    ${icon} ${text}
    `; // Zusammenfassung aktualisieren if (zusammenfassungDiv) { zusammenfassungDiv.innerHTML = `Gesamt: ${gesamt} | Sortiert: ${sortiert} | ZUGFeRD: ${zugferd} | Fehler: ${fehler}`; } break; case 'datei_fehler': fehler++; gesamt++; logContainer.innerHTML += `
    ✗ ${escapeHtml(data.original || '')} (${escapeHtml(data.fehler || 'Unbekannter Fehler')})
    `; // Zusammenfassung aktualisieren if (zusammenfassungDiv) { zusammenfassungDiv.innerHTML = `Gesamt: ${gesamt} | Sortiert: ${sortiert} | ZUGFeRD: ${zugferd} | Fehler: ${fehler}`; } break; case 'fertig': logContainer.innerHTML += `
    ✓ Feinsortierung abgeschlossen
    `; break; } // Auto-Scroll zum Ende logContainer.scrollTop = logContainer.scrollHeight; } catch (parseError) { console.error('SSE Parse-Fehler:', parseError, line); } } } } if (gesamt === 0) { logContainer.innerHTML = `
    Keine Dateien zur Verarbeitung gefunden
    `; } debugLog('Feinsortierung abgeschlossen', 'success'); } catch (error) { logContainer.innerHTML = `
    Fehler: ${escapeHtml(error.message)}
    `; debugLog('Fehler: ' + error.message, 'error'); } } // ============ Datenbank-Backup Funktionen ============ let editierterDbServerId = null; let editierteDbId = null; async function ladeDbServer() { try { const server = await api('/dbserver'); const container = document.getElementById('dbserver-liste'); if (server.length === 0) { container.innerHTML = '

    Keine Server konfiguriert

    '; return; } container.innerHTML = server.map(s => `

    ${escapeHtml(s.name)} ${s.aktiv ? '✓' : ''}

    ${s.typ} @ ${s.host}:${s.port}
    `).join(''); } catch (error) { debugLog('Fehler beim Laden der DB-Server: ' + error.message, 'error'); } } async function ladeDatenbanken() { try { const dbs = await api('/datenbanken'); const container = document.getElementById('datenbanken-liste'); if (dbs.length === 0) { container.innerHTML = '

    Keine Datenbanken konfiguriert

    '; return; } container.innerHTML = dbs.map(db => `

    ${escapeHtml(db.name)} ${db.aktiv ? '✓' : ''}

    ${escapeHtml(db.database)} (${db.server_name || 'Server'})
    `).join(''); } catch (error) { debugLog('Fehler beim Laden der Datenbanken: ' + error.message, 'error'); } } async function ladeBackups() { try { const backups = await api('/backups'); const container = document.getElementById('backups-liste'); if (!backups || backups.length === 0) { container.innerHTML = '

    Keine Backups vorhanden

    '; return; } container.innerHTML = backups.slice(0, 20).map(b => `
    ${escapeHtml(b.dateiname)} ${b.groesse_mb} MB - ${b.erstellt}
    `).join(''); } catch (error) { // Backups-Endpoint existiert evtl. noch nicht console.log('Backups-Endpoint nicht verfügbar'); } } function zeigeDbServerModal(server = null) { editierterDbServerId = server?.id || null; document.getElementById('dbserver-modal-title').textContent = server ? 'DB-Server bearbeiten' : 'DB-Server hinzufügen'; document.getElementById('dbs-name').value = server?.name || ''; document.getElementById('dbs-typ').value = server?.typ || 'mariadb'; document.getElementById('dbs-host').value = server?.host || ''; document.getElementById('dbs-port').value = server?.port || 3306; document.getElementById('dbs-user').value = server?.user || ''; document.getElementById('dbs-password').value = ''; document.getElementById('dbserver-modal').classList.remove('hidden'); } async function speichereDbServer() { const data = { name: document.getElementById('dbs-name').value.trim(), typ: document.getElementById('dbs-typ').value, host: document.getElementById('dbs-host').value.trim(), port: parseInt(document.getElementById('dbs-port').value) || 3306, user: document.getElementById('dbs-user').value.trim(), password: document.getElementById('dbs-password').value }; if (!data.name || !data.host || !data.user) { showAlert('Bitte alle Pflichtfelder ausfüllen', 'warning'); return; } try { if (editierterDbServerId) { await api(`/dbserver/${editierterDbServerId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await api('/dbserver', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('dbserver-modal'); ladeDbServer(); debugLog('DB-Server gespeichert', 'success'); } catch (error) { showAlert(error.message, 'error'); } } async function testeDbServer() { const data = { typ: document.getElementById('dbs-typ').value, host: document.getElementById('dbs-host').value.trim(), port: parseInt(document.getElementById('dbs-port').value) || 3306, user: document.getElementById('dbs-user').value.trim(), password: document.getElementById('dbs-password').value }; try { zeigeLoading('Teste Verbindung...'); const result = await api('/dbserver/test', { method: 'POST', body: JSON.stringify(data) }); showAlert(result.message || 'Verbindung erfolgreich!', 'success'); } catch (error) { showAlert('Verbindung fehlgeschlagen: ' + error.message, 'error'); } finally { versteckeLoading(); } } async function dbServerBearbeiten(id) { try { const server = await api(`/dbserver/${id}`); zeigeDbServerModal(server); } catch (error) { showAlert(error.message, 'error'); } } async function dbServerAktivieren(id) { try { await api(`/dbserver/${id}/aktivieren`, { method: 'POST' }); ladeDbServer(); } catch (error) { showAlert(error.message, 'error'); } } async function dbServerLoeschen(id) { if (!await showConfirm('DB-Server wirklich löschen?')) return; try { await api(`/dbserver/${id}`, { method: 'DELETE' }); ladeDbServer(); } catch (error) { showAlert(error.message, 'error'); } } async function zeigeDbModal(db = null) { editierteDbId = db?.id || null; document.getElementById('db-modal-title').textContent = db ? 'Datenbank bearbeiten' : 'Datenbank hinzufügen'; // Server-Dropdown befüllen const serverSelect = document.getElementById('db-server-id'); try { const server = await api('/dbserver'); serverSelect.innerHTML = '' + server.map(s => ``).join(''); } catch (error) { serverSelect.innerHTML = ''; } document.getElementById('db-name').value = db?.name || ''; document.getElementById('db-server-id').value = db?.server_id || ''; document.getElementById('db-database').value = db?.database || ''; document.getElementById('db-backup-pfad').value = db?.backup_pfad || ''; document.getElementById('db-aufbewahrung').value = db?.aufbewahrung || 7; document.getElementById('db-format').value = db?.format || 'sql'; document.getElementById('db-modal').classList.remove('hidden'); } async function speichereDb() { const data = { name: document.getElementById('db-name').value.trim(), server_id: parseInt(document.getElementById('db-server-id').value), database: document.getElementById('db-database').value.trim(), backup_pfad: document.getElementById('db-backup-pfad').value.trim(), aufbewahrung: parseInt(document.getElementById('db-aufbewahrung').value) || 7, format: document.getElementById('db-format').value }; if (!data.name || !data.server_id || !data.database || !data.backup_pfad) { showAlert('Bitte alle Pflichtfelder ausfüllen', 'warning'); return; } try { if (editierteDbId) { await api(`/datenbanken/${editierteDbId}`, { method: 'PUT', body: JSON.stringify(data) }); } else { await api('/datenbanken', { method: 'POST', body: JSON.stringify(data) }); } schliesseModal('db-modal'); ladeDatenbanken(); debugLog('Datenbank gespeichert', 'success'); } catch (error) { showAlert(error.message, 'error'); } } async function dbBearbeiten(id) { try { const db = await api(`/datenbanken/${id}`); zeigeDbModal(db); } catch (error) { showAlert(error.message, 'error'); } } async function dbAktivieren(id) { try { await api(`/datenbanken/${id}/aktivieren`, { method: 'POST' }); ladeDatenbanken(); } catch (error) { showAlert(error.message, 'error'); } } async function dbLoeschen(id) { if (!await showConfirm('Datenbank-Konfiguration wirklich löschen?')) return; try { await api(`/datenbanken/${id}`, { method: 'DELETE' }); ladeDatenbanken(); } catch (error) { showAlert(error.message, 'error'); } } async function dbBackupErstellen(id) { const logContainer = document.getElementById('dbbackup-log'); logContainer.innerHTML = '
    Erstelle Backup...
    '; try { debugLog('Erstelle Backup...', 'info'); const result = await api(`/datenbanken/${id}/backup`, { method: 'POST' }); logContainer.innerHTML = `
    ✓ Backup erstellt: ${escapeHtml(result.datei || 'Erfolgreich')} ${result.groesse_mb ? result.groesse_mb + ' MB' : ''}
    `; debugLog('Backup erstellt: ' + (result.datei || 'Erfolgreich'), 'success'); ladeBackups(); } catch (error) { logContainer.innerHTML = `
    Fehler: ${escapeHtml(error.message)}
    `; debugLog('Backup-Fehler: ' + error.message, 'error'); } } async function alleDbBackupsErstellen() { const logContainer = document.getElementById('dbbackup-log'); logContainer.innerHTML = '
    Starte Backups...
    '; try { debugLog('Starte Backup aller Datenbanken...', 'info'); const dbs = await api('/datenbanken'); const aktiveDbs = dbs.filter(db => db.aktiv); if (aktiveDbs.length === 0) { logContainer.innerHTML = '
    Keine aktiven Datenbanken konfiguriert
    '; return; } logContainer.innerHTML = ''; let erfolg = 0; let fehler = 0; for (const db of aktiveDbs) { debugLog(`Backup: ${db.name}`, 'info'); try { const result = await api(`/datenbanken/${db.id}/backup`, { method: 'POST' }); erfolg++; logContainer.innerHTML += `
    ✓ ${escapeHtml(db.name)}: ${escapeHtml(result.datei || 'OK')} ${result.groesse_mb ? result.groesse_mb + ' MB' : ''}
    `; } catch (error) { fehler++; logContainer.innerHTML += `
    ✗ ${escapeHtml(db.name)}: ${error.message}
    `; } } // Zusammenfassung logContainer.innerHTML += `
    Zusammenfassung: ${erfolg} erfolgreich, ${fehler} Fehler
    `; debugLog('Alle Backups abgeschlossen', 'success'); ladeBackups(); } catch (error) { logContainer.innerHTML = `
    Fehler: ${escapeHtml(error.message)}
    `; debugLog('Fehler: ' + error.message, 'error'); } } // ============ Zeitplan Modal erweitert ============ function zeitplanTypChanged() { const typ = document.getElementById('zp-typ').value; document.getElementById('zp-postfach-gruppe').classList.toggle('hidden', typ !== 'mail_abruf'); document.getElementById('zp-ordner-gruppe').classList.toggle('hidden', typ !== 'grobsortierung'); document.getElementById('zp-regel-gruppe').classList.toggle('hidden', typ !== 'sortierregeln'); document.getElementById('zp-db-gruppe').classList.toggle('hidden', typ !== 'db_backup'); // Datenbanken-Dropdown befüllen wenn DB-Backup gewählt if (typ === 'db_backup') { ladeDbFuerZeitplan(); } } async function ladeDbFuerZeitplan() { try { const dbs = await api('/datenbanken'); const select = document.getElementById('zp-db'); select.innerHTML = '' + dbs.map(db => ``).join(''); } catch (error) { console.error('Fehler beim Laden der Datenbanken für Zeitplan:', error); } }