added amazon importer and logging smtp

This commit is contained in:
2026-03-20 16:22:38 +01:00
parent 9fdada5dbe
commit a4e39332c7
16 changed files with 2619 additions and 255 deletions
+4 -3
View File
@@ -10,8 +10,9 @@
<nav>
<div class="nav-brand">Belegimport</div>
<div class="nav-links">
<a href="/" class="{% if active_page == 'settings' %}active{% endif %}">Einstellungen</a>
<a href="/scan" class="{% if active_page == 'scan' %}active{% endif %}">Scan-Upload</a>
<a href="/" class="{% if active_page == 'scan' %}active{% endif %}">Scan-Upload</a>
<a href="/settings" class="{% if active_page == 'settings' %}active{% endif %}">Einstellungen</a>
<a href="/platforms" class="{% if active_page == 'platforms' %}active{% endif %}">Plattformen</a>
<a href="/log" class="{% if active_page == 'log' %}active{% endif %}">Verarbeitungslog</a>
</div>
<div class="nav-status">
@@ -29,7 +30,7 @@
</div>
</nav>
<main>
<main class="{% if main_class is defined %}{{ main_class }}{% endif %}">
{% if message %}
<div class="alert alert-{{ message_type or 'info' }}">
{{ message }}
+75 -2
View File
@@ -1,21 +1,30 @@
{% extends "base.html" %}
{% set active_page = "log" %}
{% set main_class = "main-wide" %}
{% set message = None %}
{% block content %}
<div class="card">
<h2>Verarbeitungslog</h2>
<div class="card card-table">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:1px solid var(--border);">
<h2 style="margin:0;border:none;padding:0;">Verarbeitungslog</h2>
{% if logs %}
<button type="button" class="btn btn-secondary btn-small" onclick="clearLog()">Log leeren</button>
{% endif %}
</div>
{% if logs %}
<table>
<thead>
<tr>
<th>ID</th>
<th>Zeitpunkt</th>
<th>Art</th>
<th>Betreff</th>
<th>Absender</th>
<th>Anhänge</th>
<th>Gesendet an</th>
<th>Status</th>
<th>Fehlermeldung</th>
<th>SMTP</th>
</tr>
</thead>
<tbody>
@@ -23,9 +32,17 @@
<tr class="{% if log.status == 'error' %}row-error{% endif %}">
<td>{{ log.id }}</td>
<td>{{ log.timestamp }}</td>
<td>
{% if log.get('beleg_type', 'eingang') == 'ausgang' %}
<span class="badge badge-warning">Ausgang</span>
{% else %}
<span class="badge badge-info">Eingang</span>
{% endif %}
</td>
<td>{{ log.email_subject or '-' }}</td>
<td>{{ log.email_from or '-' }}</td>
<td>{{ log.attachments_count }}</td>
<td>{{ log.sent_to or '-' }}</td>
<td>
{% if log.status == 'success' %}
<span class="badge badge-success">OK</span>
@@ -34,6 +51,12 @@
{% endif %}
</td>
<td>{{ log.error_message or '-' }}</td>
<td>
{% if log.smtp_log %}
<button type="button" class="btn btn-small btn-secondary" onclick="showSmtpLog({{ log.id }})">Anzeigen</button>
<script>window._smtpLogs = window._smtpLogs || {}; window._smtpLogs[{{ log.id }}] = {{ log.smtp_log | tojson }};</script>
{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
@@ -42,4 +65,54 @@
<p class="text-muted">Noch keine Einträge vorhanden.</p>
{% endif %}
</div>
<!-- SMTP Log Modal -->
<div id="smtpModal" class="modal-overlay" style="display:none;" onclick="if(event.target===this)closeSmtpModal()">
<div class="modal" style="max-width:700px;">
<div class="modal-header">
<h3>SMTP-Protokoll</h3>
<button type="button" class="modal-close" onclick="closeSmtpModal()">&times;</button>
</div>
<div class="modal-body" style="padding:1.25rem;background:var(--bg,#f5f5f5);">
<pre id="smtpModalBody" style="margin:0;font-size:0.8rem;white-space:pre-wrap;word-break:break-all;"></pre>
</div>
</div>
</div>
<style>
.btn-small {
padding: 0.25rem 0.6rem;
font-size: 0.8rem;
}
</style>
<script>
function showSmtpLog(logId) {
const log = window._smtpLogs && window._smtpLogs[logId];
if (!log) return;
document.getElementById('smtpModalBody').textContent = log;
document.getElementById('smtpModal').style.display = 'flex';
}
function closeSmtpModal() {
document.getElementById('smtpModal').style.display = 'none';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeSmtpModal();
});
async function clearLog() {
if (!confirm('Verarbeitungslog wirklich leeren?')) return;
try {
const resp = await fetch('/api/clear-log', {method: 'POST'});
const data = await resp.json();
if (data.success) {
location.reload();
}
} catch (e) {
alert('Fehler: ' + e.message);
}
}
</script>
{% endblock %}
+363
View File
@@ -0,0 +1,363 @@
{% extends "base.html" %}
{% set active_page = "platforms" %}
{% block content %}
{% if message %}
<div class="alert alert-{{ message_type or 'info' }}">{{ message }}</div>
{% endif %}
<div class="card">
<h2>Amazon Business - Einstellungen</h2>
<div class="form-grid">
<div class="form-group">
<label for="amazon_enabled">Status</label>
<select id="amazon_enabled" name="amazon_enabled">
<option value="false" {% if settings.get('amazon_enabled') != 'true' %}selected{% endif %}>Deaktiviert</option>
<option value="true" {% if settings.get('amazon_enabled') == 'true' %}selected{% endif %}>Aktiviert</option>
</select>
</div>
<div class="form-group">
<label for="amazon_domain">Amazon-Domain</label>
<select id="amazon_domain" name="amazon_domain">
<option value="amazon.de" {% if settings.get('amazon_domain') == 'amazon.de' %}selected{% endif %}>amazon.de</option>
<option value="amazon.at" {% if settings.get('amazon_domain') == 'amazon.at' %}selected{% endif %}>amazon.at</option>
<option value="amazon.com" {% if settings.get('amazon_domain') == 'amazon.com' %}selected{% endif %}>amazon.com</option>
</select>
</div>
<div class="form-group">
<label for="amazon_email">Amazon E-Mail</label>
<input type="email" id="amazon_email" name="amazon_email"
value="{{ settings.get('amazon_email', '') }}"
placeholder="email@example.com">
</div>
<div class="form-group">
<label for="amazon_password">Amazon Passwort</label>
<input type="password" id="amazon_password" name="amazon_password"
placeholder="{% if settings.get('amazon_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
</div>
<div class="form-group">
<label for="amazon_since_date">Rechnungen ab Datum</label>
<input type="date" id="amazon_since_date" name="amazon_since_date"
value="{{ settings.get('amazon_since_date', '') }}">
<small class="text-muted">Leer = letzte 30 Tage</small>
</div>
<div class="form-group" style="align-self:end;">
{% if settings.get('amazon_last_sync') %}
<small class="text-muted">Letzter Abruf: {{ settings.get('amazon_last_sync') }}</small>
{% endif %}
</div>
</div>
<div class="form-actions" style="margin-top:1rem;">
<button type="button" class="btn btn-primary" onclick="saveAmazonSettings()">Einstellungen speichern</button>
</div>
<div id="settingsMsg" style="margin-top:0.75rem;"></div>
</div>
<div class="card">
<h2>Anmeldung &amp; Abruf</h2>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
<span>Session:</span>
<span id="sessionBadge" class="badge badge-inactive">Wird geprüft...</span>
</div>
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
<button type="button" id="btnLogin" class="btn btn-primary" onclick="doLogin()">Bei Amazon anmelden</button>
<button type="button" id="btnLogout" class="btn btn-secondary" onclick="doLogout()" style="display:none;">Session löschen</button>
<button type="button" id="btnProcess" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
</div>
<div id="processMsg" style="margin-top:0.75rem;"></div>
</div>
<!-- Interactive Browser Modal -->
<div id="browserModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1000;overflow:auto;">
<div style="max-width:1340px;margin:2rem auto;background:var(--card-bg);border-radius:8px;padding:1.5rem;position:relative;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="margin:0;">Amazon Login - Interaktiver Browser</h3>
<button type="button" class="btn btn-secondary" onclick="closeBrowser()" style="padding:0.25rem 0.75rem;">Schließen</button>
</div>
<div id="browserInfo" class="alert alert-info" style="margin-bottom:1rem;">
Klicken und tippen Sie im Browser-Bild, um sich bei Amazon anzumelden. CAPTCHAs und 2FA können Sie direkt lösen.
</div>
<div style="position:relative;display:inline-block;border:1px solid var(--border-color);cursor:crosshair;">
<img id="browserImg" src="" alt="Browser" style="display:block;max-width:100%;height:auto;"
onclick="onBrowserClick(event)">
</div>
<div style="margin-top:1rem;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<input type="text" id="browserInput" placeholder="Text eingeben und Enter drücken..."
style="flex:1;min-width:200px;padding:0.5rem;border:1px solid var(--border-color);border-radius:4px;background:var(--input-bg);color:var(--text-color);"
onkeydown="onBrowserKeydown(event)">
<button type="button" class="btn btn-primary" onclick="sendBrowserText()">Senden</button>
<button type="button" class="btn btn-secondary" onclick="sendKey('Tab')">Tab</button>
<button type="button" class="btn btn-secondary" onclick="sendKey('Escape')">Esc</button>
<button type="button" class="btn btn-secondary" onclick="sendKey('Backspace')">&#9003;</button>
</div>
</div>
</div>
<script>
// --- Settings ---
async function saveAmazonSettings() {
const btn = event.target;
btn.disabled = true;
btn.textContent = 'Speichert...';
const msgEl = document.getElementById('settingsMsg');
const data = {
amazon_enabled: document.getElementById('amazon_enabled').value,
amazon_domain: document.getElementById('amazon_domain').value,
amazon_email: document.getElementById('amazon_email').value,
amazon_password: document.getElementById('amazon_password').value,
amazon_since_date: document.getElementById('amazon_since_date').value,
};
try {
const resp = await fetch('/api/amazon-settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
const result = await resp.json();
if (result.success) {
msgEl.innerHTML = '<div class="alert alert-success">Einstellungen gespeichert</div>';
if (data.amazon_password) {
document.getElementById('amazon_password').value = '';
document.getElementById('amazon_password').placeholder = '(gespeichert)';
}
} else {
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(result.error || 'Fehler') + '</div>';
}
} catch (e) {
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
} finally {
btn.disabled = false;
btn.textContent = 'Einstellungen speichern';
}
}
// --- Session Status ---
async function checkSession() {
const badge = document.getElementById('sessionBadge');
try {
const resp = await fetch('/api/amazon-status');
const data = await resp.json();
if (data.login_active) {
badge.className = 'badge badge-warning';
badge.textContent = 'Login läuft...';
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnProcess').style.display = 'none';
} else if (data.session_valid) {
badge.className = 'badge badge-success';
badge.textContent = 'Angemeldet';
document.getElementById('btnLogout').style.display = '';
document.getElementById('btnProcess').style.display = '';
} else {
badge.className = 'badge badge-inactive';
badge.textContent = 'Nicht angemeldet';
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnProcess').style.display = 'none';
}
} catch (e) {
badge.className = 'badge badge-inactive';
badge.textContent = 'Unbekannt';
}
}
// --- Interactive Browser Login ---
let screenshotInterval = null;
let loginPollInterval = null;
async function doLogin() {
const msgEl = document.getElementById('processMsg');
msgEl.innerHTML = '<div class="alert alert-info">Browser wird gestartet...</div>';
try {
await fetch('/api/amazon-login', {method: 'POST'});
// Open browser modal
document.getElementById('browserModal').style.display = '';
msgEl.innerHTML = '';
startScreenshotPolling();
startLoginStatePolling();
} catch (e) {
msgEl.innerHTML = '<div class="alert alert-error">Browser konnte nicht gestartet werden</div>';
}
}
function startScreenshotPolling() {
if (screenshotInterval) clearInterval(screenshotInterval);
refreshScreenshot();
screenshotInterval = setInterval(refreshScreenshot, 1500);
}
async function refreshScreenshot() {
try {
const resp = await fetch('/api/amazon-browser-screenshot');
if (resp.ok) {
const blob = await resp.blob();
document.getElementById('browserImg').src = URL.createObjectURL(blob);
}
} catch (e) {}
}
function startLoginStatePolling() {
if (loginPollInterval) clearInterval(loginPollInterval);
loginPollInterval = setInterval(async () => {
try {
const resp = await fetch('/api/amazon-login-state');
const data = await resp.json();
const info = document.getElementById('browserInfo');
if (data.status === 'logged_in') {
info.className = 'alert alert-success';
info.textContent = 'Erfolgreich angemeldet! Sie können das Fenster schließen.';
} else if (data.status === 'login_failed') {
info.className = 'alert alert-error';
info.textContent = data.message || 'Login fehlgeschlagen';
} else if (data.status === 'interactive') {
info.className = 'alert alert-info';
info.textContent = data.message || 'Bitte im Browser anmelden...';
}
} catch (e) {}
}, 2000);
}
async function onBrowserClick(event) {
const img = event.target;
const rect = img.getBoundingClientRect();
// Scale click coordinates to actual browser viewport (1280x800)
const scaleX = 1280 / img.clientWidth;
const scaleY = 800 / img.clientHeight;
const x = Math.round((event.clientX - rect.left) * scaleX);
const y = Math.round((event.clientY - rect.top) * scaleY);
try {
await fetch('/api/amazon-browser-click', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({x, y}),
});
setTimeout(refreshScreenshot, 500);
} catch (e) {}
}
async function sendBrowserText() {
const input = document.getElementById('browserInput');
const text = input.value;
if (!text) return;
try {
await fetch('/api/amazon-browser-type', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text}),
});
input.value = '';
setTimeout(refreshScreenshot, 500);
} catch (e) {}
}
async function sendKey(key) {
try {
await fetch('/api/amazon-browser-key', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({key}),
});
setTimeout(refreshScreenshot, 500);
} catch (e) {}
}
function onBrowserKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
sendBrowserText();
// Also send Enter to the browser
sendKey('Enter');
}
}
async function closeBrowser() {
if (screenshotInterval) { clearInterval(screenshotInterval); screenshotInterval = null; }
if (loginPollInterval) { clearInterval(loginPollInterval); loginPollInterval = null; }
document.getElementById('browserModal').style.display = 'none';
try {
await fetch('/api/amazon-login-close', {method: 'POST'});
} catch (e) {}
checkSession();
}
// --- Logout ---
async function doLogout() {
if (!confirm('Amazon-Session wirklich löschen? Sie müssen sich danach neu anmelden.')) return;
const btn = document.getElementById('btnLogout');
btn.disabled = true;
try {
await fetch('/api/amazon-logout', {method: 'POST'});
document.getElementById('processMsg').innerHTML = '<div class="alert alert-info">Session gelöscht</div>';
} catch (e) {
document.getElementById('processMsg').innerHTML = '<div class="alert alert-error">Fehler</div>';
} finally {
btn.disabled = false;
checkSession();
}
}
// --- Process ---
async function doProcess() {
const btn = document.getElementById('btnProcess');
const msgEl = document.getElementById('processMsg');
btn.disabled = true;
btn.textContent = 'Rechnungen werden abgerufen...';
msgEl.innerHTML = '';
try {
const resp = await fetch('/api/amazon-process', {method: 'POST'});
const data = await resp.json();
if (data.error) {
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(data.error) + '</div>';
} else if (data.processed > 0 || data.errors > 0) {
let msg = data.processed + ' Rechnung(en) importiert';
if (data.skipped > 0) msg += ', ' + data.skipped + ' übersprungen';
if (data.errors > 0) msg += ', ' + data.errors + ' Fehler';
const cls = data.errors > 0 ? 'warning' : 'success';
msgEl.innerHTML = '<div class="alert alert-' + cls + '">' + escapeHtml(msg) + '</div>';
} else {
msgEl.innerHTML = '<div class="alert alert-info">Keine neuen Rechnungen gefunden</div>';
}
} catch (e) {
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
} finally {
btn.disabled = false;
btn.textContent = 'Jetzt Rechnungen abrufen';
checkSession();
}
}
// --- Reset ---
async function doReset() {
if (!confirm('Alle als importiert markierten Rechnungen zurücksetzen? Beim nächsten Abruf werden alle Rechnungen erneut heruntergeladen und gesendet.')) return;
const msgEl = document.getElementById('processMsg');
try {
const resp = await fetch('/api/amazon-reset', {method: 'POST'});
const data = await resp.json();
if (data.success) {
msgEl.innerHTML = '<div class="alert alert-info">' + data.count + ' Bestellung(en) zurückgesetzt</div>';
} else {
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(data.error || 'Fehler') + '</div>';
}
} catch (e) {
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
}
}
function escapeHtml(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
// Initial check
checkSession();
</script>
{% endblock %}
+8 -1
View File
@@ -8,6 +8,13 @@
Mehrseitige PDF hochladen. Trennseiten mit QR-Code werden automatisch erkannt und die einzelnen Dokumente gesendet.
</p>
<!-- Belegart -->
<div style="margin-bottom:1rem;display:flex;gap:1rem;align-items:center;">
<label style="margin:0;font-weight:600;">Belegart:</label>
<label style="margin:0;cursor:pointer;"><input type="radio" name="beleg_type" value="eingang" checked> Eingangsbeleg (Einkauf)</label>
<label style="margin:0;cursor:pointer;"><input type="radio" name="beleg_type" value="ausgang"> Ausgangsbeleg (Verkauf/Gutschrift)</label>
</div>
<!-- Upload Zone -->
<div id="uploadZone" class="upload-zone" onclick="document.getElementById('fileInput').click()">
<div class="upload-icon">&#128196;</div>
@@ -175,7 +182,7 @@ async function startProcessing(uploadId) {
const resp = await fetch('/api/scan-process', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ upload_id: uploadId }),
body: JSON.stringify({ upload_id: uploadId, beleg_type: document.querySelector('input[name="beleg_type"]:checked').value }),
});
if (!resp.ok) {
+86 -12
View File
@@ -77,13 +77,14 @@
</div>
<div class="card">
<h2>Import & Ordner</h2>
<h2>Import - Eingangsbelege</h2>
<div class="form-grid">
<div class="form-group form-group-wide">
<label for="import_email">Import-Emailadresse</label>
<input type="email" id="import_email" name="import_email"
value="{{ settings.get('import_email', '') }}" placeholder="import@example.com">
<label for="import_email_eingang">Import-Email Eingangsbelege</label>
<input type="email" id="import_email_eingang" name="import_email_eingang"
value="{{ settings.get('import_email_eingang', '') or settings.get('import_email', '') }}" placeholder="eingang@buchhaltung.example.com">
</div>
<input type="hidden" id="import_email" name="import_email" value="{{ settings.get('import_email_eingang', '') or settings.get('import_email', '') }}">
<div class="form-group">
<label for="source_folder">Eingangsordner (IMAP)</label>
<div class="input-with-btn">
@@ -101,6 +102,34 @@
</div>
</div>
</div>
</div>
<div class="card">
<h2>Import - Ausgangsbelege <small style="font-weight:normal;color:var(--text-muted);">(optional)</small></h2>
<div class="form-grid">
<div class="form-group form-group-wide">
<label for="import_email_ausgang">Import-Email Ausgangsbelege</label>
<input type="email" id="import_email_ausgang" name="import_email_ausgang"
value="{{ settings.get('import_email_ausgang', '') }}" placeholder="ausgang@buchhaltung.example.com">
<small class="text-muted">Leer lassen wenn keine Ausgangsbelege importiert werden sollen</small>
</div>
<div class="form-group">
<label for="source_folder_ausgang">Eingangsordner Ausgangsbelege (IMAP)</label>
<div class="input-with-btn">
<input type="text" id="source_folder_ausgang" name="source_folder_ausgang"
value="{{ settings.get('source_folder_ausgang', '') }}" placeholder="Ausgangsrechnungen">
<button type="button" class="btn btn-icon" onclick="openFolderPicker('source_folder_ausgang')" title="Ordner auswählen">&#128193;</button>
</div>
</div>
<div class="form-group">
<label for="processed_folder_ausgang">Verarbeitet-Ordner Ausgangsbelege (IMAP)</label>
<div class="input-with-btn">
<input type="text" id="processed_folder_ausgang" name="processed_folder_ausgang"
value="{{ settings.get('processed_folder_ausgang', '') }}" placeholder="Ausgangsrechnungen/Verarbeitet">
<button type="button" class="btn btn-icon" onclick="openFolderPicker('processed_folder_ausgang')" title="Ordner auswählen">&#128193;</button>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="testEmail()">
<span class="btn-text">Test-Email senden</span>
@@ -158,7 +187,7 @@
value="{{ settings.get('smb_share', '') }}" placeholder="Scans">
</div>
<div class="form-group">
<label for="smb_source_path">Quellordner</label>
<label for="smb_source_path">Quellordner Eingangsbelege</label>
<div class="input-with-btn">
<input type="text" id="smb_source_path" name="smb_source_path"
value="{{ settings.get('smb_source_path', '') }}" placeholder="(Wurzel der Freigabe)">
@@ -166,13 +195,29 @@
</div>
</div>
<div class="form-group">
<label for="smb_processed_path">Verarbeitet-Ordner</label>
<label for="smb_processed_path">Verarbeitet-Ordner Eingangsbelege</label>
<div class="input-with-btn">
<input type="text" id="smb_processed_path" name="smb_processed_path"
value="{{ settings.get('smb_processed_path', 'Verarbeitet') }}" placeholder="Verarbeitet">
<button type="button" class="btn btn-icon" onclick="openSmbFolderPicker('smb_processed_path')" title="Ordner auswählen">&#128193;</button>
</div>
</div>
<div class="form-group">
<label for="smb_source_path_ausgang">Quellordner Ausgangsbelege</label>
<div class="input-with-btn">
<input type="text" id="smb_source_path_ausgang" name="smb_source_path_ausgang"
value="{{ settings.get('smb_source_path_ausgang', '') }}" placeholder="(optional)">
<button type="button" class="btn btn-icon" onclick="openSmbFolderPicker('smb_source_path_ausgang')" title="Ordner auswählen">&#128193;</button>
</div>
</div>
<div class="form-group">
<label for="smb_processed_path_ausgang">Verarbeitet-Ordner Ausgangsbelege</label>
<div class="input-with-btn">
<input type="text" id="smb_processed_path_ausgang" name="smb_processed_path_ausgang"
value="{{ settings.get('smb_processed_path_ausgang', '') }}" placeholder="(optional)">
<button type="button" class="btn btn-icon" onclick="openSmbFolderPicker('smb_processed_path_ausgang')" title="Ordner auswählen">&#128193;</button>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="testSmb()">
@@ -206,6 +251,20 @@
</div>
</div>
<div class="card">
<h2>Debug</h2>
<div class="form-grid">
<div class="form-group">
<label for="debug_save_amazon_pdfs">Amazon-PDFs zwischenspeichern</label>
<select id="debug_save_amazon_pdfs" name="debug_save_amazon_pdfs">
<option value="false" {% if settings.get('debug_save_amazon_pdfs') != 'true' %}selected{% endif %}>Aus</option>
<option value="true" {% if settings.get('debug_save_amazon_pdfs') == 'true' %}selected{% endif %}>An</option>
</select>
<small class="text-muted">Speichert heruntergeladene Amazon-Rechnungen in /data/uploads/amazon_invoices/</small>
</div>
</div>
</div>
<div class="form-actions-main">
<button type="submit" class="btn btn-primary">Einstellungen speichern</button>
<button type="button" class="btn btn-success" onclick="manualProcess()">
@@ -228,6 +287,7 @@
<th>Betreff</th>
<th>Absender</th>
<th>Anhänge</th>
<th>Art</th>
<th>Status</th>
</tr>
</thead>
@@ -238,6 +298,13 @@
<td>{{ log.email_subject or '-' }}</td>
<td>{{ log.email_from or '-' }}</td>
<td>{{ log.attachments_count }}</td>
<td>
{% if log.get('beleg_type', 'eingang') == 'ausgang' %}
<span class="badge badge-warning">Ausgang</span>
{% else %}
<span class="badge badge-info">Eingang</span>
{% endif %}
</td>
<td>
{% if log.status == 'success' %}
<span class="badge badge-success">OK</span>
@@ -343,8 +410,11 @@ async function testEmail() {
const resp = await fetch('/api/test-email', { method: 'POST', body: getFormData() });
const data = await resp.json();
if (data.success) {
const addr = document.getElementById('import_email').value;
showAlert('Test-Email erfolgreich an ' + addr + ' gesendet! Einstellungen gespeichert.', 'success');
const eingang = document.getElementById('import_email_eingang').value;
const ausgang = document.getElementById('import_email_ausgang').value;
let targets = eingang;
if (ausgang) targets += ' + ' + ausgang;
showAlert('Test-Email erfolgreich an ' + targets + ' gesendet! Einstellungen gespeichert.', 'success');
} else {
showAlert('Test-Email fehlgeschlagen: ' + data.error, 'error');
}
@@ -447,8 +517,10 @@ function showFolderModal(targetField) {
const currentValue = folderTargetField ? document.getElementById(folderTargetField).value : '';
let html = '<div class="folder-picker-fields">';
html += '<button type="button" class="folder-field-btn ' + (folderTargetField === 'source_folder' ? 'active' : '') + '" onclick="switchFolderTarget(\'source_folder\')">Eingangsordner: <strong>' + esc(document.getElementById('source_folder').value) + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (folderTargetField === 'processed_folder' ? 'active' : '') + '" onclick="switchFolderTarget(\'processed_folder\')">Verarbeitet-Ordner: <strong>' + esc(document.getElementById('processed_folder').value) + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (folderTargetField === 'source_folder' ? 'active' : '') + '" onclick="switchFolderTarget(\'source_folder\')">Eingang Quelle: <strong>' + esc(document.getElementById('source_folder').value) + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (folderTargetField === 'processed_folder' ? 'active' : '') + '" onclick="switchFolderTarget(\'processed_folder\')">Eingang Verarbeitet: <strong>' + esc(document.getElementById('processed_folder').value) + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (folderTargetField === 'source_folder_ausgang' ? 'active' : '') + '" onclick="switchFolderTarget(\'source_folder_ausgang\')">Ausgang Quelle: <strong>' + esc(document.getElementById('source_folder_ausgang').value || '(nicht gesetzt)') + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (folderTargetField === 'processed_folder_ausgang' ? 'active' : '') + '" onclick="switchFolderTarget(\'processed_folder_ausgang\')">Ausgang Verarbeitet: <strong>' + esc(document.getElementById('processed_folder_ausgang').value || '(nicht gesetzt)') + '</strong></button>';
html += '</div>';
html += '<div class="folder-items">';
if (cachedFolders && cachedFolders.length > 0) {
@@ -651,8 +723,10 @@ function showSmbFolderModal(targetField) {
const currentValue = smbFolderTargetField ? document.getElementById(smbFolderTargetField).value : '';
let html = '<div class="folder-picker-fields">';
html += '<button type="button" class="folder-field-btn ' + (smbFolderTargetField === 'smb_source_path' ? 'active' : '') + '" onclick="switchSmbFolderTarget(\'smb_source_path\')">Quellordner: <strong>' + esc(document.getElementById('smb_source_path').value || '(Wurzel)') + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (smbFolderTargetField === 'smb_processed_path' ? 'active' : '') + '" onclick="switchSmbFolderTarget(\'smb_processed_path\')">Verarbeitet-Ordner: <strong>' + esc(document.getElementById('smb_processed_path').value) + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (smbFolderTargetField === 'smb_source_path' ? 'active' : '') + '" onclick="switchSmbFolderTarget(\'smb_source_path\')">Eingang Quelle: <strong>' + esc(document.getElementById('smb_source_path').value || '(Wurzel)') + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (smbFolderTargetField === 'smb_processed_path' ? 'active' : '') + '" onclick="switchSmbFolderTarget(\'smb_processed_path\')">Eingang Verarbeitet: <strong>' + esc(document.getElementById('smb_processed_path').value) + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (smbFolderTargetField === 'smb_source_path_ausgang' ? 'active' : '') + '" onclick="switchSmbFolderTarget(\'smb_source_path_ausgang\')">Ausgang Quelle: <strong>' + esc(document.getElementById('smb_source_path_ausgang').value || '(nicht gesetzt)') + '</strong></button>';
html += '<button type="button" class="folder-field-btn ' + (smbFolderTargetField === 'smb_processed_path_ausgang' ? 'active' : '') + '" onclick="switchSmbFolderTarget(\'smb_processed_path_ausgang\')">Ausgang Verarbeitet: <strong>' + esc(document.getElementById('smb_processed_path_ausgang').value || '(nicht gesetzt)') + '</strong></button>';
html += '</div>';
html += '<div class="folder-items">';