Keine Daten vorhanden.

'; $palette = ['#0f6f6a', '#2563eb', '#d97706', '#dc2626', '#a855f7', '#16a34a', '#6b7280']; $max = max(max($data), 1); $count = count($data); $barWidth = 38; $gap = 16; $width = $count * ($barWidth + $gap) + $gap; $chartHeight = $height - 46; $svg = ""; $x = $gap; $i = 0; foreach ($data as $label => $value) { $barH = (int) round(($value / $max) * $chartHeight); $y = $chartHeight - $barH + 18; $color = $palette[$i % count($palette)]; $svg .= ""; $svg .= "" . (int) $value . ""; $svg .= "" . htmlspecialchars((string) $label) . ""; $x += $barWidth + $gap; $i++; } $svg .= ''; return $svg; } /** Mehrlinien-Diagramm als SVG. $buckets: [label => [serieKey => wert]]. $series: [serieKey => ['label'=>..,'color'=>..]] */ function svg_multiline_chart(array $buckets, array $series): string { if (!$buckets) return '

Keine Daten im gewählten Zeitraum.

'; $labels = array_keys($buckets); $n = count($labels); $width = max(640, $n * 48); $height = 260; $padLeft = 30; $padBottom = 36; $padTop = 16; $padRight = 16; $chartW = $width - $padLeft - $padRight; $chartH = $height - $padTop - $padBottom; $maxVal = 1; foreach ($buckets as $b) { foreach (array_keys($series) as $key) { $maxVal = max($maxVal, (int) ($b[$key] ?? 0)); } } $svg = ""; $svg .= ""; $stepX = $n > 1 ? $chartW / ($n - 1) : 0; foreach ($series as $key => $info) { $points = []; foreach ($labels as $i => $label) { $val = (int) ($buckets[$label][$key] ?? 0); $x = round($padLeft + $i * $stepX, 1); $y = round($padTop + $chartH - ($val / $maxVal) * $chartH, 1); $points[] = "{$x},{$y}"; } $svg .= ''; foreach ($labels as $i => $label) { $val = (int) ($buckets[$label][$key] ?? 0); $x = round($padLeft + $i * $stepX, 1); $y = round($padTop + $chartH - ($val / $maxVal) * $chartH, 1); $svg .= ""; } } $labelEvery = max(1, (int) ceil($n / 12)); foreach ($labels as $i => $label) { if ($i % $labelEvery !== 0 && $i !== $n - 1) continue; $x = round($padLeft + $i * $stepX, 1); $svg .= "" . htmlspecialchars((string) $label) . ""; } $svg .= ''; return $svg; } // --------------------------------------------------------------- // Zeitraum bestimmen // --------------------------------------------------------------- $preset = $_GET['range'] ?? '30d'; $today = new DateTime('today'); switch ($preset) { case '7d': $from = (clone $today)->modify('-6 days'); $to = clone $today; break; case 'thismonth': $from = new DateTime(date('Y-m-01')); $to = clone $today; break; case 'thisyear': $from = new DateTime(date('Y-01-01')); $to = clone $today; break; case 'custom': $from = !empty($_GET['from']) ? new DateTime($_GET['from']) : (clone $today)->modify('-29 days'); $to = !empty($_GET['to']) ? new DateTime($_GET['to']) : clone $today; break; case '30d': default: $preset = '30d'; $from = (clone $today)->modify('-29 days'); $to = clone $today; } if ($from > $to) { [$from, $to] = [$to, $from]; } $rangeFrom = $from->format('Y-m-d 00:00:00'); $rangeTo = $to->format('Y-m-d 23:59:59'); $rangeDays = (int) $from->diff($to)->days + 1; $useWeekly = $rangeDays > 45; // --------------------------------------------------------------- // Kennzahlen (gesamt, nicht zeitraumgefiltert – schneller Überblick) // --------------------------------------------------------------- [$deptFilterT, $deptParamsT] = dept_filter_sql($myDeptIds, 'department_id'); $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE 1=1{$deptFilterT}"); $stmt->execute($deptParamsT); $totalTickets = (int) $stmt->fetchColumn(); $monthStart = date('Y-m-01 00:00:00'); $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE resolved_at >= ?{$deptFilterT}"); $stmt->execute([$monthStart, ...$deptParamsT]); $resolvedThisMonth = (int) $stmt->fetchColumn(); $respTimes = []; $stmt = $pdo->prepare("SELECT created_at, first_responded_at FROM tickets WHERE first_responded_at IS NOT NULL{$deptFilterT}"); $stmt->execute($deptParamsT); foreach ($stmt->fetchAll() as $r) { $respTimes[] = (strtotime($r['first_responded_at']) - strtotime($r['created_at'])) / 3600; } $avgResponse = $respTimes ? array_sum($respTimes) / count($respTimes) : null; $resolTimes = []; $stmt = $pdo->prepare("SELECT created_at, resolved_at FROM tickets WHERE resolved_at IS NOT NULL{$deptFilterT}"); $stmt->execute($deptParamsT); foreach ($stmt->fetchAll() as $r) { $resolTimes[] = (strtotime($r['resolved_at']) - strtotime($r['created_at'])) / 3600; } $avgResolution = $resolTimes ? array_sum($resolTimes) / count($resolTimes) : null; // --------------------------------------------------------------- // Mehrlinien-Aktivitätsdiagramm für den gewählten Zeitraum // --------------------------------------------------------------- function bucket_key_for(string $datetime, bool $weekly): string { $d = new DateTime($datetime); return $weekly ? 'KW' . $d->format('W') : $d->format('d.m'); } $buckets = []; $cursor = clone $from; while ($cursor <= $to) { $key = $useWeekly ? 'KW' . $cursor->format('W') : $cursor->format('d.m'); if (!isset($buckets[$key])) { $buckets[$key] = ['erstellt' => 0, 'zugewiesen' => 0, 'geschlossen' => 0, 'geloescht' => 0, 'ueberfaellig' => 0]; } $cursor->modify('+1 day'); } $stmt = $pdo->prepare("SELECT created_at FROM tickets WHERE created_at BETWEEN ? AND ?{$deptFilterT}"); $stmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsT)); foreach ($stmt->fetchAll() as $r) { $k = bucket_key_for($r['created_at'], $useWeekly); if (isset($buckets[$k])) $buckets[$k]['erstellt']++; } $stmt = $pdo->prepare( "SELECT al.created_at FROM activity_log al JOIN tickets t ON t.id = al.ticket_id WHERE al.action LIKE 'Mitarbeiter zugewiesen:%' AND al.created_at BETWEEN ? AND ?{$deptFilterT}" ); $stmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsT)); foreach ($stmt->fetchAll() as $r) { $k = bucket_key_for($r['created_at'], $useWeekly); if (isset($buckets[$k])) $buckets[$k]['zugewiesen']++; } $stmt = $pdo->prepare("SELECT closed_at FROM tickets WHERE closed_at BETWEEN ? AND ?{$deptFilterT}"); $stmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsT)); foreach ($stmt->fetchAll() as $r) { $k = bucket_key_for($r['closed_at'], $useWeekly); if (isset($buckets[$k])) $buckets[$k]['geschlossen']++; } [$deptFilterDel, $deptParamsDel] = dept_filter_sql($myDeptIds, 'department_id'); $stmt = $pdo->prepare("SELECT deleted_at FROM deleted_tickets_log WHERE deleted_at BETWEEN ? AND ?{$deptFilterDel}"); $stmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsDel)); foreach ($stmt->fetchAll() as $r) { $k = bucket_key_for($r['deleted_at'], $useWeekly); if (isset($buckets[$k])) $buckets[$k]['geloescht']++; } $stmt = $pdo->prepare( "SELECT al.created_at FROM activity_log al JOIN tickets t ON t.id = al.ticket_id WHERE al.action LIKE 'SLA %' AND al.created_at BETWEEN ? AND ?{$deptFilterT}" ); $stmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsT)); foreach ($stmt->fetchAll() as $r) { $k = bucket_key_for($r['created_at'], $useWeekly); if (isset($buckets[$k])) $buckets[$k]['ueberfaellig']++; } $chartSeries = [ 'erstellt' => ['label' => 'Erstellt', 'color' => '#16a34a'], 'zugewiesen' => ['label' => 'Zugewiesen', 'color' => '#2563eb'], 'geschlossen' => ['label' => 'Geschlossen', 'color' => '#0f6f6a'], 'geloescht' => ['label' => 'Gelöscht', 'color' => '#a855f7'], 'ueberfaellig' => ['label' => 'Überfällig', 'color' => '#dc2626'], ]; // --------------------------------------------------------------- // Nach Status / Priorität (Gesamtüberblick, unabhängig vom Zeitraum) // --------------------------------------------------------------- $byStatus = []; foreach (['open', 'pending', 'answered', 'on_hold', 'resolved', 'closed'] as $s) { [$label] = status_label($s); $byStatus[$label] = 0; } $stmt = $pdo->prepare("SELECT status, COUNT(*) c FROM tickets WHERE 1=1{$deptFilterT} GROUP BY status"); $stmt->execute($deptParamsT); foreach ($stmt->fetchAll() as $r) { [$label] = status_label($r['status']); $byStatus[$label] = (int) $r['c']; } $byPrio = []; $stmt = $pdo->prepare( "SELECT p.name, p.sort_order, COUNT(t.id) c FROM priorities p LEFT JOIN tickets t ON t.priority_id = p.id{$deptFilterT} GROUP BY p.id, p.name, p.sort_order ORDER BY p.sort_order" ); $stmt->execute($deptParamsT); foreach ($stmt->fetchAll() as $r) { $byPrio[$r['name']] = (int) $r['c']; } // --------------------------------------------------------------- // SLA-Einhaltung im Zeitraum // --------------------------------------------------------------- $slaStmt = $pdo->prepare( "SELECT COUNT(*) AS total, SUM(CASE WHEN resolved_at <= resolution_due_at THEN 1 ELSE 0 END) AS on_time FROM tickets WHERE status IN ('resolved','closed') AND resolved_at IS NOT NULL AND resolution_due_at IS NOT NULL AND resolved_at BETWEEN ? AND ?{$deptFilterT}" ); $slaStmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsT)); $slaRow = $slaStmt->fetch(); $slaTotal = (int)($slaRow['total'] ?? 0); $slaOk = (int)($slaRow['on_time'] ?? 0); $slaRate = $slaTotal > 0 ? round($slaOk / $slaTotal * 100) : null; // Mitarbeiter-Performance im Zeitraum $agentPerf = []; if ($staff['role'] === 'admin') { $avgResponseHours = report_avg_response_hours_expression( $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) ); $apStmt = $pdo->prepare( "SELECT s.name, COUNT(*) AS total, SUM(CASE WHEN t.status IN ('resolved','closed') THEN 1 ELSE 0 END) AS resolved, AVG(CASE WHEN t.first_responded_at IS NOT NULL THEN {$avgResponseHours} END) AS avg_resp FROM tickets t JOIN staff s ON s.id = t.assigned_staff_id WHERE t.created_at BETWEEN ? AND ?{$deptFilterT} GROUP BY t.assigned_staff_id, s.name ORDER BY resolved DESC LIMIT 10" ); $apStmt->execute(array_merge([$rangeFrom, $rangeTo], $deptParamsT)); $agentPerf = $apStmt->fetchAll(); } // Chart.js-Daten für Zeitreihe $chartLabels = array_keys($buckets); $chartCreated = array_column(array_values($buckets), 'erstellt'); $chartClosed = array_column(array_values($buckets), 'geschlossen'); // --------------------------------------------------------------- // Statistik-Tabellen je Abteilung / Thema / Agent // --------------------------------------------------------------- $deptRows = build_report_table($pdo, 'department_id', $rangeFrom, $rangeTo, $myDeptIds); $topicRows = build_report_table($pdo, 'help_topic_id', $rangeFrom, $rangeTo, $myDeptIds); $departments = $pdo->query('SELECT id, name FROM departments')->fetchAll(PDO::FETCH_KEY_PAIR); $agentNames = $pdo->query('SELECT id, name FROM staff')->fetchAll(PDO::FETCH_KEY_PAIR); $helpTopics = $pdo->query('SELECT id, name FROM help_topics')->fetchAll(PDO::FETCH_KEY_PAIR); function render_report_table(array $rows, array $labelMap, string $noneLabel): string { if (!$rows) return '

