Files
deskhub/DeskHub-Webseite/includes/my_week.php
T
2026-11-04 07:55:19 +00:00

169 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* „Meine Woche“: persönliche Kennzahlen eines Mitarbeiters (gelöste Tickets, gesendete Antworten, durchschnittliche Zeit bis zur ersten
* Antwort, Zufriedenheit der Kunden, Pünktlichkeit). Sichtbar nur für den Mitarbeiter selbst (staff/my_week.php).
*/
/** @return array<string,string> */
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<string,int> 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];
}