*/ function my_week_periods(): array { return ['week' => 'Diese Woche', 'lastweek' => 'Letzte Woche', 'month' => 'Dieser Monat', 'lastmonth' => 'Letzter Monat']; } /** * Zeitraum und der gleich lange Zeitraum davor (zum Vergleichen). * @return array{from:string,to:string,prev_from:string,prev_to:string,label:string} */ function my_week_range(string $key, ?int $now = null): array { $now = $now ?? time(); $labels = my_week_periods(); if (!isset($labels[$key])) $key = 'week'; $monday = strtotime('monday this week', $now); switch ($key) { case 'lastweek': $from = strtotime('-7 days', $monday); $to = $monday - 1; break; case 'month': $from = strtotime(date('Y-m-01', $now)); $to = $now; break; case 'lastmonth': $first = strtotime(date('Y-m-01', $now)); $from = strtotime('-1 month', $first); $to = $first - 1; break; default: $from = $monday; $to = $now; } // Vergleichszeitraum nach dem Kalender (nicht in Sekunden): Sommer-/Winterzeit verschiebt sonst die Grenze um eine Stunde if ($key === 'week' || $key === 'lastweek') { $prevFrom = strtotime('-7 days', $from); $prevTo = strtotime('-7 days', $to); } elseif ($key === 'month') { $prevFrom = strtotime('-1 month', $from); $prevTo = min(strtotime('-1 month', $to), $from - 1); // gleich weit in den Vormonat, höchstens bis dessen Ende } else { $prevFrom = strtotime('-1 month', $from); $prevTo = $from - 1; } $fmt = fn(int $ts) => date('Y-m-d H:i:s', $ts); return ['from' => $fmt($from), 'to' => $fmt($to), 'prev_from' => $fmt($prevFrom), 'prev_to' => $fmt($prevTo), 'label' => $labels[$key]]; } /** * Kennzahlen eines Mitarbeiters in einem Zeitraum. * @return array{resolved:int,replies:int,first_response_seconds:?int,first_response_count:int,csat_avg:?float,csat_count:int,csat_positive:?int,on_time:?int} */ function my_week_stats(PDO $pdo, int $staffId, string $from, string $to): array { $one = function (string $sql, array $params) use ($pdo) { $s = $pdo->prepare($sql); $s->execute($params); return $s->fetch(PDO::FETCH_NUM) ?: []; }; // Gelöst/geschlossen und mir zugewiesen; „pünktlich“ = vor Ablauf der Lösungsfrist (oder ohne Frist) [$resolved, $onTime] = $one( "SELECT COUNT(*), SUM(CASE WHEN resolution_due_at IS NULL OR COALESCE(resolved_at, closed_at) <= resolution_due_at THEN 1 ELSE 0 END) FROM tickets WHERE assigned_staff_id = ? AND is_spam = 0 AND status IN ('resolved','closed') AND COALESCE(resolved_at, closed_at) BETWEEN ? AND ?", [$staffId, $from, $to] ) + [0, 0]; [$replies] = $one( "SELECT COUNT(*) FROM ticket_threads WHERE staff_id = ? AND author_type = 'staff' AND is_internal_note = 0 AND created_at BETWEEN ? AND ?", [$staffId, $from, $to] ) + [0]; // Zeit bis zur ersten Antwort: nur Tickets, deren erste öffentliche Antwort von mir stammt (Arbeitszeit, wenn Geschäftszeiten eingerichtet sind) $st = $pdo->prepare( "SELECT t.created_at, t.first_responded_at FROM tickets t WHERE t.is_spam = 0 AND t.first_responded_at BETWEEN ? AND ? AND (SELECT th.staff_id FROM ticket_threads th WHERE th.ticket_id = t.id AND th.author_type = 'staff' AND th.is_internal_note = 0 ORDER BY th.created_at, th.id LIMIT 1) = ? ORDER BY t.id DESC LIMIT 500" ); $st->execute([$from, $to, $staffId]); $sum = 0; $n = 0; foreach ($st->fetchAll() as $r) { if (empty($r['created_at']) || empty($r['first_responded_at'])) continue; $sum += ticket_sla_working_seconds_between((string) $r['created_at'], (string) $r['first_responded_at']); $n++; } [$csatN, $csatAvg, $csatGood] = $one( "SELECT COUNT(*), AVG(rating), SUM(CASE WHEN rating >= 4 THEN 1 ELSE 0 END) FROM ticket_csat WHERE staff_id = ? AND rating IS NOT NULL AND answered_at BETWEEN ? AND ?", [$staffId, $from, $to] ) + [0, null, 0]; return [ 'resolved' => (int) $resolved, 'replies' => (int) $replies, 'first_response_seconds' => $n > 0 ? (int) round($sum / $n) : null, 'first_response_count' => $n, 'csat_avg' => (int) $csatN > 0 ? round((float) $csatAvg, 2) : null, 'csat_count' => (int) $csatN, 'csat_positive' => (int) $csatN > 0 ? (int) round(100 * (int) $csatGood / (int) $csatN) : null, 'on_time' => (int) $resolved > 0 ? (int) round(100 * (int) $onTime / (int) $resolved) : null, ]; } /** Was jetzt offen ist (unabhängig vom Zeitraum). @return array{open:int,overdue:int,waiting:int} */ function my_week_now(PDO $pdo, int $staffId, ?string $now = null): array { $now = $now ?? db_now(); $count = function (string $extra, array $params = []) use ($pdo, $staffId): int { $s = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id = ? AND is_spam = 0 AND status NOT IN ('resolved','closed'){$extra}"); $s->execute(array_merge([$staffId], $params)); return (int) $s->fetchColumn(); }; return [ 'open' => $count(''), 'overdue' => $count(' AND resolution_due_at IS NOT NULL AND resolution_due_at < ?', [$now]), 'waiting' => $count(" AND status = 'open'"), ]; } /** Gelöste Tickets je Tag im Zeitraum (für das Balkendiagramm). @return array Datum => Anzahl */ function my_week_per_day(PDO $pdo, int $staffId, string $from, string $to): array { $out = []; for ($d = strtotime(date('Y-m-d', strtotime($from))); $d <= strtotime($to); $d = strtotime('+1 day', $d)) $out[date('Y-m-d', $d)] = 0; $s = $pdo->prepare("SELECT COALESCE(resolved_at, closed_at) AS at FROM tickets WHERE assigned_staff_id = ? AND is_spam = 0 AND status IN ('resolved','closed') AND COALESCE(resolved_at, closed_at) BETWEEN ? AND ?"); $s->execute([$staffId, $from, $to]); foreach ($s->fetchAll(PDO::FETCH_COLUMN) as $at) { $day = date('Y-m-d', strtotime((string) $at)); if (isset($out[$day])) $out[$day]++; } return count($out) > 62 ? array_slice($out, -62, null, true) : $out; } /** Dauer kurz und lesbar: „2 Std 15 Min“, „40 Min“, „–“. */ function my_week_fmt_duration(?int $seconds): string { if ($seconds === null) return '–'; if ($seconds < 60) return 'unter 1 Min'; $m = (int) round($seconds / 60); if ($m < 60) return $m . ' Min'; $h = intdiv($m, 60); $rest = $m % 60; if ($h >= 48) return round($h / 24) . ' Tage'; return $h . ' Std' . ($rest ? ' ' . $rest . ' Min' : ''); } /** * Veränderung zum Zeitraum davor als Text und Richtung (+1 besser, -1 schlechter, 0 gleich/unbekannt). * @return array{text:string,trend:int} */ function my_week_delta(?float $now, ?float $before, bool $lowerIsBetter = false, string $unit = ''): array { if ($now === null || $before === null) return ['text' => 'kein Vergleich', 'trend' => 0]; $diff = $now - $before; if (abs($diff) < 0.005) return ['text' => 'wie davor', 'trend' => 0]; $good = $lowerIsBetter ? $diff < 0 : $diff > 0; $num = abs($diff) >= 10 || floor(abs($diff)) === abs($diff) ? (string) round(abs($diff)) : number_format(abs($diff), 1, ',', ''); return ['text' => ($diff > 0 ? '▲ ' : '▼ ') . $num . $unit . ' zum Zeitraum davor', 'trend' => $good ? 1 : -1]; }