first commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LexOffice Belegimport</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="nav-brand">LexOffice 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="/log" class="{% if active_page == 'log' %}active{% endif %}">Verarbeitungslog</a>
|
||||
</div>
|
||||
<div class="nav-status">
|
||||
{% if status and status.enabled %}
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
{% if status.next_run %}
|
||||
<span class="text-muted">Nächster Lauf: {{ status.next_run }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge badge-inactive">Inaktiv</span>
|
||||
{% endif %}
|
||||
{% if status and status.is_processing %}
|
||||
<span class="badge badge-warning">Verarbeitung läuft...</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% if message %}
|
||||
<div class="alert alert-{{ message_type or 'info' }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "log" %}
|
||||
{% set message = None %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Verarbeitungslog</h2>
|
||||
{% if logs %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Betreff</th>
|
||||
<th>Absender</th>
|
||||
<th>Anhänge</th>
|
||||
<th>Status</th>
|
||||
<th>Fehlermeldung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr class="{% if log.status == 'error' %}row-error{% endif %}">
|
||||
<td>{{ log.id }}</td>
|
||||
<td>{{ log.timestamp }}</td>
|
||||
<td>{{ log.email_subject or '-' }}</td>
|
||||
<td>{{ log.email_from or '-' }}</td>
|
||||
<td>{{ log.attachments_count }}</td>
|
||||
<td>
|
||||
{% if log.status == 'success' %}
|
||||
<span class="badge badge-success">OK</span>
|
||||
{% else %}
|
||||
<span class="badge badge-error">Fehler</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ log.error_message or '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted">Noch keine Einträge vorhanden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,283 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "scan" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Scan-Upload</h2>
|
||||
<p class="text-muted" style="margin-bottom:1rem;">
|
||||
Mehrseitige PDF hochladen. Trennseiten mit QR-Code werden automatisch erkannt und die einzelnen Dokumente an LexOffice gesendet.
|
||||
</p>
|
||||
|
||||
<!-- Upload Zone -->
|
||||
<div id="uploadZone" class="upload-zone" onclick="document.getElementById('fileInput').click()">
|
||||
<div class="upload-icon">📄</div>
|
||||
<div class="upload-text">PDF hierher ziehen oder klicken zum Auswählen</div>
|
||||
<div class="upload-hint">Unterstützt große Dateien (1 GB+)</div>
|
||||
<input type="file" id="fileInput" accept=".pdf,application/pdf" style="display:none;">
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div id="uploadProgress" style="display:none;">
|
||||
<div class="progress-header">
|
||||
<span id="uploadFilename"></span>
|
||||
<span id="uploadPercent">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar-track">
|
||||
<div id="uploadBar" class="progress-bar-fill" style="width:0%"></div>
|
||||
</div>
|
||||
<div id="uploadStatus" class="text-muted" style="margin-top:0.3rem;font-size:0.85rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Progress -->
|
||||
<div id="processProgress" style="display:none;">
|
||||
<div class="progress-header">
|
||||
<span>Verarbeitung</span>
|
||||
<span id="processStage"></span>
|
||||
</div>
|
||||
<div class="progress-bar-track">
|
||||
<div id="processBar" class="progress-bar-fill progress-bar-processing" style="width:0%"></div>
|
||||
</div>
|
||||
<div id="processStatus" class="text-muted" style="margin-top:0.3rem;font-size:0.85rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div id="resultArea" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Trennseiten</h2>
|
||||
<p class="text-muted" style="margin-bottom:1rem;">
|
||||
Trennseiten ausdrucken und zwischen die Dokumente legen, bevor der Stapel gescannt wird.
|
||||
</p>
|
||||
<a href="/api/separator-pdf" class="btn btn-secondary" download>Trennseiten-PDF herunterladen</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
let currentUploadId = null;
|
||||
|
||||
// Drag & Drop
|
||||
const zone = document.getElementById('uploadZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
|
||||
zone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
zone.classList.add('upload-zone-active');
|
||||
});
|
||||
|
||||
zone.addEventListener('dragleave', () => {
|
||||
zone.classList.remove('upload-zone-active');
|
||||
});
|
||||
|
||||
zone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
zone.classList.remove('upload-zone-active');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) handleFile(files[0]);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (fileInput.files.length > 0) handleFile(fileInput.files[0]);
|
||||
});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
function generateUUID() {
|
||||
return crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFile(file) {
|
||||
if (!file.name.toLowerCase().endsWith('.pdf')) {
|
||||
showResult('Bitte eine PDF-Datei auswählen.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
document.getElementById('resultArea').style.display = 'none';
|
||||
document.getElementById('processProgress').style.display = 'none';
|
||||
zone.style.display = 'none';
|
||||
|
||||
const uploadId = generateUUID();
|
||||
currentUploadId = uploadId;
|
||||
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
const uploadProgress = document.getElementById('uploadProgress');
|
||||
const uploadBar = document.getElementById('uploadBar');
|
||||
const uploadPercent = document.getElementById('uploadPercent');
|
||||
const uploadStatus = document.getElementById('uploadStatus');
|
||||
const uploadFilename = document.getElementById('uploadFilename');
|
||||
|
||||
uploadFilename.textContent = file.name + ' (' + formatBytes(file.size) + ')';
|
||||
uploadBar.style.width = '0%';
|
||||
uploadPercent.textContent = '0%';
|
||||
uploadStatus.textContent = 'Upload wird gestartet...';
|
||||
uploadProgress.style.display = '';
|
||||
|
||||
// Chunked upload
|
||||
try {
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', chunk);
|
||||
formData.append('chunk_index', i);
|
||||
formData.append('total_chunks', totalChunks);
|
||||
formData.append('upload_id', uploadId);
|
||||
formData.append('filename', file.name);
|
||||
|
||||
const resp = await fetch('/api/scan-upload-chunk', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
throw new Error(errData.error || 'Upload fehlgeschlagen (HTTP ' + resp.status + ')');
|
||||
}
|
||||
|
||||
const pct = Math.round(((i + 1) / totalChunks) * 100);
|
||||
uploadBar.style.width = pct + '%';
|
||||
uploadPercent.textContent = pct + '%';
|
||||
uploadStatus.textContent = 'Chunk ' + (i + 1) + '/' + totalChunks + ' (' + formatBytes(end) + ' von ' + formatBytes(file.size) + ')';
|
||||
}
|
||||
|
||||
uploadStatus.textContent = 'Upload abgeschlossen. Starte Verarbeitung...';
|
||||
startProcessing(uploadId);
|
||||
|
||||
} catch (e) {
|
||||
showResult('Upload-Fehler: ' + e.message, 'error');
|
||||
resetUploadZone();
|
||||
}
|
||||
}
|
||||
|
||||
async function startProcessing(uploadId) {
|
||||
const processProgress = document.getElementById('processProgress');
|
||||
const processBar = document.getElementById('processBar');
|
||||
const processStage = document.getElementById('processStage');
|
||||
const processStatus = document.getElementById('processStatus');
|
||||
|
||||
processBar.style.width = '0%';
|
||||
processStage.textContent = '';
|
||||
processStatus.textContent = 'Starte Verarbeitung...';
|
||||
processProgress.style.display = '';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/scan-process', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ upload_id: uploadId }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
throw new Error(errData.error || 'Verarbeitung konnte nicht gestartet werden');
|
||||
}
|
||||
|
||||
// Subscribe to SSE for progress
|
||||
listenProgress(uploadId);
|
||||
|
||||
} catch (e) {
|
||||
showResult('Fehler: ' + e.message, 'error');
|
||||
resetUploadZone();
|
||||
}
|
||||
}
|
||||
|
||||
function listenProgress(uploadId) {
|
||||
const processBar = document.getElementById('processBar');
|
||||
const processStage = document.getElementById('processStage');
|
||||
const processStatus = document.getElementById('processStatus');
|
||||
|
||||
const evtSource = new EventSource('/api/scan-status/' + uploadId);
|
||||
|
||||
evtSource.addEventListener('scan', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const pct = Math.round((data.current / data.total) * 100);
|
||||
processBar.style.width = pct + '%';
|
||||
processStage.textContent = pct + '%';
|
||||
processStatus.textContent = 'Seite ' + data.current + ' von ' + data.total + ' analysiert...';
|
||||
});
|
||||
|
||||
evtSource.addEventListener('status', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
processStatus.textContent = data.message || '';
|
||||
});
|
||||
|
||||
evtSource.addEventListener('send', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const pct = Math.round((data.current / data.total) * 100);
|
||||
processBar.style.width = pct + '%';
|
||||
processStage.textContent = data.current + '/' + data.total;
|
||||
processStatus.textContent = 'Dokument ' + data.current + ' von ' + data.total + ' wird gesendet...';
|
||||
});
|
||||
|
||||
evtSource.addEventListener('done', (e) => {
|
||||
evtSource.close();
|
||||
const data = JSON.parse(e.data);
|
||||
const result = data.result || {};
|
||||
|
||||
let msg = '';
|
||||
if (result.error) {
|
||||
msg = result.error;
|
||||
showResult(msg, 'error');
|
||||
} else {
|
||||
msg = result.documents + ' Dokument(e) erkannt';
|
||||
if (result.separator_pages > 0) {
|
||||
msg += ' (' + result.separator_pages + ' Trennseite(n))';
|
||||
}
|
||||
msg += ', ' + result.sent + ' an LexOffice gesendet';
|
||||
if (result.errors > 0) {
|
||||
msg += ', ' + result.errors + ' Fehler';
|
||||
showResult(msg, 'warning');
|
||||
} else {
|
||||
showResult(msg, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('processProgress').style.display = 'none';
|
||||
resetUploadZone();
|
||||
});
|
||||
|
||||
evtSource.addEventListener('error', (e) => {
|
||||
// Check if it's an SSE error event from our backend
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
showResult('Verarbeitungsfehler: ' + (data.message || 'Unbekannter Fehler'), 'error');
|
||||
} catch {
|
||||
// Connection error
|
||||
showResult('Verbindung zum Server verloren.', 'error');
|
||||
}
|
||||
evtSource.close();
|
||||
document.getElementById('processProgress').style.display = 'none';
|
||||
resetUploadZone();
|
||||
});
|
||||
}
|
||||
|
||||
function showResult(message, type) {
|
||||
const area = document.getElementById('resultArea');
|
||||
area.innerHTML = '<div class="alert alert-' + type + '">' + escapeHtml(message) + '</div>';
|
||||
area.style.display = '';
|
||||
document.getElementById('uploadProgress').style.display = 'none';
|
||||
}
|
||||
|
||||
function resetUploadZone() {
|
||||
zone.style.display = '';
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
return d.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,805 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "settings" %}
|
||||
|
||||
{% block content %}
|
||||
<form id="settingsForm" method="post" action="/settings">
|
||||
<div class="card">
|
||||
<h2>IMAP Einstellungen (Empfang)</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="imap_server">Server</label>
|
||||
<input type="text" id="imap_server" name="imap_server"
|
||||
value="{{ settings.get('imap_server', '') }}" placeholder="imap.example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imap_port">Port</label>
|
||||
<input type="number" id="imap_port" name="imap_port"
|
||||
value="{{ settings.get('imap_port', '993') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imap_ssl">Verschlüsselung</label>
|
||||
<select id="imap_ssl" name="imap_ssl">
|
||||
<option value="true" {% if settings.get('imap_ssl') == 'true' %}selected{% endif %}>SSL/TLS</option>
|
||||
<option value="false" {% if settings.get('imap_ssl') == 'false' %}selected{% endif %}>Keine</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imap_username">Benutzername</label>
|
||||
<input type="text" id="imap_username" name="imap_username"
|
||||
value="{{ settings.get('imap_username', '') }}" placeholder="user@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imap_password">Passwort</label>
|
||||
<input type="password" id="imap_password" name="imap_password"
|
||||
placeholder="{% if settings.get('imap_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="testImap()">
|
||||
<span class="btn-text">Verbindung testen & Ordner laden</span>
|
||||
<span class="btn-spinner" style="display:none;">Verbinde...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>SMTP Einstellungen (Versand)</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="smtp_server">Server</label>
|
||||
<input type="text" id="smtp_server" name="smtp_server"
|
||||
value="{{ settings.get('smtp_server', '') }}" placeholder="smtp.example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtp_port">Port</label>
|
||||
<input type="number" id="smtp_port" name="smtp_port"
|
||||
value="{{ settings.get('smtp_port', '587') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtp_ssl">Verschlüsselung</label>
|
||||
<select id="smtp_ssl" name="smtp_ssl">
|
||||
<option value="starttls" {% if settings.get('smtp_ssl') == 'starttls' %}selected{% endif %}>STARTTLS</option>
|
||||
<option value="ssl" {% if settings.get('smtp_ssl') == 'ssl' %}selected{% endif %}>SSL/TLS</option>
|
||||
<option value="none" {% if settings.get('smtp_ssl') == 'none' %}selected{% endif %}>Keine</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtp_username">Benutzername</label>
|
||||
<input type="text" id="smtp_username" name="smtp_username"
|
||||
value="{{ settings.get('smtp_username', '') }}" placeholder="user@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtp_password">Passwort</label>
|
||||
<input type="password" id="smtp_password" name="smtp_password"
|
||||
placeholder="{% if settings.get('smtp_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>LexOffice & Ordner</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group form-group-wide">
|
||||
<label for="lexoffice_email">LexOffice Import-Emailadresse</label>
|
||||
<input type="email" id="lexoffice_email" name="lexoffice_email"
|
||||
value="{{ settings.get('lexoffice_email', '') }}" placeholder="import-xyz@lexoffice.de">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="source_folder">Eingangsordner (IMAP)</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="source_folder" name="source_folder"
|
||||
value="{{ settings.get('source_folder', 'Rechnungen') }}" placeholder="Rechnungen">
|
||||
<button type="button" class="btn btn-icon" onclick="openFolderPicker('source_folder')" title="Ordner auswählen">📁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="processed_folder">Verarbeitet-Ordner (IMAP)</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="processed_folder" name="processed_folder"
|
||||
value="{{ settings.get('processed_folder', 'Rechnungen/Verarbeitet') }}" placeholder="Rechnungen/Verarbeitet">
|
||||
<button type="button" class="btn btn-icon" onclick="openFolderPicker('processed_folder')" title="Ordner auswählen">📁</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="testEmail()">
|
||||
<span class="btn-text">Test-Email an LexOffice senden</span>
|
||||
<span class="btn-spinner" style="display:none;">Sende...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>SMB-Freigabe (Netzlaufwerk)</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="smb_enabled">SMB-Import</label>
|
||||
<select id="smb_enabled" name="smb_enabled">
|
||||
<option value="true" {% if settings.get('smb_enabled') == 'true' %}selected{% endif %}>Aktiviert</option>
|
||||
<option value="false" {% if settings.get('smb_enabled') != 'true' %}selected{% endif %}>Deaktiviert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_mode">Verarbeitungsmodus</label>
|
||||
<select id="smb_mode" name="smb_mode">
|
||||
<option value="forward" {% if settings.get('smb_mode', 'forward') == 'forward' %}selected{% endif %}>Direkt weiterleiten</option>
|
||||
<option value="separator" {% if settings.get('smb_mode') == 'separator' %}selected{% endif %}>Trennseiten-Erkennung</option>
|
||||
</select>
|
||||
<small class="text-muted">Direkt: jede PDF als ein Beleg. Trennseiten: QR-Splitting wie bei Scan-Upload.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_server">Server</label>
|
||||
<input type="text" id="smb_server" name="smb_server"
|
||||
value="{{ settings.get('smb_server', '') }}" placeholder="192.168.1.100 oder nas.local">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_port">Port</label>
|
||||
<input type="number" id="smb_port" name="smb_port"
|
||||
value="{{ settings.get('smb_port', '445') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_username">Benutzername</label>
|
||||
<input type="text" id="smb_username" name="smb_username"
|
||||
value="{{ settings.get('smb_username', '') }}" placeholder="scanner">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_password">Passwort</label>
|
||||
<input type="password" id="smb_password" name="smb_password"
|
||||
placeholder="{% if settings.get('smb_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_domain">Domäne</label>
|
||||
<input type="text" id="smb_domain" name="smb_domain"
|
||||
value="{{ settings.get('smb_domain', '') }}" placeholder="WORKGROUP (optional)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_share">Freigabename</label>
|
||||
<input type="text" id="smb_share" name="smb_share"
|
||||
value="{{ settings.get('smb_share', '') }}" placeholder="Scans">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_source_path">Quellordner</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)">
|
||||
<button type="button" class="btn btn-icon" onclick="openSmbFolderPicker('smb_source_path')" title="Ordner auswählen">📁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smb_processed_path">Verarbeitet-Ordner</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">📁</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="testSmb()">
|
||||
<span class="btn-text">Verbindung testen & Ordner laden</span>
|
||||
<span class="btn-spinner" style="display:none;">Verbinde...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Zeitplan</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="interval_minutes">Abruf-Intervall (Minuten)</label>
|
||||
<input type="number" id="interval_minutes" name="interval_minutes"
|
||||
value="{{ settings.get('interval_minutes', '5') }}" min="1" max="1440">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scheduler_enabled">Automatischer Abruf</label>
|
||||
<select id="scheduler_enabled" name="scheduler_enabled">
|
||||
<option value="true" {% if settings.get('scheduler_enabled') == 'true' %}selected{% endif %}>Aktiviert</option>
|
||||
<option value="false" {% if settings.get('scheduler_enabled') != 'true' %}selected{% endif %}>Deaktiviert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fetch_since_date">Emails erst ab Datum verarbeiten</label>
|
||||
<input type="date" id="fetch_since_date" name="fetch_since_date"
|
||||
value="{{ settings.get('fetch_since_date', '') }}">
|
||||
<small class="text-muted">Leer = alle Emails im Ordner</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()">
|
||||
<span class="btn-text">Jetzt abrufen</span>
|
||||
<span class="btn-spinner" style="display:none;">Verarbeite...</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Alert-Bereich für JS-Meldungen -->
|
||||
<div id="jsAlert" class="alert" style="display:none;"></div>
|
||||
|
||||
{% if logs %}
|
||||
<div class="card">
|
||||
<h2>Letzte Verarbeitungen</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Betreff</th>
|
||||
<th>Absender</th>
|
||||
<th>Anhänge</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr class="{% if log.status == 'error' %}row-error{% endif %}">
|
||||
<td>{{ log.timestamp }}</td>
|
||||
<td>{{ log.email_subject or '-' }}</td>
|
||||
<td>{{ log.email_from or '-' }}</td>
|
||||
<td>{{ log.attachments_count }}</td>
|
||||
<td>
|
||||
{% if log.status == 'success' %}
|
||||
<span class="badge badge-success">OK</span>
|
||||
{% else %}
|
||||
<span class="badge badge-error" title="{{ log.error_message or '' }}">Fehler</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Ordner-Auswahl Modal -->
|
||||
<div id="folderModal" class="modal-overlay" style="display:none;" onclick="closeFolderModal(event)">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="folderModalTitle">Ordner auswählen</h3>
|
||||
<button type="button" class="modal-close" onclick="closeFolderModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="folderList" class="folder-list"></div>
|
||||
<div id="folderLoading" class="text-muted" style="display:none;padding:1rem;">
|
||||
Verbinde und lade Ordner...
|
||||
</div>
|
||||
<div id="folderError" class="alert alert-error" style="display:none;margin:1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMB Ordner-Auswahl Modal -->
|
||||
<div id="smbFolderModal" class="modal-overlay" style="display:none;" onclick="closeSmbFolderModal(event)">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="smbFolderModalTitle">SMB-Ordner auswählen</h3>
|
||||
<button type="button" class="modal-close" onclick="closeSmbFolderModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="smbFolderList" class="folder-list"></div>
|
||||
<div id="smbFolderLoading" class="text-muted" style="display:none;padding:1rem;">
|
||||
Verbinde und lade Ordner...
|
||||
</div>
|
||||
<div id="smbFolderError" class="alert alert-error" style="display:none;margin:1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let cachedFolders = null;
|
||||
let cachedDelimiter = '.';
|
||||
let folderTargetField = null;
|
||||
let createOpenFor = null;
|
||||
|
||||
function showAlert(message, type) {
|
||||
const el = document.getElementById('jsAlert');
|
||||
el.textContent = message;
|
||||
el.className = 'alert alert-' + type;
|
||||
el.style.display = 'block';
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
setTimeout(() => { el.style.display = 'none'; }, 8000);
|
||||
}
|
||||
|
||||
function setButtonLoading(btn, loading) {
|
||||
const text = btn.querySelector('.btn-text');
|
||||
const spinner = btn.querySelector('.btn-spinner');
|
||||
if (text && spinner) {
|
||||
text.style.display = loading ? 'none' : '';
|
||||
spinner.style.display = loading ? '' : 'none';
|
||||
}
|
||||
btn.disabled = loading;
|
||||
}
|
||||
|
||||
function getFormData() {
|
||||
return new FormData(document.getElementById('settingsForm'));
|
||||
}
|
||||
|
||||
async function testImap() {
|
||||
const btn = event.currentTarget;
|
||||
setButtonLoading(btn, true);
|
||||
try {
|
||||
const resp = await fetch('/api/test-imap', { method: 'POST', body: getFormData() });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
cachedFolders = data.folders;
|
||||
cachedDelimiter = data.delimiter || '.';
|
||||
showAlert('IMAP-Verbindung erfolgreich! Einstellungen gespeichert.', 'success');
|
||||
showFolderModal(null);
|
||||
} else {
|
||||
showAlert('IMAP-Verbindung fehlgeschlagen: ' + data.error, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('Fehler: ' + e.message, 'error');
|
||||
} finally {
|
||||
setButtonLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function testEmail() {
|
||||
const btn = event.currentTarget;
|
||||
setButtonLoading(btn, true);
|
||||
try {
|
||||
const resp = await fetch('/api/test-email', { method: 'POST', body: getFormData() });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
const addr = document.getElementById('lexoffice_email').value;
|
||||
showAlert('Test-Email erfolgreich an ' + addr + ' gesendet! Einstellungen gespeichert.', 'success');
|
||||
} else {
|
||||
showAlert('Test-Email fehlgeschlagen: ' + data.error, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('Fehler: ' + e.message, 'error');
|
||||
} finally {
|
||||
setButtonLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function manualProcess() {
|
||||
const btn = event.currentTarget;
|
||||
setButtonLoading(btn, true);
|
||||
try {
|
||||
const fd = getFormData();
|
||||
let msgs = [];
|
||||
let hasErrors = false;
|
||||
|
||||
// IMAP
|
||||
const resp = await fetch('/api/process', { method: 'POST', body: fd });
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
msgs.push('IMAP-Fehler: ' + data.error);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
msgs.push(`IMAP: ${data.processed} weitergeleitet, ${data.skipped} übersprungen, ${data.errors} Fehler`);
|
||||
if (data.errors > 0) hasErrors = true;
|
||||
}
|
||||
|
||||
// SMB
|
||||
const smbResp = await fetch('/api/process-smb', { method: 'POST', body: fd });
|
||||
const smbData = await smbResp.json();
|
||||
if (smbData.error) {
|
||||
msgs.push('SMB-Fehler: ' + smbData.error);
|
||||
hasErrors = true;
|
||||
} else if (smbData.processed > 0 || smbData.errors > 0) {
|
||||
msgs.push(`SMB: ${smbData.processed} weitergeleitet, ${smbData.skipped} übersprungen, ${smbData.errors} Fehler`);
|
||||
if (smbData.errors > 0) hasErrors = true;
|
||||
}
|
||||
|
||||
showAlert(msgs.join(' | '), hasErrors ? 'warning' : 'success');
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} catch (e) {
|
||||
showAlert('Fehler: ' + e.message, 'error');
|
||||
} finally {
|
||||
setButtonLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
function openFolderPicker(targetField) {
|
||||
folderTargetField = targetField;
|
||||
if (cachedFolders) {
|
||||
showFolderModal(targetField);
|
||||
} else {
|
||||
showFolderModalLoading(targetField);
|
||||
fetchFolders(targetField);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFolders(targetField) {
|
||||
try {
|
||||
const resp = await fetch('/api/test-imap', { method: 'POST', body: getFormData() });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
cachedFolders = data.folders;
|
||||
cachedDelimiter = data.delimiter || '.';
|
||||
showFolderModal(targetField);
|
||||
} else {
|
||||
showFolderModalError('IMAP-Verbindung fehlgeschlagen: ' + data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
showFolderModalError('Fehler: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showFolderModalLoading(targetField) {
|
||||
const modal = document.getElementById('folderModal');
|
||||
document.getElementById('folderModalTitle').textContent = 'Ordner auswählen';
|
||||
document.getElementById('folderList').innerHTML = '';
|
||||
document.getElementById('folderLoading').style.display = '';
|
||||
document.getElementById('folderError').style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function esc(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function showFolderModal(targetField) {
|
||||
if (targetField) folderTargetField = targetField;
|
||||
createOpenFor = null;
|
||||
const modal = document.getElementById('folderModal');
|
||||
document.getElementById('folderModalTitle').textContent = 'Ordner auswählen';
|
||||
document.getElementById('folderLoading').style.display = 'none';
|
||||
document.getElementById('folderError').style.display = 'none';
|
||||
|
||||
const list = document.getElementById('folderList');
|
||||
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 += '</div>';
|
||||
html += '<div class="folder-items">';
|
||||
if (cachedFolders && cachedFolders.length > 0) {
|
||||
cachedFolders.forEach(folder => {
|
||||
const isSelected = folder === currentValue;
|
||||
const escapedFolder = folder.replace(/'/g, "\\'");
|
||||
html += '<div class="folder-row">';
|
||||
html += '<button type="button" class="folder-item' + (isSelected ? ' selected' : '') + '" onclick="selectFolder(\'' + escapedFolder + '\')">';
|
||||
html += '<span class="folder-icon">📁</span> ' + esc(folder);
|
||||
if (isSelected) html += ' <span class="badge badge-success">ausgewählt</span>';
|
||||
html += '</button>';
|
||||
html += '<button type="button" class="folder-add-btn" onclick="event.stopPropagation(); toggleCreateInput(\'' + escapedFolder + '\')" title="Unterordner erstellen">📁+</button>';
|
||||
html += '</div>';
|
||||
// Placeholder for inline create input
|
||||
html += '<div id="create-row-' + CSS.escape(folder) + '" class="create-inline" style="display:none;"></div>';
|
||||
});
|
||||
} else {
|
||||
html += '<p class="text-muted" style="padding:0.5rem;">Keine Ordner gefunden.</p>';
|
||||
}
|
||||
html += '</div>';
|
||||
list.innerHTML = html;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function switchFolderTarget(field) {
|
||||
folderTargetField = field;
|
||||
showFolderModal(field);
|
||||
}
|
||||
|
||||
function showFolderModalError(msg) {
|
||||
document.getElementById('folderLoading').style.display = 'none';
|
||||
document.getElementById('folderError').textContent = msg;
|
||||
document.getElementById('folderError').style.display = '';
|
||||
}
|
||||
|
||||
function selectFolder(folder) {
|
||||
if (folderTargetField) {
|
||||
document.getElementById(folderTargetField).value = folder;
|
||||
}
|
||||
showFolderModal(folderTargetField);
|
||||
}
|
||||
|
||||
function toggleCreateInput(parentFolder) {
|
||||
// Close any previously open create row
|
||||
document.querySelectorAll('.create-inline').forEach(el => {
|
||||
if (el.id !== 'create-row-' + CSS.escape(parentFolder)) {
|
||||
el.style.display = 'none';
|
||||
el.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
const row = document.getElementById('create-row-' + CSS.escape(parentFolder));
|
||||
if (!row) return;
|
||||
|
||||
if (row.style.display !== 'none') {
|
||||
row.style.display = 'none';
|
||||
row.innerHTML = '';
|
||||
createOpenFor = null;
|
||||
return;
|
||||
}
|
||||
|
||||
createOpenFor = parentFolder;
|
||||
row.innerHTML =
|
||||
'<div class="create-folder-inline">' +
|
||||
'<span class="create-folder-prefix">' + esc(parentFolder) + cachedDelimiter + '</span>' +
|
||||
'<input type="text" class="create-folder-input" id="newSubfolderInput" placeholder="Name" autofocus>' +
|
||||
'<button type="button" class="btn btn-primary btn-sm" onclick="doCreateFolder(\'' + parentFolder.replace(/'/g, "\\'") + '\')">OK</button>' +
|
||||
'<button type="button" class="btn btn-secondary btn-sm" onclick="toggleCreateInput(\'' + parentFolder.replace(/'/g, "\\'") + '\')">Abbrechen</button>' +
|
||||
'</div>' +
|
||||
'<div id="createError" class="text-error" style="display:none;"></div>';
|
||||
row.style.display = '';
|
||||
|
||||
const input = document.getElementById('newSubfolderInput');
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
doCreateFolder(parentFolder);
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
toggleCreateInput(parentFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function doCreateFolder(parentFolder) {
|
||||
const input = document.getElementById('newSubfolderInput');
|
||||
const errorEl = document.getElementById('createError');
|
||||
if (!input) return;
|
||||
|
||||
const subName = input.value.trim();
|
||||
if (!subName) {
|
||||
errorEl.textContent = 'Bitte einen Namen eingeben.';
|
||||
errorEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const fullName = parentFolder + cachedDelimiter + subName;
|
||||
errorEl.style.display = 'none';
|
||||
input.disabled = true;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/create-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_name: fullName })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
cachedFolders = data.folders;
|
||||
if (folderTargetField) {
|
||||
document.getElementById(folderTargetField).value = fullName;
|
||||
}
|
||||
showFolderModal(folderTargetField);
|
||||
} else {
|
||||
errorEl.textContent = data.error;
|
||||
errorEl.style.display = '';
|
||||
input.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
errorEl.textContent = e.message;
|
||||
errorEl.style.display = '';
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeFolderModal(event) {
|
||||
if (event && event.target !== document.getElementById('folderModal')) return;
|
||||
document.getElementById('folderModal').style.display = 'none';
|
||||
cachedFolders = null;
|
||||
createOpenFor = null;
|
||||
}
|
||||
|
||||
// --- SMB Folder Picker ---
|
||||
let cachedSmbFolders = null;
|
||||
let smbFolderTargetField = null;
|
||||
|
||||
async function testSmb() {
|
||||
const btn = event.currentTarget;
|
||||
setButtonLoading(btn, true);
|
||||
try {
|
||||
const resp = await fetch('/api/test-smb', { method: 'POST', body: getFormData() });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
cachedSmbFolders = data.folders;
|
||||
showAlert('SMB-Verbindung erfolgreich! Einstellungen gespeichert.', 'success');
|
||||
showSmbFolderModal(null);
|
||||
} else {
|
||||
showAlert('SMB-Verbindung fehlgeschlagen: ' + data.error, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('Fehler: ' + e.message, 'error');
|
||||
} finally {
|
||||
setButtonLoading(btn, false);
|
||||
}
|
||||
}
|
||||
|
||||
function openSmbFolderPicker(targetField) {
|
||||
smbFolderTargetField = targetField;
|
||||
if (cachedSmbFolders) {
|
||||
showSmbFolderModal(targetField);
|
||||
} else {
|
||||
showSmbFolderModalLoading(targetField);
|
||||
fetchSmbFolders(targetField);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSmbFolders(targetField) {
|
||||
try {
|
||||
const resp = await fetch('/api/test-smb', { method: 'POST', body: getFormData() });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
cachedSmbFolders = data.folders;
|
||||
showSmbFolderModal(targetField);
|
||||
} else {
|
||||
showSmbFolderModalError('SMB-Verbindung fehlgeschlagen: ' + data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
showSmbFolderModalError('Fehler: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showSmbFolderModalLoading(targetField) {
|
||||
const modal = document.getElementById('smbFolderModal');
|
||||
document.getElementById('smbFolderList').innerHTML = '';
|
||||
document.getElementById('smbFolderLoading').style.display = '';
|
||||
document.getElementById('smbFolderError').style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function showSmbFolderModal(targetField) {
|
||||
if (targetField) smbFolderTargetField = targetField;
|
||||
const modal = document.getElementById('smbFolderModal');
|
||||
document.getElementById('smbFolderLoading').style.display = 'none';
|
||||
document.getElementById('smbFolderError').style.display = 'none';
|
||||
|
||||
const list = document.getElementById('smbFolderList');
|
||||
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 += '</div>';
|
||||
html += '<div class="folder-items">';
|
||||
|
||||
// Root option for source path
|
||||
if (smbFolderTargetField === 'smb_source_path') {
|
||||
const isRoot = currentValue === '';
|
||||
html += '<div class="folder-row">';
|
||||
html += '<button type="button" class="folder-item' + (isRoot ? ' selected' : '') + '" onclick="selectSmbFolder(\'\')">';
|
||||
html += '<span class="folder-icon">📁</span> (Wurzel der Freigabe)';
|
||||
if (isRoot) html += ' <span class="badge badge-success">ausgewählt</span>';
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (cachedSmbFolders && cachedSmbFolders.length > 0) {
|
||||
cachedSmbFolders.forEach(folder => {
|
||||
const isSelected = folder === currentValue;
|
||||
const escapedFolder = folder.replace(/'/g, "\\'");
|
||||
html += '<div class="folder-row">';
|
||||
html += '<button type="button" class="folder-item' + (isSelected ? ' selected' : '') + '" onclick="selectSmbFolder(\'' + escapedFolder + '\')">';
|
||||
html += '<span class="folder-icon">📁</span> ' + esc(folder);
|
||||
if (isSelected) html += ' <span class="badge badge-success">ausgewählt</span>';
|
||||
html += '</button>';
|
||||
html += '<button type="button" class="folder-add-btn" onclick="event.stopPropagation(); toggleSmbCreateInput(\'' + escapedFolder + '\')" title="Unterordner erstellen">📁+</button>';
|
||||
html += '</div>';
|
||||
html += '<div id="smb-create-row-' + CSS.escape(folder) + '" class="create-inline" style="display:none;"></div>';
|
||||
});
|
||||
} else {
|
||||
html += '<p class="text-muted" style="padding:0.5rem;">Keine Ordner gefunden.</p>';
|
||||
}
|
||||
|
||||
// Create new root folder option
|
||||
html += '<div class="folder-row" style="border-top:1px solid var(--border);margin-top:0.5rem;padding-top:0.5rem;">';
|
||||
html += '<button type="button" class="folder-item" onclick="event.stopPropagation(); toggleSmbCreateInput(\'\')" style="color:var(--primary);">';
|
||||
html += '<span class="folder-icon">📁+</span> Neuen Ordner erstellen';
|
||||
html += '</button>';
|
||||
html += '</div>';
|
||||
html += '<div id="smb-create-row-root" class="create-inline" style="display:none;"></div>';
|
||||
|
||||
html += '</div>';
|
||||
list.innerHTML = html;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function switchSmbFolderTarget(field) {
|
||||
smbFolderTargetField = field;
|
||||
showSmbFolderModal(field);
|
||||
}
|
||||
|
||||
function selectSmbFolder(folder) {
|
||||
if (smbFolderTargetField) {
|
||||
document.getElementById(smbFolderTargetField).value = folder;
|
||||
}
|
||||
showSmbFolderModal(smbFolderTargetField);
|
||||
}
|
||||
|
||||
function showSmbFolderModalError(msg) {
|
||||
document.getElementById('smbFolderLoading').style.display = 'none';
|
||||
document.getElementById('smbFolderError').textContent = msg;
|
||||
document.getElementById('smbFolderError').style.display = '';
|
||||
}
|
||||
|
||||
function toggleSmbCreateInput(parentFolder) {
|
||||
document.querySelectorAll('.create-inline[id^="smb-create-row"]').forEach(el => {
|
||||
const rowId = parentFolder === '' ? 'smb-create-row-root' : 'smb-create-row-' + CSS.escape(parentFolder);
|
||||
if (el.id !== rowId) {
|
||||
el.style.display = 'none';
|
||||
el.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
const rowId = parentFolder === '' ? 'smb-create-row-root' : 'smb-create-row-' + CSS.escape(parentFolder);
|
||||
const row = document.getElementById(rowId);
|
||||
if (!row) return;
|
||||
|
||||
if (row.style.display !== 'none') {
|
||||
row.style.display = 'none';
|
||||
row.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = parentFolder ? parentFolder + '/' : '';
|
||||
row.innerHTML =
|
||||
'<div class="create-folder-inline">' +
|
||||
'<span class="create-folder-prefix">' + esc(prefix) + '</span>' +
|
||||
'<input type="text" class="create-folder-input" id="newSmbSubfolderInput" placeholder="Name" autofocus>' +
|
||||
'<button type="button" class="btn btn-primary btn-sm" onclick="doCreateSmbFolder(\'' + parentFolder.replace(/'/g, "\\'") + '\')">OK</button>' +
|
||||
'<button type="button" class="btn btn-secondary btn-sm" onclick="toggleSmbCreateInput(\'' + parentFolder.replace(/'/g, "\\'") + '\')">Abbrechen</button>' +
|
||||
'</div>' +
|
||||
'<div id="smbCreateError" class="text-error" style="display:none;"></div>';
|
||||
row.style.display = '';
|
||||
|
||||
const input = document.getElementById('newSmbSubfolderInput');
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); doCreateSmbFolder(parentFolder); }
|
||||
if (e.key === 'Escape') { toggleSmbCreateInput(parentFolder); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function doCreateSmbFolder(parentFolder) {
|
||||
const input = document.getElementById('newSmbSubfolderInput');
|
||||
const errorEl = document.getElementById('smbCreateError');
|
||||
if (!input) return;
|
||||
|
||||
const subName = input.value.trim();
|
||||
if (!subName) {
|
||||
errorEl.textContent = 'Bitte einen Namen eingeben.';
|
||||
errorEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const fullName = parentFolder ? parentFolder + '/' + subName : subName;
|
||||
errorEl.style.display = 'none';
|
||||
input.disabled = true;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/create-smb-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_name: fullName })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
cachedSmbFolders = data.folders;
|
||||
if (smbFolderTargetField) {
|
||||
document.getElementById(smbFolderTargetField).value = fullName;
|
||||
}
|
||||
showSmbFolderModal(smbFolderTargetField);
|
||||
} else {
|
||||
errorEl.textContent = data.error;
|
||||
errorEl.style.display = '';
|
||||
input.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
errorEl.textContent = e.message;
|
||||
errorEl.style.display = '';
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeSmbFolderModal(event) {
|
||||
if (event && event.target !== document.getElementById('smbFolderModal')) return;
|
||||
document.getElementById('smbFolderModal').style.display = 'none';
|
||||
cachedSmbFolders = null;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user