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 = "'; 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 = "'; 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(); ?>| Bezeichnung | Geöffnet | Zugewiesen | Überfällig | Geschlossen | Wieder geöffnet | Gelöscht | Servicezeit (Std) | Reaktionszeit (Std) |
|---|---|---|---|---|---|---|---|---|
| = $key === 'none' ? e($noneLabel) : e($labelMap[(int) $key] ?? "#{$key}") ?> | = (int)$r['geoeffnet'] ?> | = (int)$r['zugewiesen'] ?> | = (int)$r['ueberfaellig'] ?> | = (int)$r['geschlossen'] ?> | = (int)$r['wieder_geoeffnet'] ?> | = (int)$r['geloescht'] ?> | = $r['servicezeit'] !== null ? number_format((float)$r['servicezeit'], 1) : '–' ?> | = $r['reaktionszeit'] !== null ? number_format((float)$r['reaktionszeit'], 1) : '–' ?> |
Im Zeitraum wurden noch keine Abfragen verschickt.
| = e(csat_stars($i)) ?> | = $csat['dist'][$i] ?> |
| = e($row['name']) ?> | = number_format($row['avg'], 1, ',', '') ?> ★ | = $row['count'] ?> Bewertung= $row['count'] === 1 ? '' : 'en' ?> |