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:
@@ -296,5 +296,6 @@
|
||||
"Could not load team dashboard.": "Team-Dashboard konnte nicht geladen werden.",
|
||||
"No team data available.": "Keine Team-Daten verfügbar.",
|
||||
"Current": "Aktuell",
|
||||
"Performance": "Leistung"
|
||||
"Performance": "Leistung",
|
||||
"Queue": "Warteschlange"
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Could not load team dashboard.": "Could not load team dashboard.",
|
||||
"No team data available.": "No team data available.",
|
||||
"Current": "Current",
|
||||
"Performance": "Performance"
|
||||
"Performance": "Performance",
|
||||
"Queue": "Queue"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Team workload dashboard — shows support agent metrics aggregated from BC tickets.
|
||||
* Tab 1 "Current": snapshot of open tickets per agent (no period filter).
|
||||
* Tab 2 "Performance": resolved tickets per agent in a selectable period.
|
||||
* Team workload dashboard — visual workload map for support team leads.
|
||||
* Tab "Current": agent cards with open tickets, queue at top.
|
||||
* Tab "Performance": resolved per agent in a selectable period.
|
||||
*/
|
||||
|
||||
$periods = [30, 90, 180, 365];
|
||||
@@ -17,6 +17,8 @@ $periodLabels = [
|
||||
<div class="app-details-container"
|
||||
data-team-workload-url="<?php e(lurl('helpdesk/team-workload-data')); ?>"
|
||||
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-empty="<?php e(t('No team data available.')); ?>"
|
||||
>
|
||||
@@ -41,21 +43,8 @@ $periodLabels = [
|
||||
|
||||
<div class="helpdesk-team-dashboard">
|
||||
<div id="team-loading" aria-busy="true">
|
||||
<div class="helpdesk-support-metrics">
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<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 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>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
@@ -75,33 +64,7 @@ $periodLabels = [
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="current">
|
||||
<div class="helpdesk-support-metrics" id="team-kpis-current">
|
||||
<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 id="team-current-cards"></div>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="performance" class="helpdesk-team-performance-panel">
|
||||
@@ -112,23 +75,7 @@ $periodLabels = [
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="helpdesk-support-metrics" id="team-kpis-performance">
|
||||
<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 id="team-performance-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,114 +126,141 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Team workload dashboard */
|
||||
.helpdesk-team-table {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--app-spacing) * 0.75);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
/* Team workload dashboard — agent cards */
|
||||
.helpdesk-team-card {
|
||||
border: 1px solid var(--app-muted-border-color);
|
||||
border-radius: var(--app-radius, 0.375rem);
|
||||
padding: calc(var(--app-spacing) * 0.65) calc(var(--app-spacing) * 0.75);
|
||||
margin-bottom: calc(var(--app-spacing) * 0.5);
|
||||
}
|
||||
|
||||
.helpdesk-team-table th {
|
||||
text-align: left;
|
||||
padding: calc(var(--app-spacing) * 0.3) calc(var(--app-spacing) * 0.5);
|
||||
.helpdesk-team-card-queue {
|
||||
border-style: dashed;
|
||||
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);
|
||||
}
|
||||
|
||||
.helpdesk-team-card-queue .helpdesk-team-card-name {
|
||||
font-weight: 500;
|
||||
color: var(--app-muted-color);
|
||||
border-bottom: 1px solid var(--app-muted-border-color);
|
||||
}
|
||||
|
||||
.helpdesk-team-table td {
|
||||
text-align: left;
|
||||
padding: calc(var(--app-spacing) * 0.5) calc(var(--app-spacing) * 0.5);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--app-muted-border-color) 50%, transparent);
|
||||
.helpdesk-team-card-count {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
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-table td:not(:first-child) {
|
||||
text-align: right;
|
||||
.helpdesk-team-card-count-warning {
|
||||
background: color-mix(in srgb, var(--app-warning, #f59e0b) 15%, transparent);
|
||||
color: var(--app-warning, #f59e0b);
|
||||
}
|
||||
|
||||
.helpdesk-team-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
.helpdesk-team-card-count-success {
|
||||
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;
|
||||
}
|
||||
|
||||
.helpdesk-team-row-unassigned {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.helpdesk-team-row-unassigned .helpdesk-team-agent-name {
|
||||
font-weight: 400;
|
||||
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 {
|
||||
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 */
|
||||
.app-clickable-row {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/**
|
||||
* Team workload dashboard — fetches and renders team metrics.
|
||||
* Tab "Current": snapshot of open tickets per agent.
|
||||
* Tab "Performance": resolved tickets per agent in a selectable period.
|
||||
* Team workload dashboard — visual workload map.
|
||||
* Current tab: agent cards with load bars and ticket details.
|
||||
* Performance tab: resolved-count cards per agent.
|
||||
*/
|
||||
|
||||
const container = document.querySelector('.app-details-container[data-team-workload-url]');
|
||||
if (container) {
|
||||
const dataUrl = container.dataset.teamWorkloadUrl;
|
||||
const labelNotAssigned = container.dataset.labelNotAssigned || '—';
|
||||
const labelQueue = container.dataset.labelQueue || 'Queue';
|
||||
const labelTickets = container.dataset.labelTickets || 'Tickets';
|
||||
|
||||
const elLoading = document.getElementById('team-loading');
|
||||
const elError = document.getElementById('team-error');
|
||||
const elEmpty = document.getElementById('team-empty');
|
||||
const elContent = document.getElementById('team-content');
|
||||
const elCurrentBody = document.getElementById('team-table-current');
|
||||
const elPerformanceBody = document.getElementById('team-table-performance');
|
||||
const elCurrentCards = document.getElementById('team-current-cards');
|
||||
const elPerformanceCards = document.getElementById('team-performance-cards');
|
||||
const elRefresh = container.querySelector('button[name="team-refresh"]');
|
||||
const periodSelector = document.getElementById('team-period-selector');
|
||||
|
||||
@@ -28,110 +30,113 @@ if (container) {
|
||||
if (elLoading) elLoading.setAttribute('aria-busy', state === 'loading' ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value;
|
||||
function el(tag, className, text) {
|
||||
const e = document.createElement(tag);
|
||||
if (className) e.className = className;
|
||||
if (text !== undefined) e.textContent = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
function agentName(m) {
|
||||
return m.support_user === '' ? labelNotAssigned : m.display_name;
|
||||
function formatAge(hours) {
|
||||
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.
|
||||
* Stripe-style: the bar gives instant visual comparison across rows.
|
||||
* Build a single agent card for the Current tab.
|
||||
*/
|
||||
function barCell(value, max, variant) {
|
||||
const td = document.createElement('td');
|
||||
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
||||
function buildAgentCard(m, maxOpen) {
|
||||
const isQueue = m.support_user === '';
|
||||
const name = isQueue ? labelQueue : m.display_name;
|
||||
const pct = maxOpen > 0 ? Math.round((m.open_tickets / maxOpen) * 100) : 0;
|
||||
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'helpdesk-team-bar-cell';
|
||||
const card = el('article', 'helpdesk-team-card');
|
||||
if (isQueue) card.classList.add('helpdesk-team-card-queue');
|
||||
|
||||
const bar = document.createElement('span');
|
||||
bar.className = 'helpdesk-team-bar';
|
||||
if (variant) bar.classList.add('helpdesk-team-bar-' + variant);
|
||||
bar.style.width = pct + '%';
|
||||
// Header row: name + count badge
|
||||
const header = el('div', 'helpdesk-team-card-header');
|
||||
const nameEl = el('span', 'helpdesk-team-card-name', name);
|
||||
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');
|
||||
label.className = 'helpdesk-team-bar-value';
|
||||
label.textContent = String(value);
|
||||
if (value === 0) label.classList.add('helpdesk-team-muted');
|
||||
// Load bar
|
||||
const barTrack = el('div', 'helpdesk-team-card-bar-track');
|
||||
const barFill = el('div', 'helpdesk-team-card-bar-fill');
|
||||
if (isQueue) barFill.classList.add('helpdesk-team-card-bar-muted');
|
||||
barFill.style.width = pct + '%';
|
||||
barTrack.appendChild(barFill);
|
||||
card.appendChild(barTrack);
|
||||
|
||||
wrap.appendChild(bar);
|
||||
wrap.appendChild(label);
|
||||
td.appendChild(wrap);
|
||||
return td;
|
||||
}
|
||||
// Ticket list
|
||||
const tickets = m.open_ticket_details || [];
|
||||
if (tickets.length > 0) {
|
||||
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');
|
||||
|
||||
/**
|
||||
* Create a badge cell — small pill for critical counts.
|
||||
* 0 = muted text, >0 = warning badge.
|
||||
*/
|
||||
function badgeCell(value) {
|
||||
const td = document.createElement('td');
|
||||
if (value === 0) {
|
||||
td.textContent = '0';
|
||||
td.classList.add('helpdesk-team-muted');
|
||||
} else {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'helpdesk-team-badge';
|
||||
badge.textContent = String(value);
|
||||
td.appendChild(badge);
|
||||
const no = el('span', 'helpdesk-team-card-ticket-no', t.no);
|
||||
const desc = el('span', 'helpdesk-team-card-ticket-desc', t.description || '—');
|
||||
const age = el('span', 'helpdesk-team-card-ticket-age', formatAge(t.age_hours));
|
||||
|
||||
li.appendChild(no);
|
||||
li.appendChild(desc);
|
||||
li.appendChild(age);
|
||||
list.appendChild(li);
|
||||
}
|
||||
card.appendChild(list);
|
||||
}
|
||||
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) {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = unit ? value + unit : String(value);
|
||||
if (value === 0) td.classList.add('helpdesk-team-muted');
|
||||
return td;
|
||||
function buildPerformanceCard(m, maxResolved) {
|
||||
const isQueue = m.support_user === '';
|
||||
const name = isQueue ? labelQueue : m.display_name;
|
||||
const pct = maxResolved > 0 ? Math.round((m.resolved_in_period / maxResolved) * 100) : 0;
|
||||
|
||||
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) {
|
||||
setText('team-kpi-open', kpis.open_tickets ?? 0);
|
||||
setText('team-kpi-unassigned', kpis.unassigned ?? 0);
|
||||
setText('team-kpi-avg-age', kpis.avg_age_hours ?? 0);
|
||||
|
||||
if (!elCurrentBody) return;
|
||||
elCurrentBody.replaceChildren();
|
||||
function renderCurrentTab(members) {
|
||||
if (!elCurrentCards) return;
|
||||
elCurrentCards.replaceChildren();
|
||||
|
||||
const visible = members.filter(m => m.open_tickets > 0 || m.critical_tickets > 0);
|
||||
const maxOpen = Math.max(1, ...visible.map(m => m.open_tickets));
|
||||
|
||||
for (const m of visible) {
|
||||
const tr = document.createElement('tr');
|
||||
const isUnassigned = m.support_user === '';
|
||||
// Queue (unassigned) first if present
|
||||
const queue = visible.find(m => m.support_user === '');
|
||||
const agents = visible.filter(m => m.support_user !== '');
|
||||
|
||||
// Agent name
|
||||
const nameCell = document.createElement('td');
|
||||
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);
|
||||
}
|
||||
if (queue) elCurrentCards.appendChild(buildAgentCard(queue, maxOpen));
|
||||
for (const m of agents) elCurrentCards.appendChild(buildAgentCard(m, maxOpen));
|
||||
}
|
||||
|
||||
function renderPerformanceTab(kpis, members) {
|
||||
setText('team-kpi-resolved', kpis.resolved_in_period ?? 0);
|
||||
|
||||
if (!elPerformanceBody) return;
|
||||
elPerformanceBody.replaceChildren();
|
||||
function renderPerformanceTab(members) {
|
||||
if (!elPerformanceCards) return;
|
||||
elPerformanceCards.replaceChildren();
|
||||
|
||||
const sorted = [...members]
|
||||
.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));
|
||||
|
||||
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);
|
||||
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceCard(m, maxResolved));
|
||||
}
|
||||
|
||||
async function fetchData(refresh = false) {
|
||||
@@ -184,7 +170,8 @@ if (container) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderAll(json);
|
||||
renderCurrentTab(json.members);
|
||||
renderPerformanceTab(json.members);
|
||||
showState('success');
|
||||
} catch {
|
||||
showState('error');
|
||||
|
||||
Reference in New Issue
Block a user