1398 lines
64 KiB
HTML
1398 lines
64 KiB
HTML
{% 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>Import - Eingangsbelege</h2>
|
|
<div class="form-grid">
|
|
<div class="form-group form-group-wide">
|
|
<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">
|
|
<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>
|
|
|
|
<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">📁</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">📁</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>
|
|
<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 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)">
|
|
<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 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">📁</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">📁</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">📁</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>FTP / SFTP-Server</h2>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="ftp_enabled">FTP-Import</label>
|
|
<select id="ftp_enabled" name="ftp_enabled">
|
|
<option value="true" {% if settings.get('ftp_enabled') == 'true' %}selected{% endif %}>Aktiviert</option>
|
|
<option value="false" {% if settings.get('ftp_enabled') != 'true' %}selected{% endif %}>Deaktiviert</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_protocol">Protokoll</label>
|
|
<select id="ftp_protocol" name="ftp_protocol" onchange="updateFtpDefaultPort()">
|
|
<option value="sftp" {% if settings.get('ftp_protocol', 'sftp') == 'sftp' %}selected{% endif %}>SFTP (SSH, verschluesselt)</option>
|
|
<option value="ftp" {% if settings.get('ftp_protocol') == 'ftp' %}selected{% endif %}>FTP (passiv, unverschluesselt)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_mode">Verarbeitungsmodus</label>
|
|
<select id="ftp_mode" name="ftp_mode">
|
|
<option value="forward" {% if settings.get('ftp_mode', 'forward') == 'forward' %}selected{% endif %}>Direkt weiterleiten</option>
|
|
<option value="separator" {% if settings.get('ftp_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="ftp_server">Server</label>
|
|
<input type="text" id="ftp_server" name="ftp_server"
|
|
value="{{ settings.get('ftp_server', '') }}" placeholder="ftp.example.com oder 192.168.1.100">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_port">Port</label>
|
|
<input type="number" id="ftp_port" name="ftp_port"
|
|
value="{{ settings.get('ftp_port', '22') }}">
|
|
<small class="text-muted">SFTP=22, FTP=21</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_username">Benutzername</label>
|
|
<input type="text" id="ftp_username" name="ftp_username"
|
|
value="{{ settings.get('ftp_username', '') }}" placeholder="user">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_password">Passwort</label>
|
|
<input type="password" id="ftp_password" name="ftp_password"
|
|
placeholder="{% if settings.get('ftp_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_source_path">Quellordner Eingangsbelege</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="ftp_source_path" name="ftp_source_path"
|
|
value="{{ settings.get('ftp_source_path', '') }}" placeholder="(Wurzel)">
|
|
<button type="button" class="btn btn-icon" onclick="openFtpFolderPicker('ftp_source_path')" title="Ordner auswählen">📁</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_processed_path">Verarbeitet-Ordner Eingangsbelege</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="ftp_processed_path" name="ftp_processed_path"
|
|
value="{{ settings.get('ftp_processed_path', 'Verarbeitet') }}" placeholder="Verarbeitet">
|
|
<button type="button" class="btn btn-icon" onclick="openFtpFolderPicker('ftp_processed_path')" title="Ordner auswählen">📁</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_source_path_ausgang">Quellordner Ausgangsbelege</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="ftp_source_path_ausgang" name="ftp_source_path_ausgang"
|
|
value="{{ settings.get('ftp_source_path_ausgang', '') }}" placeholder="(optional)">
|
|
<button type="button" class="btn btn-icon" onclick="openFtpFolderPicker('ftp_source_path_ausgang')" title="Ordner auswählen">📁</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ftp_processed_path_ausgang">Verarbeitet-Ordner Ausgangsbelege</label>
|
|
<div class="input-with-btn">
|
|
<input type="text" id="ftp_processed_path_ausgang" name="ftp_processed_path_ausgang"
|
|
value="{{ settings.get('ftp_processed_path_ausgang', '') }}" placeholder="(optional)">
|
|
<button type="button" class="btn btn-icon" onclick="openFtpFolderPicker('ftp_processed_path_ausgang')" title="Ordner auswählen">📁</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="testFtp()">
|
|
<span class="btn-text">Verbindung testen & Ordner laden</span>
|
|
<span class="btn-spinner" style="display:none;">Verbinde...</span>
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" onclick="processFtp()">
|
|
<span class="btn-text">Jetzt abrufen</span>
|
|
<span class="btn-spinner" style="display:none;">Verarbeite...</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="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()">
|
|
<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>Art</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.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>
|
|
{% 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>
|
|
<div style="display:flex;gap:0.5rem;align-items:center;">
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="reloadSmbFolders()" title="Ordner-Liste neu laden">↺ Neu laden</button>
|
|
<button type="button" class="modal-close" onclick="closeSmbFolderModal()">×</button>
|
|
</div>
|
|
</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>
|
|
|
|
<div id="ftpFolderModal" class="modal-overlay" style="display:none;" onclick="closeFtpFolderModal(event)">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3 id="ftpFolderModalTitle">FTP-Ordner auswählen</h3>
|
|
<div style="display:flex;gap:0.5rem;align-items:center;">
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="reloadFtpFolders()" title="Ordner-Liste neu laden">↺ Neu laden</button>
|
|
<button type="button" class="modal-close" onclick="closeFtpFolderModal()">×</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="ftpFolderList" class="folder-list"></div>
|
|
<div id="ftpFolderLoading" class="text-muted" style="display:none;padding:1rem;">
|
|
Verbinde und lade Ordner...
|
|
</div>
|
|
<div id="ftpFolderError" 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;
|
|
|
|
// --- Generic Tree-View Helpers (with Lazy Loading) ---
|
|
// Used by both SMB and FTP folder pickers
|
|
const expandedTreeNodes = { smb: new Set(), ftp: new Set() };
|
|
const loadedTreeNodes = { smb: new Set(['']), ftp: new Set([''])}; // root is always loaded
|
|
const loadingTreeNodes = { smb: new Set(), ftp: new Set() };
|
|
|
|
function buildFolderTree(paths) {
|
|
// Build nested tree from flat paths like ["A", "A/B", "A/B/C", "X"]
|
|
const root = { name: '', path: '', children: {} };
|
|
paths.forEach(p => {
|
|
if (!p) return;
|
|
const parts = p.split('/');
|
|
let node = root;
|
|
let curPath = '';
|
|
for (const part of parts) {
|
|
curPath = curPath ? curPath + '/' + part : part;
|
|
if (!node.children[part]) {
|
|
node.children[part] = { name: part, path: curPath, children: {} };
|
|
}
|
|
node = node.children[part];
|
|
}
|
|
});
|
|
return root;
|
|
}
|
|
|
|
function renderFolderTree(node, ns, currentValue, selectFn, addBtnFn, depth) {
|
|
let html = '';
|
|
const childKeys = Object.keys(node.children).sort((a, b) => a.localeCompare(b));
|
|
|
|
childKeys.forEach(key => {
|
|
const child = node.children[key];
|
|
const hasChildren = Object.keys(child.children).length > 0;
|
|
const isLoaded = loadedTreeNodes[ns].has(child.path);
|
|
const isLoading = loadingTreeNodes[ns].has(child.path);
|
|
const isExpanded = expandedTreeNodes[ns].has(child.path);
|
|
const isSelected = child.path === currentValue;
|
|
const escapedPath = child.path.replace(/'/g, "\\'");
|
|
|
|
// Always show toggle button - we don't know yet if there are children until loaded
|
|
const arrow = isLoading ? '⌛' // ⌛
|
|
: isExpanded ? '▼' // ▼
|
|
: '▶'; // ▶
|
|
|
|
html += '<div class="folder-row">';
|
|
html += '<button type="button" class="folder-tree-toggle" onclick="toggleTreeNode(\'' + ns + '\',\'' + escapedPath + '\')">' + arrow + '</button>';
|
|
html += '<button type="button" class="folder-item' + (isSelected ? ' selected' : '') + '" onclick="' + selectFn + '(\'' + escapedPath + '\')">';
|
|
html += '<span class="folder-icon">📁</span> ' + esc(child.name);
|
|
if (isSelected) html += ' <span class="badge badge-success">ausgewählt</span>';
|
|
html += '</button>';
|
|
html += '<button type="button" class="folder-add-btn" onclick="event.stopPropagation(); ' + addBtnFn + '(\'' + escapedPath + '\')" title="Unterordner erstellen">📁+</button>';
|
|
html += '</div>';
|
|
html += '<div id="' + ns + '-create-row-' + CSS.escape(child.path) + '" class="create-inline" style="display:none;"></div>';
|
|
|
|
if (isExpanded && (hasChildren || isLoaded)) {
|
|
html += '<div class="folder-tree-children">';
|
|
if (hasChildren) {
|
|
html += renderFolderTree(child, ns, currentValue, selectFn, addBtnFn, depth + 1);
|
|
} else if (isLoaded) {
|
|
html += '<p class="text-muted" style="padding:0.25rem 0.5rem;font-size:0.85rem;">(keine Unterordner)</p>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
});
|
|
return html;
|
|
}
|
|
|
|
async function toggleTreeNode(ns, path) {
|
|
if (expandedTreeNodes[ns].has(path)) {
|
|
// Collapse
|
|
expandedTreeNodes[ns].delete(path);
|
|
if (ns === 'smb') showSmbFolderModal(smbFolderTargetField);
|
|
else if (ns === 'ftp') showFtpFolderModal(ftpFolderTargetField);
|
|
return;
|
|
}
|
|
|
|
// Expand - load subfolders if not loaded yet
|
|
if (!loadedTreeNodes[ns].has(path)) {
|
|
loadingTreeNodes[ns].add(path);
|
|
if (ns === 'smb') showSmbFolderModal(smbFolderTargetField);
|
|
else if (ns === 'ftp') showFtpFolderModal(ftpFolderTargetField);
|
|
|
|
try {
|
|
const endpoint = '/api/list-' + ns + '-subfolders?path=' + encodeURIComponent(path);
|
|
const resp = await fetch(endpoint);
|
|
const data = await resp.json();
|
|
if (data.success && data.folders) {
|
|
// Merge new folders into cache
|
|
const cacheKey = ns === 'smb' ? 'cachedSmbFolders' : 'cachedFtpFolders';
|
|
const existingSet = new Set(window[cacheKey] || []);
|
|
data.folders.forEach(f => existingSet.add(f));
|
|
window[cacheKey] = Array.from(existingSet).sort();
|
|
loadedTreeNodes[ns].add(path);
|
|
}
|
|
} catch (e) {
|
|
console.error('Lazy load failed:', e);
|
|
} finally {
|
|
loadingTreeNodes[ns].delete(path);
|
|
}
|
|
}
|
|
|
|
expandedTreeNodes[ns].add(path);
|
|
if (ns === 'smb') showSmbFolderModal(smbFolderTargetField);
|
|
else if (ns === 'ftp') showFtpFolderModal(ftpFolderTargetField);
|
|
}
|
|
|
|
function expandTreePathsForValue(ns, value) {
|
|
if (!value) return;
|
|
const parts = value.split('/');
|
|
let cur = '';
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
cur = cur ? cur + '/' + parts[i] : parts[i];
|
|
expandedTreeNodes[ns].add(cur);
|
|
}
|
|
}
|
|
|
|
async function preloadTreePathForValue(ns, value) {
|
|
// Lazy-load all ancestor paths so the tree displays the selected value
|
|
if (!value) return;
|
|
const parts = value.split('/');
|
|
let cur = '';
|
|
const cacheKey = ns === 'smb' ? 'cachedSmbFolders' : 'cachedFtpFolders';
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
cur = cur ? cur + '/' + parts[i] : parts[i];
|
|
if (loadedTreeNodes[ns].has(cur)) continue;
|
|
try {
|
|
const endpoint = '/api/list-' + ns + '-subfolders?path=' + encodeURIComponent(cur);
|
|
const resp = await fetch(endpoint);
|
|
const data = await resp.json();
|
|
if (data.success && data.folders) {
|
|
const existingSet = new Set(window[cacheKey] || []);
|
|
data.folders.forEach(f => existingSet.add(f));
|
|
window[cacheKey] = Array.from(existingSet).sort();
|
|
loadedTreeNodes[ns].add(cur);
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
function resetTreeState(ns) {
|
|
expandedTreeNodes[ns].clear();
|
|
loadedTreeNodes[ns].clear();
|
|
loadedTreeNodes[ns].add('');
|
|
loadingTreeNodes[ns].clear();
|
|
}
|
|
|
|
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 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');
|
|
}
|
|
} 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\')">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) {
|
|
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;
|
|
// Always reload when opening to ensure fresh state
|
|
cachedSmbFolders = null;
|
|
window.cachedSmbFolders = null;
|
|
resetTreeState('smb');
|
|
showSmbFolderModalLoading(targetField);
|
|
fetchSmbFolders(targetField);
|
|
}
|
|
|
|
function reloadSmbFolders() {
|
|
cachedSmbFolders = null;
|
|
resetTreeState('smb');
|
|
showSmbFolderModalLoading(smbFolderTargetField);
|
|
fetchSmbFolders(smbFolderTargetField);
|
|
}
|
|
|
|
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;
|
|
window.cachedSmbFolders = data.folders;
|
|
// Preload ancestor paths if a value is already selected
|
|
if (targetField) {
|
|
const currentValue = document.getElementById(targetField).value;
|
|
await preloadTreePathForValue('smb', currentValue);
|
|
}
|
|
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\')">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">';
|
|
|
|
// 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>';
|
|
}
|
|
|
|
const smbFolders = window.cachedSmbFolders || cachedSmbFolders;
|
|
if (smbFolders && smbFolders.length > 0) {
|
|
// Auto-expand path to current value, then render tree
|
|
expandTreePathsForValue('smb', currentValue);
|
|
const tree = buildFolderTree(smbFolders);
|
|
html += renderFolderTree(tree, 'smb', currentValue, 'selectSmbFolder', 'toggleSmbCreateInput', 0);
|
|
} 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;
|
|
}
|
|
|
|
// --- FTP / SFTP ---
|
|
let cachedFtpFolders = null;
|
|
let ftpFolderTargetField = null;
|
|
|
|
function updateFtpDefaultPort() {
|
|
const proto = document.getElementById('ftp_protocol').value;
|
|
const portInput = document.getElementById('ftp_port');
|
|
const current = portInput.value;
|
|
if (proto === 'sftp' && (current === '21' || !current)) {
|
|
portInput.value = '22';
|
|
} else if (proto === 'ftp' && (current === '22' || !current)) {
|
|
portInput.value = '21';
|
|
}
|
|
}
|
|
|
|
async function testFtp() {
|
|
const btn = event.currentTarget;
|
|
setButtonLoading(btn, true);
|
|
try {
|
|
const resp = await fetch('/api/test-ftp', { method: 'POST', body: getFormData() });
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
cachedFtpFolders = data.folders;
|
|
showAlert('FTP-Verbindung erfolgreich! Einstellungen gespeichert.', 'success');
|
|
showFtpFolderModal(null);
|
|
} else {
|
|
showAlert('FTP-Verbindung fehlgeschlagen: ' + data.error, 'error');
|
|
}
|
|
} catch (e) {
|
|
showAlert('Fehler: ' + e.message, 'error');
|
|
} finally {
|
|
setButtonLoading(btn, false);
|
|
}
|
|
}
|
|
|
|
async function processFtp() {
|
|
const btn = event.currentTarget;
|
|
setButtonLoading(btn, true);
|
|
try {
|
|
const resp = await fetch('/api/process-ftp', { method: 'POST', body: getFormData() });
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
showAlert('FTP-Fehler: ' + data.error, 'error');
|
|
} else {
|
|
showAlert(`FTP-Abruf fertig: ${data.processed} verarbeitet, ${data.skipped || 0} uebersprungen, ${data.errors} Fehler`, data.errors > 0 ? 'warning' : 'success');
|
|
}
|
|
} catch (e) {
|
|
showAlert('Fehler: ' + e.message, 'error');
|
|
} finally {
|
|
setButtonLoading(btn, false);
|
|
}
|
|
}
|
|
|
|
function openFtpFolderPicker(targetField) {
|
|
ftpFolderTargetField = targetField;
|
|
// Always reload when opening to ensure fresh state
|
|
cachedFtpFolders = null;
|
|
window.cachedFtpFolders = null;
|
|
resetTreeState('ftp');
|
|
showFtpFolderModalLoading(targetField);
|
|
fetchFtpFolders(targetField);
|
|
}
|
|
|
|
function reloadFtpFolders() {
|
|
cachedFtpFolders = null;
|
|
resetTreeState('ftp');
|
|
showFtpFolderModalLoading(ftpFolderTargetField);
|
|
fetchFtpFolders(ftpFolderTargetField);
|
|
}
|
|
|
|
async function fetchFtpFolders(targetField) {
|
|
try {
|
|
const resp = await fetch('/api/test-ftp', { method: 'POST', body: getFormData() });
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
cachedFtpFolders = data.folders;
|
|
window.cachedFtpFolders = data.folders;
|
|
if (targetField) {
|
|
const currentValue = document.getElementById(targetField).value;
|
|
await preloadTreePathForValue('ftp', currentValue);
|
|
}
|
|
showFtpFolderModal(targetField);
|
|
} else {
|
|
showFtpFolderModalError('FTP-Verbindung fehlgeschlagen: ' + data.error);
|
|
}
|
|
} catch (e) {
|
|
showFtpFolderModalError('Fehler: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function showFtpFolderModalLoading(targetField) {
|
|
const modal = document.getElementById('ftpFolderModal');
|
|
document.getElementById('ftpFolderList').innerHTML = '';
|
|
document.getElementById('ftpFolderLoading').style.display = '';
|
|
document.getElementById('ftpFolderError').style.display = 'none';
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
function showFtpFolderModal(targetField) {
|
|
if (targetField) ftpFolderTargetField = targetField;
|
|
const modal = document.getElementById('ftpFolderModal');
|
|
document.getElementById('ftpFolderLoading').style.display = 'none';
|
|
document.getElementById('ftpFolderError').style.display = 'none';
|
|
|
|
const list = document.getElementById('ftpFolderList');
|
|
const currentValue = ftpFolderTargetField ? document.getElementById(ftpFolderTargetField).value : '';
|
|
|
|
let html = '<div class="folder-picker-fields">';
|
|
html += '<button type="button" class="folder-field-btn ' + (ftpFolderTargetField === 'ftp_source_path' ? 'active' : '') + '" onclick="switchFtpFolderTarget(\'ftp_source_path\')">Eingang Quelle: <strong>' + esc(document.getElementById('ftp_source_path').value || '(Wurzel)') + '</strong></button>';
|
|
html += '<button type="button" class="folder-field-btn ' + (ftpFolderTargetField === 'ftp_processed_path' ? 'active' : '') + '" onclick="switchFtpFolderTarget(\'ftp_processed_path\')">Eingang Verarbeitet: <strong>' + esc(document.getElementById('ftp_processed_path').value) + '</strong></button>';
|
|
html += '<button type="button" class="folder-field-btn ' + (ftpFolderTargetField === 'ftp_source_path_ausgang' ? 'active' : '') + '" onclick="switchFtpFolderTarget(\'ftp_source_path_ausgang\')">Ausgang Quelle: <strong>' + esc(document.getElementById('ftp_source_path_ausgang').value || '(nicht gesetzt)') + '</strong></button>';
|
|
html += '<button type="button" class="folder-field-btn ' + (ftpFolderTargetField === 'ftp_processed_path_ausgang' ? 'active' : '') + '" onclick="switchFtpFolderTarget(\'ftp_processed_path_ausgang\')">Ausgang Verarbeitet: <strong>' + esc(document.getElementById('ftp_processed_path_ausgang').value || '(nicht gesetzt)') + '</strong></button>';
|
|
html += '</div>';
|
|
html += '<div class="folder-items">';
|
|
|
|
// Root option for source path
|
|
if (ftpFolderTargetField === 'ftp_source_path') {
|
|
const isRoot = currentValue === '';
|
|
html += '<div class="folder-row">';
|
|
html += '<button type="button" class="folder-item' + (isRoot ? ' selected' : '') + '" onclick="selectFtpFolder(\'\')">';
|
|
html += '<span class="folder-icon">📁</span> (Wurzel des Servers)';
|
|
if (isRoot) html += ' <span class="badge badge-success">ausgewählt</span>';
|
|
html += '</button>';
|
|
html += '</div>';
|
|
}
|
|
|
|
const ftpFolders = window.cachedFtpFolders || cachedFtpFolders;
|
|
if (ftpFolders && ftpFolders.length > 0) {
|
|
// Auto-expand path to current value, then render tree
|
|
expandTreePathsForValue('ftp', currentValue);
|
|
const tree = buildFolderTree(ftpFolders);
|
|
html += renderFolderTree(tree, 'ftp', currentValue, 'selectFtpFolder', 'toggleFtpCreateInput', 0);
|
|
} 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(); toggleFtpCreateInput(\'\')" style="color:var(--primary);">';
|
|
html += '<span class="folder-icon">📁+</span> Neuen Ordner erstellen';
|
|
html += '</button>';
|
|
html += '</div>';
|
|
html += '<div id="ftp-create-row-root" class="create-inline" style="display:none;"></div>';
|
|
|
|
html += '</div>';
|
|
list.innerHTML = html;
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
function switchFtpFolderTarget(field) {
|
|
ftpFolderTargetField = field;
|
|
showFtpFolderModal(field);
|
|
}
|
|
|
|
function selectFtpFolder(folder) {
|
|
if (ftpFolderTargetField) {
|
|
document.getElementById(ftpFolderTargetField).value = folder;
|
|
}
|
|
showFtpFolderModal(ftpFolderTargetField);
|
|
}
|
|
|
|
function showFtpFolderModalError(msg) {
|
|
document.getElementById('ftpFolderLoading').style.display = 'none';
|
|
document.getElementById('ftpFolderError').textContent = msg;
|
|
document.getElementById('ftpFolderError').style.display = '';
|
|
}
|
|
|
|
function toggleFtpCreateInput(parentFolder) {
|
|
document.querySelectorAll('.create-inline[id^="ftp-create-row"]').forEach(el => {
|
|
const rowId = parentFolder === '' ? 'ftp-create-row-root' : 'ftp-create-row-' + CSS.escape(parentFolder);
|
|
if (el.id !== rowId) {
|
|
el.style.display = 'none';
|
|
el.innerHTML = '';
|
|
}
|
|
});
|
|
|
|
const rowId = parentFolder === '' ? 'ftp-create-row-root' : 'ftp-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="newFtpSubfolderInput" placeholder="Name" autofocus>' +
|
|
'<button type="button" class="btn btn-primary btn-sm" onclick="doCreateFtpFolder(\'' + parentFolder.replace(/'/g, "\\'") + '\')">OK</button>' +
|
|
'<button type="button" class="btn btn-secondary btn-sm" onclick="toggleFtpCreateInput(\'' + parentFolder.replace(/'/g, "\\'") + '\')">Abbrechen</button>' +
|
|
'</div>' +
|
|
'<div id="ftpCreateError" class="text-error" style="display:none;"></div>';
|
|
row.style.display = '';
|
|
|
|
const input = document.getElementById('newFtpSubfolderInput');
|
|
if (input) {
|
|
input.focus();
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') { e.preventDefault(); doCreateFtpFolder(parentFolder); }
|
|
if (e.key === 'Escape') { toggleFtpCreateInput(parentFolder); }
|
|
});
|
|
}
|
|
}
|
|
|
|
async function doCreateFtpFolder(parentFolder) {
|
|
const input = document.getElementById('newFtpSubfolderInput');
|
|
const errorEl = document.getElementById('ftpCreateError');
|
|
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-ftp-folder', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ folder_name: fullName })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
cachedFtpFolders = data.folders;
|
|
if (ftpFolderTargetField) {
|
|
document.getElementById(ftpFolderTargetField).value = fullName;
|
|
}
|
|
showFtpFolderModal(ftpFolderTargetField);
|
|
} else {
|
|
errorEl.textContent = data.error;
|
|
errorEl.style.display = '';
|
|
input.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
errorEl.textContent = e.message;
|
|
errorEl.style.display = '';
|
|
input.disabled = false;
|
|
}
|
|
}
|
|
|
|
function closeFtpFolderModal(event) {
|
|
if (event && event.target !== document.getElementById('ftpFolderModal')) return;
|
|
document.getElementById('ftpFolderModal').style.display = 'none';
|
|
cachedFtpFolders = null;
|
|
}
|
|
</script>
|
|
{% endblock %}
|