feat(helpdesk): redesign team dashboard with agent cards and ticket details

Replace KPI tiles + table with visual agent cards showing workload at a
glance. Each card has the agent name, ticket count badge (warning-colored
when critical), a proportional load bar, and the actual open tickets
listed underneath (number, description, age). Critical tickets are
highlighted with warning color. Queue (unassigned) shown as dashed card
at top. Performance tab uses compact cards with success-colored bars.
Backend now returns open_ticket_details per member (sorted: critical
first, then by age descending).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 19:09:15 +02:00
parent 82c76d6119
commit 397f9fe659
6 changed files with 243 additions and 257 deletions

View File

@@ -1593,7 +1593,7 @@ class DebitorDetailService
$periodStart = $nowTs - ($periodDays * 86400);
$criticalThresholdHours = 48;
/** @var array<string, array{display_name: string, support_user: string, open_tickets: int, critical_tickets: int, age_hours_sum: int, resolved_in_period: int}> $agents */
/** @var array<string, array{display_name: string, support_user: string, open_tickets: int, critical_tickets: int, age_hours_sum: int, resolved_in_period: int, open_ticket_details: list<array{no: string, description: string, age_hours: int, critical: bool, customer: string}>}> $agents */
$agents = [];
$globalOpen = 0;
$globalUnassigned = 0;
@@ -1619,6 +1619,7 @@ class DebitorDetailService
'critical_tickets' => 0,
'age_hours_sum' => 0,
'resolved_in_period' => 0,
'open_ticket_details' => [],
];
}
@@ -1635,17 +1636,28 @@ class DebitorDetailService
$globalUnassigned++;
}
$ageHours = 0;
$isCritical = false;
if ($activityTs !== null) {
$ageHours = intdiv(max(0, $nowTs - $activityTs), 3600);
$agents[$normalizedUser]['age_hours_sum'] += $ageHours;
if ($ageHours > $criticalThresholdHours) {
$agents[$normalizedUser]['critical_tickets']++;
$isCritical = true;
}
$globalAgeSum += $ageHours;
$globalOpenCount++;
}
$agents[$normalizedUser]['open_ticket_details'][] = [
'no' => trim((string) ($ticket['No'] ?? '')),
'description' => trim((string) ($ticket['Description'] ?? '')),
'age_hours' => $ageHours,
'critical' => $isCritical,
'customer' => trim((string) ($ticket['Customer_No'] ?? '')),
];
}
}
@@ -1655,6 +1667,16 @@ class DebitorDetailService
? (int) round($agent['age_hours_sum'] / $agent['open_tickets'])
: 0;
// Sort ticket details: critical first, then by age descending
$details = $agent['open_ticket_details'];
usort($details, static function (array $a, array $b): int {
if ($a['critical'] !== $b['critical']) {
return $b['critical'] <=> $a['critical'];
}
return $b['age_hours'] <=> $a['age_hours'];
});
$members[] = [
'support_user' => $agent['support_user'],
'display_name' => $agent['display_name'],
@@ -1662,6 +1684,7 @@ class DebitorDetailService
'critical_tickets' => $agent['critical_tickets'],
'avg_age_hours' => $avgAge,
'resolved_in_period' => $agent['resolved_in_period'],
'open_ticket_details' => $details,
];
}