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

@@ -296,5 +296,6 @@
"Could not load team dashboard.": "Team-Dashboard konnte nicht geladen werden.", "Could not load team dashboard.": "Team-Dashboard konnte nicht geladen werden.",
"No team data available.": "Keine Team-Daten verfügbar.", "No team data available.": "Keine Team-Daten verfügbar.",
"Current": "Aktuell", "Current": "Aktuell",
"Performance": "Leistung" "Performance": "Leistung",
"Queue": "Warteschlange"
} }

View File

@@ -296,5 +296,6 @@
"Could not load team dashboard.": "Could not load team dashboard.", "Could not load team dashboard.": "Could not load team dashboard.",
"No team data available.": "No team data available.", "No team data available.": "No team data available.",
"Current": "Current", "Current": "Current",
"Performance": "Performance" "Performance": "Performance",
"Queue": "Queue"
} }

View File

@@ -1593,7 +1593,7 @@ class DebitorDetailService
$periodStart = $nowTs - ($periodDays * 86400); $periodStart = $nowTs - ($periodDays * 86400);
$criticalThresholdHours = 48; $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 = []; $agents = [];
$globalOpen = 0; $globalOpen = 0;
$globalUnassigned = 0; $globalUnassigned = 0;
@@ -1619,6 +1619,7 @@ class DebitorDetailService
'critical_tickets' => 0, 'critical_tickets' => 0,
'age_hours_sum' => 0, 'age_hours_sum' => 0,
'resolved_in_period' => 0, 'resolved_in_period' => 0,
'open_ticket_details' => [],
]; ];
} }
@@ -1635,17 +1636,28 @@ class DebitorDetailService
$globalUnassigned++; $globalUnassigned++;
} }
$ageHours = 0;
$isCritical = false;
if ($activityTs !== null) { if ($activityTs !== null) {
$ageHours = intdiv(max(0, $nowTs - $activityTs), 3600); $ageHours = intdiv(max(0, $nowTs - $activityTs), 3600);
$agents[$normalizedUser]['age_hours_sum'] += $ageHours; $agents[$normalizedUser]['age_hours_sum'] += $ageHours;
if ($ageHours > $criticalThresholdHours) { if ($ageHours > $criticalThresholdHours) {
$agents[$normalizedUser]['critical_tickets']++; $agents[$normalizedUser]['critical_tickets']++;
$isCritical = true;
} }
$globalAgeSum += $ageHours; $globalAgeSum += $ageHours;
$globalOpenCount++; $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']) ? (int) round($agent['age_hours_sum'] / $agent['open_tickets'])
: 0; : 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[] = [ $members[] = [
'support_user' => $agent['support_user'], 'support_user' => $agent['support_user'],
'display_name' => $agent['display_name'], 'display_name' => $agent['display_name'],
@@ -1662,6 +1684,7 @@ class DebitorDetailService
'critical_tickets' => $agent['critical_tickets'], 'critical_tickets' => $agent['critical_tickets'],
'avg_age_hours' => $avgAge, 'avg_age_hours' => $avgAge,
'resolved_in_period' => $agent['resolved_in_period'], 'resolved_in_period' => $agent['resolved_in_period'],
'open_ticket_details' => $details,
]; ];
} }

View File