Keine Daten im gewählten Zeitraum.

'; ob_start(); ?> $r): ?>
BezeichnungGeöffnetZugewiesenÜberfälligGeschlossen Wieder geöffnetGelöschtServicezeit (Std)Reaktionszeit (Std)
prepare("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed'){$deptFilterT}"); $stmt->execute($deptParamsT); $dashOpenTickets = (int) $stmt->fetchColumn(); $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id = ? AND status NOT IN ('resolved','closed'){$deptFilterT}"); $stmt->execute(array_merge([$staff['id']], $deptParamsT)); $dashMine = (int) $stmt->fetchColumn(); $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id IS NULL AND status NOT IN ('resolved','closed'){$deptFilterT}"); $stmt->execute($deptParamsT); $dashUnassigned = (int) $stmt->fetchColumn(); $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed') AND resolution_due_at IS NOT NULL AND resolution_due_at < ?{$deptFilterT}"); $stmt->execute(array_merge([$now], $deptParamsT)); $dashOverdue = (int) $stmt->fetchColumn(); $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status = 'open'{$deptFilterT}"); $stmt->execute($deptParamsT); $dashUnanswered = (int) $stmt->fetchColumn(); $mentionStmt = $pdo->prepare('SELECT COUNT(*) FROM staff_mentions WHERE mentioned_staff_id=? AND is_read=0'); $mentionStmt->execute([$staff['id']]); $unreadMentions = (int) $mentionStmt->fetchColumn(); $tsColors = ['available'=>'#22c55e','busy'=>'#f59e0b','break'=>'#6366f1','offline'=>'#94a3b8']; $tsLabels = ['available'=>'Verfügbar','busy'=>'Beschäftigt','break'=>'Pause','offline'=>'Offline']; $teamStatus = []; $myAvailStatus = 'available'; try { staff_run_auto_offline($pdo); $tsStmt = $pdo->query( "SELECT s.id, s.name, s.is_on_vacation, sa.status AS avail_status, sa.note AS avail_note FROM staff s LEFT JOIN staff_availability sa ON sa.staff_id = s.id WHERE s.is_active = 1 ORDER BY s.name" ); foreach ($tsStmt->fetchAll() as $row) { if ($row['id'] === $staff['id']) { $myAvailStatus = $row['avail_status'] ?? 'available'; } else { $teamStatus[] = $row; } } } catch (Throwable $e) {} $pageTitle = 'Übersicht'; $activePage = 'reports'; require __DIR__ . '/_header.php'; ?>

