Files
deskhub/DeskHub-Webseite/staff/settings.php
T
2026-11-05 21:21:52 +00:00

1290 lines
82 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/crontab_installer.php';
$staff = require_admin();
$pdo = get_db();
$errors = [];
$branding = realpath(__DIR__ . '/../assets/branding') ?: (__DIR__ . '/../assets/branding');
$departments = departments_for_select($pdo, [(int) get_setting('default_department_id', '0'), (int) get_setting('live_chat_department_id', '0')]);
// Cronjob-Skripte, die manuell per Button aus dem Backend gestartet werden können.
// Jeder Job definiert eine run_*()-Funktion in seiner Datei (siehe cron/*.php).
$cronJobs = [
'fetch_emails' => ['label' => 'E-Mail-Abruf', 'file' => 'fetch_emails.php', 'fn' => 'run_fetch_emails'],
'check_sla' => ['label' => 'SLA-Prüfung', 'file' => 'check_sla.php', 'fn' => 'run_check_sla'],
'check_escalations' => ['label' => 'Eskalationsregeln', 'file' => 'check_escalations.php', 'fn' => 'run_check_escalations'],
'process_recurring' => ['label' => 'Wiederkehrende Tickets', 'file' => 'process_recurring.php', 'fn' => 'run_process_recurring'],
'process_backups' => ['label' => 'Automatische Backups', 'file' => 'process_backups.php', 'fn' => 'run_process_backups'],
'anonymize_data' => ['label' => 'Datenanonymisierung', 'file' => 'anonymize_data.php', 'fn' => 'run_anonymization'],
'auto_close_tickets'=> ['label' => 'Automatisches Schließen', 'file' => 'auto_close_tickets.php','fn' => 'run_auto_close_tickets'],
'process_snoozes' => ['label' => 'Wiedervorlagen', 'file' => 'process_snoozes.php', 'fn' => 'run_process_snoozes'],
'process_mail_queue' => ['label' => 'Mail-Warteschlange', 'file' => 'process_mail_queue.php', 'fn' => 'run_process_mail_queue'],
'remind_customers' => ['label' => 'Kundenerinnerung', 'file' => 'remind_customers.php', 'fn' => 'run_remind_customers'],
'send_csat' => ['label' => 'Zufriedenheitsabfrage', 'file' => 'send_csat.php', 'fn' => 'run_send_csat'],
'status_checks' => ['label' => 'Statusprüfungen', 'file' => 'status_checks.php', 'fn' => 'run_status_checks'],
'send_scheduled_replies' => ['label' => 'Geplante Antworten', 'file' => 'send_scheduled_replies.php', 'fn' => 'run_send_scheduled_replies'],
'send_report_schedules' => ['label' => 'Berichte per E-Mail', 'file' => 'send_report_schedules.php', 'fn' => 'run_send_report_schedules'],
'run_plugins' => ['label' => 'Plugins (Hintergrundaufgaben)', 'file' => 'run_plugins.php', 'fn' => 'run_plugins_cron'],
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'save_system') {
$status = ($_POST['helpdesk_status'] ?? 'online') === 'offline' ? 'offline' : 'online';
$offlineMsg = trim($_POST['offline_message'] ?? '');
$defaultDept = (int)($_POST['default_department_id'] ?? 0);
$kbEnabled = !empty($_POST['kb_enabled']) ? '1' : '0';
if (!in_array($defaultDept, array_column($departments, 'id'))) {
$errors[] = 'Bitte eine gültige Standardabteilung wählen.';
} else {
set_setting('helpdesk_status', $status);
set_setting('offline_message', $offlineMsg ?: 'Das Supportcenter ist derzeit nicht erreichbar.');
set_setting('default_department_id', (string)$defaultDept);
set_setting('kb_enabled', $kbEnabled);
set_setting('minecraft_enabled', !empty($_POST['minecraft_enabled']) ? '1' : '0');
set_setting('ticket_ratings_enabled', !empty($_POST['ticket_ratings_enabled']) ? '1' : '0');
set_setting('auto_time_tracking_enabled', !empty($_POST['auto_time_tracking_enabled']) ? '1' : '0');
set_setting('auto_assign_on_open', !empty($_POST['auto_assign_on_open']) ? '1' : '0');
set_setting('live_chat_enabled', !empty($_POST['live_chat_enabled']) ? '1' : '0');
$liveChatDept = $_POST['live_chat_department_id'] ?? '';
set_setting('live_chat_department_id', ($liveChatDept !== '' && in_array((int)$liveChatDept, array_column($departments, 'id'))) ? (string)(int)$liveChatDept : '');
set_setting('live_chat_send_transcript', !empty($_POST['live_chat_send_transcript']) ? '1' : '0');
set_setting('auto_close_enabled', !empty($_POST['auto_close_enabled']) ? '1' : '0');
$autoCloseDays = max(1, min(90, (int) ($_POST['auto_close_days'] ?? 7)));
set_setting('auto_close_days', (string) $autoCloseDays);
set_setting('sla_notify_mode', ($_POST['sla_notify_mode'] ?? 'admins') === 'assignee' ? 'assignee' : 'admins');
set_setting('wait_reminder_enabled', !empty($_POST['wait_reminder_enabled']) ? '1' : '0');
set_setting('wait_reminder_hours', (string) max(1, min(72, (int) ($_POST['wait_reminder_hours'] ?? 4))));
set_setting('wait_reminder_email', !empty($_POST['wait_reminder_email']) ? '1' : '0');
set_setting('remind_enabled', !empty($_POST['remind_enabled']) ? '1' : '0');
set_setting('remind_after_days', (string) max(1, min(60, (int) ($_POST['remind_after_days'] ?? 3))));
csat_set_enabled(!empty($_POST['csat_enabled']), (int) ($_POST['csat_delay_minutes'] ?? 30));
$timeout = max(5, min(480, (int)($_POST['staff_session_timeout'] ?? 30)));
set_setting('staff_session_timeout', (string)$timeout);
audit_log($staff['id'], "Systemeinstellungen geändert");
flash('success', 'Systemeinstellungen gespeichert.');
redirect('/staff/settings.php?tab=allgemein');
}
}
if ($action === 'save_privacy') {
$enabled = !empty($_POST['retention_enabled']) ? '1' : '0';
$months = max(1, min(120, (int)($_POST['retention_months'] ?? 24)));
$ipAnon = !empty($_POST['ip_anonymize']) ? '1' : '0';
set_setting('retention_enabled', $enabled);
set_setting('retention_months', (string)$months);
set_setting('ip_anonymize', $ipAnon);
set_setting('gdpr_tools_enabled', !empty($_POST['gdpr_tools_enabled']) ? '1' : '0');
audit_log($staff['id'], 'Datenschutzeinstellungen geändert');
flash('success', 'Datenschutzeinstellungen gespeichert.');
redirect('/staff/settings.php?tab=datenschutz');
}
if ($action === 'run_anonymize_now') {
define('DESKHUB_CRON_RUNNER', true);
require_once __DIR__ . '/../cron/anonymize_data.php';
$result = run_anonymization(get_db(), true);
audit_log($staff['id'], "Datenanonymisierung manuell ausgeführt: {$result['count']} Ticket(s)");
flash('success', "{$result['count']} Ticket(s) anonymisiert.");
redirect('/staff/settings.php?tab=datenschutz');
}
// Manueller Start eines Cronjobs aus dem Backend ("Jetzt ausführen"-Button, Tab Cron-Jobs).
// Führt exakt die gleiche Logik aus wie der echte Cronjob, nur direkt per Klick statt per crontab.
if ($action === 'run_cron_now') {
$job = $_POST['job'] ?? '';
if (!isset($cronJobs[$job])) {
flash('error', 'Unbekannter Cronjob.');
redirect('/staff/settings.php?tab=cron');
}
$meta = $cronJobs[$job];
define('DESKHUB_CRON_RUNNER', true);
require_once __DIR__ . '/../cron/' . $meta['file'];
$fn = $meta['fn'];
try {
if ($job === 'anonymize_data') {
$result = $fn(get_db(), true);
$summary = "{$result['count']} Ticket(s) anonymisiert.";
} elseif ($job === 'process_backups') {
// Manueller Klick erzwingt ein Backup unabhängig vom Zeitplan.
$result = $fn(get_db(), true, true);
$summary = $result['ok'] ? ($result['ran'] ? 'Backup erstellt.' : 'Kein Backup nötig.') : 'Backup fehlgeschlagen.';
} else {
$result = $fn(get_db(), true);
$summary = end($result['log']) ?: 'Ausgeführt.';
}
audit_log($staff['id'], "Cronjob manuell ausgeführt: {$meta['label']} – {$summary}");
flash('success', "{$meta['label']}: {$summary}");
} catch (Throwable $e) {
audit_log($staff['id'], "Cronjob manuell fehlgeschlagen: {$meta['label']} – " . $e->getMessage());
flash('error', "{$meta['label']} fehlgeschlagen: " . $e->getMessage());
}
redirect('/staff/settings.php?tab=cron');
}
// Ein-/Ausschalten des eingebauten Auto-Cron (siehe includes/pseudo_cron.php).
if ($action === 'save_pseudo_cron') {
$enabled = !empty($_POST['pseudo_cron_enabled']) ? '1' : '0';
set_setting('pseudo_cron_enabled', $enabled);
audit_log($staff['id'], $enabled === '1' ? 'Automatischer Hintergrund-Cron aktiviert' : 'Automatischer Hintergrund-Cron deaktiviert');
flash('success', 'Gespeichert.');
redirect('/staff/settings.php?tab=cron');
}
// Echte System-Crontab installieren/entfernen (nur auf Servern mit Shell-Zugriff möglich,
// siehe includes/crontab_installer.php – schreibt ausschließlich feste, fest hinterlegte Zeilen).
if ($action === 'install_crontab') {
$result = crontab_install();
if ($result['ok']) {
audit_log($staff['id'], 'Server-Crontab installiert/aktualisiert (6 DeskHub-Cronjobs)');
flash('success', 'Crontab installiert. Die 6 Cronjobs laufen jetzt direkt über den Server-Scheduler.');
} else {
audit_log($staff['id'], 'Server-Crontab-Installation fehlgeschlagen: ' . $result['error']);
flash('error', 'Installation fehlgeschlagen: ' . $result['error']);
}
redirect('/staff/settings.php?tab=cron');
}
if ($action === 'remove_crontab') {
$result = crontab_uninstall();
if ($result['ok']) {
audit_log($staff['id'], 'Server-Crontab-Eintrag von DeskHub entfernt');
flash('success', 'Crontab-Eintrag entfernt.');
} else {
audit_log($staff['id'], 'Entfernen der Server-Crontab fehlgeschlagen: ' . $result['error']);
flash('error', 'Entfernen fehlgeschlagen: ' . $result['error']);
}
redirect('/staff/settings.php?tab=cron');
}
if ($action === 'save_name') {
$name = trim($_POST['display_name'] ?? '');
$alertTo = trim((string) ($_POST['error_alert_email'] ?? ''));
foreach (preg_split('/[,;\s]+/', $alertTo, -1, PREG_SPLIT_NO_EMPTY) ?: [] as $m) {
if (!filter_var($m, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Die Adresse für Fehler-Mails ist ungültig: ' . $m; break; }
}
if (!$name) { $errors[] = 'Bitte einen Anzeigenamen angeben.'; }
elseif (!$errors) {
set_setting('display_name', $name);
$siteUrl = rtrim(trim($_POST['site_url'] ?? ''), '/');
if ($siteUrl) set_setting('site_url', $siteUrl);
$tz = trim($_POST['timezone'] ?? '');
if ($tz && in_array($tz, timezone_identifiers_list(), true)) set_setting('timezone', $tz);
set_setting('mail_enabled', !empty($_POST['mail_enabled']) ? '1' : '0');
set_setting('error_alert', !empty($_POST['error_alert']) ? '1' : '0');
set_setting('error_alert_email', mb_substr($alertTo, 0, 300));
audit_log($staff['id'], "Anzeigename / Basis-Einstellungen geändert: $name");
flash('success', 'Einstellungen gespeichert.');
redirect('/staff/settings.php?tab=allgemein');
}
}
if ($action === 'upload_logo') {
$file = $_FILES['logo'] ?? [];
$allowed = ['png','jpg','jpeg','gif','svg'];
$ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
if (empty($file['name'])) { $errors[] = 'Bitte eine Bilddatei auswählen.'; }
elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; }
elseif ($file['size'] > 2*1024*1024) { $errors[] = 'Max. 2 MB.'; }
elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF, SVG.'; }
elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; }
else {
foreach ($allowed as $e2) @unlink($branding.'/logo.'.$e2);
$dest = $branding.'/logo.'.$ext;
if (!move_uploaded_file($file['tmp_name'], $dest)) { $errors[] = 'Speichern fehlgeschlagen.'; }
else { set_setting('logo_path', '/assets/branding/logo.'.$ext.'?v='.time()); audit_log($staff['id'],'Logo aktualisiert'); flash('success','Logo aktualisiert.'); redirect('/staff/settings.php?tab=design'); }
}
}
if ($action === 'upload_favicon') {
$file = $_FILES['favicon'] ?? [];
$ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
$err = null;
if (empty($file['name'])) $err = 'Bitte eine Datei auswählen.';
elseif (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) $err = 'Upload fehlgeschlagen' . (($file['error'] ?? 0) === UPLOAD_ERR_INI_SIZE ? ' (die Datei ist größer als das Upload-Limit des Servers).' : '.');
elseif ($file['size'] > 1024 * 1024) $err = 'Das Favicon darf höchstens 1 MB groß sein.';
elseif (!isset(favicon_types()[$ext])) $err = 'Erlaubt: ICO, PNG, JPG, GIF, WebP, SVG.';
else $err = favicon_validate_file($file['tmp_name'], $ext);
if ($err !== null) { $errors[] = $err; }
else {
foreach (array_keys(favicon_types()) as $e2) @unlink($branding . '/favicon.' . $e2);
if (!move_uploaded_file($file['tmp_name'], $branding . '/favicon.' . $ext)) { $errors[] = 'Speichern fehlgeschlagen (Ordner assets/branding/ nicht beschreibbar?).'; }
else { set_setting('favicon_path', '/assets/branding/favicon.' . $ext . '?v=' . time()); audit_log($staff['id'], 'Favicon aktualisiert'); flash('success', 'Favicon aktualisiert. Browser zeigen Änderungen oft erst nach Neuladen (Strg+F5) oder in einem neuen Tab.'); redirect('/staff/settings.php?tab=design'); }
}
}
if ($action === 'remove_favicon') {
foreach (array_keys(favicon_types()) as $e2) @unlink($branding . '/favicon.' . $e2);
set_setting('favicon_path', null); audit_log($staff['id'], 'Favicon entfernt'); flash('success', 'Favicon entfernt.'); redirect('/staff/settings.php?tab=design');
}
if ($action === 'save_logo_options') {
$hide = !empty($_POST['logo_hide_name']) ? '1' : '0';
set_setting('logo_hide_name', $hide);
audit_log($staff['id'], $hide === '1' ? 'Name neben dem Logo ausgeblendet' : 'Name neben dem Logo wird angezeigt');
flash('success', $hide === '1' ? 'Der Name wird neben dem Logo nicht mehr angezeigt.' : 'Der Name wird wieder neben dem Logo angezeigt.');
redirect('/staff/settings.php?tab=design');
}
if ($action === 'remove_logo') {
foreach (['png','jpg','jpeg','gif','svg'] as $e2) @unlink($branding.'/logo.'.$e2);
set_setting('logo_path', null); audit_log($staff['id'],'Logo entfernt'); flash('success','Logo entfernt.'); redirect('/staff/settings.php?tab=design');
}
if ($action === 'upload_banner') {
$file = $_FILES['banner'] ?? [];
$allowed = ['png','jpg','jpeg','gif'];
$ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
if (empty($file['name'])) { $errors[] = 'Bitte eine Bilddatei auswählen.'; }
elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; }
elseif ($file['size'] > 5*1024*1024) { $errors[] = 'Max. 5 MB.'; }
elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF.'; }
elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; }
else {
foreach ($allowed as $e2) @unlink($branding.'/banner.'.$e2);
$dest = $branding.'/banner.'.$ext;
if (!move_uploaded_file($file['tmp_name'], $dest)) { $errors[] = 'Speichern fehlgeschlagen.'; }
else { set_setting('banner_path', '/assets/branding/banner.'.$ext.'?v='.time()); audit_log($staff['id'],'Titelbild aktualisiert'); flash('success','Titelbild aktualisiert.'); redirect('/staff/settings.php?tab=design'); }
}
}
if ($action === 'remove_banner') {
foreach (['png','jpg','jpeg','gif'] as $e2) @unlink($branding.'/banner.'.$e2);
set_setting('banner_path', null); audit_log($staff['id'],'Titelbild entfernt'); flash('success','Titelbild entfernt.'); redirect('/staff/settings.php?tab=design');
}
if ($action === 'save_homepage_content') {
$popularArticlesEnabled = !empty($_POST['homepage_popular_articles_enabled']) ? '1' : '0';
set_setting('homepage_popular_articles_enabled', $popularArticlesEnabled);
audit_log(
$staff['id'],
$popularArticlesEnabled === '1'
? 'Beliebte Artikel auf der Startseite eingeblendet'
: 'Beliebte Artikel auf der Startseite ausgeblendet'
);
flash('success', 'Startseiten-Inhalte gespeichert.');
redirect('/staff/settings.php?tab=design');
}
if ($action === 'upload_custom_theme') {
try {
$theme = install_custom_theme_upload($_FILES['theme_zip'] ?? []);
$actionLabel = $theme['replaced'] ? 'aktualisiert' : 'hochgeladen';
audit_log($staff['id'], "Kundenportal-Theme {$actionLabel}: {$theme['name']}");
flash('success', "Theme „{$theme['name']}“ wurde {$actionLabel}.");
redirect('/staff/settings.php?tab=design');
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if ($action === 'save_theme_catalog' && plugin_strict_mode()) {
$errors[] = 'Im geprüften Modus ist der Katalog fest vorgegeben.';
} elseif ($action === 'save_theme_catalog') {
$url = trim((string) ($_POST['theme_catalog_url'] ?? ''));
if ($url !== '' && ($urlError = plugin_url_check($url))) {
$errors[] = 'Katalog-Adresse: ' . $urlError;
} else {
set_setting('theme_catalog_url', $url);
flash('success', 'Katalog-Adresse gespeichert.');
redirect('/staff/settings.php?tab=design');
}
}
if ($action === 'theme_catalog_install') {
$res = theme_catalog_install((string) ($_POST['slug'] ?? ''));
if ($res['ok']) {
audit_log($staff['id'], "Kundenportal-Theme aus dem Katalog installiert: {$res['theme']['name']} (SHA-256 {$res['hash']}" . ($res['signed'] ? ', signiert' : ', nicht signiert') . ')');
flash('success', "Theme „{$res['theme']['name']}“ installiert (" . ($res['signed'] ? 'Signatur geprüft' : 'nicht signiert') . '). Es ist noch nicht aktiv.');
redirect('/staff/settings.php?tab=design');
}
$errors[] = $res['error'];
}
if ($action === 'activate_custom_theme') {
$themeId = (string) ($_POST['theme_id'] ?? '');
$theme = null;
foreach (list_custom_themes() as $candidate) {
if ($candidate['id'] === $themeId) {
$theme = $candidate;
break;
}
}
if ($theme === null) {
$errors[] = 'Das ausgewählte Theme wurde nicht gefunden.';
} else {
set_setting('active_custom_theme', $themeId);
audit_log($staff['id'], "Kundenportal-Theme aktiviert: {$theme['name']}");
flash('success', "Theme „{$theme['name']}“ ist jetzt aktiv.");
redirect('/staff/settings.php?tab=design');
}
}
if ($action === 'deactivate_custom_theme') {
set_setting('active_custom_theme', null);
audit_log($staff['id'], 'Benutzerdefiniertes Kundenportal-Theme deaktiviert');
flash('success', 'Das Standarddesign ist jetzt aktiv.');
redirect('/staff/settings.php?tab=design');
}
if ($action === 'delete_custom_theme') {
$themeId = (string) ($_POST['theme_id'] ?? '');
$theme = null;
foreach (list_custom_themes() as $candidate) {
if ($candidate['id'] === $themeId) {
$theme = $candidate;
break;
}
}
if ($theme === null || !delete_custom_theme($themeId)) {
$errors[] = 'Das Theme konnte nicht gelöscht werden.';
} else {
if (get_setting('active_custom_theme', '') === $themeId) {
set_setting('active_custom_theme', null);
}
audit_log($staff['id'], "Kundenportal-Theme gelöscht: {$theme['name']}");
flash('success', "Theme „{$theme['name']}“ wurde gelöscht.");
redirect('/staff/settings.php?tab=design');
}
}
if ($action === 'save_uploads') {
$dangerous = ['php','php3','php4','php5','phtml','phar','exe','bat','cmd','sh','pl','py','asp','aspx','jsp','cgi'];
$extList = array_filter(array_map('trim', explode(',', strtolower($_POST['upload_allowed_extensions'] ?? ''))));
$extList = array_diff($extList, $dangerous);
set_setting('upload_allowed_extensions', implode(', ', $extList));
$mailExt = array_diff(array_filter(array_map(fn($e) => ltrim(trim($e), '.'), explode(',', strtolower($_POST['mail_attachment_extra_extensions'] ?? '')))), $dangerous);
set_setting('mail_attachment_extra_extensions', implode(', ', $mailExt));
set_setting('upload_max_size_mb', max(1, min(100, (int)($_POST['upload_max_size_mb'] ?? 10))));
audit_log($staff['id'], 'Upload-Einstellungen geändert');
flash('success', 'Upload-Einstellungen gespeichert.');
redirect('/staff/settings.php?tab=uploads');
}
if ($action === 'save_footer_pages') {
foreach (['footer_privacy_label','footer_privacy_url','footer_privacy_html','footer_imprint_label','footer_imprint_url','footer_imprint_html'] as $k)
set_setting($k, trim($_POST[$k] ?? '') ?: null);
audit_log($staff['id'], 'Footer-Seiten geändert');
flash('success', 'Footer-Einstellungen gespeichert.');
redirect('/staff/settings.php?tab=footer');
}
if ($action === 'save_notifications') {
$discordNew = trim($_POST['discord_webhook_url'] ?? '');
$tgTokenNew = trim($_POST['telegram_bot_token'] ?? '');
$tgChatId = trim($_POST['telegram_chat_id'] ?? '');
// Leer lassen = unverändert; Wert "-" = explizit löschen; sonst verschlüsseln
if ($discordNew !== '') {
set_setting('discord_webhook_url', $discordNew === '-' ? null : encrypt_secret($discordNew));
}
if ($tgTokenNew !== '') {
set_setting('telegram_bot_token', $tgTokenNew === '-' ? null : encrypt_secret($tgTokenNew));
}
if ($tgChatId !== '') {
set_setting('telegram_chat_id', $tgChatId === '-' ? null : $tgChatId);
}
audit_log($staff['id'], 'Benachrichtigungen geändert');
flash('success', 'Benachrichtigungseinstellungen gespeichert.');
redirect('/staff/settings.php?tab=benachrichtigungen');
}
// Push-Benachrichtigungen für die Android-App (Firebase Cloud Messaging)
if ($action === 'save_fcm') {
set_setting('fcm_enabled', !empty($_POST['fcm_enabled']) ? '1' : '0');
set_setting('fcm_show_details', !empty($_POST['fcm_show_details']) ? '1' : '0');
set_setting('push_brand', fcm_clean_brand((string) ($_POST['push_brand'] ?? '')));
flash('success', 'Einstellungen für die Android-Benachrichtigungen gespeichert.');
redirect('/staff/settings.php?tab=benachrichtigungen');
}
if ($action === 'remove_fcm') {
set_setting('fcm_service_account', '');
set_setting('fcm_access_token', '');
audit_log($staff['id'], 'Firebase-Dienstkonto für Android-Push entfernt');
flash('success', 'Der Firebase-Schlüssel wurde vom Server entfernt.');
redirect('/staff/settings.php?tab=benachrichtigungen');
}
if ($action === 'test_fcm') {
if (fcm_mode() === 'none') { flash('error', 'Push ist nicht eingerichtet: Es ist kein Push-Dienst eingetragen und kein eigener Firebase-Schlüssel hinterlegt.'); redirect('/staff/settings.php?tab=benachrichtigungen'); }
$t = fcm_send_test($pdo, (int) $staff['id']);
if ($t['devices'] === 0) { flash('error', 'Auf deinem Konto ist noch kein Android-Gerät angemeldet. Melde dich in der App an (Benachrichtigungen erlauben) und versuche es erneut.'); redirect('/staff/settings.php?tab=benachrichtigungen'); }
$t['sent'] > 0 ? flash('success', "Testnachricht an {$t['sent']} Gerät(e) gesendet. Sie sollte jetzt auf dem Handy erscheinen.") : flash('error', 'Senden fehlgeschlagen: ' . ($t['error'] !== '' ? $t['error'] : 'unbekannter Fehler'));
redirect('/staff/settings.php?tab=benachrichtigungen');
}
if ($action === 'test_notifications') {
$sent = [];
if (discord_notify('🔔 Testnachricht von DeskHub')) $sent[] = 'Discord';
if (telegram_notify('🔔 Testnachricht von DeskHub')) $sent[] = 'Telegram';
$sent ? flash('success', 'Gesendet an: '.implode(', ', $sent)) : flash('error', 'Kein Kanal konfiguriert oder Fehler.');
redirect('/staff/settings.php?tab=benachrichtigungen');
}
if ($action === 'save_smtp') {
set_setting('smtp_host', trim($_POST['smtp_host'] ?? '') ?: null);
set_setting('smtp_port', trim($_POST['smtp_port'] ?? '') ?: null);
set_setting('smtp_encryption', $_POST['smtp_encryption'] ?? 'tls');
set_setting('smtp_username', trim($_POST['smtp_username'] ?? '') ?: null);
if (trim($_POST['smtp_password'] ?? '') !== '') set_setting('smtp_password', encrypt_secret(trim($_POST['smtp_password'])));
set_setting('smtp_from_name', trim($_POST['smtp_from_name'] ?? '') ?: null);
set_setting('smtp_from_email', trim($_POST['smtp_from_email'] ?? '') ?: null);
audit_log($staff['id'], 'SMTP geändert');
flash('success', 'SMTP-Einstellungen gespeichert.');
redirect('/staff/settings.php?tab=email');
}
if ($action === 'test_smtp') {
$testBody = email_wrap(email_p('Diese Test-E-Mail bestätigt, dass Ihr SMTP-Versand korrekt eingerichtet ist. 🎉'));
$ok = send_mail($staff['email'], 'DeskHub – Test-E-Mail', $testBody, null, false);
$ok ? flash('success', 'Test-Mail an '.$staff['email'].' gesendet.') : flash('error', 'Versand fehlgeschlagen. Bitte SMTP-Daten prüfen. '.smtp_last_error());
redirect('/staff/settings.php?tab=email');
}
// Einzelnes globales IMAP-Konto wurde durch die Tabelle imap_accounts abgelöst
// (mehrere Postfächer möglich, siehe staff/imap_accounts.php). Die alten
// Einzel-Settings (imap_host/imap_user/...) werden nur noch einmalig per
// Migration php_0011_imap_accounts als erstes Postfach übernommen.
}
// ── Lazy-Encryption: Klartext-Credentials beim ersten Admin-Besuch sichern ──
// Direkt über PDO (nicht get_setting) um den statischen Cache zu umgehen.
// Idempotent: AES-GCM-Blobs (base64, min. 28 Bytes) werden nicht erneut verschlüsselt.
foreach (['discord_webhook_url', 'telegram_bot_token', 'smtp_password', 'imap_pass'] as $_lek) {
$stmt_le = $pdo->prepare('SELECT setting_value FROM settings WHERE setting_key = ?');
$stmt_le->execute([$_lek]);
$raw_le = $stmt_le->fetchColumn();
if ($raw_le !== false && $raw_le !== '' && $raw_le !== null) {
$decoded_le = base64_decode($raw_le, true);
if ($decoded_le === false || strlen($decoded_le) < 28) {
// Klartext gefunden → jetzt verschlüsseln
$pdo->prepare('UPDATE settings SET setting_value = ? WHERE setting_key = ?')
->execute([encrypt_secret($raw_le), $_lek]);
}
}
}
unset($_lek, $stmt_le, $raw_le, $decoded_le);
// ── Werte laden ───────────────────────────────────────────────────────────────
$currentLogo = get_setting('logo_path');
$currentBanner = get_setting('banner_path');
$homepagePopularArticlesEnabled = get_setting('homepage_popular_articles_enabled', '1') === '1';
$currentName = get_setting('display_name', config()['site']['name']);
$helpdeskStatus = get_setting('helpdesk_status', 'online');
$offlineMessage = get_setting('offline_message', 'Das Supportcenter ist derzeit nicht erreichbar.');
$defaultDeptId = get_setting('default_department_id', (string)($departments[0]['id'] ?? ''));
$kbEnabled = get_setting('kb_enabled', '1') === '1';
$discordUrl = get_setting('discord_webhook_url', '');
$telegramToken = get_setting('telegram_bot_token', '');
$telegramChatId = get_setting('telegram_chat_id', '');
$smtpHost = get_setting('smtp_host', '');
$smtpPort = get_setting('smtp_port', '587');
$smtpEncryption = get_setting('smtp_encryption', 'tls');
$smtpUsername = get_setting('smtp_username', '');
$smtpFromName = get_setting('smtp_from_name', '');
$smtpFromEmail = get_setting('smtp_from_email', '');
$footerPrivacyLabel = get_setting('footer_privacy_label', 'Datenschutz');
$footerPrivacyUrl = get_setting('footer_privacy_url', '');
$footerPrivacyHtml = get_setting('footer_privacy_html', '');
$footerImprintLabel = get_setting('footer_imprint_label', 'Impressum');
$footerImprintUrl = get_setting('footer_imprint_url', '');
$footerImprintHtml = get_setting('footer_imprint_html', '');
$uploadExtensions = get_setting('upload_allowed_extensions', implode(', ', config()['uploads']['allowed_extensions']));
$mailExtraExtensions = get_setting('mail_attachment_extra_extensions', MAIL_ATTACHMENT_EXTRA_DEFAULT);
$uploadMaxSize = get_setting('upload_max_size_mb', '10');
$customThemes = list_custom_themes();
$activeCustomTheme = get_setting('active_custom_theme', '');
$cronFetch = cron_last_run_label(get_setting('cron_fetch_emails_last_run'));
$cronSla = cron_last_run_label(get_setting('cron_check_sla_last_run'));
$cronRecurring = cron_last_run_label(get_setting('cron_process_recurring_last_run'));
$cronBackups = cron_last_run_label(get_setting('cron_process_backups_last_run'));
$cronEscalations = cron_last_run_label(get_setting('cron_check_escalations_last_run'));
$cronAnonymize = cron_last_run_label(get_setting('cron_anonymize_data_last_run'));
$cronAutoClose = cron_last_run_label(get_setting('cron_auto_close_tickets_last_run'));
$cronSnoozes = cron_last_run_label(get_setting('cron_process_snoozes_last_run'));
$cronRemind = cron_last_run_label(get_setting('cron_remind_customers_last_run'));
$cronPlugins = cron_last_run_label(get_setting('cron_run_plugins_last_run'));
$cronCsat = cron_last_run_label(get_setting('cron_send_csat_last_run'));
$tab = $_GET['tab'] ?? 'allgemein';
$pageTitle = 'Einstellungen';
$activePage = 'settings';
require __DIR__ . '/_header.php';
?>
<div class="topbar">
<h1>Einstellungen</h1>
<a href="/staff/import_osticket.php" class="btn secondary">📥 osTicket Import</a>
</div>
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
<!-- Tab-Navigation -->
<div style="display:flex; border-bottom:2px solid var(--border); margin-bottom:24px; gap:0">
<?php
$tabs = [
'allgemein' => 'Allgemein',
'design' => 'Design',
'email' => 'E-Mail',
'benachrichtigungen' => 'Benachrichtigungen',
'uploads' => 'Uploads',
'footer' => 'Footer',
'cron' => 'Cron-Jobs',
'datenschutz' => '🔒 Datenschutz',
];
foreach ($tabs as $key => $label):
$active = $tab === $key;
?>
<a href="?tab=<?= $key ?>"
style="padding:11px 20px; font-size:.88rem; font-weight:<?= $active?'600':'400' ?>; color:<?= $active?'var(--primary)':'var(--muted)' ?>; text-decoration:none; border-bottom:2px solid <?= $active?'var(--primary)':'transparent' ?>; margin-bottom:-2px; transition:color .15s, border-color .15s; white-space:nowrap">
<?= $label ?>
</a>
<?php endforeach; ?>
</div>
<!-- ── TAB: ALLGEMEIN ─────────────────────────────────────────────────────── -->
<?php if ($tab === 'allgemein'): ?>
<?php $sessionTimeout = (int)(get_setting('staff_session_timeout', '30') ?: 30); ?>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px">
<div class="card">
<h3>System</h3>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_system">
<label>Helpdesk-Status</label>
<select name="helpdesk_status">
<option value="online" <?= $helpdeskStatus==='online' ?'selected':'' ?>>Online</option>
<option value="offline" <?= $helpdeskStatus==='offline'?'selected':'' ?>>Offline (Wartungsmodus)</option>
</select>
<label style="margin-top:12px">Offline-Nachricht</label>
<textarea name="offline_message" rows="3"><?= e($offlineMessage) ?></textarea>
<label style="margin-top:12px">Standardabteilung</label>
<select name="default_department_id">
<?php foreach ($departments as $d): ?>
<option value="<?= $d['id'] ?>" <?= $defaultDeptId==$d['id']?'selected':'' ?>><?= e($d['name']) ?></option>
<?php endforeach; ?>
</select>
<label style="display:flex;align-items:center;gap:8px;margin-top:14px;font-weight:400;cursor:pointer">
<input type="checkbox" name="kb_enabled" value="1" style="width:auto" <?= $kbEnabled?'checked':'' ?>>
Wissensdatenbank im Kundenportal anzeigen
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="minecraft_enabled" value="1" style="width:auto" <?= get_setting('minecraft_enabled','0')==='1'?'checked':'' ?>>
Minecraft-Plugin auf der Apps-Seite anzeigen (Download und Einrichtung, Ausfall-Tickets für Minecraft-Server)
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="ticket_ratings_enabled" value="1" style="width:auto" <?= get_setting('ticket_ratings_enabled','1')==='1'?'checked':'' ?>>
Ticket-Bewertung aktivieren (Kunden können nach Schließung 1–5 Sterne vergeben)
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="auto_time_tracking_enabled" value="1" style="width:auto" <?= get_setting('auto_time_tracking_enabled','1')==='1'?'checked':'' ?>>
Automatische Zeiterfassung (misst automatisch, wie lange ein Mitarbeiter ein Ticket geöffnet hat)
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="auto_assign_on_open" value="1" style="width:auto" <?= get_setting('auto_assign_on_open','1')==='1'?'checked':'' ?>>
Beim Öffnen automatisch zuweisen (ein noch nicht zugewiesenes Ticket geht an den Mitarbeiter, der es öffnet)
</label>
<?php $autoCloseEnabled = get_setting('auto_close_enabled','0')==='1'; ?>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="auto_close_enabled" value="1" style="width:auto" id="auto_close_enabled" onchange="document.getElementById('auto-close-days-row').style.display=this.checked?'block':'none'" <?= $autoCloseEnabled?'checked':'' ?>>
Tickets automatisch schließen, wenn der Kunde nach einer Antwort nicht mehr reagiert
</label>
<div id="auto-close-days-row" style="display:<?= $autoCloseEnabled?'block':'none' ?>;margin-top:8px;margin-left:26px">
<label style="font-size:.82rem">Nach wie vielen Tagen ohne Kundenreaktion</label><br>
<input type="number" name="auto_close_days" value="<?= (int) get_setting('auto_close_days','7') ?>" min="1" max="90" style="width:90px">
<span class="muted" style="font-size:.82rem">Tage</span>
</div>
<label for="sla_notify_mode" style="margin-top:12px">Bei überschrittener SLA-Frist benachrichtigen</label>
<select id="sla_notify_mode" name="sla_notify_mode" style="max-width:420px">
<option value="admins" <?= sla_notify_mode() === 'admins' ? 'selected' : '' ?>>Nur die Administratoren (per E-Mail)</option>
<option value="assignee" <?= sla_notify_mode() === 'assignee' ? 'selected' : '' ?>>Bearbeiter bzw. Abteilung (E-Mail und Push in der App)</option>
</select>
<?php $waitOn = wait_reminder_enabled(); ?>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="wait_reminder_enabled" value="1" style="width:auto" id="wait_reminder_enabled" onchange="document.getElementById('wait-reminder-row').style.display=this.checked?'block':'none'" <?= $waitOn ? 'checked' : '' ?>>
Bearbeiter erinnern, wenn ein Ticket zu lange auf ihre Antwort wartet (Hinweis vor Ablauf der SLA-Frist)
</label>
<div id="wait-reminder-row" style="display:<?= $waitOn ? 'block' : 'none' ?>;margin-top:8px;margin-left:26px">
<label style="font-size:.82rem">Nach wie vielen Arbeitsstunden ohne Antwort</label><br>
<input type="number" name="wait_reminder_hours" value="<?= wait_reminder_hours() ?>" min="1" max="72" style="width:90px">
<span class="muted" style="font-size:.82rem">Stunden (zählt nur die Geschäftszeiten, wenn sie in den SLA-Einstellungen eingerichtet sind)</span>
<label style="display:flex;align-items:center;gap:8px;margin-top:6px;font-weight:400;cursor:pointer"><input type="checkbox" name="wait_reminder_email" value="1" style="width:auto" <?= wait_reminder_email() ? 'checked' : '' ?>> Zusätzlich per E-Mail (sonst nur Push in der App und im Browser)</label>
</div>
<?php $remindEnabled = customer_reminder_enabled(); ?>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="remind_enabled" value="1" style="width:auto" id="remind_enabled" onchange="document.getElementById('remind-days-row').style.display=this.checked?'block':'none'" <?= $remindEnabled?'checked':'' ?>>
Kunden erinnern, wenn sie auf eine Antwort nicht reagieren (einmalig, vor dem automatischen Schließen)
</label>
<div id="remind-days-row" style="display:<?= $remindEnabled?'block':'none' ?>;margin-top:8px;margin-left:26px">
<label style="font-size:.82rem">Nach wie vielen Tagen ohne Reaktion</label><br>
<input type="number" name="remind_after_days" value="<?= customer_reminder_days() ?>" min="1" max="60" style="width:90px">
<span class="muted" style="font-size:.82rem">Tage (am besten kürzer als die Frist zum automatischen Schließen)</span>
</div>
<?php $csatOn = csat_enabled(); ?>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="csat_enabled" value="1" style="width:auto" id="csat_enabled" onchange="document.getElementById('csat-delay-row').style.display=this.checked?'block':'none'" <?= $csatOn?'checked':'' ?>>
Zufriedenheitsabfrage: Nach dem Schließen oder Lösen eines Tickets den Kunden um eine Bewertung (1 bis 5 Sterne) bitten
</label>
<div id="csat-delay-row" style="display:<?= $csatOn?'block':'none' ?>;margin-top:8px;margin-left:26px">
<label style="font-size:.82rem">Mail senden nach</label><br>
<input type="number" name="csat_delay_minutes" value="<?= csat_delay_minutes() ?>" min="0" max="1440" style="width:90px">
<span class="muted" style="font-size:.82rem">Minuten (so bleibt Zeit, ein versehentlich geschlossenes Ticket wieder zu öffnen). Gilt nur für Tickets, die ab dem Einschalten geschlossen werden. Die Auswertung steht unter Übersicht.</span>
</div>
<?php $liveChatEnabled = get_setting('live_chat_enabled','0')==='1'; ?>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer">
<input type="checkbox" name="live_chat_enabled" value="1" style="width:auto" id="live_chat_enabled" onchange="document.getElementById('live-chat-dept-row').style.display=this.checked?'block':'none'" <?= $liveChatEnabled?'checked':'' ?>>
Live-Chat im Kundenportal (Chat-Widget unten rechts, auch ohne Kundenkonto nutzbar)
</label>
<div id="live-chat-dept-row" style="display:<?= $liveChatEnabled?'block':'none' ?>;margin-top:8px;margin-left:26px">
<label style="font-size:.82rem">Abteilung für Live-Chat-Tickets</label>
<select name="live_chat_department_id" style="max-width:280px">
<option value="">Standardabteilung verwenden</option>
<?php foreach ($departments as $d): ?>
<option value="<?= $d['id'] ?>" <?= get_setting('live_chat_department_id')==$d['id']?'selected':'' ?>><?= e($d['name']) ?></option>
<?php endforeach; ?>
</select>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-weight:400;cursor:pointer;font-size:.85rem">
<input type="checkbox" name="live_chat_send_transcript" value="1" style="width:auto" <?= get_setting('live_chat_send_transcript','1')==='1'?'checked':'' ?>>
Chatverlauf per E-Mail an den Kunden senden, sobald ein Mitarbeiter den Chat beendet
</label>
</div>
<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border-soft)">
<label for="session_timeout" style="font-weight:500;display:block;margin-bottom:6px">Automatischer Logout nach Inaktivität</label>
<div style="display:flex;align-items:center;gap:10px">
<input type="number" id="session_timeout" name="staff_session_timeout" value="<?= $sessionTimeout ?>" min="5" max="480" style="width:90px">
<span class="muted" style="font-size:.85rem">Minuten (5–480). Countdown wird oben rechts angezeigt.</span>
</div>
</div>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</div>
<div class="card">
<h3>Anzeigename & URL</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:14px">Wird im Browser-Tab, in E-Mails und in der Web-App angezeigt.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_name">
<label>Name</label>
<input type="text" name="display_name" value="<?= e($currentName) ?>" required>
<label style="margin-top:12px">Website-URL</label>
<input type="text" name="site_url" value="<?= e(get_setting('site_url', config()['site']['url'] ?? '')) ?>" placeholder="https://support.beispiel.de">
<p class="muted" style="font-size:.78rem;margin-top:3px">Wird in E-Mail-Links verwendet. Kein abschließendes /</p>
<label style="margin-top:12px">Zeitzone</label>
<select name="timezone">
<?php foreach (['Europe/Berlin','Europe/Vienna','Europe/Zurich','Europe/London','UTC','America/New_York','America/Los_Angeles','Asia/Tokyo'] as $tz): ?>
<option value="<?= $tz ?>" <?= get_setting('timezone', config()['site']['timezone'] ?? 'Europe/Berlin')===$tz?'selected':'' ?>><?= $tz ?></option>
<?php endforeach; ?>
</select>
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;font-weight:400;cursor:pointer">
<input type="checkbox" name="mail_enabled" value="1" style="width:auto" <?= get_setting('mail_enabled','0')==='1'?'checked':'' ?>>
E-Mail-Versand aktiv (PHP mail() Fallback)
</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;font-weight:400;cursor:pointer">
<input type="checkbox" name="error_alert" value="1" style="width:auto" <?= get_setting('error_alert','1')==='1'?'checked':'' ?>>
Bei schweren Fehlern auf der Webseite eine E-Mail senden
</label>
<input type="text" name="error_alert_email" value="<?= e(get_setting('error_alert_email', '')) ?>" placeholder="Empfänger (leer = Administratoren), mehrere mit Komma">
<p class="muted" style="font-size:.78rem;margin-top:3px">Gemeldet werden Fehler, bei denen eine Seite nicht angezeigt werden kann. Dieselbe Meldung kommt höchstens einmal pro Stunde, insgesamt höchstens 10 Mails pro Stunde. Voraussetzung ist ein funktionierender E-Mail-Versand und eine erreichbare Datenbank.</p>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</div>
</div>
<!-- ── TAB: DESIGN ────────────────────────────────────────────────────────── -->
<?php elseif ($tab === 'design'): ?>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px">
<div class="card">
<h3>Firmenlogo</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">PNG/SVG empfohlen, max. 2 MB. Wird überall statt „DeskHub" angezeigt. Der Name der Installation steht standardmäßig daneben und lässt sich unten ausblenden.</p>
<?php if ($currentLogo): ?>
<div style="background:#16202a;padding:14px;border-radius:8px;margin-bottom:14px;display:inline-block">
<img src="<?= e($currentLogo) ?>" alt="Logo" style="max-height:48px;max-width:220px">
</div><br>
<form method="post" style="display:inline" onsubmit="return confirm('Logo entfernen?')">
<?= csrf_field() ?><input type="hidden" name="action" value="remove_logo">
<button type="submit" class="btn secondary">Entfernen</button>
</form>
<form method="post" style="margin-top:14px">
<?= csrf_field() ?><input type="hidden" name="action" value="save_logo_options">
<label style="display:flex;align-items:center;gap:8px;font-weight:400;cursor:pointer">
<input type="checkbox" name="logo_hide_name" value="1" style="width:auto" <?= get_setting('logo_hide_name', '0') === '1' ? 'checked' : '' ?>>
Name „<?= e(get_setting('display_name', 'DeskHub')) ?>“ neben dem Logo ausblenden
</label>
<div class="btn-row"><button type="submit" class="btn secondary">Speichern</button></div>
</form>
<?php else: ?>
<p class="muted">Kein Logo hinterlegt.</p>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" style="margin-top:16px">
<?= csrf_field() ?><input type="hidden" name="action" value="upload_logo">
<input type="file" name="logo" accept=".png,.jpg,.jpeg,.gif,.svg">
<div class="btn-row"><button type="submit" class="btn">Hochladen</button></div>
</form>
</div>
<div class="card">
<h3>Favicon (Symbol im Browser-Tab)</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">ICO, PNG, JPG, GIF, WebP oder SVG, höchstens 1 MB, am besten quadratisch (z. B. 256×256). Ohne Favicon verwendet DeskHub das Logo.</p>
<?php $currentFavicon = get_setting('favicon_path'); ?>
<?php if ($currentFavicon): ?>
<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px">
<span style="background:repeating-conic-gradient(#e5e7eb 0% 25%, #fff 0% 50%) 50% / 12px 12px;border:1px solid var(--border);border-radius:8px;padding:8px;display:inline-flex">
<img src="<?= e($currentFavicon) ?>" alt="Favicon" style="width:32px;height:32px;object-fit:contain">
</span>
<form method="post" style="display:inline" onsubmit="return confirm('Favicon entfernen?')">
<?= csrf_field() ?><input type="hidden" name="action" value="remove_favicon">
<button type="submit" class="btn secondary">Entfernen</button>
</form>
</div>
<?php else: ?>
<p class="muted">Kein eigenes Favicon hinterlegt.</p>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" style="margin-top:16px">
<?= csrf_field() ?><input type="hidden" name="action" value="upload_favicon">
<input type="file" name="favicon" accept=".ico,.png,.jpg,.jpeg,.gif,.webp,.svg">
<div class="btn-row"><button type="submit" class="btn">Hochladen</button></div>
</form>
</div>
<div class="card">
<h3>Titelbild (Hero-Banner)</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">Hintergrundbild im dunklen Hero-Bereich. Mind. 1600×500 px, max. 5 MB.</p>
<?php if ($currentBanner): ?>
<img src="<?= e($currentBanner) ?>" alt="Banner" style="width:100%;max-height:120px;object-fit:cover;border-radius:6px;margin-bottom:12px">
<form method="post" onsubmit="return confirm('Titelbild entfernen?')">
<?= csrf_field() ?><input type="hidden" name="action" value="remove_banner">
<button type="submit" class="btn secondary">Entfernen</button>
</form>
<?php else: ?>
<p class="muted">Kein Titelbild hinterlegt.</p>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" style="margin-top:16px">
<?= csrf_field() ?><input type="hidden" name="action" value="upload_banner">
<input type="file" name="banner" accept=".png,.jpg,.jpeg,.gif">
<div class="btn-row"><button type="submit" class="btn">Hochladen</button></div>
</form>
</div>
</div>
<div class="card" style="margin-top:20px">
<h3>Startseiten-Inhalte</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">
Legen Sie fest, welche zusätzlichen Bereiche auf der Kundenportal-Startseite angezeigt werden.
</p>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_homepage_content">
<label style="display:flex;align-items:center;gap:9px;font-weight:400;cursor:pointer">
<input type="checkbox" name="homepage_popular_articles_enabled" value="1" style="width:auto" <?= $homepagePopularArticlesEnabled ? 'checked' : '' ?>>
Bereich „Beliebte Artikel“ auf der Startseite anzeigen
</label>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</div>
<div class="card" style="margin-top:20px">
<h3>Eigene Kundenportal-Themes</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">
Laden Sie mehrere Designs als ZIP hoch und aktivieren Sie eines davon global für alle Kunden.
Das ZIP muss im Stammverzeichnis eine <code>theme.css</code> enthalten. Zulässig sind CSS,
PNG, JPG, GIF, WebP, WOFF und WOFF2; maximal 10 MB.
Autor und Version können im CSS-Kopf mit <code>Author:</code> und <code>Version:</code> angegeben werden.
</p>
<form method="post" enctype="multipart/form-data" style="display:flex;align-items:end;gap:12px;margin-bottom:22px;flex-wrap:wrap">
<?= csrf_field() ?><input type="hidden" name="action" value="upload_custom_theme">
<div>
<label for="theme_zip">Theme-Paket</label>
<input id="theme_zip" type="file" name="theme_zip" accept=".zip,application/zip" required>
</div>
<button type="submit" class="btn">Theme hochladen</button>
</form>
<p class="muted" style="font-size:.78rem;margin-top:-12px;margin-bottom:18px">
Der Anzeigename wird automatisch aus dem ZIP-Dateinamen übernommen, zum Beispiel
<code>Classic-Support-Blue.zip</code> → <strong>Classic Support Blue</strong>.
Ein erneuter Upload mit demselben Namen ersetzt das vorhandene Theme.
</p>
<?php if (!$customThemes): ?>
<p class="muted">Noch keine eigenen Themes hochgeladen.</p>
<?php else: ?>
<div style="display:grid;gap:10px">
<?php $__catThemes = theme_catalog_url() !== '' ? (theme_catalog_cached()['themes'] ?? []) : []; ?>
<?php foreach ($customThemes as $theme): ?>
<?php $isActiveTheme = $activeCustomTheme === $theme['id']; ?>
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:13px 14px;border:1px solid <?= $isActiveTheme ? 'var(--primary)' : 'var(--border)' ?>;border-radius:8px">
<div style="display:flex;align-items:center;gap:14px;min-width:0"><?= installed_theme_thumb($theme, $__catThemes) ?><div style="min-width:0">
<strong><?= e($theme['name']) ?> <span class="muted" style="font-weight:500">· Version <?= e($theme['theme_version']) ?></span></strong>
<?php if ($isActiveTheme): ?>
<span style="display:inline-block;margin-left:8px;padding:2px 7px;border-radius:999px;background:var(--primary-lt);color:var(--primary);font-size:.75rem;font-weight:700">Aktiv</span>
<?php endif; ?>
<div class="muted" style="font-size:.78rem">Von <?= e($theme['author']) ?></div>
</div></div>
<div class="btn-row" style="margin:0;flex-wrap:nowrap">
<?php if ($isActiveTheme): ?>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="deactivate_custom_theme">
<button type="submit" class="btn secondary">Standarddesign nutzen</button>
</form>
<?php else: ?>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="activate_custom_theme">
<input type="hidden" name="theme_id" value="<?= e($theme['id']) ?>">
<button type="submit" class="btn">Aktivieren</button>
</form>
<?php endif; ?>
<form method="post" onsubmit="return confirm('Theme wirklich löschen?')">
<?= csrf_field() ?><input type="hidden" name="action" value="delete_custom_theme">
<input type="hidden" name="theme_id" value="<?= e($theme['id']) ?>">
<button type="submit" class="btn secondary">Löschen</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="card" style="margin-top:20px">
<h3>Themes aus dem Katalog</h3>
<p class="muted" style="font-size:.85rem">Fertige Designs direkt installieren. Prüfsumme und Signatur werden vor der Installation geprüft.</p>
<?php if (plugin_strict_mode()): ?>
<?php if (theme_catalog_url() !== ''): ?><p><a class="btn secondary" href="/staff/settings.php?tab=design&amp;theme_refresh=1">Katalog aktualisieren</a></p><?php endif; ?>
<?php else: ?>
<form method="post" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap">
<?= csrf_field() ?><input type="hidden" name="action" value="save_theme_catalog">
<div style="flex:1;min-width:260px"><label for="theme_catalog_url">Katalog-Adresse (https)</label>
<input id="theme_catalog_url" type="text" name="theme_catalog_url" value="<?= e(theme_catalog_url()) ?>" placeholder="https://plugins.example.de/catalog.php?type=themes"></div>
<button type="submit" class="btn secondary">Speichern</button>
<?php if (theme_catalog_url() !== ''): ?><a class="btn secondary" href="/staff/settings.php?tab=design&amp;theme_refresh=1">Katalog aktualisieren</a><?php endif; ?>
</form>
<?php endif; ?>
<?php if (theme_catalog_url() !== ''): $themeCatalog = theme_catalog_cached(isset($_GET['theme_refresh']), 60); ?>
<?php if (!$themeCatalog['ok']): ?>
<div class="flash error" style="margin-top:12px"><?= e($themeCatalog['error']) ?></div>
<?php else: ?>
<table style="margin-top:12px">
<thead><tr><th>Theme</th><th>Version</th><th>Signatur</th><th></th></tr></thead>
<tbody>
<?php foreach ($themeCatalog['themes'] as $t): ?>
<tr>
<td><div style="display:flex;gap:12px;align-items:flex-start"><?= catalog_thumb('theme', $t) ?><div style="min-width:0"><strong><?= e($t['name']) ?></strong><div class="muted" style="font-size:.82rem"><?= e($t['description']) ?></div>
<?php if ($t['author'] !== ''): ?><div class="muted" style="font-size:.78rem">von <?= e($t['author']) ?></div><?php endif; ?></div></div></td>
<td><?= e($t['version']) ?><?php $__s2 = theme_catalog_state($t); if ($__s2['installed'] && $__s2['version'] !== '0'): ?><div class="muted" style="font-size:.76rem">installiert: <?= e($__s2['version']) ?></div><?php endif; ?></td>
<td style="font-size:.82rem"><?= $t['signature'] !== '' ? '<span style="color:#15803d;font-weight:600">✓ Signiert</span>' : '<span style="color:#b45309">Nicht signiert</span>' ?></td>
<?php $__state = theme_catalog_state($t); ?>
<td><?php if ($__state['installed'] && !$__state['update']): ?>
<span style="color:#15803d;font-weight:600;font-size:.85rem">✓ Installiert</span>
<?php else: ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="theme_catalog_install"><input type="hidden" name="slug" value="<?= e($t['slug']) ?>"><button type="submit" class="btn"><?= $__state['update'] ? 'Aktualisieren' : 'Installieren' ?></button></form>
<?php endif; ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$themeCatalog['themes']): ?><tr><td colspan="4" class="muted">Der Katalog enthält keine Themes.</td></tr><?php endif; ?>
</tbody>
</table>
<?php endif; ?>
<?php endif; ?>
</div>
<!-- ── TAB: E-MAIL ────────────────────────────────────────────────────────── -->
<?php elseif ($tab === 'email'): ?>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px">
<div class="card">
<h3>SMTP-Versand</h3>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_smtp">
<label>SMTP-Server</label>
<input type="text" name="smtp_host" value="<?= e($smtpHost) ?>" placeholder="smtp.beispiel.de">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px">
<div>
<label>Port</label>
<input type="number" name="smtp_port" value="<?= e($smtpPort) ?>" placeholder="587">
</div>
<div>
<label>Verschlüsselung</label>
<select name="smtp_encryption" onchange="var p=this.form.elements.smtp_port;if(p&&['','25','465','587'].indexOf(String(p.value).trim())!==-1)p.value={tls:'587',ssl:'465',none:'25'}[this.value]">
<option value="tls" <?= $smtpEncryption==='tls'?'selected':'' ?>>STARTTLS (587)</option>
<option value="ssl" <?= $smtpEncryption==='ssl'?'selected':'' ?>>SSL/TLS (465)</option>
<option value="none" <?= $smtpEncryption==='none'?'selected':'' ?>>Keine</option>
</select>
</div>
</div>
<label style="margin-top:10px">Benutzername</label>
<input type="text" name="smtp_username" value="<?= e($smtpUsername) ?>" autocomplete="off">
<label style="margin-top:10px">Passwort</label>
<input type="password" name="smtp_password" placeholder="<?= get_setting('smtp_password') ? '(unverändert)' : '' ?>" autocomplete="new-password">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px">
<div>
<label>Absendername</label>
<input type="text" name="smtp_from_name" value="<?= e($smtpFromName) ?>" placeholder="Support">
</div>
<div>
<label>Absender-E-Mail</label>
<input type="email" name="smtp_from_email" value="<?= e($smtpFromEmail) ?>" placeholder="support@firma.de">
</div>
</div>
<div class="btn-row" style="gap:8px">
<button type="submit" class="btn">Speichern</button>
</div>
</form>
<form method="post" style="margin-top:8px">
<?= csrf_field() ?><input type="hidden" name="action" value="test_smtp">
<button type="submit" class="btn secondary">Test-Mail senden</button>
</form>
</div>
<div class="card">
<h3>E-Mail-Abruf (IMAP)</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">Postfächer, aus denen eingehende E-Mails als Tickets importiert werden. Es können mehrere Postfächer angelegt werden (z.B. ein eigenes je Abteilung) – jedes mit eigenem Server, Zugangsdaten und optionaler Standardabteilung.</p>
<?php $imapAccountCount = (int) $pdo->query('SELECT COUNT(*) FROM imap_accounts WHERE is_active = 1')->fetchColumn(); ?>
<p style="margin-bottom:16px">
<?php if ($imapAccountCount > 0): ?>
<strong style="color:var(--success)">● <?= $imapAccountCount ?> aktive(s) Postfach/Postfächer eingerichtet</strong>
<?php else: ?>
<strong class="muted">○ Noch kein Postfach eingerichtet</strong>
<?php endif; ?>
· Letzter Abruf: <?= e($cronFetch) ?>
</p>
<a href="/staff/imap_accounts.php" class="btn">Postfächer verwalten →</a>
</div>
</div>
<!-- ── TAB: BENACHRICHTIGUNGEN ────────────────────────────────────────────── -->
<?php elseif ($tab === 'benachrichtigungen'): ?>
<div style="display:flex;flex-wrap:wrap;gap:20px;align-items:flex-start">
<div class="card" style="max-width:640px;flex:1 1 420px;min-width:0">
<h3>Discord & Telegram</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">Mitarbeiter werden bei neuen Tickets und Antworten benachrichtigt. Ticket-Daten werden dabei an den jeweiligen Drittanbieter übermittelt.</p>
<p class="muted" style="font-size:.8rem;margin-bottom:12px;padding:8px 10px;background:var(--primary-lt);border-radius:6px;border:1px solid var(--border)">
🔒 Webhook-URL und Bot-Token werden verschlüsselt gespeichert. Leer lassen = unverändert. Wert <code>-</code> löscht ihn.
</p>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_notifications">
<label>Discord Webhook-URL</label>
<input type="password" name="discord_webhook_url" autocomplete="new-password" placeholder="<?= get_setting('discord_webhook_url') ? '(gesetzt – leer lassen = unverändert)' : 'https://discord.com/api/webhooks/...' ?>">
<p class="muted" style="font-size:.78rem;margin-top:4px">Kanal-Einstellungen → Integrationen → Webhooks → Neuer Webhook</p>
<label style="margin-top:14px">Telegram Bot-Token</label>
<input type="password" name="telegram_bot_token" autocomplete="new-password" placeholder="<?= get_setting('telegram_bot_token') ? '(gesetzt – leer lassen = unverändert)' : '123456789:AAExampleToken' ?>">
<label style="margin-top:10px">Telegram Chat-ID</label>
<input type="text" name="telegram_chat_id" value="<?= e(get_setting('telegram_chat_id', '')) ?>" placeholder="-1001234567890">
<p class="muted" style="font-size:.78rem;margin-top:4px">Bot über @BotFather erstellen, Chat-ID via @userinfobot ermitteln.</p>
<div class="btn-row" style="gap:8px">
<button type="submit" class="btn">Speichern</button>
</div>
</form>
<form method="post" style="margin-top:8px">
<?= csrf_field() ?><input type="hidden" name="action" value="test_notifications">
<button type="submit" class="btn secondary">Testnachricht senden</button>
</form>
</div>
<?php
$__fcmMode = fcm_mode();
$__fcmSa = fcm_service_account();
$__fcmDevices = (int) $pdo->query('SELECT COUNT(*) FROM push_devices')->fetchColumn();
$__fcmErr = (string) get_setting('fcm_last_error', '');
$__fcmOk = (string) get_setting('fcm_last_ok', '');
$__fcmFromConfig = (string) (config()['fcm']['service_account_file'] ?? '') !== '';
?>
<div class="card" style="max-width:640px;flex:1 1 420px;min-width:0">
<h3>📱 Android-App: Push-Benachrichtigungen</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:12px">Popups auf dem Handy bei neuen Tickets, Kundenantworten und Zuweisungen, auch wenn die App geschlossen ist. Die Nachrichten laufen über Google (Firebase Cloud Messaging).</p>
<p style="margin:0 0 10px;font-size:.9rem">
<?php if ($__fcmMode === 'own'): ?>
<strong style="color:#15803d">✓ Eigenes Firebase-Projekt</strong> · <code><?= e($__fcmSa['project_id']) ?></code><?= $__fcmFromConfig ? ' (aus config/config.php)' : '' ?><br>
<?php elseif ($__fcmMode === 'relay'): ?>
<strong style="color:#15803d">✓ Push ist aktiv</strong> <span class="muted">· über den DeskHub-Push-Dienst, es ist nichts einzurichten</span><br>
<?php else: ?>
<strong style="color:#b45309">Nicht verfügbar.</strong> <span class="muted">In dieser Version ist kein Push-Dienst eingetragen. Ihr könnt unten ein eigenes Firebase-Projekt verwenden.</span><br>
<?php endif; ?>
<?php if ($__fcmMode !== 'none'): ?><span class="muted"><?= (int) $__fcmDevices ?> Gerät(e) angemeldet<?= $__fcmOk !== '' ? ' · letzter erfolgreicher Versand: ' . e($__fcmOk) : '' ?></span><?php endif; ?>
<?php if ($__fcmErr !== ''): ?><br><span style="color:#b91c1c;font-size:.85rem">Letzter Fehler: <?= e($__fcmErr) ?></span><?php endif; ?>
</p>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_fcm">
<label style="display:flex;align-items:center;gap:8px;margin-top:0"><input type="checkbox" name="fcm_enabled" value="1" style="width:auto" <?= get_setting('fcm_enabled', '1') === '1' ? 'checked' : '' ?>> Push-Benachrichtigungen senden</label>
<label style="display:flex;align-items:flex-start;gap:8px;margin-top:10px"><input type="checkbox" name="fcm_show_details" value="1" style="width:auto;margin-top:3px" <?= get_setting('fcm_show_details', '0') === '1' ? 'checked' : '' ?>>
<span>Betreff und Kundenname in der Benachrichtigung anzeigen<br><span class="muted" style="font-size:.8rem">Aus (Standard): Es steht nur „Neues Ticket #T-123“ drin, Betreff und Kundenname verlassen euren Server nicht. An: Betreff und Name laufen dann über den Push-Dienst und die Server von Google.</span></span></label>
<label for="push_brand" style="margin-top:14px">Name in der Benachrichtigung (optional)</label>
<input type="text" id="push_brand" name="push_brand" maxlength="40" value="<?= e(fcm_brand()) ?>" placeholder="z. B. Windelgeschichten Support">
<p class="muted" style="font-size:.78rem;margin-top:4px">Erscheint in jedem Popup neben dem App-Namen („DeskHub“). Den App-Namen selbst kann Android pro Server nicht ändern.</p>
<div class="btn-row" style="gap:8px;margin-top:12px"><button type="submit" class="btn">Speichern</button></div>
</form>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="test_fcm"><button type="submit" class="btn secondary" <?= $__fcmMode !== 'none' ? '' : 'disabled' ?>>Testnachricht an meine Geräte</button></form>
<?php if ($__fcmSa && !$__fcmFromConfig): ?><form method="post" onsubmit="return confirm('Den eigenen Firebase-Schlüssel vom Server entfernen? Danach wird wieder der DeskHub-Push-Dienst verwendet.')"><?= csrf_field() ?><input type="hidden" name="action" value="remove_fcm"><button type="submit" class="btn secondary" style="border-color:var(--danger);color:var(--danger)">Eigenen Schlüssel entfernen</button></form><?php endif; ?>
</div>
</div>
</div>
<!-- ── TAB: UPLOADS ───────────────────────────────────────────────────────── -->
<?php elseif ($tab === 'uploads'): ?>
<div class="card" style="max-width:640px">
<h3>Erlaubte Dateitypen</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:16px">Gefährliche Typen (PHP, EXE, SH etc.) werden immer blockiert – unabhängig von dieser Liste.</p>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_uploads">
<label>Erlaubte Datei-Endungen <span class="muted">(kommagetrennt)</span></label>
<input type="text" name="upload_allowed_extensions" value="<?= e($uploadExtensions) ?>" placeholder="png, jpg, pdf, docx, xlsx, zip">
<label style="margin-top:14px;display:block">Zusätzlich erlaubt für Anhänge eingehender E-Mails <span class="muted">(kommagetrennt)</span></label>
<input type="text" name="mail_attachment_extra_extensions" value="<?= e($mailExtraExtensions) ?>" placeholder="doc, xls, ppt, pptx, odt, ods, odp, csv, rtf, msg">
<div class="muted" style="font-size:.78rem;margin-top:4px">Kunden schicken oft Büro-Dateien per Mail, die im Formular nicht erlaubt sind. Diese Endungen gelten zusätzlich zur Liste oben, aber nur für Mails. Gefährliche Typen bleiben gesperrt.</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:10px">
<?php foreach (explode(',', $uploadExtensions) as $ext): $ext = trim($ext); if (!$ext) continue; ?>
<span style="background:var(--primary-lt);color:var(--primary);border-radius:20px;padding:3px 10px;font-size:.78rem;font-weight:600"><?= e($ext) ?></span>
<?php endforeach; ?>
</div>
<label style="margin-top:16px">Maximale Dateigröße</label>
<div style="display:flex;align-items:center;gap:8px">
<input type="number" name="upload_max_size_mb" value="<?= e($uploadMaxSize) ?>" min="1" max="100" style="width:90px"> MB
</div>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</div>
<!-- ── TAB: FOOTER ────────────────────────────────────────────────────────── -->
<?php elseif ($tab === 'footer'): ?>
<div class="card">
<h3>Datenschutz & Impressum</h3>
<p class="muted" style="font-size:.85rem;margin-bottom:20px">Externer Link <em>oder</em> HTML-Inhalt (öffnet Popup). Ist ein Link gesetzt, wird das HTML-Feld ignoriert.</p>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_footer_pages">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
<div>
<div style="font-weight:700;font-size:.88rem;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)">🔒 Datenschutzerklärung</div>
<label>Beschriftung im Footer</label>
<input type="text" name="footer_privacy_label" value="<?= e($footerPrivacyLabel) ?>" placeholder="Datenschutz">
<label style="margin-top:10px">Externer Link (optional)</label>
<input type="text" name="footer_privacy_url" value="<?= e($footerPrivacyUrl) ?>" placeholder="https://..." style="width:100%">
<label style="margin-top:10px">HTML-Inhalt (Popup)</label>
<textarea name="footer_privacy_html" rows="8" style="font-family:monospace;font-size:.8rem;width:100%"><?= e($footerPrivacyHtml) ?></textarea>
</div>
<div>
<div style="font-weight:700;font-size:.88rem;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)">📄 Impressum</div>
<label>Beschriftung im Footer</label>
<input type="text" name="footer_imprint_label" value="<?= e($footerImprintLabel) ?>" placeholder="Impressum">
<label style="margin-top:10px">Externer Link (optional)</label>
<input type="text" name="footer_imprint_url" value="<?= e($footerImprintUrl) ?>" placeholder="https://..." style="width:100%">
<label style="margin-top:10px">HTML-Inhalt (Popup)</label>
<textarea name="footer_imprint_html" rows="8" style="font-family:monospace;font-size:.8rem;width:100%"><?= e($footerImprintHtml) ?></textarea>
</div>
</div>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</div>
<!-- ── TAB: DATENSCHUTZ ──────────────────────────────────────────────────── -->
<?php elseif ($tab === 'datenschutz'): ?>
<?php
$retEnabled = get_setting('retention_enabled', '0') === '1';
$retMonths = (int)(get_setting('retention_months', '24'));
$ipAnon = get_setting('ip_anonymize', '0') === '1';
// Vorschau: wie viele Tickets wären betroffen?
$previewSql = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite'
? "SELECT COUNT(*) FROM tickets WHERE status IN ('closed','resolved')
AND updated_at < datetime('now', '-' || ? || ' months')
AND requester_email NOT LIKE 'anon_%'"
: "SELECT COUNT(*) FROM tickets WHERE status IN ('closed','resolved')
AND updated_at < DATE_SUB(NOW(), INTERVAL ? MONTH)
AND requester_email NOT LIKE 'anon_%'";
$previewStmt = $pdo->prepare($previewSql);
$previewStmt->execute([$retMonths]);
$previewCount = (int)$previewStmt->fetchColumn();
?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<div class="card">
<h3>Automatische Datenlöschung</h3>
<p class="muted" style="font-size:.83rem;margin-bottom:16px">
Geschlossene und gelöste Tickets werden nach der gewählten Zeit automatisch anonymisiert.
Name, E-Mail-Adresse und IP werden unkenntlich gemacht — das Ticket und seine Historie bleiben erhalten.
</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_privacy">
<label style="display:flex;align-items:center;gap:10px;font-weight:500;cursor:pointer;margin-bottom:18px">
<input type="checkbox" name="retention_enabled" value="1" style="width:auto" <?= $retEnabled ? 'checked' : '' ?>>
Automatische Anonymisierung aktivieren
</label>
<label for="ret_months">Aufbewahrungsdauer nach Schließung</label>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
<input type="number" id="ret_months" name="retention_months" value="<?= $retMonths ?>" min="1" max="120" style="width:100px">
<span class="muted" style="font-size:.85rem">Monate</span>
</div>
<label style="display:flex;align-items:center;gap:10px;font-weight:500;cursor:pointer;margin-bottom:18px">
<input type="checkbox" name="ip_anonymize" value="1" style="width:auto" <?= $ipAnon ? 'checked' : '' ?>>
IP-Adressen sofort beim Speichern kürzen (letztes Oktett entfernen)
</label>
<label style="display:flex;align-items:center;gap:10px;font-weight:500;cursor:pointer;margin-bottom:6px">
<input type="checkbox" name="gdpr_tools_enabled" value="1" style="width:auto" <?= gdpr_tools_enabled() ? 'checked' : '' ?>>
Auskunft und Löschung auf Anfrage im Kundenprofil anbieten (nur Administratoren)
</label>
<p class="muted" style="font-size:.78rem;margin:0 0 18px 28px">Zeigt im Kundenprofil die Knöpfe „Alle Daten exportieren“ und „Kundendaten löschen“. Jede Nutzung wird im Audit-Log protokolliert.</p>
<div class="btn-row">
<button type="submit" class="btn">Speichern</button>
</div>
</form>
</div>
<div class="card">
<h3>Status & Vorschau</h3>
<div style="margin-bottom:20px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<span style="width:10px;height:10px;border-radius:50%;background:<?= $retEnabled ? '#22c55e' : '#94a3b8' ?>;flex-shrink:0"></span>
<span style="font-size:.85rem;font-weight:500"><?= $retEnabled ? 'Aktiv – Anonymisierung läuft täglich via Cron' : 'Deaktiviert' ?></span>
</div>
<p class="muted" style="font-size:.82rem;margin:0">
Aktuell würden <strong><?= $previewCount ?></strong> Ticket(s) anonymisiert
(geschlossen/gelöst, älter als <?= $retMonths ?> Monate).
</p>
</div>
<h3 style="margin-top:0">Was wird anonymisiert?</h3>
<ul style="font-size:.83rem;color:var(--ink-soft);line-height:1.8;padding-left:18px;margin:0 0 20px">
<li><strong>Name</strong> → <code>Anonymisiert</code></li>
<li><strong>E-Mail</strong> → <code>anon_[hash]@deleted.local</code></li>
<li><strong>IP-Adresse</strong> → <code>0.0.0.0</code></li>
<li><strong>Ticket-Nachrichten</strong> → bleiben erhalten</li>
<li><strong>Ticket-Nummer, Betreff, Status</strong> → bleiben erhalten</li>
</ul>
<h3 style="margin-top:0">Crontab-Eintrag</h3>
<pre style="background:var(--bg-soft);border:1px solid var(--border);border-radius:6px;padding:10px;font-size:.76rem;margin:0 0 16px">0 3 * * * php /pfad/zu/deskhub/cron/anonymize_data.php</pre>
<form method="post" onsubmit="return confirm('Anonymisierung jetzt ausführen? Dies kann nicht rückgängig gemacht werden.')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="run_anonymize_now">
<button type="submit" class="btn secondary" style="font-size:.83rem">
▶ Jetzt ausführen (<?= $previewCount ?> Tickets)
</button>
</form>
</div>
</div>
<!-- ── TAB: CRON ──────────────────────────────────────────────────────────── -->
<?php elseif ($tab === 'cron'): ?>
<?php
$pseudoCronEnabled = get_setting('pseudo_cron_enabled', '1') === '1';
$pseudoCronGate = get_setting('pseudo_cron_gate');
$pseudoCronLastTick = cron_last_run_label($pseudoCronGate);
?>
<div class="card" style="margin-bottom:20px">
<h3>🔄 Automatischer Hintergrund-Betrieb (ohne Server-Cronjob)</h3>
<p class="muted" style="font-size:.85rem;margin:6px 0 14px">
Läuft komplett eingebaut, ganz ohne echten Server-Cronjob und ohne externen Dienst:
bei jeder Anmeldung/Nutzung im Backend <strong>und</strong> in der App wird im Hintergrund geprüft,
ob einer der Jobs unten fällig ist, und automatisch ausgeführt. Solange regelmäßig jemand
das Backend oder die App nutzt, laufen alle Jobs von selbst dauerhaft weiter – ganz ohne
Einrichtung bei Strato oder einem externen Anbieter.
</p>
<div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap">
<form method="post" style="display:flex; align-items:center; gap:10px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_pseudo_cron">
<label style="display:flex;align-items:center;gap:8px;font-weight:400;cursor:pointer;margin:0">
<input type="checkbox" name="pseudo_cron_enabled" value="1" style="width:auto" <?= $pseudoCronEnabled?'checked':'' ?> onchange="this.form.submit()">
Automatischen Hintergrund-Betrieb aktiv halten
</label>
</form>
<span class="muted" style="font-size:.82rem">
Status: <?= $pseudoCronEnabled ? '<span style="color:var(--success)">● aktiv</span>' : '<span style="color:var(--muted)">○ deaktiviert</span>' ?>
· Letzte automatische Prüfung: <?= e($pseudoCronLastTick) ?>
</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<div class="card">
<h3>Cron-Status</h3>
<p class="muted" style="font-size:.82rem;margin-bottom:12px">Jeder Job kann hier zusätzlich jederzeit sofort manuell gestartet werden.</p>
<?php
$cronLastRun = [
'fetch_emails' => $cronFetch,
'check_sla' => $cronSla,
'check_escalations' => $cronEscalations,
'process_recurring' => $cronRecurring,
'process_backups' => $cronBackups,
'anonymize_data' => $cronAnonymize,
'auto_close_tickets'=> $cronAutoClose,
'process_snoozes' => $cronSnoozes,
'process_mail_queue' => cron_last_run_label(get_setting('cron_process_mail_queue_last_run')),
'remind_customers' => $cronRemind,
'run_plugins' => $cronPlugins,
'send_csat' => $cronCsat,
'status_checks' => cron_last_run_label(get_setting('cron_status_checks_last_run')),
'send_scheduled_replies' => cron_last_run_label(get_setting('cron_send_scheduled_replies_last_run')),
'send_report_schedules' => cron_last_run_label(get_setting('cron_send_report_schedules_last_run')),
];
?>
<table style="width:100%">
<tbody>
<?php foreach ($cronJobs as $jobKey => $jobMeta): ?>
<tr>
<td><?= e($jobMeta['label']) ?></td>
<td class="muted" style="text-align:right; white-space:nowrap"><?= e($cronLastRun[$jobKey]) ?></td>
<td style="text-align:right; width:1%">
<form method="post" style="display:inline" onsubmit="return confirm('<?= e($jobMeta['label']) ?> jetzt manuell ausführen?')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="run_cron_now">
<input type="hidden" name="job" value="<?= e($jobKey) ?>">
<button type="submit" class="btn secondary" style="padding:4px 10px; font-size:.78rem">Jetzt ausführen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card">
<h3>Echter Server-Cronjob</h3>
<?php
$crontabCheck = crontab_capability_check();
$crontabInstalled = $crontabCheck['available'] && crontab_is_installed();
?>
<?php if ($crontabCheck['available']): ?>
<p class="muted" style="font-size:.82rem;margin-bottom:10px">
✅ Dieser Server unterstützt eine automatische Installation (Shell-Zugriff erkannt).
DeskHub trägt dafür einen klar markierten Block in die Crontab des Server-Benutzers ein –
alle sonstigen vorhandenen Einträge bleiben unangetastet.
</p>
<p style="margin-bottom:14px">
Status:
<?php if ($crontabInstalled): ?>
<strong style="color:var(--success)">● Installiert</strong>
<?php else: ?>
<strong class="muted">○ Nicht installiert</strong>
<?php endif; ?>
</p>
<div class="btn-row" style="margin-bottom:14px">
<form method="post" onsubmit="return confirm('DeskHub-Cronjobs jetzt fest in die Server-Crontab eintragen? Bestehende, nicht von DeskHub verwaltete Einträge bleiben erhalten.')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="install_crontab">
<button type="submit" class="btn"><?= $crontabInstalled ? 'Neu installieren / aktualisieren' : 'Jetzt installieren' ?></button>
</form>
<?php if ($crontabInstalled): ?>
<form method="post" onsubmit="return confirm('DeskHub-Block wieder aus der Server-Crontab entfernen?')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="remove_crontab">
<button type="submit" class="btn secondary">Entfernen</button>
</form>
<?php endif; ?>
</div>
<?php else: ?>
<p class="muted" style="font-size:.82rem;margin-bottom:10px">
❌ Auf diesem Server nicht möglich: <?= e($crontabCheck['reason']) ?><br>
Typisch bei Shared-Hosting (z.B. den meisten Strato-Webhosting-Paketen). Der oben aktive
automatische Hintergrund-Betrieb deckt das in der Regel bereits ab – alternativ unten die
Zeilen manuell im Hosting-Kundenlogin eintragen, falls dort ein Cronjob-Manager verfügbar ist.
</p>
<?php endif; ?>
<details style="margin-top:4px">
<summary style="cursor:pointer;font-size:.82rem;color:var(--muted)">Crontab-Zeilen anzeigen</summary>
<pre style="background:var(--bg-soft);border:1px solid var(--border);border-radius:6px;padding:12px;font-size:.76rem;overflow-x:auto;white-space:pre-wrap;margin-top:8px"><?= e(crontab_marker_block()) ?></pre>
</details>
</div>
</div>
<?php endif; ?>
<?php require __DIR__ . '/_footer.php'; ?>