- LlmService::testConnection() gibt cURL-Fehler, HTTP-Code, Antwortzeit und verfügbare Modelle zurück - probeOllama() schreibt cURL-Fehler ins PHP-Fehlerlog - Einstellungsseite: Button "Verbindung testen" mit Inline-Diagnose und Firewall-Hinweis - Route POST /settings/llm-test
633 lines
35 KiB
PHP
633 lines
35 KiB
PHP
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||
|
||
<form id="settings-form" method="post" action="/settings">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
|
||
|
||
<!-- Firmendaten -->
|
||
<div>
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Firmendaten</div></div>
|
||
<div class="card-body">
|
||
<div class="form-group">
|
||
<label>Firma / Name *</label>
|
||
<input type="text" name="firma" value="<?= $h($settings['firma'] ?? '') ?>"
|
||
class="<?= isset($errors['firma']) ? 'input-error' : '' ?>" required>
|
||
<?php if (isset($errors['firma'])): ?><div class="error-text"><?= $h($errors['firma']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Inhaber</label>
|
||
<input type="text" name="inhaber" value="<?= $h($settings['inhaber'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Straße *</label>
|
||
<input type="text" name="strasse" value="<?= $h($settings['strasse'] ?? '') ?>" required>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:100px 1fr;gap:.5rem">
|
||
<div class="form-group">
|
||
<label>PLZ *</label>
|
||
<input type="text" name="plz" value="<?= $h($settings['plz'] ?? '') ?>" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Ort *</label>
|
||
<input type="text" name="ort" value="<?= $h($settings['ort'] ?? '') ?>" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Telefon</label>
|
||
<input type="tel" name="telefon" value="<?= $h($settings['telefon'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>E-Mail</label>
|
||
<input type="email" name="email" value="<?= $h($settings['email'] ?? '') ?>">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Website</label>
|
||
<input type="url" name="website" value="<?= $h($settings['website'] ?? '') ?>">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Steuerdaten</div></div>
|
||
<div class="card-body">
|
||
<div class="form-group">
|
||
<label>Umsatzsteuer-ID</label>
|
||
<input type="text" name="ust_id" value="<?= $h($settings['ust_id'] ?? '') ?>" placeholder="DE123456789">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Steuernummer (Finanzamt)</label>
|
||
<input type="text" name="steuernummer" value="<?= $h($settings['steuernummer'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Finanzamt</label>
|
||
<input type="text" name="finanzamt" value="<?= $h($settings['finanzamt'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>ELSTER-Steuernummer (13-stellig, Format: FFFBBBBBBUUUUP)</label>
|
||
<input type="text" name="ustva_steuernummer" value="<?= $h($settings['ustva_steuernummer'] ?? '') ?>"
|
||
placeholder="z.B. 2893012345678">
|
||
<div class="form-hint">Wird für die ELSTER XML-Datei benötigt</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Bundesland (für ELSTER)</label>
|
||
<select name="ustva_bundesland">
|
||
<option value="">– wählen –</option>
|
||
<?php foreach (['BW'=>'Baden-Württemberg','BY'=>'Bayern','BE'=>'Berlin','BB'=>'Brandenburg','HB'=>'Bremen','HH'=>'Hamburg','HE'=>'Hessen','MV'=>'Mecklenburg-Vorpommern','NI'=>'Niedersachsen','NW'=>'Nordrhein-Westfalen','RP'=>'Rheinland-Pfalz','SL'=>'Saarland','SN'=>'Sachsen','ST'=>'Sachsen-Anhalt','SH'=>'Schleswig-Holstein','TH'=>'Thüringen'] as $code => $name): ?>
|
||
<option value="<?= $code ?>"<?= ($settings['ustva_bundesland'] ?? '') === $code ? ' selected' : '' ?>><?= $name ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Bankdaten</div></div>
|
||
<div class="card-body">
|
||
<div class="form-group">
|
||
<label>Bankname</label>
|
||
<input type="text" name="bank_name" value="<?= $h($settings['bank_name'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>IBAN</label>
|
||
<input type="text" name="iban" value="<?= $h($settings['iban'] ?? '') ?>" placeholder="DE89 3704 0044 0532 0130 00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>BIC</label>
|
||
<input type="text" name="bic" value="<?= $h($settings['bic'] ?? '') ?>">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Nummernkreise</div></div>
|
||
<div class="card-body">
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Rechnungs-Präfix</label>
|
||
<input type="text" name="invoice_nr_prefix" value="<?= $h($settings['invoice_nr_prefix'] ?? 'AR') ?>" maxlength="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Beleg-Präfix</label>
|
||
<input type="text" name="beleg_nr_prefix" value="<?= $h($settings['beleg_nr_prefix'] ?? 'BLG') ?>" maxlength="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Anlagen-Präfix</label>
|
||
<input type="text" name="anlage_nr_prefix" value="<?= $h($settings['anlage_nr_prefix'] ?? 'ANL') ?>" maxlength="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Kunden-Präfix</label>
|
||
<input type="text" name="customer_nr_prefix" value="<?= $h($settings['customer_nr_prefix'] ?? 'K-') ?>" maxlength="10">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Standard-Zahlungsziel (Tage)</label>
|
||
<input type="number" name="payment_days_default" value="<?= $h($settings['payment_days_default'] ?? 14) ?>" min="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- rechte Spalte -->
|
||
<div>
|
||
<!-- Betriebsmodus -->
|
||
<div class="card mb-2" style="border:2px solid <?= ($settings['test_mode'] ?? 1) ? 'var(--color-warning,#f59e0b)' : 'var(--color-success,#22c55e)' ?>">
|
||
<div class="card-header">
|
||
<div class="card-title">Betriebsmodus</div>
|
||
<?php if ($settings['test_mode'] ?? 1): ?>
|
||
<span class="badge badge-yellow">Testinstallation</span>
|
||
<?php else: ?>
|
||
<span class="badge badge-green">Produktiv</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="card-body">
|
||
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;user-select:none">
|
||
<div style="position:relative;width:48px;height:26px;flex-shrink:0">
|
||
<input type="checkbox" name="test_mode" id="test_mode_toggle"
|
||
<?= ($settings['test_mode'] ?? 1) ? 'checked' : '' ?>
|
||
style="opacity:0;width:0;height:0;position:absolute"
|
||
onchange="updateModeLabel(this)">
|
||
<span id="toggle_track" style="
|
||
position:absolute;inset:0;border-radius:13px;transition:background .2s;
|
||
background:<?= ($settings['test_mode'] ?? 1) ? '#f59e0b' : '#22c55e' ?>;
|
||
"></span>
|
||
<span id="toggle_thumb" style="
|
||
position:absolute;top:3px;width:20px;height:20px;border-radius:50%;
|
||
background:#fff;transition:left .2s;box-shadow:0 1px 3px rgba(0,0,0,.3);
|
||
left:<?= ($settings['test_mode'] ?? 1) ? '3px' : '25px' ?>;
|
||
"></span>
|
||
</div>
|
||
<span id="mode_label" style="font-weight:600">
|
||
<?= ($settings['test_mode'] ?? 1) ? 'Testinstallation (Löschen von Buchungen erlaubt)' : 'Produktivbetrieb (GoBD-strikt)' ?>
|
||
</span>
|
||
</label>
|
||
<div class="form-hint mt-1" id="mode_hint" style="color:<?= ($settings['test_mode'] ?? 1) ? '#92400e' : '#166534' ?>">
|
||
<?php if ($settings['test_mode'] ?? 1): ?>
|
||
Im Testmodus können Buchungen und Belege direkt gelöscht werden. Nicht GoBD-konform.
|
||
<?php else: ?>
|
||
Im Produktivmodus sind Buchungen unveränderlich. Korrekturen nur per Stornobuchung moeglich.
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Verschlüsselungs-Schlüssel (APP_KEY) -->
|
||
<?php
|
||
$appKey = (string)\Steuerkram\Config::get('app_key', '');
|
||
$appKeyValid = $appKey !== '' && preg_match('/^[0-9a-f]{64}$/i', $appKey) === 1;
|
||
$appKeyColor = $appKeyValid ? 'var(--color-success,#22c55e)' : 'var(--color-danger,#dc2626)';
|
||
?>
|
||
<div class="card mb-2" style="border:2px solid <?= $appKeyColor ?>">
|
||
<div class="card-header">
|
||
<div class="card-title">App-Verschlüsselungsschlüssel</div>
|
||
<?php if ($appKeyValid): ?>
|
||
<span class="badge badge-green">Gesetzt</span>
|
||
<?php elseif ($appKey !== ''): ?>
|
||
<span class="badge badge-red">Ungültig</span>
|
||
<?php else: ?>
|
||
<span class="badge badge-red">Fehlt</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-sm" style="margin:0 0 .75rem 0">
|
||
<?php if ($appKeyValid): ?>
|
||
<span style="color:#166534">✓ <code>APP_KEY</code> ist gültig (64-stellige Hex-Zeichenkette).</span><br>
|
||
Wird zum Verschlüsseln sensibler Felder (PayPal-Signature, FinTS-PIN) genutzt.
|
||
<strong>Niemals ändern</strong> – sonst sind alle bestehenden verschlüsselten Werte verloren.
|
||
<?php elseif ($appKey !== ''): ?>
|
||
<span style="color:#dc2626">✗ <code>APP_KEY</code> ist gesetzt, aber kein gültiges Format (erwartet: 64 Hex-Zeichen).</span><br>
|
||
Bitte prüfen und korrigieren.
|
||
<?php else: ?>
|
||
<span style="color:#dc2626">✗ <code>APP_KEY</code> ist nicht gesetzt.</span><br>
|
||
Ohne Schlüssel können keine Bank-Direktverbindungen mit PIN/Secret gespeichert werden.
|
||
<?php endif; ?>
|
||
</p>
|
||
|
||
<?php if (!$appKeyValid): ?>
|
||
<button type="button" id="btn-generate-app-key" class="btn btn-primary btn-sm">
|
||
Neuen Schlüssel generieren
|
||
</button>
|
||
<?php endif; ?>
|
||
|
||
<div id="app-key-result" style="display:none;margin-top:.75rem">
|
||
<div class="text-sm" style="margin-bottom:.4rem;font-weight:600">Neuer Schlüssel:</div>
|
||
<div style="display:flex;gap:.5rem;align-items:stretch">
|
||
<code id="app-key-value"
|
||
style="flex:1;padding:.5rem .6rem;background:#f1f5f9;border:1px solid var(--color-border,#e2e8f0);border-radius:var(--radius);font-family:monospace;font-size:.8rem;word-break:break-all;user-select:all"></code>
|
||
<button type="button" id="btn-copy-app-key" class="btn btn-secondary btn-sm" title="In Zwischenablage kopieren">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="text-sm" style="margin-top:.75rem;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:var(--radius);padding:.6rem .75rem">
|
||
<strong>So aktivierst du den Schlüssel:</strong>
|
||
<ol style="margin:.4rem 0 0 1.2rem;padding:0">
|
||
<li>Schlüssel oben kopieren.</li>
|
||
<li>Auf dem Host in <code>.env</code> die Zeile <code>APP_KEY=...</code> mit dem neuen Wert ergänzen.</li>
|
||
<li>Container neu starten: <code>docker compose restart web</code></li>
|
||
</ol>
|
||
Der Schlüssel wird <strong>nur einmal angezeigt</strong> und nirgends gespeichert.
|
||
Sichere ihn auch im Backup mit – ohne ihn sind alle verschlüsselten Werte verloren.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KI-Kontierungsvorschlag -->
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">KI-Kontierungsvorschlag</div></div>
|
||
<div class="card-body">
|
||
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;user-select:none">
|
||
<input type="checkbox" name="ki_vorschlag_aktiv" value="1"
|
||
<?= !empty($settings['ki_vorschlag_aktiv']) ? 'checked' : '' ?>>
|
||
<span style="font-weight:600">Automatischen Kontierungsvorschlag erzeugen</span>
|
||
</label>
|
||
<p class="text-muted text-sm" style="margin:.5rem 0 .75rem 1.75rem">
|
||
Beim automatischen Nextcloud-Import wird für jeden Beleg ein Kontierungsvorschlag per LLM erzeugt.
|
||
Konfiguration via <code>.env</code> (lokales Ollama oder Remote-API).
|
||
</p>
|
||
<?php
|
||
$llm = new \Steuerkram\Services\LlmService();
|
||
$providerLabel = $llm->getProviderLabel();
|
||
$reachable = $providerLabel !== null ? $llm->isReachable() : false;
|
||
$ollamaUrlEnv = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? '');
|
||
?>
|
||
<p class="text-sm" style="margin:0 0 .5rem 1.75rem;color:<?= $reachable ? 'var(--color-success,#16a34a)' : '#dc2626' ?>">
|
||
<?php if ($reachable): ?>
|
||
✓ Provider erreichbar: <?= $h($providerLabel) ?>
|
||
<?php elseif ($providerLabel): ?>
|
||
✗ Provider konfiguriert (<?= $h($providerLabel) ?>), aber Server nicht erreichbar.
|
||
Prüfe ob Ollama auf dem Host läuft und auf allen Interfaces hört (<code>OLLAMA_HOST=0.0.0.0:11434</code>).
|
||
Details im PHP-Fehlerlog des Webservers (<code>docker compose logs web</code>).
|
||
<?php elseif ($ollamaUrlEnv !== ''): ?>
|
||
✗ OLLAMA_URL gesetzt (<?= $h(rtrim($ollamaUrlEnv, '/')) ?>), aber kein Provider erkannt.
|
||
<?php else: ?>
|
||
— Kein LLM-Provider konfiguriert (Auto-Discovery localhost:11434 fehlgeschlagen)
|
||
<?php endif; ?>
|
||
</p>
|
||
<div style="margin-left:1.75rem;display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||
<button type="button" class="btn btn-secondary btn-sm" id="btn-llm-test">Verbindung testen</button>
|
||
<span id="llm-test-result" style="font-size:.8rem"></span>
|
||
</div>
|
||
<div id="llm-test-detail" style="margin:.5rem 0 0 1.75rem;font-size:.8rem;display:none"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nextcloud-Import -->
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Nextcloud-Belegimport</div></div>
|
||
<div class="card-body">
|
||
<p class="text-muted text-sm" style="margin:0 0 .75rem">
|
||
Belege werden automatisch per WebDAV aus dem angegebenen Nextcloud-Ordner importiert.
|
||
Verarbeitete Dateien werden in den Erledigte-Ordner verschoben (oder gelöscht, wenn leer).
|
||
</p>
|
||
<div class="form-group">
|
||
<label>Nextcloud-URL</label>
|
||
<input type="url" name="nextcloud_url"
|
||
value="<?= $h($settings['nextcloud_url'] ?? '') ?>"
|
||
placeholder="https://cloud.example.com">
|
||
</div>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Benutzername</label>
|
||
<input type="text" name="nextcloud_user"
|
||
value="<?= $h($settings['nextcloud_user'] ?? '') ?>"
|
||
autocomplete="off">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>App-Passwort</label>
|
||
<input type="password" name="nextcloud_pass"
|
||
value="<?= $h($settings['nextcloud_pass'] ?? '') ?>"
|
||
autocomplete="new-password"
|
||
placeholder="<?= !empty($settings['nextcloud_pass']) ? '(gespeichert)' : '' ?>">
|
||
<div class="form-hint">Empfohlen: App-Passwort aus Nextcloud → Einstellungen → Sicherheit</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Import-Ordner</label>
|
||
<input type="text" name="nextcloud_folder"
|
||
value="<?= $h($settings['nextcloud_folder'] ?? '/Belege/Import') ?>"
|
||
placeholder="/Belege/Import">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Erledigte-Ordner <span class="text-muted">(leer = Datei löschen)</span></label>
|
||
<input type="text" name="nextcloud_done"
|
||
value="<?= $h($settings['nextcloud_done'] ?? '') ?>"
|
||
placeholder="/Belege/Importiert">
|
||
</div>
|
||
<div class="form-hint">
|
||
Import manuell starten:
|
||
<code>docker compose exec web php scripts/nextcloud-import.php</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-Mail-Versand -->
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">E-Mail-Versand (SMTP)</div></div>
|
||
<div class="card-body">
|
||
<div class="form-group">
|
||
<label>SMTP-Host</label>
|
||
<input type="text" name="mail_smtp_host" value="<?= $h($settings['mail_smtp_host'] ?? '') ?>" placeholder="smtp.example.com">
|
||
</div>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Port</label>
|
||
<input type="number" name="mail_smtp_port" value="<?= $h($settings['mail_smtp_port'] ?? 587) ?>" min="1" max="65535">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Verschlüsselung</label>
|
||
<select name="mail_smtp_encryption">
|
||
<option value="tls" <?= ($settings['mail_smtp_encryption'] ?? 'tls') === 'tls' ? 'selected' : '' ?>>STARTTLS (Port 587)</option>
|
||
<option value="ssl" <?= ($settings['mail_smtp_encryption'] ?? '') === 'ssl' ? 'selected' : '' ?>>SSL/TLS (Port 465)</option>
|
||
<option value="none" <?= ($settings['mail_smtp_encryption'] ?? '') === 'none' ? 'selected' : '' ?>>Keine</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>SMTP-Benutzername</label>
|
||
<input type="text" name="mail_smtp_user" value="<?= $h($settings['mail_smtp_user'] ?? '') ?>" autocomplete="off">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>SMTP-Passwort</label>
|
||
<input type="password" name="mail_smtp_pass" value="<?= $h($settings['mail_smtp_pass'] ?? '') ?>" autocomplete="new-password">
|
||
</div>
|
||
</div>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Absendername</label>
|
||
<input type="text" name="mail_from_name" value="<?= $h($settings['mail_from_name'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Absender-E-Mail</label>
|
||
<input type="email" name="mail_from_email" value="<?= $h($settings['mail_from_email'] ?? '') ?>">
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||
<button type="button" class="btn btn-secondary" id="btn-mail-test">Test-Mail senden</button>
|
||
<span id="mail-test-result" style="font-size:.8rem"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Logo -->
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Logo</div></div>
|
||
<div class="card-body">
|
||
<?php if (!empty($settings['logo_path'])): ?>
|
||
<div style="text-align:center;margin-bottom:1rem">
|
||
<img src="/<?= $h($settings['logo_path']) ?>" alt="Logo" style="max-height:80px;max-width:400px">
|
||
</div>
|
||
<?php endif; ?>
|
||
<form method="post" action="/settings/logo" enctype="multipart/form-data">
|
||
<div class="form-group">
|
||
<label>Logo hochladen (PNG, JPG)</label>
|
||
<input type="file" name="logo" accept=".png,.jpg,.jpeg">
|
||
<div class="form-hint">Empfohlen: transparentes PNG, mind. 300px breit</div>
|
||
</div>
|
||
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||
<button type="submit" class="btn btn-secondary">Hochladen</button>
|
||
<?php if (!empty($settings['logo_path'])): ?>
|
||
<button type="submit" form="delete-logo-form"
|
||
style="background:none;border:none;cursor:pointer;color:var(--color-danger);padding:.2rem;line-height:1;display:inline-flex;align-items:center"
|
||
title="Logo löschen">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="3,6 5,6 21,6"/>
|
||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
||
<path d="M10 11v6M14 11v6"/>
|
||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||
</svg>
|
||
</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
</form>
|
||
<?php if (!empty($settings['logo_path'])): ?>
|
||
<form id="delete-logo-form" method="post" action="/settings/logo/delete"></form>
|
||
<?php endif; ?>
|
||
|
||
<div style="margin-top:1rem;border-top:1px solid var(--color-border,#e5e7eb);padding-top:1rem">
|
||
<div class="form-hint" style="margin-bottom:.75rem">Darstellung in der Rechnung</div>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Max-Breite</label>
|
||
<input type="number" name="logo_max_width" form="settings-form"
|
||
value="<?= $h($settings['logo_max_width'] ?? 600) ?>" min="50" max="2000">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Max-Höhe</label>
|
||
<input type="number" name="logo_max_height" form="settings-form"
|
||
value="<?= $h($settings['logo_max_height'] ?? 100) ?>" min="20" max="500">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Position</label>
|
||
<div style="display:flex;gap:1.5rem;margin-top:.35rem">
|
||
<?php foreach (['center' => 'Zentriert', 'left' => 'Links', 'right' => 'Rechts'] as $val => $label): ?>
|
||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer">
|
||
<input type="radio" name="logo_position" value="<?= $val ?>" form="settings-form"
|
||
<?= ($settings['logo_position'] ?? 'center') === $val ? 'checked' : '' ?>
|
||
onchange="logoPositionChanged()">
|
||
<?= $label ?>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php $showMargins = in_array($settings['logo_position'] ?? 'center', ['left', 'right']); ?>
|
||
<div id="logo-margin-fields" style="display:<?= $showMargins ? 'block' : 'none' ?>">
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label>Rand Seite</label>
|
||
<input type="number" name="logo_margin_side" form="settings-form"
|
||
value="<?= $h($settings['logo_margin_side'] ?? 75) ?>" min="0">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Rand oben</label>
|
||
<input type="number" name="logo_margin_top" form="settings-form"
|
||
value="<?= $h($settings['logo_margin_top'] ?? 6) ?>" min="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Beleg-Vorschau -->
|
||
<div class="card mb-2">
|
||
<div class="card-header"><div class="card-title">Beleg-Vorschau (Tooltip)</div></div>
|
||
<div class="card-body">
|
||
<p class="text-muted text-sm" style="margin-bottom:1rem">Größe der Vorschau beim Hover über Beleg-Links in der Bank-Buchungs-Maske.</p>
|
||
<div class="form-grid form-grid-2">
|
||
<div class="form-group">
|
||
<label for="beleg_vorschau_breite">Breite (px)</label>
|
||
<input type="number" id="beleg_vorschau_breite" name="beleg_vorschau_breite" form="settings-form"
|
||
value="<?= $h($settings['beleg_vorschau_breite'] ?? 200) ?>" min="50" max="800">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="beleg_vorschau_hoehe">Höhe (px)</label>
|
||
<input type="number" id="beleg_vorschau_hoehe" name="beleg_vorschau_hoehe" form="settings-form"
|
||
value="<?= $h($settings['beleg_vorschau_hoehe'] ?? 300) ?>" min="50" max="1200">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.getElementById('btn-llm-test')?.addEventListener('click', function () {
|
||
var btn = this;
|
||
var result = document.getElementById('llm-test-result');
|
||
var detail = document.getElementById('llm-test-detail');
|
||
btn.disabled = true;
|
||
result.textContent = 'Teste…';
|
||
result.style.color = '';
|
||
detail.style.display = 'none';
|
||
fetch('/settings/llm-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data.ok) {
|
||
result.textContent = '✓ Verbindung OK (' + (data.elapsed || 0) + ' ms)';
|
||
result.style.color = 'green';
|
||
if (data.models && data.models.length > 0) {
|
||
detail.textContent = 'Modelle: ' + data.models.join(', ');
|
||
detail.style.display = 'block';
|
||
detail.style.color = '#374151';
|
||
} else if (data.info) {
|
||
detail.textContent = data.info;
|
||
detail.style.display = 'block';
|
||
detail.style.color = '#374151';
|
||
}
|
||
} else {
|
||
result.textContent = '✗ Verbindung fehlgeschlagen';
|
||
result.style.color = 'red';
|
||
if (data.error) {
|
||
var lines = ['Fehler: ' + data.error];
|
||
if (data.url) lines.push('URL: ' + data.url);
|
||
if (data.elapsed) lines.push('Nach ' + data.elapsed + ' ms');
|
||
lines.push('Tipp: sudo ufw allow from 172.16.0.0/12 to any port 11434 (oder firewall prüfen)');
|
||
lines.push('Logs: docker compose logs web');
|
||
detail.innerHTML = lines.map(function (l) { return '<div>' + l + '</div>'; }).join('');
|
||
detail.style.display = 'block';
|
||
detail.style.color = '#dc2626';
|
||
}
|
||
}
|
||
})
|
||
.catch(function () {
|
||
result.textContent = '✗ Netzwerkfehler';
|
||
result.style.color = 'red';
|
||
})
|
||
.finally(function () { btn.disabled = false; });
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||
var btn = this;
|
||
var result = document.getElementById('mail-test-result');
|
||
btn.disabled = true;
|
||
result.textContent = 'Sende…';
|
||
result.style.color = '';
|
||
fetch('/settings/mail-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data.ok) {
|
||
result.textContent = '✓ Test-Mail erfolgreich gesendet.';
|
||
result.style.color = 'green';
|
||
} else {
|
||
result.textContent = '✗ ' + (data.error || 'Unbekannter Fehler');
|
||
result.style.color = 'red';
|
||
}
|
||
})
|
||
.catch(function () {
|
||
result.textContent = '✗ Netzwerkfehler';
|
||
result.style.color = 'red';
|
||
})
|
||
.finally(function () { btn.disabled = false; });
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
function updateModeLabel(cb) {
|
||
const track = document.getElementById('toggle_track');
|
||
const thumb = document.getElementById('toggle_thumb');
|
||
const label = document.getElementById('mode_label');
|
||
const hint = document.getElementById('mode_hint');
|
||
if (cb.checked) {
|
||
track.style.background = '#f59e0b';
|
||
thumb.style.left = '3px';
|
||
label.textContent = 'Testinstallation (Löschen von Buchungen erlaubt)';
|
||
hint.textContent = 'Im Testmodus können Buchungen und Belege direkt gelöscht werden. Nicht GoBD-konform.';
|
||
hint.style.color = '#92400e';
|
||
cb.closest('.card').style.borderColor = '#f59e0b';
|
||
cb.closest('.card').querySelector('.badge').className = 'badge badge-yellow';
|
||
cb.closest('.card').querySelector('.badge').textContent = 'Testinstallation';
|
||
} else {
|
||
track.style.background = '#22c55e';
|
||
thumb.style.left = '25px';
|
||
label.textContent = 'Produktivbetrieb (GoBD-strikt)';
|
||
hint.textContent = 'Im Produktivmodus sind Buchungen unveränderlich. Korrekturen nur per Stornobuchung möglich.';
|
||
hint.style.color = '#166534';
|
||
cb.closest('.card').style.borderColor = '#22c55e';
|
||
cb.closest('.card').querySelector('.badge').className = 'badge badge-green';
|
||
cb.closest('.card').querySelector('.badge').textContent = 'Produktiv';
|
||
}
|
||
}
|
||
|
||
function logoPositionChanged() {
|
||
const pos = document.querySelector('input[name="logo_position"]:checked')?.value;
|
||
document.getElementById('logo-margin-fields').style.display =
|
||
(pos === 'left' || pos === 'right') ? 'block' : 'none';
|
||
}
|
||
|
||
document.getElementById('btn-generate-app-key')?.addEventListener('click', function () {
|
||
const btn = this;
|
||
const result = document.getElementById('app-key-result');
|
||
const out = document.getElementById('app-key-value');
|
||
btn.disabled = true;
|
||
fetch('/settings/generate-app-key', { method: 'POST' })
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data && data.key) {
|
||
out.textContent = data.key;
|
||
result.style.display = 'block';
|
||
btn.style.display = 'none';
|
||
} else {
|
||
alert('Schlüssel konnte nicht erzeugt werden.');
|
||
btn.disabled = false;
|
||
}
|
||
})
|
||
.catch(() => {
|
||
alert('Netzwerkfehler beim Erzeugen des Schlüssels.');
|
||
btn.disabled = false;
|
||
});
|
||
});
|
||
|
||
document.getElementById('btn-copy-app-key')?.addEventListener('click', function () {
|
||
const out = document.getElementById('app-key-value');
|
||
const txt = out.textContent || '';
|
||
if (!txt) return;
|
||
const btn = this;
|
||
const restore = btn.innerHTML;
|
||
navigator.clipboard.writeText(txt)
|
||
.then(() => {
|
||
btn.textContent = '✓';
|
||
setTimeout(() => { btn.innerHTML = restore; }, 1500);
|
||
})
|
||
.catch(() => {
|
||
const range = document.createRange();
|
||
range.selectNodeContents(out);
|
||
const sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
});
|
||
});
|
||
</script>
|