Upload via GUI (290 Dateien)
This commit is contained in:
@@ -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,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%">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user