169 lines
7.7 KiB
PHP
169 lines
7.7 KiB
PHP
<?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];
|
||
}
|