Upload via GUI (340 Dateien)
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
<td><?= e($t['department_name']) ?></td>
|
||||
<td><span class="badge" style="background:<?= e($t['priority_color']) ?>"><?= e($t['priority_name']) ?></span></td>
|
||||
<td><span class="badge" style="background:<?= $color ?>"><?= e($label) ?></span></td>
|
||||
<td class="muted"><?= time_until($t['resolution_due_at']) ?></td>
|
||||
<td class="muted"><?= !empty($t['sla_paused_at']) && ticket_sla_is_paused_status((string) $t['status']) ? 'Pausiert' : time_until($t['resolution_due_at']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@@ -58,7 +58,6 @@ if ('serviceWorker' in navigator) {
|
||||
<a href="/staff/tickets.php" class="<?= $activePage==='tickets'?'active':'' ?>">Tickets</a>
|
||||
<a href="/staff/tasks.php" class="<?= $activePage==='tasks'?'active':'' ?>">Aufgaben</a>
|
||||
<a href="/staff/banlist.php" class="<?= $activePage==='banlist'?'active':'' ?>">Sperrliste</a>
|
||||
<a href="/staff/agent_directory.php" class="<?= $activePage==='agent_directory'?'active':'' ?>">Verzeichnis</a>
|
||||
<a href="/staff/apps.php" class="<?= $activePage==='apps'?'active':'' ?>">Apps</a>
|
||||
<?php foreach (plugin_menu_items($staff ?? null) as $__pm): ?>
|
||||
<a href="<?= e($__pm['url']) ?>" class="<?= ($activePage ?? '') === 'plugin_' . $__pm['plugin'] ? 'active' : '' ?>"><?= e($__pm['label']) ?></a>
|
||||
@@ -107,7 +106,7 @@ if ('serviceWorker' in navigator) {
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/canned_responses.php" class="<?= $activePage==='canned'?'active':'' ?>">Vorlagen</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/site_pages.php" class="<?= $activePage==='site_pages'?'active':'' ?>">Seiten</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/languages.php" class="<?= $activePage==='languages'?'active':'' ?>">Sprachen</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/ticket_templates.php" class="<?= $activePage==='ticket_templates'?'active':'' ?>">Ticket-Vorlagen</a><a href="/staff/status_page.php" class="<?= $activePage==='status_page'?'active':'' ?>">Statusseite</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/ticket_templates.php" class="<?= $activePage==='ticket_templates'?'active':'' ?>">Ticket-Vorlagen</a><a href="/staff/status_page.php" class="<?= $activePage==='status_page'?'active':'' ?>">Statusseite</a><a href="/staff/problems.php" class="<?= $activePage==='problems'?'active':'' ?>">Problemmanagement</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_email_addresses')): ?><a href="/staff/email_addresses.php" class="<?= $activePage==='email_addresses'?'active':'' ?>">E-Mail-Adressen</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_email_templates')): ?><a href="/staff/email_templates.php" class="<?= $activePage==='email_templates'?'active':'' ?>">E-Mail-Vorlagen</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_kb')): ?><a href="/staff/kb_manage.php" class="<?= $activePage==='kb'?'active':'' ?>">Wissensdatenbank</a><?php endif; ?>
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
<tbody>
|
||||
<?php foreach ($tickets as $t):
|
||||
[$label, $color] = status_label($t['status']);
|
||||
$isOverdue = $t['resolution_due_at'] && strtotime($t['resolution_due_at']) < time() && !in_array($t['status'], ['resolved','closed']);
|
||||
$isPaused = !empty($t['sla_paused_at']) && ticket_sla_is_paused_status((string) $t['status']);
|
||||
$isOverdue = !empty($t['is_overdue']) || (!$isPaused && $t['resolution_due_at'] && strtotime($t['resolution_due_at']) < time() && !in_array($t['status'], ['resolved','closed']));
|
||||
?>
|
||||
<tr class="<?= $isOverdue ? 'is-overdue' : '' ?>">
|
||||
<td><input type="checkbox" class="ticket-check" name="ticket_ids[]" value="<?= $t['id'] ?>"></td>
|
||||
@@ -60,7 +61,7 @@
|
||||
<td><span class="badge" style="background:<?= $color ?>"><?= e($label) ?></span></td>
|
||||
<td><?= $t['staff_name'] ? e($t['staff_name']) : '<span class="muted">–</span>' ?></td>
|
||||
<td class="muted" style="white-space:nowrap" title="Ticket eingegangen"><?= e(format_dt($t['created_at'] ?? null)) ?></td>
|
||||
<td class="muted"><?= time_until($t['resolution_due_at']) ?></td>
|
||||
<td class="muted"><?= $isPaused ? 'Pausiert' : time_until($t['resolution_due_at']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@@ -21,11 +21,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} elseif ($action === 'component_state') {
|
||||
status_set_component($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['status'] ?? ''));
|
||||
flash('success', 'Zustand geändert.');
|
||||
} elseif ($action === 'component_enabled') {
|
||||
$enabled = !empty($_POST['enabled']);
|
||||
if (!status_set_component_enabled($pdo, (int) ($_POST['id'] ?? 0), $enabled)) $errors[] = 'Dienst konnte nicht gefunden werden.';
|
||||
else flash('success', $enabled ? 'Dienst aktiviert und wieder zur Prüfung freigegeben.' : 'Dienst deaktiviert, ausgeblendet und von Prüfungen ausgenommen.');
|
||||
} elseif ($action === 'component_maintenance') {
|
||||
if (!status_toggle_component_maintenance($pdo, (int) ($_POST['id'] ?? 0))) $errors[] = 'Dienst konnte nicht gefunden werden.';
|
||||
else flash('success', 'Wartungsstatus geändert.');
|
||||
} elseif ($action === 'component_monitor') {
|
||||
$ok = status_set_component_monitor($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['monitor_type'] ?? 'auto'), (string) ($_POST['monitor_target'] ?? ''));
|
||||
$ok = status_set_component_monitor($pdo, (int) ($_POST['id'] ?? 0), (string) ($_POST['monitor_type'] ?? 'auto'), (string) ($_POST['monitor_target'] ?? ''), (int) ($_POST['check_interval_minutes'] ?? 1));
|
||||
if (!$ok) $errors[] = 'Bitte Prüfart und eine gültige Zieladresse prüfen.';
|
||||
else flash('success', 'Automatische Prüfung gespeichert.');
|
||||
} elseif ($action === 'auto_discover') {
|
||||
@@ -52,7 +56,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} elseif ($action === 'component_restore_core') {
|
||||
$restored = status_restore_core_components($pdo);
|
||||
audit_log($admin['id'], 'Statusseite: fehlende DeskHub-Standarddienste wiederhergestellt');
|
||||
flash('success', 'Fehlende DeskHub-Dienste und passende Kategorien wurden hinzugefuegt. Vorhandene Eintraege blieben erhalten.');
|
||||
flash('success', $restored > 0
|
||||
? 'Fehlende DeskHub-Dienste wurden hinzugefügt. Der Minecraft-Server liegt unter „Apps“ und bleibt bis zur Adresseingabe deaktiviert.'
|
||||
: 'Alle DeskHub-Dienste sind bereits vorhanden. Der Minecraft-Server liegt unter „Apps“; Adresse und Port eintragen und danach aktivieren.');
|
||||
} elseif ($action === 'incident_create') {
|
||||
$cid = (int) ($_POST['component_id'] ?? 0) ?: null;
|
||||
$r = status_create_incident($pdo, (string) ($_POST['title'] ?? ''), (string) ($_POST['body'] ?? ''), (string) ($_POST['status'] ?? 'investigating'), $cid, ($_POST['component_state'] ?? '') !== '' ? (string) $_POST['component_state'] : null, (int) $admin['id']);
|
||||
@@ -63,21 +69,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} elseif ($action === 'incident_delete') {
|
||||
status_delete_incident($pdo, (int) ($_POST['id'] ?? 0));
|
||||
flash('success', 'Meldung gelöscht.');
|
||||
} elseif ($action === 'maintenance_create') {
|
||||
$componentId = (int) ($_POST['component_id'] ?? 0) ?: null;
|
||||
$result = status_create_maintenance_window($pdo, (string) ($_POST['title'] ?? ''), (string) ($_POST['body'] ?? ''), (string) ($_POST['starts_at'] ?? ''), (string) ($_POST['ends_at'] ?? ''), $componentId, (int) $admin['id']);
|
||||
if (!$result['ok']) $errors[] = $result['error'];
|
||||
else { audit_log($admin['id'], 'Statusseite: Wartungsfenster geplant'); flash('success', 'Wartungsfenster wurde geplant.'); }
|
||||
} elseif ($action === 'maintenance_cancel') {
|
||||
if (!status_cancel_maintenance_window($pdo, (int) ($_POST['id'] ?? 0))) $errors[] = 'Wartungsfenster konnte nicht gefunden werden.';
|
||||
else { audit_log($admin['id'], 'Statusseite: Wartungsfenster abgesagt'); flash('success', 'Wartungsfenster wurde abgesagt.'); }
|
||||
}
|
||||
if (!$errors) redirect('/staff/status_page.php');
|
||||
}
|
||||
|
||||
$components = status_components($pdo);
|
||||
$components = status_components($pdo, true);
|
||||
$categories = status_categories($pdo);
|
||||
$automaticChecks = 0;
|
||||
$pendingChecks = 0;
|
||||
foreach ($components as $component) {
|
||||
if (empty($component['is_enabled'])) continue;
|
||||
if (status_component_monitor_target($pdo, $component) !== null) $automaticChecks++;
|
||||
elseif (($component['monitor_type'] ?? 'auto') === 'auto') $pendingChecks++;
|
||||
}
|
||||
$incidents = status_incidents($pdo, 60);
|
||||
$incidents = status_incidents($pdo, 60, false, true);
|
||||
$maintenanceWindows = status_maintenance_windows($pdo, true);
|
||||
$states = status_component_states();
|
||||
$monitorTypes = status_monitor_types();
|
||||
$checkIntervals = [1 => 'Jede Minute', 2 => 'Alle 2 Minuten', 5 => 'Alle 5 Minuten', 10 => 'Alle 10 Minuten', 15 => 'Alle 15 Minuten', 30 => 'Alle 30 Minuten', 60 => 'Jede Stunde'];
|
||||
$incStates = status_incident_states();
|
||||
$pageTitle = 'Statusseite';
|
||||
$activePage = 'status_page';
|
||||
@@ -87,11 +104,22 @@ require __DIR__ . '/_header.php';
|
||||
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
|
||||
<p class="muted">Eine öffentliche Seite, auf der Ihre Kunden sehen, ob alles läuft, und über Störungen und Wartungen informiert werden.
|
||||
<?php if (status_page_enabled()): ?>Adresse: <a href="/status.php" target="_blank"><?= e(base_url()) ?>/status.php</a><?php endif; ?></p>
|
||||
<style>
|
||||
.status-tabs{display:flex;gap:8px;margin:18px 0 12px;border-bottom:1px solid var(--border,#d8e0e8)}
|
||||
.status-tab-button{border:0;border-bottom:3px solid transparent;border-radius:7px 7px 0 0;background:transparent;color:var(--muted,#64748b);padding:10px 16px;font-weight:700;cursor:pointer}
|
||||
.status-tab-button[aria-selected="true"]{color:var(--text,#0f172a);border-bottom-color:var(--primary,#0f766e);background:var(--surface,#fff)}
|
||||
.status-tab-panel[hidden]{display:none!important}
|
||||
</style>
|
||||
<div class="status-tabs" role="tablist" aria-label="Statusseiten-Verwaltung">
|
||||
<button type="button" class="status-tab-button" id="status-tab-services" role="tab" aria-controls="status-panel-services" aria-selected="true">Dienste</button>
|
||||
<button type="button" class="status-tab-button" id="status-tab-maintenance" role="tab" aria-controls="status-panel-maintenance" aria-selected="false">Wartungen & Meldungen</button>
|
||||
</div>
|
||||
<section class="status-tab-panel" id="status-panel-services" role="tabpanel" aria-labelledby="status-tab-services">
|
||||
<div class="card" style="margin-bottom:16px;padding:12px 16px">
|
||||
<strong><?= $automaticChecks ?> Dienst<?= $automaticChecks === 1 ? '' : 'e' ?> werden automatisch geprüft</strong>
|
||||
<?php if ($pendingChecks): ?><span class="muted"> · <?= $pendingChecks ?> <?= $pendingChecks === 1 ? 'Dienst wartet' : 'Dienste warten' ?> auf ein Prüfziel</span><?php endif; ?>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:6px">
|
||||
<span class="muted" style="font-size:.8rem">DeskHub erkennt Datenbank, API, konfigurierte Mailserver, Push-Zugang sowie Android- und Windows-Verbindungen automatisch. Android-Apps gelten bei einem API-Kontakt innerhalb der letzten 60 Minuten, Windows-Apps innerhalb der letzten 10 Minuten als verbunden. Statusprüfungen laufen jede Minute; externe Webseiten und weitere Apps benötigen eine Prüf-URL.</span>
|
||||
<span class="muted" style="font-size:.8rem">DeskHub erkennt Datenbank, API, konfigurierte Mailserver, Push-Zugang sowie Android- und Windows-Verbindungen automatisch. Android-Apps gelten bei einem API-Kontakt innerhalb der letzten 60 Minuten, Windows-Apps innerhalb der letzten 10 Minuten als verbunden. Der Zeitgeber prüft jede Minute, einzelne Dienste können aber mit einem längeren Intervall geprüft werden.</span>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="auto_discover"><button class="btn secondary" type="submit">Jetzt erkennen und prüfen</button></form>
|
||||
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_restore_core"><button class="btn" type="submit">DeskHub-Dienste automatisch hinzufügen</button></form>
|
||||
@@ -120,12 +148,22 @@ require __DIR__ . '/_header.php';
|
||||
.status-component-add{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:0!important;padding:0!important;border:0!important}
|
||||
.status-component-add input,.status-component-add select{width:100%;min-width:0;box-sizing:border-box}
|
||||
.status-component-add button{grid-column:1/-1;justify-self:start}
|
||||
.status-monitor-form{display:grid;grid-template-columns:minmax(160px,210px) minmax(220px,1fr) minmax(145px,175px) auto;gap:8px;align-items:end;padding:10px 0}
|
||||
.status-monitor-form>div{min-width:0}
|
||||
.status-monitor-form label{display:block;margin:0 0 6px}
|
||||
.status-monitor-form input,.status-monitor-form select{box-sizing:border-box;width:100%;min-height:40px}
|
||||
.status-monitor-form>button{grid-column:4;grid-row:1;min-height:40px;white-space:nowrap}
|
||||
.status-monitor-help{grid-column:2;margin-top:-4px}
|
||||
.status-minecraft-row{flex-wrap:nowrap!important;gap:10px!important}
|
||||
.status-minecraft-row>form:last-child{margin-left:auto}
|
||||
@media(max-width:650px){.status-monitor-form{grid-template-columns:1fr}.status-monitor-form>button{grid-column:1;grid-row:auto}.status-monitor-help{grid-column:1}}
|
||||
@media(max-width:900px){.status-minecraft-row{flex-wrap:wrap!important}}
|
||||
@media(max-width:420px){.status-category-row{grid-template-columns:minmax(0,1fr) 94px 30px;gap:5px}}
|
||||
</style>
|
||||
<div class="status-admin-layout">
|
||||
<section class="card">
|
||||
<h3>Dienste & automatische Prüfungen</h3>
|
||||
<p class="muted">Automatisch geprüfte Dienste erhalten ihren Status selbst. Während geplanter Arbeiten kannst du sie auf Wartung setzen. Für nicht erkannte externe Dienste ist eine Prüf-URL nötig.</p>
|
||||
<p class="muted">Automatisch geprüfte Dienste erhalten ihren Status selbst. Jeder Dienst lässt sich vollständig deaktivieren; er wird dann auf der öffentlichen Seite ausgeblendet und nicht mehr geprüft.</p>
|
||||
<div style="margin:14px 0;padding:12px;border:1px solid var(--border,#e3e9ee);border-radius:8px;background:var(--bg-soft,#f8fafc)">
|
||||
<strong style="display:block;margin-bottom:8px">Eigenen Dienst hinzufügen</strong>
|
||||
<form method="post" class="status-component-add"><?= csrf_field() ?><input type="hidden" name="action" value="component_save">
|
||||
@@ -137,20 +175,22 @@ require __DIR__ . '/_header.php';
|
||||
</div>
|
||||
<?php if (!$components): ?><p class="muted">Noch keine Dienste. Legen Sie unten zum Beispiel Webseite, E-Mail oder Telefon an.</p><?php endif; ?>
|
||||
<?php foreach ($components as $c): $st = $states[$c['status']] ?? $states['operational']; ?>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:8px 0;border-top:1px solid var(--border,#e3e9ee)">
|
||||
<div class="<?= str_contains(mb_strtolower((string) $c['name']), 'minecraft') ? 'status-minecraft-row' : '' ?>" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:8px 0;border-top:1px solid var(--border,#e3e9ee);<?= empty($c['is_enabled']) ? 'opacity:.58' : '' ?>">
|
||||
<?php
|
||||
$checkInfo = status_component_monitor_target($pdo, $c);
|
||||
$selectedMonitorType = ($c['monitor_type'] ?? 'auto') === 'auto' && $checkInfo ? $checkInfo['type'] : (string) ($c['monitor_type'] ?? 'auto');
|
||||
$selectedMonitorType = ($c['monitor_type'] ?? 'auto') === 'auto' && $checkInfo ? ($checkInfo['type'] === 'minecraft' ? 'tcp' : $checkInfo['type']) : (string) ($c['monitor_type'] ?? 'auto');
|
||||
$isMinecraftComponent = str_contains(mb_strtolower((string) $c['name']), 'minecraft');
|
||||
$monitorTargetValue = trim((string) ($c['monitor_target'] ?? ''));
|
||||
if ($monitorTargetValue === '' && $checkInfo && $checkInfo['type'] === 'http') $monitorTargetValue = (string) $checkInfo['target'];
|
||||
$showsDetectedTargets = $checkInfo && in_array($checkInfo['type'], ['database', 'mail', 'fcm', 'android_app', 'windows_app'], true);
|
||||
$detectedTargets = $showsDetectedTargets ? status_component_detected_targets($pdo, $checkInfo) : [];
|
||||
?>
|
||||
<div style="flex:1;min-width:180px"><strong><?= e($c['name']) ?></strong><?php if ($c['description']): ?><div class="muted" style="font-size:.8rem"><?= e($c['description']) ?></div><?php endif; ?>
|
||||
<div class="muted" style="font-size:.76rem;margin-top:3px"><?php if ($checkInfo): ?>Automatische Prüfung: <?= e($checkInfo['label']) ?> · <?= $c['last_checked_at'] ? 'zuletzt ' . e(format_dt((string) $c['last_checked_at'])) : 'wartet auf ersten Lauf' ?><?php elseif (($c['monitor_type'] ?? 'auto') === 'manual'): ?>Manuell verwaltet<?php else: ?>Prüfziel fehlt – wartet auf Zuordnung<?php endif; ?><?php if (!empty($c['last_check_ms'])): ?> · <?= (int) $c['last_check_ms'] ?> ms<?php endif; ?></div>
|
||||
<div style="flex:<?= str_contains(mb_strtolower((string) $c['name']), 'minecraft') ? '0 0 249px' : '1' ?>;min-width:180px"><strong><?= e($c['name']) ?></strong><?php if ($c['description']): ?><div class="muted" style="font-size:.8rem"><?= e($c['description']) ?></div><?php endif; ?><?php if (empty($c['is_enabled'])): ?><div class="muted" style="font-size:.75rem">Deaktiviert – nicht öffentlich sichtbar und wird nicht geprüft</div><?php endif; ?>
|
||||
<div class="muted" style="font-size:.76rem;margin-top:3px"><?php if ($checkInfo): ?>Automatische Prüfung: <?= e($checkInfo['label']) ?> · <?= $c['last_checked_at'] ? 'zuletzt ' . e(format_dt((string) $c['last_checked_at'])) : 'wartet auf ersten Lauf' ?> · alle <?= max(1, (int) ($c['check_interval_minutes'] ?? 1)) ?> Min.<?php elseif (($c['monitor_type'] ?? 'auto') === 'manual'): ?>Manuell verwaltet<?php else: ?>Prüfziel fehlt – wartet auf Zuordnung<?php endif; ?><?php if (!empty($c['last_check_ms'])): ?> · <?= (int) $c['last_check_ms'] ?> ms<?php endif; ?></div>
|
||||
<?php if (!empty($c['last_check_error'])): ?><div style="color:#b91c1c;font-size:.76rem"><?= e((string) $c['last_check_error']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<form method="post" style="display:flex;gap:5px;align-items:center"><?= csrf_field() ?><input type="hidden" name="action" value="component_category"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><select name="category_id" aria-label="Kategorie für <?= e($c['name']) ?>" style="max-width:170px;padding:5px"><option value="">Ohne Kategorie</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>" <?= (int) ($c['category_id'] ?? 0) === (int) $category['id'] ? 'selected' : '' ?>><?= e($category['name']) ?></option><?php endforeach; ?></select><input type="number" name="sort_order" value="<?= (int) $c['sort_order'] ?>" title="Reihenfolge" aria-label="Reihenfolge für <?= e($c['name']) ?>" style="width:58px;padding:4px"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem">Speichern</button></form>
|
||||
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_enabled"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><input type="hidden" name="enabled" value="<?= empty($c['is_enabled']) ? '1' : '0' ?>"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem"><?= empty($c['is_enabled']) ? 'Aktivieren' : 'Deaktivieren' ?></button></form>
|
||||
<?php if ($checkInfo): ?>
|
||||
<div style="display:flex;gap:8px;align-items:center;white-space:nowrap">
|
||||
<span style="color:<?= e($st[1]) ?>;font-weight:600"><?= e($st[0]) ?></span>
|
||||
@@ -164,13 +204,15 @@ require __DIR__ . '/_header.php';
|
||||
</div>
|
||||
<details style="padding:0 0 10px 0;border-bottom:1px solid var(--border,#e3e9ee)">
|
||||
<summary style="cursor:pointer;color:var(--muted,#64748b);font-size:.82rem">Prüfung konfigurieren</summary>
|
||||
<form method="post" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;padding:10px 0"><?= csrf_field() ?><input type="hidden" name="action" value="component_monitor"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>">
|
||||
<form method="post" class="status-monitor-form"><?= csrf_field() ?><input type="hidden" name="action" value="component_monitor"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>">
|
||||
<div><label>Prüfart</label><select name="monitor_type"><?php foreach ($monitorTypes as $type => $label): ?><option value="<?= e($type) ?>" <?= $selectedMonitorType === $type ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select></div>
|
||||
<?php if ($showsDetectedTargets): ?>
|
||||
<div style="flex:1;min-width:220px"><label>Automatisch erkannte Prüfziele</label><div style="min-height:38px;box-sizing:border-box;padding:9px 12px;border:1px solid var(--border,#d8e0e8);border-radius:8px;background:var(--surface,#f5f7f9)"><?php if ($detectedTargets): ?><?= e(implode(' · ', $detectedTargets)) ?><?php else: ?>Keine passenden Ziele in der Dienst-Konfiguration gefunden<?php endif; ?></div></div>
|
||||
<?php else: ?>
|
||||
<div style="flex:1;min-width:220px"><label>Prüf-URL oder Host:Port</label><input type="text" name="monitor_target" value="<?= e($monitorTargetValue) ?>" placeholder="https://example.com"></div>
|
||||
<div><label><?= $selectedMonitorType === 'tcp' ? ($isMinecraftComponent ? 'IP-Adresse oder Domain (Port optional)' : 'IP-Adresse oder Domain mit Port') : 'Prüf-URL' ?></label><input type="text" name="monitor_target" value="<?= e($monitorTargetValue) ?>" placeholder="<?= $selectedMonitorType === 'tcp' ? ($isMinecraftComponent ? 'mc.viper.ipv64.net' : 'play.example.de:25565 oder 203.0.113.10:25565') : 'https://example.com' ?>"></div>
|
||||
<?php endif; ?>
|
||||
<div><label>Prüfintervall</label><select name="check_interval_minutes"><?php foreach ($checkIntervals as $minutes => $intervalLabel): ?><option value="<?= (int) $minutes ?>" <?= (int) ($c['check_interval_minutes'] ?? 1) === $minutes ? 'selected' : '' ?>><?= e($intervalLabel) ?></option><?php endforeach; ?></select></div>
|
||||
<?php if ($selectedMonitorType === 'tcp'): ?><span class="status-monitor-help muted" style="font-size:.75rem"><?= $isMinecraftComponent ? 'Ohne Port: Minecraft-SRV, sonst 25565. IPv6: [2001:db8::1] oder [2001:db8::1]:25565.' : 'IPv6: [2001:db8::1]:25565' ?></span><?php endif; ?>
|
||||
<button class="btn secondary" type="submit">Prüfung speichern</button>
|
||||
</form>
|
||||
</details>
|
||||
@@ -192,6 +234,36 @@ require __DIR__ . '/_header.php';
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<section class="status-tab-panel" id="status-panel-maintenance" role="tabpanel" aria-labelledby="status-tab-maintenance" hidden>
|
||||
|
||||
<details class="card" style="max-width:820px;margin-bottom:16px">
|
||||
<summary style="cursor:pointer;font-weight:700">Wartungsfenster planen</summary>
|
||||
<p class="muted" style="margin:8px 0">Kündigt eine geplante Wartung mit Start- und Endzeit auf der öffentlichen Statusseite an. Während des Zeitraums wird der betroffene Dienst als „Wartung“ angezeigt.</p>
|
||||
<form method="post" style="margin-top:12px"><?= csrf_field() ?><input type="hidden" name="action" value="maintenance_create">
|
||||
<label for="maintenance_title">Titel</label><input type="text" id="maintenance_title" name="title" maxlength="200" required placeholder="z. B. Server- und Datenbankwartung">
|
||||
<label for="maintenance_body" style="margin-top:8px">Beschreibung (optional)</label><textarea id="maintenance_body" name="body" rows="3" maxlength="4000" placeholder="Welche Arbeiten sind geplant und welche Auswirkungen gibt es?"></textarea>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin-top:8px">
|
||||
<div><label for="maintenance_starts_at">Startzeit</label><input type="datetime-local" id="maintenance_starts_at" name="starts_at" required></div>
|
||||
<div><label for="maintenance_ends_at">Endzeit</label><input type="datetime-local" id="maintenance_ends_at" name="ends_at" required></div>
|
||||
<div><label for="maintenance_component">Betrifft</label><select id="maintenance_component" name="component_id"><option value="">Gesamte Plattform</option><?php foreach ($components as $c): if (empty($c['is_enabled'])) continue; ?><option value="<?= (int) $c['id'] ?>"><?= e($c['name']) ?></option><?php endforeach; ?></select></div>
|
||||
</div>
|
||||
<div class="btn-row"><button type="submit" class="btn">Wartung planen</button></div>
|
||||
</form>
|
||||
<?php if ($maintenanceWindows): ?><h3 style="margin-top:18px">Geplante und kürzliche Wartungen</h3>
|
||||
<?php foreach ($maintenanceWindows as $window):
|
||||
$windowActive = empty($window['cancelled_at']) && strtotime((string) $window['starts_at']) <= time() && strtotime((string) $window['ends_at']) >= time();
|
||||
$windowFuture = empty($window['cancelled_at']) && strtotime((string) $window['starts_at']) > time();
|
||||
?>
|
||||
<div style="padding:10px 0;border-top:1px solid var(--border,#e3e9ee)">
|
||||
<strong><?= e($window['title']) ?></strong> <span class="muted" style="font-size:.8rem">· <?= e($window['cancelled_at'] ? 'Abgesagt' : ($windowActive ? 'Läuft jetzt' : ($windowFuture ? 'Geplant' : 'Beendet'))) ?> · <?= e(format_dt((string) $window['starts_at'])) ?>–<?= e(format_dt((string) $window['ends_at'])) ?><?= $window['component_name'] ? ' · ' . e($window['component_name']) : ' · Gesamte Plattform' ?></span>
|
||||
<?php if (!empty($window['body'])): ?><div class="muted" style="font-size:.85rem;margin-top:3px"><?= nl2br(e((string) $window['body'])) ?></div><?php endif; ?>
|
||||
<?php if ($windowFuture || $windowActive): ?><form method="post" onsubmit="return confirm('Wartungsfenster absagen?')" style="margin-top:5px"><?= csrf_field() ?><input type="hidden" name="action" value="maintenance_cancel"><input type="hidden" name="id" value="<?= (int) $window['id'] ?>"><button type="submit" class="btn secondary" style="padding:2px 10px;font-size:.76rem">Absagen</button></form><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
|
||||
<details class="card" style="max-width:820px;margin-bottom:16px">
|
||||
<summary style="cursor:pointer;font-weight:700">Störung oder Wartung melden</summary>
|
||||
<form method="post" style="margin-top:12px"><?= csrf_field() ?><input type="hidden" name="action" value="incident_create">
|
||||
@@ -225,4 +297,33 @@ require __DIR__ . '/_header.php';
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</details>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
const tabs = Array.from(document.querySelectorAll('.status-tab-button'));
|
||||
const panels = Array.from(document.querySelectorAll('.status-tab-panel'));
|
||||
const storageKey = 'deskhub-status-admin-tab';
|
||||
function selectTab(name, save = true) {
|
||||
for (const tab of tabs) {
|
||||
const active = tab.id === 'status-tab-' + name;
|
||||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
tab.tabIndex = active ? 0 : -1;
|
||||
}
|
||||
for (const panel of panels) panel.hidden = panel.id !== 'status-panel-' + name;
|
||||
if (save) { try { localStorage.setItem(storageKey, name); } catch (_) {} }
|
||||
}
|
||||
let saved = 'services';
|
||||
try { saved = localStorage.getItem(storageKey) || saved; } catch (_) {}
|
||||
if (!['services', 'maintenance'].includes(saved)) saved = 'services';
|
||||
selectTab(saved, false);
|
||||
for (const tab of tabs) tab.addEventListener('click', () => selectTab(tab.id === 'status-tab-maintenance' ? 'maintenance' : 'services'));
|
||||
for (const tab of tabs) tab.addEventListener('keydown', event => {
|
||||
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
const next = tabs[(tabs.indexOf(tab) + (event.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length];
|
||||
next.focus();
|
||||
next.click();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php require __DIR__ . '/_footer.php'; ?>
|
||||
|
||||
@@ -86,6 +86,9 @@ $myWorkSessionStartedAt = $myWorkSessionStmt->fetchColumn() ?: null;
|
||||
$collabStmt = $pdo->prepare('SELECT * FROM ticket_collaborators WHERE ticket_id = ? ORDER BY created_at');
|
||||
$collabStmt->execute([$ticket['id']]);
|
||||
$collaborators = $collabStmt->fetchAll();
|
||||
$ticketProblems = ticket_problems_for_ticket($pdo, (int) $ticket['id']);
|
||||
$openProblems = ticket_problems($pdo, true);
|
||||
$resolutionReasons = ticket_resolution_reasons();
|
||||
|
||||
$departments = departments_for_select($pdo, [(int) $ticket['department_id']]);
|
||||
$priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetchAll();
|
||||
@@ -164,16 +167,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
if ($banEmail) {
|
||||
$priorStmt = $pdo->prepare("SELECT id,status FROM tickets WHERE requester_email=? AND status NOT IN ('closed')");
|
||||
$priorStmt->execute([$banEmail]); $priorTickets = $priorStmt->fetchAll();
|
||||
$affected = $pdo->prepare(
|
||||
"UPDATE tickets SET status='closed', closed_at=?, updated_at=? WHERE requester_email=? AND status NOT IN ('closed')"
|
||||
"UPDATE tickets SET status='closed', resolution_code='spam', resolution_note='Absender gesperrt', closed_at=?, updated_at=? WHERE requester_email=? AND status NOT IN ('closed')"
|
||||
);
|
||||
$affected->execute([db_now(), db_now(), $banEmail]);
|
||||
ticket_sla_transition_rows($pdo, $priorTickets, 'closed', (int) $staff['id']);
|
||||
}
|
||||
if ($banDomain) {
|
||||
$priorStmt = $pdo->prepare("SELECT id,status FROM tickets WHERE " . sql_email_domain("requester_email") . "=? AND status NOT IN ('closed')");
|
||||
$priorStmt->execute([$banDomain]); $priorTickets = $priorStmt->fetchAll();
|
||||
$affected2 = $pdo->prepare(
|
||||
"UPDATE tickets SET status='closed', closed_at=?, updated_at=? WHERE " . sql_email_domain("requester_email") . "=? AND status NOT IN ('closed')"
|
||||
"UPDATE tickets SET status='closed', resolution_code='spam', resolution_note='Domain gesperrt', closed_at=?, updated_at=? WHERE " . sql_email_domain("requester_email") . "=? AND status NOT IN ('closed')"
|
||||
);
|
||||
$affected2->execute([db_now(), db_now(), $banDomain]);
|
||||
ticket_sla_transition_rows($pdo, $priorTickets, 'closed', (int) $staff['id']);
|
||||
}
|
||||
flash('success', 'Gesperrt und alle zugehörigen Tickets geschlossen.');
|
||||
redirect('/staff/ticket_view.php?n=' . urlencode($ticket['ticket_number']));
|
||||
@@ -235,6 +244,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$errors[] = 'Bitte geben Sie eine Nachricht ein.';
|
||||
} else {
|
||||
$isNote = $action === 'note' ? 1 : 0;
|
||||
$newStatus = !$isNote ? (string) ($_POST['set_status'] ?? 'answered') : (string) $ticket['status'];
|
||||
$resolutionCode = (string) ($_POST['resolution_code'] ?? '');
|
||||
$resolutionNote = trim(mb_substr((string) ($_POST['resolution_note'] ?? ''), 0, 2000));
|
||||
if (!$isNote && in_array($newStatus, ['resolved', 'closed'], true)) {
|
||||
$requirePerm('close_ticket');
|
||||
if (!isset(ticket_resolution_reasons()[$resolutionCode])) { flash('error', 'Bitte beim Schließen einen Abschlussgrund auswählen.'); redirect('/staff/ticket_view.php?n=' . urlencode($number)); }
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at)
|
||||
VALUES (?,?,?,?,?,?)'
|
||||
@@ -255,24 +271,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
if (!$isNote) {
|
||||
$newStatus = $_POST['set_status'] ?: 'answered';
|
||||
$updateFirstResponse = $ticket['first_responded_at'] === null;
|
||||
|
||||
// Auto-Zuweisung an antwortenden Mitarbeiter, falls Ticket noch niemandem zugewiesen ist
|
||||
$autoAssign = ($ticket['assigned_staff_id'] === null || $ticket['assigned_staff_id'] === '');
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE tickets SET status = ?, updated_at = ?' .
|
||||
($updateFirstResponse ? ', first_responded_at = ?' : '') .
|
||||
($autoAssign ? ', assigned_staff_id = ?' : '') .
|
||||
' WHERE id = ?'
|
||||
);
|
||||
$args = [];
|
||||
$args[] = $newStatus;
|
||||
$args[] = $now;
|
||||
if ($updateFirstResponse) $args[] = $now;
|
||||
if ($autoAssign) $args[] = $staff['id'];
|
||||
$args[] = $ticket['id'];
|
||||
$stmt->execute($args);
|
||||
$resolvedAt = $newStatus === 'resolved' && empty($ticket['resolved_at']) ? $now : $ticket['resolved_at'];
|
||||
$closedAt = $newStatus === 'closed' && empty($ticket['closed_at']) ? $now : $ticket['closed_at'];
|
||||
$stmt = $pdo->prepare('UPDATE tickets SET status = ?, updated_at = ?, first_responded_at = COALESCE(first_responded_at, ?), assigned_staff_id = COALESCE(assigned_staff_id, ?), resolved_at = ?, closed_at = ?, resolution_code = ?, resolution_note = ? WHERE id = ?');
|
||||
$stmt->execute([
|
||||
$newStatus, $now, $now, (int) $staff['id'], $resolvedAt, $closedAt,
|
||||
in_array($newStatus, ['resolved', 'closed'], true) ? $resolutionCode : $ticket['resolution_code'],
|
||||
in_array($newStatus, ['resolved', 'closed'], true) ? ($resolutionNote !== '' ? $resolutionNote : $ticket['resolution_note']) : $ticket['resolution_note'],
|
||||
$ticket['id'],
|
||||
]);
|
||||
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], $newStatus, (int) $staff['id']);
|
||||
if ($autoAssign) {
|
||||
log_activity($pdo, $ticket['id'], $staff['id'], 'Automatisch zugewiesen (erste Antwort)');
|
||||
}
|
||||
@@ -491,14 +503,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
)->execute([$dst['id'], 'staff', $staff['id'], $note, $now]);
|
||||
|
||||
// Quell-Ticket schließen + merged_into setzen
|
||||
$pdo->prepare('UPDATE tickets SET status = ?, merged_into = ?, updated_at = ? WHERE id = ?')
|
||||
->execute(['closed', $dst['id'], $now, $src['id']]);
|
||||
$pdo->prepare('UPDATE tickets SET status = ?, merged_into = ?, resolution_code = ?, resolution_note = ?, updated_at = ? WHERE id = ?')
|
||||
->execute(['closed', $dst['id'], 'duplicate', 'Zusammengeführt mit Ticket #' . $dst['ticket_number'], $now, $src['id']]);
|
||||
|
||||
// Ziel-Ticket aktualisieren (updated_at)
|
||||
$pdo->prepare('UPDATE tickets SET updated_at = ? WHERE id = ?')
|
||||
->execute([$now, $dst['id']]);
|
||||
|
||||
$pdo->commit();
|
||||
ticket_sla_status_transition($pdo, (int) $src['id'], (string) $src['status'], 'closed', (int) $staff['id']);
|
||||
log_activity($pdo, $dst['id'], $staff['id'], "Ticket #{$src['ticket_number']} hierher zusammengeführt");
|
||||
log_activity($pdo, $src['id'], $staff['id'], "Zusammengeführt mit Ticket #{$dst['ticket_number']}");
|
||||
audit_log($staff['id'], "Merge: #{$src['ticket_number']} → #{$dst['ticket_number']}");
|
||||
@@ -512,6 +525,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
redirect('/staff/ticket_view.php?n=' . urlencode($number));
|
||||
}
|
||||
|
||||
if ($action === 'problem_link') {
|
||||
if ($staff['role'] !== 'admin') { flash('error', 'Nur Administratoren dürfen Tickets mit Problemen verknüpfen.'); redirect('/staff/ticket_view.php?n=' . urlencode($number)); }
|
||||
$problemIdToLink = (int) ($_POST['problem_id'] ?? 0);
|
||||
if (!ticket_problem_link($pdo, $problemIdToLink, (int) $ticket['id'], (int) $staff['id'])) flash('error', 'Problem konnte nicht gefunden werden.');
|
||||
else { log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Mit ' . ticket_problem_number($problemIdToLink) . ' verknüpft'); flash('success', 'Ticket mit Problem verknüpft.'); }
|
||||
redirect('/staff/ticket_view.php?n=' . urlencode($number));
|
||||
}
|
||||
|
||||
if ($action === 'problem_unlink') {
|
||||
if ($staff['role'] !== 'admin') { flash('error', 'Nur Administratoren dürfen Problem-Verknüpfungen ändern.'); redirect('/staff/ticket_view.php?n=' . urlencode($number)); }
|
||||
$problemIdToUnlink = (int) ($_POST['problem_id'] ?? 0);
|
||||
ticket_problem_unlink($pdo, $problemIdToUnlink, (int) $ticket['id']);
|
||||
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Problemverknüpfung ' . ticket_problem_number($problemIdToUnlink) . ' entfernt');
|
||||
flash('success', 'Ticket vom Problem getrennt.');
|
||||
redirect('/staff/ticket_view.php?n=' . urlencode($number));
|
||||
}
|
||||
|
||||
if ($action === 'update_props') {
|
||||
$deptId = (int) $_POST['department_id'];
|
||||
if ($myDeptIds !== null && !in_array($deptId, $myDeptIds, true) && $deptId !== (int) $ticket['department_id']) {
|
||||
@@ -527,10 +557,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($assignedTo !== ($ticket['assigned_staff_id'] === null ? null : (int) $ticket['assigned_staff_id']) || $teamId !== ($ticket['team_id'] === null ? null : (int) $ticket['team_id'])) $requirePerm('assign_ticket');
|
||||
if ($status !== $ticket['status'] && in_array($status, ['resolved', 'closed'], true)) $requirePerm('close_ticket');
|
||||
|
||||
$resolutionCode = (string) ($_POST['resolution_code'] ?? $ticket['resolution_code'] ?? '');
|
||||
$resolutionNote = trim(mb_substr((string) ($_POST['resolution_note'] ?? $ticket['resolution_note'] ?? ''), 0, 2000));
|
||||
if ($status !== $ticket['status'] && in_array($status, ['resolved', 'closed'], true)) {
|
||||
if (!isset(ticket_resolution_reasons()[$resolutionCode])) { flash('error', 'Bitte beim Schließen einen Abschlussgrund auswählen.'); redirect('/staff/ticket_view.php?n=' . urlencode($number)); }
|
||||
}
|
||||
|
||||
$prio = null;
|
||||
foreach ($priorities as $p) { if ($p['id'] == $prioId) $prio = $p; }
|
||||
|
||||
$sla = $prio ? calc_sla_dates($prio, $ticket['created_at'], $deptId, !empty($ticket['help_topic_id']) ? (int) $ticket['help_topic_id'] : null) : null;
|
||||
$slaChanged = $deptId !== (int) $ticket['department_id'] || $prioId !== (int) $ticket['priority_id'];
|
||||
$sla = $prio && $slaChanged ? calc_sla_dates($prio, $ticket['created_at'], $deptId, !empty($ticket['help_topic_id']) ? (int) $ticket['help_topic_id'] : null) : null;
|
||||
if ($sla) {
|
||||
$priorPauses = ticket_sla_completed_pause_seconds($pdo, (int) $ticket['id']);
|
||||
foreach (['first_response_due_at', 'resolution_due_at'] as $deadline) if (!empty($sla[$deadline]) && $priorPauses > 0) $sla[$deadline] = ticket_sla_shift_deadline($sla[$deadline], $priorPauses);
|
||||
if (!empty($sla['first_response_due_at'])) ticket_sla_history_add($pdo, (int) $ticket['id'], 'adjusted', 'first_response', 'Neue Frist: ' . $sla['first_response_due_at'], (int) $staff['id']);
|
||||
if (!empty($sla['resolution_due_at'])) ticket_sla_history_add($pdo, (int) $ticket['id'], 'adjusted', 'resolution', 'Neue Frist: ' . $sla['resolution_due_at'], (int) $staff['id']);
|
||||
}
|
||||
|
||||
$resolvedAt = $ticket['resolved_at'];
|
||||
$closedAt = $ticket['closed_at'];
|
||||
@@ -539,14 +582,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE tickets SET department_id=?, priority_id=?, status=?, assigned_staff_id=?, team_id=?,
|
||||
first_response_due_at=?, resolution_due_at=?, resolved_at=?, closed_at=?, updated_at=? WHERE id=?'
|
||||
first_response_due_at=?, resolution_due_at=?, resolved_at=?, closed_at=?, resolution_code=?, resolution_note=?, updated_at=? WHERE id=?'
|
||||
);
|
||||
$stmt->execute([
|
||||
$deptId, $prioId, $status, $assignedTo, $teamId,
|
||||
$sla['first_response_due_at'] ?? $ticket['first_response_due_at'],
|
||||
$sla['resolution_due_at'] ?? $ticket['resolution_due_at'],
|
||||
$resolvedAt, $closedAt, $now, $ticket['id'],
|
||||
$resolvedAt, $closedAt,
|
||||
in_array($status, ['resolved', 'closed'], true) ? ($resolutionCode ?: $ticket['resolution_code']) : $ticket['resolution_code'],
|
||||
in_array($status, ['resolved', 'closed'], true) ? ($resolutionNote ?: $ticket['resolution_note']) : $ticket['resolution_note'],
|
||||
$now, $ticket['id'],
|
||||
]);
|
||||
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], $status, (int) $staff['id']);
|
||||
|
||||
if ($assignedTo !== (int) ($ticket['assigned_staff_id'] ?? 0) && $assignedTo !== null) {
|
||||
$agentName = '';
|
||||
@@ -648,7 +695,9 @@ $pageTitle = "Ticket #{$ticket['ticket_number']}";
|
||||
$activePage = 'tickets';
|
||||
require __DIR__ . '/_header.php';
|
||||
[$statusLabel, $statusColor] = status_label($ticket['status']);
|
||||
$isOverdue = $ticket['resolution_due_at'] && strtotime($ticket['resolution_due_at']) < time() && !in_array($ticket['status'], ['resolved','closed']);
|
||||
$slaPaused = !empty($ticket['sla_paused_at']) && ticket_sla_is_paused_status((string) $ticket['status']);
|
||||
$slaHistory = ticket_sla_history_for_ticket($pdo, $ticket);
|
||||
$isOverdue = !empty($ticket['is_overdue']) || (!$slaPaused && $ticket['resolution_due_at'] && strtotime($ticket['resolution_due_at']) < time() && !in_array($ticket['status'], ['resolved','closed']));
|
||||
// Bewertung laden
|
||||
$ticketRating = null;
|
||||
if (in_array($ticket['status'], ['resolved','closed'])) {
|
||||
@@ -1143,13 +1192,22 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
</div>
|
||||
<div>
|
||||
<label for="set_status">Status nach dem Senden</label>
|
||||
<select name="set_status" id="set_status">
|
||||
<select name="set_status" id="set_status" onchange="syncReplyResolutionFields()">
|
||||
<option value="answered">Beantwortet</option>
|
||||
<option value="pending">Wartet auf Kunde</option>
|
||||
<option value="resolved">Gelöst</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reply-resolution-fields" style="display:none;margin-top:10px;padding:10px;border:1px solid var(--border-soft);border-radius:8px">
|
||||
<label for="reply_resolution_code">Abschlussgrund <span style="color:var(--danger)">*</span></label>
|
||||
<select id="reply_resolution_code" name="resolution_code"><?php foreach ($resolutionReasons as $code => $label): ?><option value="<?= e($code) ?>" <?= $code === 'fixed' ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select>
|
||||
<label for="reply_resolution_note" style="margin-top:7px">Lösungshinweis (optional)</label><textarea id="reply_resolution_note" name="resolution_note" rows="2" maxlength="2000" placeholder="Kurze Notiz zur Lösung"></textarea>
|
||||
</div>
|
||||
<script>
|
||||
function syncReplyResolutionFields(){var status=document.getElementById('set_status'),box=document.getElementById('reply-resolution-fields'),reason=document.getElementById('reply_resolution_code');if(!status||!box||!reason)return;var required=['resolved','closed'].includes(status.value);box.style.display=required?'block':'none';reason.required=required;}
|
||||
syncReplyResolutionFields();
|
||||
</script>
|
||||
<div class="btn-row">
|
||||
<button type="submit" class="btn" onclick="document.getElementById('form-action').value='reply'">Öffentliche Antwort senden</button>
|
||||
<button type="submit" class="btn secondary" onclick="document.getElementById('form-action').value='note'">Nur interne Notiz speichern</button>
|
||||
@@ -1189,13 +1247,19 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
<?php foreach (['status'=>['Offen'=>'open','Wartet auf Kunde'=>'pending','Beantwortet'=>'answered','Pausiert'=>'on_hold','Gelöst'=>'resolved','Geschlossen'=>'closed']] as $k=>$opts): ?>
|
||||
<div>
|
||||
<label for="status" style="margin:0 0 4px; font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Status</label>
|
||||
<select name="status" id="status" style="font-size:.875rem; padding:7px 10px">
|
||||
<select name="status" id="status" onchange="syncTicketResolutionFields()" style="font-size:.875rem; padding:7px 10px">
|
||||
<?php foreach (['open'=>'Offen','pending'=>'Wartet auf Kunde','answered'=>'Beantwortet','on_hold'=>'Pausiert','resolved'=>'Gelöst','closed'=>'Geschlossen'] as $val=>$lbl): ?>
|
||||
<option value="<?= $val ?>" <?= $ticket['status']===$val?'selected':'' ?>><?= $lbl ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div id="ticket-resolution-fields">
|
||||
<label for="resolution_code" style="margin:0 0 4px;font-size:.71rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">Abschlussgrund</label>
|
||||
<select name="resolution_code" id="resolution_code" data-ticket-was-terminal="<?= in_array($ticket['status'], ['resolved', 'closed'], true) ? '1' : '0' ?>" style="font-size:.875rem;padding:7px 10px"><option value="" <?= empty($ticket['resolution_code']) ? 'selected' : '' ?>>Grund auswählen</option><?php foreach ($resolutionReasons as $code => $label): ?><option value="<?= e($code) ?>" <?= ($ticket['resolution_code'] ?? '') === $code ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select>
|
||||
<label for="resolution_note" style="margin:7px 0 4px;font-size:.71rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">Lösungshinweis (optional)</label>
|
||||
<input type="text" name="resolution_note" id="resolution_note" maxlength="2000" value="<?= e((string) ($ticket['resolution_note'] ?? '')) ?>" placeholder="Kurze Notiz zur Lösung">
|
||||
</div>
|
||||
<div>
|
||||
<label for="priority_id" style="margin:0 0 4px; font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Priorität</label>
|
||||
<select name="priority_id" id="priority_id" style="font-size:.875rem; padding:7px 10px">
|
||||
@@ -1236,25 +1300,49 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
</div>
|
||||
<button type="submit" class="btn" style="margin-top:12px">Speichern</button>
|
||||
</form>
|
||||
<script>
|
||||
function syncTicketResolutionFields(){var status=document.getElementById('status'),reason=document.getElementById('resolution_code');if(reason)reason.required=!!status&&['resolved','closed'].includes(status.value)&&reason.dataset.ticketWasTerminal!=='1';}
|
||||
syncTicketResolutionFields();
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:14px 20px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px"><span style="font-size:.71rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">Zugehörige Probleme</span><?php if ($staff['role'] === 'admin'): ?><a class="btn secondary" style="padding:3px 8px;font-size:.75rem" href="/staff/problems.php">Verwalten</a><?php endif; ?></div>
|
||||
<?php if ($ticketProblems): foreach ($ticketProblems as $problemItem): ?>
|
||||
<div style="padding:8px 0;border-top:1px solid var(--border-soft);margin-top:8px"><a href="/staff/problems.php?id=<?= (int) $problemItem['id'] ?>"><strong><?= e(ticket_problem_number((int) $problemItem['id'])) ?> · <?= e($problemItem['title']) ?></strong></a><div class="muted" style="font-size:.78rem;margin-top:3px"><?= e(ticket_problem_statuses()[$problemItem['status']] ?? $problemItem['status']) ?></div><?php if (!empty($problemItem['workaround'])): ?><div style="font-size:.82rem;margin-top:5px"><strong>Workaround:</strong> <?= nl2br(e((string) $problemItem['workaround'])) ?></div><?php endif; ?><?php if ($staff['role'] === 'admin'): ?><form method="post" style="margin-top:6px"><?= csrf_field() ?><input type="hidden" name="action" value="problem_unlink"><input type="hidden" name="problem_id" value="<?= (int) $problemItem['id'] ?>"><button class="btn secondary" type="submit" style="padding:2px 8px;font-size:.75rem">Trennen</button></form><?php endif; ?></div>
|
||||
<?php endforeach; else: ?><p class="muted" style="font-size:.84rem">Dieses Ticket ist keinem bekannten Problem zugeordnet.</p><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin' && $openProblems): ?><form method="post" style="display:flex;gap:6px;align-items:end;margin-top:8px"><?= csrf_field() ?><input type="hidden" name="action" value="problem_link"><div style="flex:1;min-width:150px"><label style="font-size:.75rem">Mit Problem verknüpfen</label><select name="problem_id" required><?php foreach ($openProblems as $problemOption): ?><option value="<?= (int) $problemOption['id'] ?>"><?= e(ticket_problem_number((int) $problemOption['id']) . ' · ' . $problemOption['title']) ?></option><?php endforeach; ?></select></div><button class="btn secondary" type="submit">Verknüpfen</button></form><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- SLA + Individuelle Felder -->
|
||||
<div class="card" style="padding:16px 20px">
|
||||
<div style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-bottom:10px">SLA-Fristen</div>
|
||||
<?php if ($slaPaused): ?><div style="padding:8px 10px;margin-bottom:12px;border-radius:7px;background:#f1f5f9;color:#475569;font-size:.82rem"><strong>SLA pausiert</strong> seit <?= e(format_dt((string) $ticket['sla_paused_at'])) ?> – die Fristen laufen bei „<?= e($statusLabel) ?>“ nicht weiter.</div><?php endif; ?>
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px">
|
||||
<div>
|
||||
<div style="font-size:.7rem; color:var(--muted-lt); margin-bottom:2px">Erstreaktion</div>
|
||||
<?php $responseOverdue = $ticket['first_response_due_at'] && !$ticket['first_responded_at'] && strtotime($ticket['first_response_due_at']) < time(); ?>
|
||||
<?php $responseOverdue = !$slaPaused && $ticket['first_response_due_at'] && !$ticket['first_responded_at'] && strtotime($ticket['first_response_due_at']) < time(); ?>
|
||||
<div style="font-size:.8rem; font-weight:700; color:<?= $responseOverdue ? 'var(--danger)' : 'var(--ink)' ?>"><?= format_dt($ticket['first_response_due_at']) ?></div>
|
||||
<div style="font-size:.74rem; color:<?= $responseOverdue ? 'var(--danger)' : 'var(--muted)' ?>"><?= $ticket['first_responded_at'] ? '✓ beantwortet' : time_until($ticket['first_response_due_at']) ?></div>
|
||||
<div style="font-size:.74rem; color:<?= $responseOverdue ? 'var(--danger)' : 'var(--muted)' ?>"><?= $ticket['first_responded_at'] ? '✓ beantwortet' : ($slaPaused ? 'Pausiert' : time_until($ticket['first_response_due_at'])) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:.7rem; color:var(--muted-lt); margin-bottom:2px">Lösung</div>
|
||||
<?php $resolutionOverdue = $ticket['resolution_due_at'] && !$ticket['resolved_at'] && strtotime($ticket['resolution_due_at']) < time(); ?>
|
||||
<?php $resolutionOverdue = !$slaPaused && $ticket['resolution_due_at'] && !$ticket['resolved_at'] && strtotime($ticket['resolution_due_at']) < time(); ?>
|
||||
<div style="font-size:.8rem; font-weight:700; color:<?= $resolutionOverdue ? 'var(--danger)' : 'var(--ink)' ?>"><?= format_dt($ticket['resolution_due_at']) ?></div>
|
||||
<div style="font-size:.74rem; color:<?= $resolutionOverdue ? 'var(--danger)' : 'var(--muted)' ?>"><?= $ticket['resolved_at'] ? '✓ gelöst' : time_until($ticket['resolution_due_at']) ?></div>
|
||||
<div style="font-size:.74rem; color:<?= $resolutionOverdue ? 'var(--danger)' : 'var(--muted)' ?>"><?= $ticket['resolved_at'] ? '✓ gelöst' : ($slaPaused ? 'Pausiert' : time_until($ticket['resolution_due_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<details style="margin-top:12px;border-top:1px solid var(--border-soft);padding-top:9px">
|
||||
<summary style="cursor:pointer;font-size:.8rem;font-weight:700">SLA-Verlauf</summary>
|
||||
<?php if ($slaHistory): ?><div style="display:grid;gap:6px;margin-top:8px">
|
||||
<?php foreach ($slaHistory as $slaEvent):
|
||||
$eventLabels = ['started' => 'Frist gestartet', 'adjusted' => 'Frist angepasst', 'paused' => 'Frist pausiert', 'resumed' => 'Frist fortgesetzt', 'overdue' => 'Frist überschritten'];
|
||||
$deadlineLabels = ['first_response' => 'Erstreaktion', 'resolution' => 'Lösung'];
|
||||
?>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;font-size:.76rem"><time class="muted" style="white-space:nowrap"><?= e(format_dt((string) $slaEvent['created_at'])) ?></time><strong><?= e($eventLabels[$slaEvent['event_type']] ?? $slaEvent['event_type']) ?><?= !empty($slaEvent['deadline_type']) ? ' · ' . e($deadlineLabels[$slaEvent['deadline_type']] ?? $slaEvent['deadline_type']) : '' ?></strong><span class="muted"><?= e((string) ($slaEvent['details'] ?? '')) ?><?= !empty($slaEvent['staff_name']) ? ' · ' . e($slaEvent['staff_name']) : '' ?></span></div>
|
||||
<?php endforeach; ?>
|
||||
</div><?php else: ?><p class="muted" style="font-size:.78rem">Noch keine SLA-Ereignisse erfasst.</p><?php endif; ?>
|
||||
</details>
|
||||
<?php if ($customFieldValues): ?>
|
||||
<div style="border-top:1px solid var(--border-soft); margin-top:12px; padding-top:12px">
|
||||
<div style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-bottom:10px">Weitere Felder</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ $priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetc
|
||||
$agents = $pdo->query('SELECT id, name FROM staff WHERE is_active = 1 ORDER BY name')->fetchAll();
|
||||
|
||||
$bulkErrors = [];
|
||||
$resolutionReasons = ticket_resolution_reasons();
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Gespeicherte Ansichten: speichern / löschen
|
||||
@@ -60,6 +61,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['bulk_action'] ?? '') !== '
|
||||
if (count($allowedTickets) < count($ticketIds)) {
|
||||
$bulkErrors[] = 'Einige ausgewählte Tickets gehören zu Abteilungen, auf die Sie keinen Zugriff haben, und wurden übersprungen.';
|
||||
}
|
||||
$bulkResolutionCode = (string) ($_POST['bulk_resolution_code'] ?? '');
|
||||
$bulkResolutionNote = trim(mb_substr((string) ($_POST['bulk_resolution_note'] ?? ''), 0, 2000));
|
||||
if ($bulkAction === 'status' && in_array((string) ($_POST['bulk_status'] ?? ''), ['resolved', 'closed'], true) && !isset($resolutionReasons[$bulkResolutionCode])) {
|
||||
$bulkErrors[] = 'Bitte für das Schließen einen Abschlussgrund auswählen.';
|
||||
$allowedTickets = [];
|
||||
}
|
||||
|
||||
$now = db_now();
|
||||
// Abteilung wechseln: Ziel prüfen (muss existieren und für den Mitarbeiter erreichbar sein), Rechte je Ticket später
|
||||
@@ -88,6 +95,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['bulk_action'] ?? '') !== '
|
||||
$prioStmt->execute([$t['priority_id']]);
|
||||
$prioRow = $prioStmt->fetch();
|
||||
$sla = $prioRow ? calc_sla_dates($prioRow, $t['created_at'], (int) $moveTarget['id'], !empty($t['help_topic_id']) ? (int) $t['help_topic_id'] : null) : null;
|
||||
if ($sla) {
|
||||
$priorPauses = ticket_sla_completed_pause_seconds($pdo, (int) $t['id']);
|
||||
foreach (['first_response_due_at', 'resolution_due_at'] as $deadline) if (!empty($sla[$deadline]) && $priorPauses > 0) $sla[$deadline] = ticket_sla_shift_deadline($sla[$deadline], $priorPauses);
|
||||
if (!empty($sla['first_response_due_at'])) ticket_sla_history_add($pdo, (int) $t['id'], 'adjusted', 'first_response', 'Neue Frist nach Abteilungswechsel: ' . $sla['first_response_due_at'], (int) $staff['id']);
|
||||
if (!empty($sla['resolution_due_at'])) ticket_sla_history_add($pdo, (int) $t['id'], 'adjusted', 'resolution', 'Neue Frist nach Abteilungswechsel: ' . $sla['resolution_due_at'], (int) $staff['id']);
|
||||
}
|
||||
$pdo->prepare('UPDATE tickets SET department_id=?, first_response_due_at=?, resolution_due_at=?, updated_at=? WHERE id=?')
|
||||
->execute([(int) $moveTarget['id'], $sla['first_response_due_at'] ?? $t['first_response_due_at'], $sla['resolution_due_at'] ?? $t['resolution_due_at'], $now, $t['id']]);
|
||||
$oldDeptName = '';
|
||||
@@ -103,14 +116,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['bulk_action'] ?? '') !== '
|
||||
log_activity($pdo, $t['id'], $staff['id'], 'Sammelaktion: Mitarbeiter zugewiesen');
|
||||
} elseif ($bulkAction === 'priority') {
|
||||
$newPrio = (int) $_POST['bulk_priority_id'];
|
||||
$pdo->prepare('UPDATE tickets SET priority_id=?, updated_at=? WHERE id=?')->execute([$newPrio, $now, $t['id']]);
|
||||
$prioStmt = $pdo->prepare('SELECT * FROM priorities WHERE id = ?');
|
||||
$prioStmt->execute([$newPrio]); $newPriority = $prioStmt->fetch();
|
||||
$sla = $newPriority ? calc_sla_dates($newPriority, $t['created_at'], (int) $t['department_id'], !empty($t['help_topic_id']) ? (int) $t['help_topic_id'] : null) : null;
|
||||
if ($sla) {
|
||||
$priorPauses = ticket_sla_completed_pause_seconds($pdo, (int) $t['id']);
|
||||
foreach (['first_response_due_at', 'resolution_due_at'] as $deadline) if (!empty($sla[$deadline]) && $priorPauses > 0) $sla[$deadline] = ticket_sla_shift_deadline($sla[$deadline], $priorPauses);
|
||||
ticket_sla_history_add($pdo, (int) $t['id'], 'adjusted', 'first_response', 'Neue Frist nach Prioritaetswechsel: ' . $sla['first_response_due_at'], (int) $staff['id']);
|
||||
ticket_sla_history_add($pdo, (int) $t['id'], 'adjusted', 'resolution', 'Neue Frist nach Prioritaetswechsel: ' . $sla['resolution_due_at'], (int) $staff['id']);
|
||||
}
|
||||
$pdo->prepare('UPDATE tickets SET priority_id=?, first_response_due_at=?, resolution_due_at=?, updated_at=? WHERE id=?')
|
||||
->execute([$newPrio, $sla['first_response_due_at'] ?? $t['first_response_due_at'], $sla['resolution_due_at'] ?? $t['resolution_due_at'], $now, $t['id']]);
|
||||
log_activity($pdo, $t['id'], $staff['id'], 'Sammelaktion: Priorität geändert');
|
||||
} elseif ($bulkAction === 'status') {
|
||||
$newStatus = $_POST['bulk_status'];
|
||||
$resolvedAt = $newStatus === 'resolved' ? $now : $t['resolved_at'];
|
||||
$closedAt = $newStatus === 'closed' ? $now : $t['closed_at'];
|
||||
$pdo->prepare('UPDATE tickets SET status=?, resolved_at=?, closed_at=?, updated_at=? WHERE id=?')
|
||||
->execute([$newStatus, $resolvedAt, $closedAt, $now, $t['id']]);
|
||||
$pdo->prepare('UPDATE tickets SET status=?, resolved_at=?, closed_at=?, resolution_code=?, resolution_note=?, updated_at=? WHERE id=?')
|
||||
->execute([$newStatus, $resolvedAt, $closedAt, in_array($newStatus, ['resolved', 'closed'], true) ? $bulkResolutionCode : $t['resolution_code'], in_array($newStatus, ['resolved', 'closed'], true) ? ($bulkResolutionNote ?: $t['resolution_note']) : $t['resolution_note'], $now, $t['id']]);
|
||||
ticket_sla_status_transition($pdo, (int) $t['id'], (string) $t['status'], (string) $newStatus, (int) $staff['id']);
|
||||
log_activity($pdo, $t['id'], $staff['id'], 'Sammelaktion: Status geändert');
|
||||
} elseif ($bulkAction === 'delete') {
|
||||
delete_ticket($pdo, $t, $staff['id']);
|
||||
@@ -118,6 +142,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['bulk_action'] ?? '') !== '
|
||||
$email = $t['requester_email'];
|
||||
$domain = $email ? substr($email, strpos($email,'@')+1) : '';
|
||||
if ($email) {
|
||||
$priorStmt = $pdo->prepare("SELECT id,status FROM tickets WHERE requester_email=? AND status NOT IN ('closed')");
|
||||
$priorStmt->execute([$email]); $priorTickets = $priorStmt->fetchAll();
|
||||
$chk = $pdo->prepare('SELECT id FROM banlist WHERE value=?');
|
||||
$chk->execute([$email]);
|
||||
if (!$chk->fetchColumn()) {
|
||||
@@ -125,8 +151,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['bulk_action'] ?? '') !== '
|
||||
->execute(['email', $email, 'Bulk-Sperre', $staff['id'], $now]);
|
||||
}
|
||||
// Alle Tickets dieses Absenders schließen
|
||||
$pdo->prepare("UPDATE tickets SET status='closed',closed_at=?,updated_at=? WHERE requester_email=? AND status NOT IN ('closed')")
|
||||
$pdo->prepare("UPDATE tickets SET status='closed',resolution_code='spam',resolution_note='Absender im Rahmen einer Sammelaktion gesperrt',closed_at=?,updated_at=? WHERE requester_email=? AND status NOT IN ('closed')")
|
||||
->execute([$now, $now, $email]);
|
||||
ticket_sla_transition_rows($pdo, $priorTickets, 'closed', (int) $staff['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +205,7 @@ if ($activeQuick === 'me') {
|
||||
$where[] = 't.assigned_staff_id IS NULL';
|
||||
$where[] = "t.status NOT IN ('resolved','closed')";
|
||||
} elseif ($activeQuick === 'overdue') {
|
||||
$where[] = 't.resolution_due_at < ?'; $params[] = db_now();
|
||||
$where[] = '(t.is_overdue = 1 OR (t.sla_paused_at IS NULL AND t.resolution_due_at < ?))'; $params[] = db_now();
|
||||
$where[] = "t.status NOT IN ('resolved','closed')";
|
||||
} elseif ($activeQuick === 'highprio') {
|
||||
$where[] = 't.priority_id IN (SELECT id FROM (SELECT id FROM priorities ORDER BY sort_order DESC LIMIT 2) AS top_priorities)';
|
||||
@@ -203,13 +230,13 @@ if ($filterSla !== '') {
|
||||
$now = db_now();
|
||||
$riskThreshold = date('Y-m-d H:i:s', time() + 4 * 3600);
|
||||
if ($filterSla === 'overdue') {
|
||||
$where[] = "t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')";
|
||||
$where[] = "t.resolution_due_at IS NOT NULL AND (t.is_overdue = 1 OR (t.sla_paused_at IS NULL AND t.resolution_due_at < ?)) AND t.status NOT IN ('resolved','closed')";
|
||||
$params[] = $now;
|
||||
} elseif ($filterSla === 'at_risk') {
|
||||
$where[] = "t.resolution_due_at IS NOT NULL AND t.resolution_due_at >= ? AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')";
|
||||
$where[] = "t.sla_paused_at IS NULL AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at >= ? AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')";
|
||||
array_push($params, $now, $riskThreshold);
|
||||
} elseif ($filterSla === 'on_time') {
|
||||
$where[] = "(t.resolution_due_at IS NULL OR t.resolution_due_at >= ?) AND t.status NOT IN ('resolved','closed')";
|
||||
$where[] = "t.sla_paused_at IS NULL AND (t.resolution_due_at IS NULL OR t.resolution_due_at >= ?) AND t.status NOT IN ('resolved','closed')";
|
||||
$params[] = $riskThreshold;
|
||||
}
|
||||
}
|
||||
@@ -307,7 +334,7 @@ $_stMe = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id=? A
|
||||
$_stMe->execute([$staff['id']]);
|
||||
$countMe = (int)$_stMe->fetchColumn();
|
||||
$countUnassigned= (int)$pdo->query("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id IS NULL AND status NOT IN ('resolved','closed')")->fetchColumn();
|
||||
$_stOd = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE resolution_due_at < ? AND status NOT IN ('resolved','closed')");
|
||||
$_stOd = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE (is_overdue = 1 OR (sla_paused_at IS NULL AND resolution_due_at < ?)) AND status NOT IN ('resolved','closed')");
|
||||
$_stOd->execute([db_now()]);
|
||||
$countOverdue = (int)$_stOd->fetchColumn();
|
||||
$countHighPrio = (int)$pdo->query("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed') AND priority_id IN (SELECT id FROM (SELECT id FROM priorities ORDER BY sort_order DESC LIMIT 2) AS top_priorities)")->fetchColumn();
|
||||
@@ -602,6 +629,9 @@ document.getElementById('qf-panel')?.addEventListener('click', function(e){ e.st
|
||||
<option value="<?= $val ?>"><?= $lbl ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="bulk_resolution_code" aria-label="Abschlussgrund für Tickets">
|
||||
<option value="">Abschlussgrund …</option><?php foreach ($resolutionReasons as $code => $label): ?><option value="<?= e($code) ?>"><?= e($label) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="btn secondary" style="white-space:nowrap" onclick="submitBulk('status')">Status setzen</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user