Willkommen,

v 🔄 Updates
true, CURLOPT_TIMEOUT=>4, CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_PROTOCOLS=>CURLPROTO_HTTPS, CURLOPT_HTTPHEADER=>update_http_headers()]); $__resp = curl_exec($__ch); curl_close($__ch); $__rel = @json_decode($__resp, true); if (!empty($__rel[0]['tag_name'])) $__latestVersion = $__rel[0]['tag_name']; } $__hasUpdate = $__latestVersion && version_compare($__latestVersion, DESKHUB_VERSION, '>'); ?>
🆕
Update verfügbar: Version
Aktuell installiert: v
Zum Update-Bereich →
Offene Tickets
Mir zugewiesen
Nicht zugewiesen
Überfällig
Unbeantwortet
0): ?>
🔔 Neue Erwähnungen
Mein Status: $sc): $active = $myAvailStatus === $sk; ?>
Team:

Berichte

Tickets gesamt
Gelöst diesen Monat
Ø Erstreaktion
Ø Lösungszeit
0)): ?>

Kundenzufriedenheit (Bewertungen im gewählten Zeitraum)

Im Zeitraum wurden noch keine Abfragen verschickt.

Ø Bewertung
Antworten von Abfragen
Rücklaufquote
= 1; $i--): ?>

Nach Mitarbeiter

★ Bewertung

Letzte Bewertungen

#
„“

Ticketaktivität (format('d.m.Y')) ?> – format('d.m.Y')) ?>)

0): ?>
SLA-Einhaltung im Zeitraum
%
von Tickets innerhalb der Frist gelöst
Im ZeitlimitÜberfällig
✓ ✗

Nach Status

Nach Priorität

Statistiken (format('d.m.Y')) ?> – format('d.m.Y')) ?>)

$preset, 'from' => $from->format('Y-m-d'), 'to' => $to->format('Y-m-d')]; ?>

Exportieren (CSV)

query('SELECT name, color FROM priorities ORDER BY sort_order')->fetchAll() as $pc) { $prioColorMap[$pc['name']] = $pc['color']; } $jsChartLabels = json_encode($chartLabels); $jsChartCreated = json_encode($chartCreated); $jsChartClosed = json_encode($chartClosed); $jsStatusLabels = json_encode(array_keys($byStatus)); $jsStatusData = json_encode(array_values($byStatus)); $jsPrioLabels = json_encode(array_keys($byPrio)); $jsPrioData = json_encode(array_values($byPrio)); $jsPrioColors = json_encode($prioColorMap); ?>