Upload via GUI (290 Dateien)

This commit is contained in:
2026-10-23 17:22:21 +00:00
parent 14b74ef747
commit 80be774c3e
4 changed files with 125 additions and 2 deletions
@@ -28,6 +28,12 @@ $templates = [
'default_subject' => 'SLA überschritten: Ticket #{ticket_number}',
'default_body' => email_ticket_overdue_default_body(),
],
'customer_reminder' => [
'label' => 'Erinnerung (an Kunde, wenn er nicht reagiert)',
'placeholders' => ['ticket_number', 'subject', 'customer_name', 'ticket_url', 'close_note'],
'default_subject' => 'Erinnerung zu Ihrem Ticket [#{ticket_number}]',
'default_body' => email_customer_reminder_default_body(),
],
'live_chat_transcript' => [
'label' => 'Chatverlauf (an Kunde, nach Chatende)',
'placeholders' => ['ticket_number', 'customer_name', 'transcript', 'ticket_url'],
+18
View File
@@ -18,6 +18,8 @@ $cronJobs = [
'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'],
'remind_customers' => ['label' => 'Kundenerinnerung', 'file' => 'remind_customers.php', 'fn' => 'run_remind_customers'],
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@@ -46,6 +48,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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('remind_enabled', !empty($_POST['remind_enabled']) ? '1' : '0');
set_setting('remind_after_days', (string) max(1, min(60, (int) ($_POST['remind_after_days'] ?? 3))));
$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");
@@ -460,6 +464,8 @@ $cronBackups = cron_last_run_label(get_setting('cron_process_backups_last_ru
$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'));
$tab = $_GET['tab'] ?? 'allgemein';
@@ -547,6 +553,16 @@ require __DIR__ . '/_header.php';
<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>
<?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 $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':'' ?>>
@@ -1120,6 +1136,8 @@ require __DIR__ . '/_header.php';
'process_backups' => $cronBackups,
'anonymize_data' => $cronAnonymize,
'auto_close_tickets'=> $cronAutoClose,
'process_snoozes' => $cronSnoozes,
'remind_customers' => $cronRemind,
];
?>
<table style="width:100%">
+92
View File
@@ -1,5 +1,6 @@
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/ticket_extras.php'; // falls bootstrap.php noch die alte Version ist
require_once __DIR__ . '/../includes/imap_spam.php';
$staff = require_staff_login();
$pdo = get_db();
@@ -128,6 +129,21 @@ $allTags = $pdo->query('SELECT name FROM tags ORDER BY name')->fetchAll(PDO::FET
$errors = [];
$snoozedUntil = null;
$followers = [];
$isFollowing = false;
$similar = ['articles' => [], 'tickets' => []];
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
try {
$snoozedUntil = !empty($ticket['snoozed_until']) && $ticket['snoozed_until'] > db_now() ? $ticket['snoozed_until'] : null;
$followers = ticket_followers($pdo, (int) $ticket['id']);
foreach ($followers as $f) if ((int) $f['id'] === (int) $staff['id']) $isFollowing = true;
$similar = ticket_similar($pdo, $ticket, staff_department_ids($staff));
} catch (Throwable $e) {
// Datenbank noch nicht aktualisiert: Zusatzfunktionen bleiben leer
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Kunden sperren (eigener Block ohne globale verify_csrf)
if (($_POST['action'] ?? '') === 'ban_user') {
@@ -272,6 +288,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_once __DIR__ . '/../includes/push.php';
push_new_reply($ticket, $staff['name']);
}
ticket_followers_notify($ticket, 'Antwort von ' . $staff['name'], (int) $staff['id']);
} else {
save_mentions($pdo, $ticket['id'], $threadId, $staff['id'], $message, $ticket);
log_activity($pdo, $ticket['id'], $staff['id'], 'Interne Notiz hinzugefügt');
@@ -330,6 +347,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
redirect('/staff/ticket_view.php?n=' . urlencode($number));
}
if ($action === 'snooze') {
$until = ticket_snooze_until((string) ($_POST['snooze_choice'] ?? ''), (string) ($_POST['snooze_custom'] ?? ''));
if ($until === null) {
flash('error', 'Bitte einen Zeitpunkt in der Zukunft wählen (höchstens 1 Jahr).');
} else {
ticket_snooze_set($pdo, (int) $ticket['id'], (int) $staff['id'], $until);
flash('success', 'Wiedervorlage bis ' . date('d.m.Y H:i', (int) strtotime($until)) . ' Uhr. Das Ticket kommt dann zurück und du wirst benachrichtigt.');
}
redirect('/staff/ticket_view.php?n=' . urlencode($number));
}
if ($action === 'unsnooze') {
ticket_snooze_clear($pdo, (int) $ticket['id'], (int) $staff['id'], 'Wiedervorlage aufgehoben');
flash('success', 'Wiedervorlage aufgehoben.');
redirect('/staff/ticket_view.php?n=' . urlencode($number));
}
if ($action === 'follow' || $action === 'unfollow') {
try {
if ($action === 'follow') { ticket_follow($pdo, (int) $ticket['id'], (int) $staff['id']); flash('success', 'Du beobachtest dieses Ticket jetzt und bekommst Benachrichtigungen bei Antworten.'); }
else { ticket_unfollow($pdo, (int) $ticket['id'], (int) $staff['id']); flash('success', 'Du beobachtest dieses Ticket nicht mehr.'); }
} catch (Throwable $e) {
flash('error', 'Das ging nicht. Bitte unter System-Update die Datenbank aktualisieren.');
}
redirect('/staff/ticket_view.php?n=' . urlencode($number));
}
if ($action === 'add_collaborator') {
$ccEmail = trim($_POST['collaborator_email'] ?? '');
$ccName = trim($_POST['collaborator_name'] ?? '');
@@ -1224,6 +1268,54 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<?php endif; ?>
</div>
<!-- Wiedervorlage und Beobachter -->
<div class="card" style="padding:16px 20px">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Wiedervorlage</span>
<?php if ($snoozedUntil): ?>
<p style="font-size:.84rem;margin:8px 0">💤 Bis <strong><?= e(date('d.m.Y H:i', (int) strtotime($snoozedUntil))) ?> Uhr</strong> ausgeblendet, dann kommt es zurück.</p>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="unsnooze"><button type="submit" class="btn secondary" style="width:100%;font-size:.82rem;justify-content:center">Jetzt zurückholen</button></form>
<?php else: ?>
<form method="post" style="margin-top:8px">
<?= csrf_field() ?><input type="hidden" name="action" value="snooze">
<select name="snooze_choice" style="font-size:.84rem;padding:7px 10px;margin-bottom:6px" onchange="document.getElementById('snooze-custom').style.display=this.value==='custom'?'':'none'">
<?php foreach (ticket_snooze_options() as $k => $l): ?><option value="<?= e($k) ?>"><?= e($l) ?></option><?php endforeach; ?>
</select>
<input type="datetime-local" name="snooze_custom" id="snooze-custom" style="display:none;font-size:.84rem;padding:7px 10px;margin-bottom:6px">
<button type="submit" class="btn secondary" style="width:100%;font-size:.82rem;justify-content:center">Auf später legen</button>
</form>
<p style="font-size:.76rem;color:var(--muted);margin:6px 0 0">Das Ticket verschwindet aus den Listen und kommt zum Zeitpunkt zurück, mit Benachrichtigung. Antwortet der Kunde früher, kommt es sofort zurück.</p>
<?php endif; ?>
<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border-soft)">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Beobachter</span>
<?php if ($followers): ?>
<div style="font-size:.83rem;margin:6px 0"><?= e(implode(', ', array_column($followers, 'name'))) ?></div>
<?php else: ?>
<p style="font-size:.79rem;color:var(--muted);margin:6px 0">Niemand. Beobachter bekommen Benachrichtigungen, ohne dass ihnen das Ticket zugewiesen ist.</p>
<?php endif; ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="<?= $isFollowing ? 'unfollow' : 'follow' ?>"><button type="submit" class="btn secondary" style="width:100%;font-size:.82rem;justify-content:center"><?= $isFollowing ? 'Nicht mehr beobachten' : '👁 Ticket beobachten' ?></button></form>
</div>
</div>
<?php if ($similar['articles'] || $similar['tickets']): ?>
<!-- Ähnliche Artikel und Tickets -->
<div class="card" style="padding:16px 20px">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Dazu passend</span>
<?php if ($similar['articles']): ?>
<div style="font-size:.76rem;color:var(--muted);margin:8px 0 2px">Wissensdatenbank</div>
<?php foreach ($similar['articles'] as $sa): ?>
<div style="font-size:.84rem;padding:3px 0"><a href="<?= e($sa['url']) ?>" target="_blank" rel="noopener"><?= e($sa['title']) ?></a> <button type="button" class="link" style="background:none;border:0;color:var(--muted);cursor:pointer;font-size:.74rem" onclick="navigator.clipboard&&navigator.clipboard.writeText(location.origin+'<?= e($sa['url']) ?>');this.textContent='kopiert'">Link kopieren</button></div>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($similar['tickets']): ?>
<div style="font-size:.76rem;color:var(--muted);margin:8px 0 2px">Ähnliche gelöste Tickets</div>
<?php foreach ($similar['tickets'] as $st): ?>
<div style="font-size:.84rem;padding:3px 0"><a href="/staff/ticket_view.php?n=<?= e(urlencode((string) $st['number'])) ?>" target="_blank" rel="noopener">#<?= e($st['number']) ?></a> <?= e(mb_strimwidth((string) $st['subject'], 0, 48, '…')) ?></div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Kollaboratoren -->
<div class="card" style="padding:16px 20px">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px">
+9 -2
View File
@@ -1,5 +1,6 @@
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/ticket_extras.php'; // falls bootstrap.php noch die alte Version ist
$staff = require_staff_login();
$pdo = get_db();
$myDeptIds = staff_department_ids($staff); // null = Admin, alle Abteilungen
@@ -236,9 +237,13 @@ if ($activeTab === 'banned') {
} else {
// Gesperrte Absender in ALLEN anderen Tabs ausblenden
$where[] = $notBannedSql;
// Tickets in der Wiedervorlage erscheinen nur im eigenen Tab, bis ihre Zeit gekommen ist
$where[] = $activeTab === 'snoozed' ? (ticket_snooze_available() ? "t.snoozed_until > '" . db_now() . "'" : '1=0') : ticket_not_snoozed_sql('t');
if ($activeTab === 'open') {
$where[] = "t.status IN ('open','pending','answered')";
} elseif ($activeTab === 'snoozed') {
// alle Status
} elseif ($activeTab === 'on_hold') {
$where[] = "t.status = 'on_hold'";
} elseif ($activeTab === 'resolved') {
@@ -324,8 +329,9 @@ require __DIR__ . '/_header.php';
// Ticket-Zähler für Tabs
$tabCounts = [];
foreach (['open'=>"status IN ('open','pending','answered')", 'on_hold'=>"status='on_hold'", 'resolved'=>"status='resolved'", 'closed'=>"status='closed'"] as $tk => $tcond) {
$tabCounts[$tk] = (int)$pdo->query("SELECT COUNT(*) FROM tickets WHERE $tcond")->fetchColumn();
$tabCounts[$tk] = (int)$pdo->query("SELECT COUNT(*) FROM tickets WHERE $tcond AND " . ticket_not_snoozed_sql('tickets'))->fetchColumn();
}
$tabCounts['snoozed'] = ticket_snooze_available() ? (int)$pdo->query("SELECT COUNT(*) FROM tickets WHERE snoozed_until > '" . db_now() . "'")->fetchColumn() : 0;
// Gesperrt-Tab: sicher mit try/catch
try {
$tabCounts['banned'] = (int)$pdo->query(
@@ -347,8 +353,9 @@ $statusTabs = [
'on_hold' => ['Pausiert', $tabCounts['on_hold']],
'resolved' => ['Gelöst', $tabCounts['resolved']],
'closed' => ['Geschlossen', $tabCounts['closed']],
'snoozed' => ['💤 Wiedervorlage', $tabCounts['snoozed']],
'banned' => ['⛔ Gesperrt', $tabCounts['banned']],
'all' => ['Alle', array_sum($tabCounts)],
'all' => ['Alle', array_sum($tabCounts) - $tabCounts['snoozed']],
];
$quickFilters = [
'me' => ['Mir zugewiesen', $countMe, 'var(--primary)', '#e6f4f3'],