Neue Features: - Filter-Dropdown für Regeln nach Ordner - Option "Ohne Zuordnung" zeigt nur Regeln ohne Ordner - Regeln ohne Ordner-Zuweisung werden mit orangem Rand und Badge markiert - Zugeordnete Ordner werden unter jeder Regel angezeigt - Filter-Einstellung wird in localStorage gespeichert Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3408 lines
130 KiB
JavaScript
Executable file
3408 lines
130 KiB
JavaScript
Executable file
/**
|
||
* 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 aller Ordner...', 'info');
|
||
|
||
const ordner = await api('/ordner');
|
||
const aktiveOrdner = ordner.filter(o => o.aktiv);
|
||
|
||
if (aktiveOrdner.length === 0) {
|
||
logContainer.innerHTML = '<div class="log-entry warning">Keine aktiven Ordner konfiguriert</div>';
|
||
return;
|
||
}
|
||
|
||
logContainer.innerHTML = '';
|
||
let gesamtSortiert = 0;
|
||
let gesamtFehler = 0;
|
||
|
||
for (const o of aktiveOrdner) {
|
||
debugLog(`Verarbeite: ${o.name}`, 'info');
|
||
try {
|
||
const result = await api(`/ordner/${o.id}/verarbeiten`, { method: 'POST' });
|
||
gesamtSortiert += result.sortiert || 0;
|
||
|
||
// Detaillierte Ausgabe pro Ordner
|
||
let ordnerHtml = `<div class="log-entry success">
|
||
<strong>✓ ${escapeHtml(o.name)}</strong>: ${result.sortiert || 0} sortiert, ${result.zugferd || 0} ZUGFeRD
|
||
</div>`;
|
||
|
||
// Dateien im Ordner 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');
|
||
ordnerHtml += `<div class="log-entry ${klasse}" style="padding-left: 2rem;">
|
||
<span>→ ${escapeHtml(d.original)} → ${escapeHtml(d.neuer_name || d.status)}</span>
|
||
</div>`;
|
||
});
|
||
}
|
||
|
||
logContainer.innerHTML += ordnerHtml;
|
||
} catch (error) {
|
||
gesamtFehler++;
|
||
logContainer.innerHTML += `<div class="log-entry error">
|
||
<span>✗ ${escapeHtml(o.name)}: ${error.message}</span>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
// Zusammenfassung am Ende
|
||
logContainer.innerHTML += `<div class="log-entry info" style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 0.5rem;">
|
||
<strong>Zusammenfassung:</strong> ${gesamtSortiert} Dateien sortiert, ${gesamtFehler} Fehler
|
||
</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);
|
||
}
|
||
}
|