docker.dateiverwaltung/Source/frontend/static/js/app.js
data e2fe187c5d V 2.0.2 - Grobsortierung Live-Streaming
- Grobsortierung zeigt jetzt live den Fortschritt (wie Feinsortierung)
- Neuer Streaming-Endpoint /api/grobsortierung/stream
- Sticky Header mit laufender Statistik
- Auto-Scroll zum aktuellen Eintrag

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 15:03:39 +01:00

3469 lines
133 KiB
JavaScript
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

/**
* Dateiverwaltung Frontend
* Zwei getrennte Bereiche: Mail-Abruf und Datei-Sortierung
*/
// ============ API ============
async function api(endpoint, options = {}) {
const response = await fetch(`/api${endpoint}`, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
// 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<boolean>} - 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 =
`<li class="file-browser-item" style="color: var(--danger);">${data.error}</li>`;
return;
}
browserCurrentPath = data.current;
document.getElementById('browser-path-input').value = data.current;
let html = '';
// Parent directory
if (data.parent) {
html += `<li class="file-browser-item" onclick="ladeBrowserInhalt('${data.parent}')">
<span class="file-icon">📁</span> ..
</li>`;
}
// Directories
for (const entry of data.entries) {
html += `<li class="file-browser-item" ondblclick="ladeBrowserInhalt('${entry.path}')" onclick="browserSelect(this, '${entry.path}')">
<span class="file-icon">📁</span> ${entry.name}
</li>`;
}
if (data.entries.length === 0 && !data.parent) {
html = '<li class="file-browser-item">Keine Unterordner</li>';
}
document.getElementById('browser-list').innerHTML = html;
} catch (error) {
// 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 =
`<li class="file-browser-item" style="color: var(--danger);">Fehler: ${error.message}</li>`;
}
}
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 = '<p class="empty-state">Keine Log-Einträge</p>';
return;
}
container.innerHTML = logs.map(log => {
const levelClass = log.level === 'ERROR' ? 'error' : log.level === 'WARNING' ? 'warning' : 'info';
return `<div class="log-entry ${levelClass}">
<span class="log-time">${log.zeit}</span>
<span class="log-level">${log.level}</span>
<span class="log-msg">${escapeHtml(log.nachricht)}</span>
</div>`;
}).join('');
// Nach unten scrollen
container.scrollTop = container.scrollHeight;
} catch (error) {
container.innerHTML = `<p class="empty-state">Fehler: ${error.message}</p>`;
}
}
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 = '<p class="empty-state">Keine Postfächer konfiguriert</p>';
return;
}
container.innerHTML = postfaecher.map(p => {
const letzterAbruf = p.letzter_abruf ? formatDatum(p.letzter_abruf) : 'Nie';
return `
<div class="config-item">
<div class="config-item-info">
<h4>${escapeHtml(p.name)}</h4>
<small>${escapeHtml(p.email)}${truncatePath(p.ziel_ordner)}</small>
<small style="display:block;">Letzter Abruf: ${letzterAbruf} (${p.letzte_anzahl || 0} Dateien)</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="postfachAbrufen(${p.id})">Abrufen</button>
<button class="btn btn-sm" onclick="postfachBearbeiten(${p.id})">Bearbeiten</button>
<button class="btn btn-sm" onclick="postfachTesten(${p.id})">Testen</button>
<button class="btn btn-sm btn-danger" onclick="postfachLoeschen(${p.id})">×</button>
</div>
</div>
`}).join('');
}
function zeigePostfachModal(postfach = null) {
bearbeitetesPostfachId = postfach?.id || null;
document.getElementById('pf-name').value = postfach?.name || '';
document.getElementById('pf-server').value = postfach?.imap_server || '';
document.getElementById('pf-port').value = postfach?.imap_port || '993';
document.getElementById('pf-email').value = postfach?.email || '';
document.getElementById('pf-passwort').value = ''; // Passwort nicht vorausfüllen
document.getElementById('pf-ordner').value = postfach?.ordner || 'INBOX';
document.getElementById('pf-alle-ordner').value = postfach?.alle_ordner ? 'true' : 'false';
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 = '<p style="color: var(--text-secondary); font-size: 0.8rem;">Zuerst Dateitypen auswählen</p>';
return;
}
let html = `
<table class="groessen-filter-table">
<thead>
<tr>
<th>Dateityp</th>
<th>Min (KB)</th>
<th>Max (MB)</th>
</tr>
</thead>
<tbody>
`;
for (const typ of selectedTypes) {
const existing = existingValues[typ] || { min_kb: defaultMin, max_mb: defaultMax };
html += `
<tr class="groessen-filter-row" data-typ="${typ}">
<td class="groessen-filter-type">${typ}</td>
<td><input type="number" class="groessen-min" value="${existing.min_kb}" placeholder="${defaultMin}"></td>
<td><input type="number" class="groessen-max" value="${existing.max_mb}" placeholder="${defaultMax}"></td>
</tr>
`;
}
html += '</tbody></table>';
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 = `
<table class="groessen-filter-table">
<thead>
<tr>
<th>Dateityp</th>
<th>Min (KB)</th>
<th>Max (MB)</th>
</tr>
</thead>
<tbody>
`;
for (const typ of selectedTypes) {
const filter = groessenFilter[typ] || { min_kb: defaultMin, max_mb: defaultMax };
html += `
<tr class="groessen-filter-row" data-typ="${typ}">
<td class="groessen-filter-type">${typ}</td>
<td><input type="number" class="groessen-min" value="${filter.min_kb || defaultMin}"></td>
<td><input type="number" class="groessen-max" value="${filter.max_mb || defaultMax}"></td>
</tr>
`;
}
html += '</tbody></table>';
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 = '<div class="log-entry info"><span>Verbinde...</span></div>';
// 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 = `<div class="log-entry info">
<span>Starte Abruf: ${escapeHtml(data.postfach)}</span>
<small>${data.bereits_verarbeitet} bereits verarbeitet</small>
</div>`;
break;
case 'info':
logContainer.innerHTML += `<div class="log-entry info">
<span>${escapeHtml(data.nachricht)}</span>
</div>`;
break;
case 'ordner':
currentOrdner = data.name;
logContainer.innerHTML += `<div class="log-entry info" id="ordner-status">
<span>📁 ${escapeHtml(data.name)}</span>
</div>`;
break;
case 'mails':
const ordnerStatus = document.getElementById('ordner-status');
if (ordnerStatus) {
ordnerStatus.innerHTML = `<span>📁 ${escapeHtml(data.ordner)}: ${data.anzahl} Mails</span>`;
ordnerStatus.id = ''; // ID entfernen für nächsten Ordner
}
break;
case 'datei':
dateiCount++;
logContainer.innerHTML += `<div class="log-entry success">
<span>✓ ${escapeHtml(data.original_name)}</span>
<small>${formatBytes(data.groesse)}</small>
</div>`;
// Scroll nach unten
logContainer.scrollTop = logContainer.scrollHeight;
break;
case 'skip':
logContainer.innerHTML += `<div class="log-entry" style="opacity:0.6;">
<span>⊘ ${escapeHtml(data.datei)}: ${data.grund}</span>
</div>`;
break;
case 'fehler':
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ ${escapeHtml(data.nachricht)}</span>
</div>`;
break;
case 'fertig':
logContainer.innerHTML += `<div class="log-entry success" style="font-weight:bold;">
<span>✓ Fertig: ${data.anzahl} Dateien gespeichert</span>
</div>`;
eventSource.close();
ladePostfaecher();
break;
}
};
eventSource.onerror = (error) => {
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ Verbindung unterbrochen</span>
</div>`;
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 = '<div class="log-entry info">Starte Abruf...</div>';
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 += `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
}
}
function renderStreamEvent(container, event) {
let html = '';
switch (event.type) {
case 'init':
html = `<div class="log-entry info">📧 ${event.anzahl_postfaecher} Postfächer werden abgerufen...</div>`;
break;
case 'postfach_start':
html = `<div class="log-entry info"><strong>📬 ${escapeHtml(event.name)}</strong> (${event.bereits_verarbeitet} bereits verarbeitet)</div>`;
break;
case 'ordner':
html = `<div class="log-entry" style="padding-left: 1rem;">📁 Ordner: ${escapeHtml(event.name)}</div>`;
break;
case 'mails':
html = `<div class="log-entry" style="padding-left: 1.5rem;">${event.anzahl} Mails gefunden</div>`;
break;
case 'datei':
html = `<div class="log-entry success" style="padding-left: 1.5rem;">✓ ${escapeHtml(event.datei)} (${formatBytes(event.groesse)})</div>`;
break;
case 'skip':
html = `<div class="log-entry" style="padding-left: 1.5rem; opacity: 0.6;">→ ${escapeHtml(event.datei)} - ${event.grund}</div>`;
break;
case 'postfach_done':
html = `<div class="log-entry success"><strong>✓ ${escapeHtml(event.name)}: ${event.anzahl} Dateien</strong></div>`;
break;
case 'postfach_error':
html = `<div class="log-entry error">✗ ${escapeHtml(event.name)}: ${escapeHtml(event.fehler)}</div>`;
break;
case 'done':
html = `<div class="log-entry info" style="margin-top: 0.5rem;"><strong>Fertig!</strong></div>`;
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 = '<p class="empty-state">Keine neuen Attachments gefunden</p>';
return;
}
let html = '';
for (const r of result.ergebnisse) {
const status = r.fehler ? 'error' : 'success';
const icon = r.fehler ? '✗' : '✓';
html += `<div class="log-entry ${status}">
<span>${icon} ${escapeHtml(r.postfach)}: ${r.anzahl || 0} Dateien</span>
${r.fehler ? `<small>${escapeHtml(r.fehler)}</small>` : ''}
</div>`;
if (r.dateien) {
for (const d of r.dateien) {
html += `<div class="log-entry info">
<span style="padding-left: 1rem;">→ ${escapeHtml(d)}</span>
</div>`;
}
}
}
container.innerHTML = html;
}
// ============ BEREICH 2: Datei-Sortierung ============
async function ladeOrdner() {
try {
const ordner = await api('/ordner');
renderOrdner(ordner);
} catch (error) {
console.error('Fehler:', error);
}
}
function renderOrdner(ordner) {
const container = document.getElementById('ordner-liste');
if (!ordner || ordner.length === 0) {
container.innerHTML = '<p class="empty-state">Keine Ordner konfiguriert</p>';
return;
}
container.innerHTML = ordner.map(o => {
const aktivClass = o.aktiv ? '' : 'opacity: 0.5;';
const aktivBadge = o.aktiv ? '<span class="badge badge-success">Aktiv</span>' : '<span class="badge badge-danger">Inaktiv</span>';
const letzte = o.letzte_verarbeitung ? formatDatum(o.letzte_verarbeitung) : 'Nie';
return `
<div class="config-item" style="${aktivClass}">
<div class="config-item-info">
<h4>${escapeHtml(o.name)} ${aktivBadge} ${o.rekursiv ? '<span class="badge badge-info">rekursiv</span>' : ''}</h4>
<small>${truncatePath(o.pfad)}${truncatePath(o.ziel_ordner)}</small>
<small style="display:block;">${(o.dateitypen || []).join(', ')} | Letzte: ${letzte} (${o.letzte_anzahl || 0} Dateien)</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="ordnerVorschau(${o.id})">Vorschau</button>
<button class="btn btn-sm btn-primary" onclick="ordnerVerarbeiten(${o.id})">Verarbeiten</button>
<button class="btn btn-sm" onclick="ordnerBearbeiten(${o.id})" title="Bearbeiten">✎</button>
<button class="btn btn-sm" onclick="ordnerAktivieren(${o.id})" title="${o.aktiv ? 'Deaktivieren' : 'Aktivieren'}">${o.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm" onclick="kopiereOrdner(${o.id})" title="Ordner kopieren">📋</button>
<button class="btn btn-sm btn-danger" onclick="ordnerLoeschen(${o.id})" title="Löschen">×</button>
</div>
</div>
`}).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 = '<div class="log-entry info">Verarbeite...</div>';
try {
debugLog('Verarbeite Ordner...', 'info');
const result = await api(`/ordner/${id}/verarbeiten`, { method: 'POST' });
// Ergebnis in der Mitte anzeigen
let html = `<div class="log-entry success">
<strong>Verarbeitung abgeschlossen</strong><br>
Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Keine Regel: ${result.keine_regel || 0} | Fehler: ${result.fehler}
</div>`;
// 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 += `<div class="log-entry ${klasse}">
<span>${icon} ${escapeHtml(d.original)}${escapeHtml(d.neuer_name || d.status)}</span>
</div>`;
});
}
logContainer.innerHTML = html;
debugLog('Verarbeitung abgeschlossen', 'success');
} catch (error) {
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
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 = '<p class="empty-state">Keine Regeln definiert</p>';
return;
}
container.innerHTML = regeln.map(r => {
const aktivClass = r.aktiv ? '' : 'opacity: 0.5;';
const aktivBadge = r.aktiv ? '<span class="badge badge-success">Aktiv</span>' : '<span class="badge badge-danger">Inaktiv</span>';
// Warnung wenn keine Ordner zugeordnet
const ohneOrdner = !r.ordner_ids || r.ordner_ids.length === 0;
const ohneOrdnerBadge = ohneOrdner ? '<span class="badge badge-warning" title="Keinem Ordner zugeordnet">⚠ Ohne Ordner</span>' : '';
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 = `<br><small style="color: var(--text-secondary);">📁 ${escapeHtml(ordnerNamen)}</small>`;
}
return `
<div class="config-item" style="${aktivClass}${ohneOrdnerStyle}">
<div class="config-item-info">
<h4>${escapeHtml(r.name)} ${aktivBadge} ${ohneOrdnerBadge} <span class="badge badge-info">Prio ${r.prioritaet}</span></h4>
<small>${escapeHtml(r.schema)}</small>${ordnerInfo}
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="bearbeiteRegel(${r.id})" title="Bearbeiten">✎</button>
<button class="btn btn-sm" onclick="regelAktivieren(${r.id})" title="${r.aktiv ? 'Deaktivieren' : 'Aktivieren'}">${r.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm" onclick="kopiereRegel(${r.id})" title="Regel kopieren">📋</button>
<button class="btn btn-sm btn-danger" onclick="regelLoeschen(${r.id})" title="Löschen">×</button>
</div>
</div>
`}).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 = `
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3>Import: ${regelListe.length} Regeln</h3>
</div>
<div class="modal-body">
<p>Wie sollen die Regeln importiert werden?</p>
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: 1rem;">
<button class="btn btn-primary" onclick="this.closest('.modal').resolve('hinzufuegen')">
Nur neue hinzufügen
</button>
<button class="btn" onclick="this.closest('.modal').resolve('aktualisieren')">
Bestehende aktualisieren
</button>
<button class="btn btn-danger" onclick="this.closest('.modal').resolve('ersetzen')">
Alle ersetzen
</button>
<button class="btn" onclick="this.closest('.modal').resolve(null)">
Abbrechen
</button>
</div>
</div>
</div>
`;
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 = '<p style="color: var(--text-secondary); text-align: center; padding: 2rem;">PDF hochladen um Text anzuzeigen</p>';
}
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 = `
<td>
<input type="text" class="ext-feld" value="${escapeHtml(feld)}" placeholder="feldname" ${!removable ? 'readonly' : ''}>
</td>
<td>
<select class="ext-typ" onchange="updateExtPlaceholder(this)">
<option value="auto" ${!istWert && !istRegex ? 'selected' : ''}>Auto</option>
<option value="regex" ${istRegex ? 'selected' : ''}>Regex</option>
<option value="wert" ${istWert ? 'selected' : ''}>Wert</option>
</select>
</td>
<td>
<textarea class="ext-wert" rows="2" placeholder="${escapeHtml(placeholder)}">${escapeHtml(wert)}</textarea>
</td>
<td>
<select class="ext-auswahl" title="Bei mehreren Treffern: welchen wählen?">
<option value="first" ${auswahl === 'first' ? 'selected' : ''}>Erster</option>
<option value="last" ${auswahl === 'last' ? 'selected' : ''}>Letzter</option>
<option value="max" ${auswahl === 'max' ? 'selected' : ''}>Max</option>
<option value="min" ${auswahl === 'min' ? 'selected' : ''}>Min</option>
</select>
</td>
<td>
${removable ? '<button type="button" class="btn btn-sm btn-danger" onclick="this.closest(\'tr\').remove()">×</button>' : ''}
</td>
`;
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 = `
<td>
<input type="text" class="ext-feld" value="${escapeHtml(feld)}" placeholder="z.B. nummer" ${!removable ? 'readonly' : ''}>
</td>
<td>
<select class="ext-typ" onchange="updateExtPlaceholder(this)">
<option value="auto" ${!istWert && !istRegex ? 'selected' : ''}>Auto</option>
<option value="regex" ${istRegex ? 'selected' : ''}>Regex</option>
<option value="wert" ${istWert ? 'selected' : ''}>Wert</option>
</select>
</td>
<td>
<textarea class="ext-wert" rows="2" placeholder="${escapeHtml(placeholder)}">${escapeHtml(wert)}</textarea>
</td>
<td>
<select class="ext-auswahl" title="Bei mehreren Treffern: welchen wählen?">
<option value="first" ${auswahl === 'first' ? 'selected' : ''}>Erster</option>
<option value="last" ${auswahl === 'last' ? 'selected' : ''}>Letzter</option>
<option value="max" ${auswahl === 'max' ? 'selected' : ''}>Max</option>
<option value="min" ${auswahl === 'min' ? 'selected' : ''}>Min</option>
</select>
</td>
<td>
${removable ? '<button type="button" class="btn btn-sm btn-danger" onclick="this.closest(\'tr\').remove()">×</button>' : ''}
</td>
`;
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 = '<p style="color: var(--text-secondary);">Keine Grobsortierung vorhanden. Bitte zuerst Ordner anlegen.</p>';
} else {
container.innerHTML = Array.from(zielOrdnerMap.values()).map(ordner => `
<label class="checkbox-item ordner-checkbox">
<input type="checkbox" value="${ordner.id}" data-ziel="${escapeHtml(ordner.ziel_ordner)}" ${zugewieseneIds.includes(ordner.id) ? 'checked' : ''}>
<span>${escapeHtml(ordner.name)}</span>
<small style="color: var(--text-secondary);">${escapeHtml(ordner.ziel_ordner)}</small>
</label>
`).join('');
}
// Freie Ordner anzeigen
renderFreieOrdner();
} catch (error) {
container.innerHTML = `<p style="color: var(--danger);">Fehler: ${error.message}</p>`;
}
}
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) => `
<div class="freier-ordner-item" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; padding: 0.25rem 0.5rem; background: var(--bg-tertiary); border-radius: var(--radius);">
<span style="flex: 1; font-size: 0.85rem;">${escapeHtml(pfad)}</span>
<button class="btn btn-sm btn-danger" type="button" onclick="entferneFreienOrdner(${index})" style="padding: 0.1rem 0.4rem;">×</button>
</div>
`).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
? `<span style="display:inline-block;background:var(--bg-tertiary);padding:0.15rem 0.4rem;border-radius:3px;font-size:0.7rem;margin-bottom:0.25rem;color:var(--text-secondary);">${badges.join(' · ')}</span><br>`
: '';
displayEl.innerHTML = infoHtml + `<div id="pdf-text-content">${escapeHtml(result.text)}</div>`;
// 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 += '<div style="font-size: 0.75rem; margin-top: 0.25rem;">';
if (gefunden.length > 0) {
statusHtml += `<span style="color: var(--success);">✓ ${gefunden.join(', ')}</span>`;
}
if (nichtGefunden.length > 0) {
statusHtml += ` <span style="color: var(--danger);">✗ ${nichtGefunden.join(', ')}</span>`;
}
statusHtml += '</div>';
}
}
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 += `<div class="feld-item">
<span class="feld-name">{${key}}</span>
<span class="feld-wert">${escapeHtml(String(value))}</span>
</div>`;
}
extrahiertDiv.innerHTML = html;
} else {
extrahiertDiv.innerHTML = '<span style="color: var(--text-secondary);">Keine Felder extrahiert</span>';
}
// 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, '<span class="highlight-keyword">$1</span>');
}
});
}
// 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, '<span class="highlight-extracted">$1</span>');
}
});
}
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 = `
<div style="padding: 0.75rem; background: var(--bg-tertiary); border-radius: var(--radius); margin-top: 0.5rem;">
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${beschreibung}</div>
<div style="font-size: 0.8rem; margin-bottom: 0.5rem;">
<strong>Markierter Text:</strong> <code style="color: var(--success);">${escapeHtml(originalText)}</code>
</div>
<div style="font-size: 0.8rem; margin-bottom: 0.5rem;">
<strong>Regex-Vorschlag:</strong>
</div>
<input type="text" id="regex-vorschlag-input" value="${escapeHtml(regex)}"
style="width: 100%; font-family: monospace; font-size: 0.7rem; padding: 0.2rem; margin-bottom: 0.3rem;"
onclick="this.select()">
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-sm btn-primary" onclick="kopieRegelRegex()">📋 Kopieren</button>
<button class="btn btn-sm" onclick="document.getElementById('regex-helfer-ergebnis').classList.add('hidden')">Schließen</button>
</div>
</div>
`;
} 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 += `<div style="margin-bottom: 0.5rem;"><strong>Erkennung:</strong> Dokument muss "${keywords}" enthalten</div>`;
} else {
html += `<div style="margin-bottom: 0.5rem; color: var(--warning);"><strong>Erkennung:</strong> Passt auf ALLE Dateien (keine Keywords)</div>`;
}
// Firma
const firma = document.getElementById('ass-firma').value.trim();
if (firma) {
html += `<div style="margin-bottom: 0.5rem;"><strong>Firma:</strong> ${escapeHtml(firma)}</div>`;
}
// Felder
html += `<div style="margin-bottom: 0.5rem;"><strong>Extrahiere:</strong> `;
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(', ') || '<em>nichts</em>';
html += '</div>';
// 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 += `<div style="margin-top: 0.5rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: var(--radius);"><strong>Beispiel-Dateiname:</strong><br><code style="color: var(--success);">${escapeHtml(beispiel)}</code></div>`;
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 = '<table style="width: 100%; font-size: 0.85rem;">';
html += '<tr style="color: var(--text-secondary);"><th style="text-align: left;">Feld</th><th style="text-align: left;">Wert</th><th style="text-align: left;">Kontext</th></tr>';
for (const feld of result.erkannte_felder) {
const feldName = {
'datum': '📅 Datum',
'betrag': '💰 Betrag',
'nummer': '🔢 Nummer',
'firma': '🏢 Firma'
}[feld.feld] || feld.feld;
html += `<tr>
<td><strong>${feldName}</strong></td>
<td style="color: var(--success);">${escapeHtml(feld.extrahiert || feld.wert)}</td>
<td style="color: var(--text-secondary); font-size: 0.8rem;">${escapeHtml(feld.kontext)}</td>
</tr>`;
}
html += '</table>';
felderDiv.innerHTML = html;
} else {
felderDiv.innerHTML = '<p style="color: var(--text-secondary);">Keine Felder automatisch erkannt</p>';
}
// Keywords anzeigen
if (result.gefundene_keywords && result.gefundene_keywords.length > 0) {
keywordsDiv.innerHTML = `<div style="margin-top: 0.5rem;">
<strong>🏷️ Keywords:</strong>
<span style="color: var(--primary);">${result.gefundene_keywords.join(', ')}</span>
</div>`;
} 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 = `<div style="color: var(--success); font-weight: bold;">
✓ Erkannte Muster wurden übernommen!<br>
<small style="font-weight: normal;">Die Felder wurden automatisch ausgefüllt.</small>
</div>`;
}
// Auto-Test starten
setTimeout(() => testeRegelLive(), 500);
}
// ============ Sortierung starten ============
async function sortierungStarten() {
const container = document.getElementById('sortierung-log');
container.innerHTML = '<p class="empty-state">Sortierung läuft...</p>';
try {
const result = await api('/sortierung/starten', { method: 'POST' });
zeigeSortierungLog(result);
} catch (error) {
container.innerHTML = `<div class="log-entry error"><span>✗ Fehler: ${escapeHtml(error.message)}</span></div>`;
}
}
function zeigeSortierungLog(result) {
const container = document.getElementById('sortierung-log');
if (!result.verarbeitet || result.verarbeitet.length === 0) {
container.innerHTML = '<p class="empty-state">Keine Dateien verarbeitet</p>';
return;
}
let html = `<div class="log-entry info">
<span>Gesamt: ${result.gesamt} | Sortiert: ${result.sortiert} | ZUGFeRD: ${result.zugferd} | Fehler: ${result.fehler}</span>
</div>`;
for (const d of result.verarbeitet) {
const status = d.fehler ? 'error' : (d.zugferd ? 'info' : 'success');
const icon = d.fehler ? '✗' : (d.zugferd ? '🧾' : '✓');
html += `<div class="log-entry ${status}">
<span>${icon} ${escapeHtml(d.neuer_name || d.original)}</span>
${d.fehler ? `<small>${escapeHtml(d.fehler)}</small>` : ''}
</div>`;
}
container.innerHTML = html;
}
// ============ 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 = '<p class="empty-state">Keine Zeitpläne konfiguriert</p>';
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 `
<div class="config-item" style="${aktivClass}">
<div class="config-item-info">
<h4>${typIcon} ${escapeHtml(zp.name)}
<span class="badge ${zp.aktiv ? 'badge-success' : 'badge-danger'}">${zp.aktiv ? 'Aktiv' : 'Inaktiv'}</span>
<span class="badge badge-info">${zp.intervall}</span>
</h4>
<small>Nächste: ${naechste} | Letzte: ${letzte}</small>
${zp.letzter_status ? `<small style="display:block;" class="${statusClass}">Status: ${zp.letzter_status}${zp.letzte_meldung ? ' - ' + escapeHtml(zp.letzte_meldung.substring(0, 80)) : ''}</small>` : ''}
</div>
<div class="config-item-actions">
<button class="btn btn-sm btn-success" onclick="zeitplanAusfuehren(${zp.id})" title="Jetzt ausführen">▶</button>
<button class="btn btn-sm" onclick="zeitplanBearbeiten(${zp.id})" title="Bearbeiten">✎</button>
<button class="btn btn-sm" onclick="zeitplanAktivieren(${zp.id})" title="${zp.aktiv ? 'Deaktivieren' : 'Aktivieren'}">${zp.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm btn-danger" onclick="zeitplanLoeschen(${zp.id})" title="Löschen">×</button>
</div>
</div>
`}).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 = '<div class="status-grid">';
// Postfächer
html += '<div class="status-section"><h4>📧 Postfächer</h4>';
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 += `<div class="status-item">${aktiv} ${escapeHtml(p.name)}: ${letzte} (${p.letzte_anzahl || 0} Dateien)</div>`;
}
} else {
html += '<div class="status-item">Keine Postfächer</div>';
}
html += '</div>';
// Grobsortierung
html += '<div class="status-section"><h4>📁 Grobsortierung</h4>';
if (status.quell_ordner && status.quell_ordner.length > 0) {
for (const o of status.quell_ordner) {
const aktiv = o.aktiv ? '🟢' : '⚪';
html += `<div class="status-item">${aktiv} ${escapeHtml(o.name)}</div>`;
}
} else {
html += '<div class="status-item">Keine Ordner</div>';
}
html += '</div>';
// Scheduler
html += '<div class="status-section"><h4>⏰ Scheduler</h4>';
const schedulerStatus = status.scheduler?.scheduler_laeuft ? '🟢 Läuft' : '🔴 Gestoppt';
html += `<div class="status-item">${schedulerStatus}</div>`;
html += '</div>';
html += '</div>';
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 = '<option value="">Alle aktiven Postfächer</option>' +
postfaecher.map(p => `<option value="${p.id}">${escapeHtml(p.name)}</option>`).join('');
// Ordner laden
const ordner = await api('/ordner');
const ordSelect = document.getElementById('zp-ordner');
ordSelect.innerHTML = '<option value="">Alle aktiven Ordner</option>' +
ordner.map(o => `<option value="${o.id}">${escapeHtml(o.name)}</option>`).join('');
// Regeln laden
const regeln = await api('/regeln');
const regelSelect = document.getElementById('zp-regel');
regelSelect.innerHTML = '<option value="">Alle aktiven Regeln</option>' +
regeln.map(r => `<option value="${r.id}">${escapeHtml(r.name)}</option>`).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 `<span title="${escaped}" style="cursor:help;">${escapeHtml(start)}...${escapeHtml(end)}</span>`;
}
// 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 = '<p class="empty-state">Keine Zeitpläne</p>';
return;
}
container.innerHTML = gefiltert.map(z => `
<div class="config-item">
<div class="config-item-info">
<h4>${escapeHtml(z.name)} ${z.aktiv ? '✓' : ''}</h4>
<small>${z.intervall} ${z.stunde != null ? `um ${z.stunde}:${String(z.minute || 0).padStart(2, '0')}` : ''}</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="zeitplanAusfuehren(${z.id})" title="Jetzt ausführen">▶</button>
<button class="btn btn-sm" onclick="zeitplanAktivieren(${z.id})">${z.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm btn-danger" onclick="zeitplanLoeschen(${z.id})">🗑</button>
</div>
</div>
`).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 = '<div class="log-entry info">Starte Grobsortierung...</div>';
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 = `<div class="log-entry info">
<strong>Starte Grobsortierung...</strong> ${data.ordner_count} Ordner, ${data.gesamt} Dateien
</div>`;
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 = `<strong>Gesamt: 0 | Sortiert: 0 | Fehler: 0</strong>`;
logContainer.appendChild(zusammenfassungDiv);
break;
case 'ordner':
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
<strong>📁 ${escapeHtml(data.ordner)}</strong> (${data.dateien} Dateien)
</div>`;
break;
case 'datei_fertig':
sortiert++;
gesamt++;
let text = escapeHtml(data.original || '');
if (data.neuer_name) {
text += `${escapeHtml(data.neuer_name)}`;
}
if (data.regel) {
text += ` <small style="opacity:0.7">[${escapeHtml(data.regel)}]</small>`;
}
logContainer.innerHTML += `<div class="log-entry success">
<span>✓ ${text}</span>
</div>`;
if (zusammenfassungDiv) {
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | Fehler: ${fehler}</strong>`;
}
break;
case 'datei_keine_regel':
gesamt++;
logContainer.innerHTML += `<div class="log-entry warning">
<span>⚠ ${escapeHtml(data.original)} - keine passende Regel</span>
</div>`;
if (zusammenfassungDiv) {
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | Fehler: ${fehler}</strong>`;
}
break;
case 'datei_fehler':
case 'ordner_fehler':
fehler++;
gesamt++;
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ ${escapeHtml(data.original || data.ordner)} <small>(${escapeHtml(data.fehler || 'Fehler')})</small></span>
</div>`;
if (zusammenfassungDiv) {
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | Fehler: ${fehler}</strong>`;
}
break;
case 'fertig':
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
<strong>✓ Grobsortierung abgeschlossen</strong>
</div>`;
break;
}
logContainer.scrollTop = logContainer.scrollHeight;
} catch (parseError) {
console.error('SSE Parse-Fehler:', parseError, line);
}
}
}
}
if (gesamt === 0) {
logContainer.innerHTML = `<div class="log-entry info">Keine Dateien zur Verarbeitung gefunden</div>`;
}
debugLog('Grobsortierung abgeschlossen', 'success');
} catch (error) {
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
debugLog('Fehler: ' + error.message, 'error');
}
}
// ============ Feinsortierung starten ============
async function feinsortierungStarten() {
const logContainer = document.getElementById('sortierung-log');
logContainer.innerHTML = '<div class="log-entry info">Starte Feinsortierung...</div>';
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 = `<div class="log-entry info">
<strong>Starte Feinsortierung...</strong> ${data.ordner_count} Ordner, ${data.gesamt} Dateien
</div>`;
// 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 = `<strong>Gesamt: 0 | Sortiert: 0 | ZUGFeRD: 0 | Fehler: 0</strong>`;
logContainer.appendChild(zusammenfassungDiv);
break;
case 'ordner':
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
<strong>📁 ${escapeHtml(data.ordner)}</strong> (${data.dateien} Dateien)
</div>`;
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 += ` <small style="opacity:0.7">[${escapeHtml(data.regel)}]</small>`;
}
if (data.zugferd) {
text += ` <small style="color:var(--success)">(ZUGFeRD)</small>`;
}
logContainer.innerHTML += `<div class="log-entry success">
<span>${icon} ${text}</span>
</div>`;
// Zusammenfassung aktualisieren
if (zusammenfassungDiv) {
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | ZUGFeRD: ${zugferd} | Fehler: ${fehler}</strong>`;
}
break;
case 'datei_fehler':
fehler++;
gesamt++;
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ ${escapeHtml(data.original || '')} <small style="color:var(--danger)">(${escapeHtml(data.fehler || 'Unbekannter Fehler')})</small></span>
</div>`;
// Zusammenfassung aktualisieren
if (zusammenfassungDiv) {
zusammenfassungDiv.innerHTML = `<strong>Gesamt: ${gesamt} | Sortiert: ${sortiert} | ZUGFeRD: ${zugferd} | Fehler: ${fehler}</strong>`;
}
break;
case 'fertig':
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
<strong>✓ Feinsortierung abgeschlossen</strong>
</div>`;
break;
}
// Auto-Scroll zum Ende
logContainer.scrollTop = logContainer.scrollHeight;
} catch (parseError) {
console.error('SSE Parse-Fehler:', parseError, line);
}
}
}
}
if (gesamt === 0) {
logContainer.innerHTML = `<div class="log-entry info">Keine Dateien zur Verarbeitung gefunden</div>`;
}
debugLog('Feinsortierung abgeschlossen', 'success');
} catch (error) {
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
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 = '<p class="empty-state">Keine Server konfiguriert</p>';
return;
}
container.innerHTML = server.map(s => `
<div class="config-item">
<div class="config-item-info">
<h4>${escapeHtml(s.name)} ${s.aktiv ? '✓' : ''}</h4>
<small>${s.typ} @ ${s.host}:${s.port}</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm" onclick="dbServerBearbeiten(${s.id})">✏</button>
<button class="btn btn-sm" onclick="dbServerAktivieren(${s.id})">${s.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm btn-danger" onclick="dbServerLoeschen(${s.id})">🗑</button>
</div>
</div>
`).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 = '<p class="empty-state">Keine Datenbanken konfiguriert</p>';
return;
}
container.innerHTML = dbs.map(db => `
<div class="config-item">
<div class="config-item-info">
<h4>${escapeHtml(db.name)} ${db.aktiv ? '✓' : ''}</h4>
<small>${escapeHtml(db.database)} (${db.server_name || 'Server'})</small>
</div>
<div class="config-item-actions">
<button class="btn btn-sm btn-success" onclick="dbBackupErstellen(${db.id})" title="Backup jetzt erstellen">💾</button>
<button class="btn btn-sm" onclick="dbBearbeiten(${db.id})">✏</button>
<button class="btn btn-sm" onclick="dbAktivieren(${db.id})">${db.aktiv ? '⏸' : '▶'}</button>
<button class="btn btn-sm btn-danger" onclick="dbLoeschen(${db.id})">🗑</button>
</div>
</div>
`).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 = '<p class="empty-state">Keine Backups vorhanden</p>';
return;
}
container.innerHTML = backups.slice(0, 20).map(b => `
<div class="log-entry info">
<span>${escapeHtml(b.dateiname)}</span>
<small>${b.groesse_mb} MB - ${b.erstellt}</small>
</div>
`).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 = '<option value="">Server wählen...</option>' +
server.map(s => `<option value="${s.id}">${escapeHtml(s.name)} (${s.typ})</option>`).join('');
} catch (error) {
serverSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
}
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 = '<div class="log-entry info">Erstelle Backup...</div>';
try {
debugLog('Erstelle Backup...', 'info');
const result = await api(`/datenbanken/${id}/backup`, { method: 'POST' });
logContainer.innerHTML = `<div class="log-entry success">
<span>✓ Backup erstellt: ${escapeHtml(result.datei || 'Erfolgreich')}</span>
<small>${result.groesse_mb ? result.groesse_mb + ' MB' : ''}</small>
</div>`;
debugLog('Backup erstellt: ' + (result.datei || 'Erfolgreich'), 'success');
ladeBackups();
} catch (error) {
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
debugLog('Backup-Fehler: ' + error.message, 'error');
}
}
async function alleDbBackupsErstellen() {
const logContainer = document.getElementById('dbbackup-log');
logContainer.innerHTML = '<div class="log-entry info">Starte Backups...</div>';
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 = '<div class="log-entry warning">Keine aktiven Datenbanken konfiguriert</div>';
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 += `<div class="log-entry success">
<span>✓ ${escapeHtml(db.name)}: ${escapeHtml(result.datei || 'OK')}</span>
<small>${result.groesse_mb ? result.groesse_mb + ' MB' : ''}</small>
</div>`;
} catch (error) {
fehler++;
logContainer.innerHTML += `<div class="log-entry error">
<span>✗ ${escapeHtml(db.name)}: ${error.message}</span>
</div>`;
}
}
// Zusammenfassung
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
<strong>Zusammenfassung:</strong> ${erfolg} erfolgreich, ${fehler} Fehler
</div>`;
debugLog('Alle Backups abgeschlossen', 'success');
ladeBackups();
} catch (error) {
logContainer.innerHTML = `<div class="log-entry error">Fehler: ${escapeHtml(error.message)}</div>`;
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 = '<option value="">Alle aktiven Datenbanken</option>' +
dbs.map(db => `<option value="${db.id}">${escapeHtml(db.name)}</option>`).join('');
} catch (error) {
console.error('Fehler beim Laden der Datenbanken für Zeitplan:', error);
}
}