@@ -1,9 +1,9 @@
<?php <?php
/** /**
* Team workload dashboard — shows support agent metrics aggregated from BC tickets. * Team workload dashboard — visual workload map for support team leads.
* Tab 1 "Current": snapshot of open tickets per agent (no period filter). * Tab "Current": agent cards with open tickets, queue at top.
* Tab 2 "Performance": resolved tickets per agent in a selectable period. * Tab "Performance": resolved per agent in a selectable period.
*/ */
$periods = [30, 90, 180, 365]; $periods = [30, 90, 180, 365];
@@ -17,6 +17,8 @@ $periodLabels = [
<div class="app-details-container" <div class="app-details-container"
data-team-workload-url="<?php e(lurl('helpdesk/team-workload-data')); ?>" data-team-workload-url="<?php e(lurl('helpdesk/team-workload-data')); ?>"
data-label-not-assigned="<?php e(t('Not assigned')); ?>" data-label-not-assigned="<?php e(t('Not assigned')); ?>"
data-label-queue="<?php e(t('Queue')); ?>"
data-label-tickets="<?php e(t('Tickets')); ?>"
data-label-error="<?php e(t('Could not load team dashboard.')); ?>" data-label-error="<?php e(t('Could not load team dashboard.')); ?>"
data-label-empty="<?php e(t('No team data available.')); ?>" data-label-empty="<?php e(t('No team data available.')); ?>"
> >
@@ -41,21 +43,8 @@ $periodLabels = [
<div class="helpdesk-team-dashboard"> <div class="helpdesk-team-dashboard">
<div id="team-loading" aria-busy="true"> <div id="team-loading" aria-busy="true">
<div class="helpdesk-support-metrics"> <?php for ($i = 0; $i < 3; $i++): ?>
<?php for ($i = 0; $i < 3; $i++): ?> <div class="helpdesk-skeleton" style="height: 5rem; margin-bottom: 0.75rem; border-radius: var(--app-radius, 0.375rem);"></div>
<div class="helpdesk-skeleton-kpi">
<div class="helpdesk-skeleton helpdesk-skeleton-kpi-label"></div>
<div class="helpdesk-skeleton helpdesk-skeleton-kpi-value"></div>
</div>
<?php endfor; ?>
</div>
<?php for ($i = 0; $i < 4; $i++): ?>
<div class="helpdesk-skeleton-row">
<div class="helpdesk-skeleton helpdesk-skeleton-cell"></div>
<div class="helpdesk-skeleton helpdesk-skeleton-cell helpdesk-skeleton-cell-narrow"></div>
<div class="helpdesk-skeleton helpdesk-skeleton-cell helpdesk-skeleton-cell-narrow"></div>
<div class="helpdesk-skeleton helpdesk-skeleton-cell helpdesk-skeleton-cell-narrow"></div>
</div>
<?php endfor; ?> <?php endfor; ?>
</div> </div>
@@ -75,33 +64,7 @@ $periodLabels = [
</div> </div>
<div data-tab-panel="current"> <div data-tab-panel="current">
<div class="helpdesk-support-metrics" id="team-kpis-current"> <div id="team-current-cards"></div>
<article class="helpdesk-support-metric">
<p class="helpdesk-support-metric-label"><?php e(t('Open tickets')); ?></p>
<p class="helpdesk-support-metric-value" id="team-kpi-open"></p>
</article>
<article class="helpdesk-support-metric">
<p class="helpdesk-support-metric-label"><?php e(t('Unassigned')); ?></p>
<p class="helpdesk-support-metric-value" id="team-kpi-unassigned"></p>
</article>
<article class="helpdesk-support-metric">
<p class="helpdesk-support-metric-label"><?php e(t('Avg. age (h)')); ?></p>
<p class="helpdesk-support-metric-value" id="team-kpi-avg-age"></p>
</article>
</div>
<table class="helpdesk-team-table">
<thead>
<tr>
<th scope="col"><?php e(t('Support agent')); ?></th>
<th scope="col"><?php e(t('Open')); ?></th>
<th scope="col"><?php e(t('Critical (>48h open)')); ?></th>
<th scope="col"><?php e(t('Avg. age (h)')); ?></th>
</tr>
</thead>
<tbody id="team-table-current">
</tbody>
</table>
</div> </div>
<div data-tab-panel="performance" class="helpdesk-team-performance-panel"> <div data-tab-panel="performance" class="helpdesk-team-performance-panel">
@@ -112,23 +75,7 @@ $periodLabels = [
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<div class="helpdesk-support-metrics" id="team-kpis-performance"> <div id="team-performance-cards"></div>
<article class="helpdesk-support-metric">
<p class="helpdesk-support-metric-label"><?php e(t('Resolved in period')); ?></p>
<p class="helpdesk-support-metric-value" id="team-kpi-resolved"></p>
</article>
</div>
<table class="helpdesk-team-table">
<thead>
<tr>
<th scope="col"><?php e(t('Support agent')); ?></th>
<th scope="col"><?php e(t('Resolved in period')); ?></th>
</tr>
</thead>
<tbody id="team-table-performance">
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -126,114 +126,141 @@
} }
} }
/* Team workload dashboard */ /* Team workload dashboard — agent cards */
.helpdesk-team-table { .helpdesk-team-card {
width: 100%; border: 1px solid var(--app-muted-border-color);
margin-top: calc(var(--app-spacing) * 0.75); border-radius: var(--app-radius, 0.375rem);
border-collapse: separate; padding: calc(var(--app-spacing) * 0.65) calc(var(--app-spacing) * 0.75);
border-spacing: 0; margin-bottom: calc(var(--app-spacing) * 0.5);
} }
.helpdesk-team-table th { .helpdesk-team-card-queue {
text-align: left; border-style: dashed;
padding: calc(var(--app-spacing) * 0.3) calc(var(--app-spacing) * 0.5); background: color-mix(in srgb, var(--app-muted-border-color) 15%, transparent);
}
.helpdesk-team-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: calc(var(--app-spacing) * 0.5);
margin-bottom: calc(var(--app-spacing) * 0.35);
}
.helpdesk-team-card-name {
font-weight: 600;
font-size: var(--text-sm, 0.875rem); font-size: var(--text-sm, 0.875rem);
}
.helpdesk-team-card-queue .helpdesk-team-card-name {
font-weight: 500; font-weight: 500;
color: var(--app-muted-color); color: var(--app-muted-color);
border-bottom: 1px solid var(--app-muted-border-color);
} }
.helpdesk-team-table td { .helpdesk-team-card-count {
text-align: left; font-variant-numeric: tabular-nums;
padding: calc(var(--app-spacing) * 0.5) calc(var(--app-spacing) * 0.5); font-weight: 600;
border-bottom: 1px solid color-mix(in srgb, var(--app-muted-border-color) 50%, transparent); font-size: var(--text-sm, 0.875rem);
padding: 0.1em 0.55em;
border-radius: 999px;
background: color-mix(in srgb, var(--app-primary, #0d6efd) 12%, transparent);
color: var(--app-primary, #0d6efd);
} }
.helpdesk-team-table th:not(:first-child), .helpdesk-team-card-count-warning {
.helpdesk-team-table td:not(:first-child) { background: color-mix(in srgb, var(--app-warning, #f59e0b) 15%, transparent);
text-align: right; color: var(--app-warning, #f59e0b);
} }
.helpdesk-team-table tbody tr:last-child td { .helpdesk-team-card-count-success {
border-bottom: none; background: color-mix(in srgb, var(--app-success, #198754) 12%, transparent);
color: var(--app-success, #198754);
} }
.helpdesk-team-agent-name { .helpdesk-team-card-bar-track {
height: 4px;
border-radius: 999px;
background: color-mix(in srgb, var(--app-muted-border-color) 60%, transparent);
margin-bottom: calc(var(--app-spacing) * 0.4);
}
.helpdesk-team-card-bar-fill {
height: 100%;
border-radius: 999px;
background: var(--app-primary, #0d6efd);
transition: width 0.3s ease;
min-width: 2px;
}
.helpdesk-team-card-bar-muted {
background: var(--app-muted-color);
opacity: 0.4;
}
.helpdesk-team-card-bar-success {
background: var(--app-success, #198754);
}
.helpdesk-team-card-tickets {
list-style: none;
margin: 0;
padding: 0;
}
.helpdesk-team-card-ticket {
display: flex;
align-items: baseline;
gap: calc(var(--app-spacing) * 0.4);
padding: calc(var(--app-spacing) * 0.2) 0;
font-size: var(--text-sm, 0.875rem);
border-top: 1px solid color-mix(in srgb, var(--app-muted-border-color) 40%, transparent);
}
.helpdesk-team-card-ticket:first-child {
border-top: none;
}
.helpdesk-team-card-ticket-no {
font-variant-numeric: tabular-nums;
font-weight: 500; font-weight: 500;
}
.helpdesk-team-row-unassigned {
font-style: italic;
}
.helpdesk-team-row-unassigned .helpdesk-team-agent-name {
font-weight: 400;
color: var(--app-muted-color); color: var(--app-muted-color);
flex-shrink: 0;
}
.helpdesk-team-card-ticket-desc {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.helpdesk-team-card-ticket-age {
flex-shrink: 0;
font-variant-numeric: tabular-nums;
color: var(--app-muted-color);
}
.helpdesk-team-card-ticket-critical .helpdesk-team-card-ticket-age {
color: var(--app-warning, #f59e0b);
font-weight: 600;
}
.helpdesk-team-card-ticket-critical .helpdesk-team-card-ticket-no {
color: var(--app-warning, #f59e0b);
}
.helpdesk-team-card-compact {
padding: calc(var(--app-spacing) * 0.5) calc(var(--app-spacing) * 0.75);
}
.helpdesk-team-card-compact .helpdesk-team-card-bar-track {
margin-bottom: 0;
} }
.helpdesk-team-performance-panel > .helpdesk-segment-control { .helpdesk-team-performance-panel > .helpdesk-segment-control {
margin-bottom: calc(var(--app-spacing) * 0.75); margin-bottom: calc(var(--app-spacing) * 0.75);
} }
/* Muted zero values — de-emphasize non-information */
.helpdesk-team-muted {
color: var(--app-muted-color);
opacity: 0.5;
}
/* Inline proportional bar — Stripe-style visual comparison */
.helpdesk-team-bar-cell {
display: inline-flex;
align-items: center;
gap: calc(var(--app-spacing) * 0.35);
min-width: 5rem;
justify-content: flex-end;
}
.helpdesk-team-bar {
height: 0.5rem;
border-radius: 999px;
flex-shrink: 0;
min-width: 2px;
width: 0;
transition: width 0.3s ease;
}
.helpdesk-team-bar-primary {
background: var(--app-primary, #0d6efd);
}
.helpdesk-team-bar-success {
background: var(--app-success, #198754);
}
.helpdesk-team-bar-muted {
background: var(--app-muted-color);
opacity: 0.35;
}
.helpdesk-team-bar-value {
font-variant-numeric: tabular-nums;
min-width: 2ch;
text-align: right;
}
/* Critical badge — draws attention to non-zero values */
.helpdesk-team-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5em;
padding: 0.1em 0.45em;
font-size: var(--text-sm, 0.875rem);
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1.3;
border-radius: 999px;
background: color-mix(in srgb, var(--app-warning, #f59e0b) 15%, transparent);
color: var(--app-warning, #f59e0b);
}
/* Clickable rows — shared between search results and ticket list */ /* Clickable rows — shared between search results and ticket list */
.app-clickable-row { .app-clickable-row {
cursor: pointer; cursor: pointer;

View File

@@ -1,20 +1,22 @@
/** /**
* Team workload dashboard — fetches and renders team metrics. * Team workload dashboard — visual workload map.
* Tab "Current": snapshot of open tickets per agent. * Current tab: agent cards with load bars and ticket details.
* Tab "Performance": resolved tickets per agent in a selectable period. * Performance tab: resolved-count cards per agent.
*/ */
const container = document.querySelector('.app-details-container[data-team-workload-url]'); const container = document.querySelector('.app-details-container[data-team-workload-url]');
if (container) { if (container) {
const dataUrl = container.dataset.teamWorkloadUrl; const dataUrl = container.dataset.teamWorkloadUrl;
const labelNotAssigned = container.dataset.labelNotAssigned || '—'; const labelNotAssigned = container.dataset.labelNotAssigned || '—';
const labelQueue = container.dataset.labelQueue || 'Queue';
const labelTickets = container.dataset.labelTickets || 'Tickets';
const elLoading = document.getElementById('team-loading'); const elLoading = document.getElementById('team-loading');
const elError = document.getElementById('team-error'); const elError = document.getElementById('team-error');
const elEmpty = document.getElementById('team-empty'); const elEmpty = document.getElementById('team-empty');
const elContent = document.getElementById('team-content'); const elContent = document.getElementById('team-content');
const elCurrentBody = document.getElementById('team-table-current'); const elCurrentCards = document.getElementById('team-current-cards');
const elPerformanceBody = document.getElementById('team-table-performance'); const elPerformanceCards = document.getElementById('team-performance-cards');
const elRefresh = container.querySelector('button[name="team-refresh"]'); const elRefresh = container.querySelector('button[name="team-refresh"]');
const periodSelector = document.getElementById('team-period-selector'); const periodSelector = document.getElementById('team-period-selector');
@@ -28,110 +30,113 @@ if (container) {
if (elLoading) elLoading.setAttribute('aria-busy', state === 'loading' ? 'true' : 'false'); if (elLoading) elLoading.setAttribute('aria-busy', state === 'loading' ? 'true' : 'false');
} }
function setText(id, value) { function el(tag, className, text) {
const el = document.getElementById(id); const e = document.createElement(tag);
if (el) el.textContent = value; if (className) e.className = className;
if (text !== undefined) e.textContent = text;
return e;
} }
function agentName(m) { function formatAge(hours) {
return m.support_user === '' ? labelNotAssigned : m.display_name; if (hours < 24) return hours + 'h';
const days = Math.floor(hours / 24);
return days + 'd';
} }
/** /**
* Create a table cell with an inline proportional bar behind the value. * Build a single agent card for the Current tab.
* Stripe-style: the bar gives instant visual comparison across rows.
*/ */
function barCell(value, max, variant) { function buildAgentCard(m, maxOpen) {
const td = document.createElement('td'); const isQueue = m.support_user === '';
const pct = max > 0 ? Math.round((value / max) * 100) : 0; const name = isQueue ? labelQueue : m.display_name;
const pct = maxOpen > 0 ? Math.round((m.open_tickets / maxOpen) * 100) : 0;
const wrap = document.createElement('span'); const card = el('article', 'helpdesk-team-card');
wrap.className = 'helpdesk-team-bar-cell'; if (isQueue) card.classList.add('helpdesk-team-card-queue');
const bar = document.createElement('span'); // Header row: name + count badge
bar.className = 'helpdesk-team-bar'; const header = el('div', 'helpdesk-team-card-header');
if (variant) bar.classList.add('helpdesk-team-bar-' + variant); const nameEl = el('span', 'helpdesk-team-card-name', name);
bar.style.width = pct + '%'; const countEl = el('span', 'helpdesk-team-card-count', String(m.open_tickets));
if (m.critical_tickets > 0) countEl.classList.add('helpdesk-team-card-count-warning');
header.appendChild(nameEl);
header.appendChild(countEl);
card.appendChild(header);
const label = document.createElement('span'); // Load bar
label.className = 'helpdesk-team-bar-value'; const barTrack = el('div', 'helpdesk-team-card-bar-track');
label.textContent = String(value); const barFill = el('div', 'helpdesk-team-card-bar-fill');
if (value === 0) label.classList.add('helpdesk-team-muted'); if (isQueue) barFill.classList.add('helpdesk-team-card-bar-muted');
barFill.style.width = pct + '%';
barTrack.appendChild(barFill);
card.appendChild(barTrack);
wrap.appendChild(bar); // Ticket list
wrap.appendChild(label); const tickets = m.open_ticket_details || [];
td.appendChild(wrap); if (tickets.length > 0) {
return td; const list = el('ul', 'helpdesk-team-card-tickets');
} for (const t of tickets) {
const li = el('li', 'helpdesk-team-card-ticket');
if (t.critical) li.classList.add('helpdesk-team-card-ticket-critical');
/** const no = el('span', 'helpdesk-team-card-ticket-no', t.no);
* Create a badge cell — small pill for critical counts. const desc = el('span', 'helpdesk-team-card-ticket-desc', t.description || '—');
* 0 = muted text, >0 = warning badge. const age = el('span', 'helpdesk-team-card-ticket-age', formatAge(t.age_hours));
*/
function badgeCell(value) { li.appendChild(no);
const td = document.createElement('td'); li.appendChild(desc);
if (value === 0) { li.appendChild(age);
td.textContent = '0'; list.appendChild(li);
td.classList.add('helpdesk-team-muted'); }
} else { card.appendChild(list);
const badge = document.createElement('span');
badge.className = 'helpdesk-team-badge';
badge.textContent = String(value);
td.appendChild(badge);
} }
return td;
return card;
} }
/** /**
* Create a plain text cell, muted when value is 0. * Build a single agent card for the Performance tab.
*/ */
function valueCell(value, unit) { function buildPerformanceCard(m, maxResolved) {
const td = document.createElement('td'); const isQueue = m.support_user === '';
td.textContent = unit ? value + unit : String(value); const name = isQueue ? labelQueue : m.display_name;
if (value === 0) td.classList.add('helpdesk-team-muted'); const pct = maxResolved > 0 ? Math.round((m.resolved_in_period / maxResolved) * 100) : 0;
return td;
const card = el('article', 'helpdesk-team-card helpdesk-team-card-compact');
if (isQueue) card.classList.add('helpdesk-team-card-queue');
const header = el('div', 'helpdesk-team-card-header');
header.appendChild(el('span', 'helpdesk-team-card-name', name));
header.appendChild(el('span', 'helpdesk-team-card-count helpdesk-team-card-count-success', String(m.resolved_in_period)));
card.appendChild(header);
const barTrack = el('div', 'helpdesk-team-card-bar-track');
const barFill = el('div', 'helpdesk-team-card-bar-fill helpdesk-team-card-bar-success');
barFill.style.width = pct + '%';
barTrack.appendChild(barFill);
card.appendChild(barTrack);
return card;
} }
function renderCurrentTab(kpis, members) { function renderCurrentTab(members) {
setText('team-kpi-open', kpis.open_tickets ?? 0); if (!elCurrentCards) return;
setText('team-kpi-unassigned', kpis.unassigned ?? 0); elCurrentCards.replaceChildren();
setText('team-kpi-avg-age', kpis.avg_age_hours ?? 0);
if (!elCurrentBody) return;
elCurrentBody.replaceChildren();
const visible = members.filter(m => m.open_tickets > 0 || m.critical_tickets > 0); const visible = members.filter(m => m.open_tickets > 0 || m.critical_tickets > 0);
const maxOpen = Math.max(1, ...visible.map(m => m.open_tickets)); const maxOpen = Math.max(1, ...visible.map(m => m.open_tickets));
for (const m of visible) { // Queue (unassigned) first if present
const tr = document.createElement('tr'); const queue = visible.find(m => m.support_user === '');
const isUnassigned = m.support_user === ''; const agents = visible.filter(m => m.support_user !== '');
// Agent name if (queue) elCurrentCards.appendChild(buildAgentCard(queue, maxOpen));
const nameCell = document.createElement('td'); for (const m of agents) elCurrentCards.appendChild(buildAgentCard(m, maxOpen));
nameCell.className = 'helpdesk-team-agent-name';
nameCell.textContent = agentName(m);
tr.appendChild(nameCell);
// Open tickets with proportional bar
tr.appendChild(barCell(m.open_tickets, maxOpen, isUnassigned ? 'muted' : 'primary'));
// Critical with badge
tr.appendChild(badgeCell(m.critical_tickets));
// Avg age
tr.appendChild(valueCell(m.avg_age_hours, 'h'));
if (isUnassigned) tr.classList.add('helpdesk-team-row-unassigned');
elCurrentBody.appendChild(tr);
}
} }
function renderPerformanceTab(kpis, members) { function renderPerformanceTab(members) {
setText('team-kpi-resolved', kpis.resolved_in_period ?? 0); if (!elPerformanceCards) return;
elPerformanceCards.replaceChildren();
if (!elPerformanceBody) return;
elPerformanceBody.replaceChildren();
const sorted = [...members] const sorted = [...members]
.filter(m => m.resolved_in_period > 0) .filter(m => m.resolved_in_period > 0)
@@ -142,26 +147,7 @@ if (container) {
}); });
const maxResolved = Math.max(1, ...sorted.map(m => m.resolved_in_period)); const maxResolved = Math.max(1, ...sorted.map(m => m.resolved_in_period));
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceCard(m, maxResolved));
for (const m of sorted) {
const tr = document.createElement('tr');
const isUnassigned = m.support_user === '';
const nameCell = document.createElement('td');
nameCell.className = 'helpdesk-team-agent-name';
nameCell.textContent = agentName(m);
tr.appendChild(nameCell);
tr.appendChild(barCell(m.resolved_in_period, maxResolved, isUnassigned ? 'muted' : 'success'));
if (isUnassigned) tr.classList.add('helpdesk-team-row-unassigned');
elPerformanceBody.appendChild(tr);
}
}
function renderAll(data) {
renderCurrentTab(data.kpis, data.members);
renderPerformanceTab(data.kpis, data.members);
} }
async function fetchData(refresh = false) { async function fetchData(refresh = false) {
@@ -184,7 +170,8 @@ if (container) {
return; return;
} }
renderAll(json); renderCurrentTab(json.members);
renderPerformanceTab(json.members);
showState('success'); showState('success');
} catch { } catch {
showState('error'); showState('error');