feat(helpdesk): redesign Historical tab with per-agent performance widgets

Each agent gets a card with three insight columns:
- Resolution time: Ø / Min / Max (formatted as hours or days)
- Top 3 customers by resolved ticket count (ranked list)
- Top 3 categories by resolved ticket count (ranked list)

Backend collects resolved ticket details per agent: customer names,
categories, and resolution hours. Aggregates top-3 and min/max/avg.
Replaces the old compact single-row layout with rich widget cards
matching the Current tab pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 20:11:44 +02:00
parent 7a574768db
commit 4ce7a32bfc
6 changed files with 211 additions and 38 deletions

View File

@@ -300,39 +300,93 @@
background: var(--app-warning, #f59e0b);
}
/* Performance tab — compact rows */
.helpdesk-team-perf-row {
display: flex;
align-items: center;
gap: calc(var(--app-spacing) * 0.5);
padding: calc(var(--app-spacing) * 0.35) 0;
border-bottom: 1px solid color-mix(in srgb, var(--app-muted-border-color) 40%, transparent);
font-size: var(--text-sm, 0.875rem);
}
.helpdesk-team-perf-row:last-child {
border-bottom: none;
}
.helpdesk-team-perf-name {
flex: 1;
font-weight: 500;
}
.helpdesk-team-perf-count {
font-variant-numeric: tabular-nums;
font-weight: 600;
font-size: 0.75rem;
padding: 0.1em 0.5em;
border-radius: 999px;
background: color-mix(in srgb, var(--app-success, #198754) 12%, transparent);
color: var(--app-success, #198754);
}
.helpdesk-team-performance-panel > .helpdesk-segment-control {
margin-bottom: calc(var(--app-spacing) * 0.75);
}
/* Performance insights grid inside agent widget */
.helpdesk-team-perf-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: calc(var(--app-spacing) * 0.75);
padding: calc(var(--app-spacing) * 0.65) calc(var(--app-spacing) * 0.75);
}
@media (max-width: 760px) {
.helpdesk-team-perf-grid {
grid-template-columns: minmax(0, 1fr);
}
}
.helpdesk-team-perf-stat-label {
display: block;
font-size: var(--text-xs, 0.75rem);
font-weight: 600;
color: var(--app-muted-color);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: calc(var(--app-spacing) * 0.3);
}
.helpdesk-team-perf-times {
display: flex;
gap: calc(var(--app-spacing) * 0.75);
}
.helpdesk-team-perf-time {
display: flex;
flex-direction: column;
}
.helpdesk-team-perf-time-value {
font-size: clamp(1rem, 0.9rem + 0.3vw, 1.2rem);
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.helpdesk-team-perf-time-label {
font-size: var(--text-xs, 0.75rem);
color: var(--app-muted-color);
}
.helpdesk-team-perf-ranked {
list-style: none;
margin: 0;
padding: 0;
counter-reset: rank;
}
.helpdesk-team-perf-ranked li {
display: flex;
align-items: baseline;
gap: calc(var(--app-spacing) * 0.3);
padding: calc(var(--app-spacing) * 0.15) 0;
font-size: var(--text-sm, 0.875rem);
counter-increment: rank;
}
.helpdesk-team-perf-ranked li::before {
content: counter(rank) ".";
color: var(--app-muted-color);
font-size: var(--text-xs, 0.75rem);
font-variant-numeric: tabular-nums;
min-width: 1.5ch;
}
.helpdesk-team-perf-ranked-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.helpdesk-team-perf-ranked-count {
font-variant-numeric: tabular-nums;
color: var(--app-muted-color);
font-size: var(--text-xs, 0.75rem);
}
/* Clickable rows — shared between search results and ticket list */
.app-clickable-row {
cursor: pointer;

View File

@@ -160,19 +160,86 @@ if (container) {
return section;
}
function formatResolution(hours) {
if (hours === null || hours === undefined) return '—';
if (hours < 1) return '< 1h';
if (hours < 24) return hours + 'h';
const d = Math.floor(hours / 24);
const remainder = hours % 24;
return remainder > 0 ? d + 'd ' + remainder + 'h' : d + 'd';
}
/**
* Build a compact row for the performance tab.
* Build a small ranked list (top customers / top categories).
*/
function buildPerformanceRow(m) {
const isQueue = m.support_user === '';
const section = h('div', 'helpdesk-team-perf-row');
function buildRankedList(items, label) {
if (!items || items.length === 0) return null;
if (!isQueue) {
section.appendChild(h('span', 'helpdesk-team-avatar-inline', initials(m.display_name)));
const wrap = h('div', 'helpdesk-team-perf-stat');
wrap.appendChild(h('span', 'helpdesk-team-perf-stat-label', label));
const list = h('ol', 'helpdesk-team-perf-ranked');
for (const item of items) {
const li = h('li');
li.appendChild(h('span', 'helpdesk-team-perf-ranked-name', item.name));
li.appendChild(h('span', 'helpdesk-team-perf-ranked-count', String(item.count)));
list.appendChild(li);
}
section.appendChild(h('span', 'helpdesk-team-perf-name', isQueue ? labelQueue : m.display_name));
section.appendChild(h('span', 'helpdesk-team-perf-count', String(m.resolved_in_period)));
wrap.appendChild(list);
return wrap;
}
/**
* Build a performance widget for one agent — card with insights.
*/
function buildPerformanceWidget(m) {
const section = h('section', 'helpdesk-team-widget');
const perf = m.performance || {};
// Header: avatar + name + resolved count
const title = h('h3', 'helpdesk-support-section-title');
if (m.support_user !== '') {
title.appendChild(h('span', 'helpdesk-team-avatar-inline', initials(m.display_name)));
}
title.appendChild(document.createTextNode(m.support_user === '' ? labelQueue : m.display_name));
title.appendChild(h('span', 'helpdesk-team-title-count', String(m.resolved_in_period)));
section.appendChild(title);
// Stats grid
const grid = h('div', 'helpdesk-team-perf-grid');
// Resolution time KPIs
const timeStats = h('div', 'helpdesk-team-perf-stat');
timeStats.appendChild(h('span', 'helpdesk-team-perf-stat-label', container.dataset.labelResolutionTime || 'Resolution time'));
const times = h('div', 'helpdesk-team-perf-times');
const avgEl = h('div', 'helpdesk-team-perf-time');
avgEl.appendChild(h('span', 'helpdesk-team-perf-time-value', formatResolution(perf.avg_resolution_hours)));
avgEl.appendChild(h('span', 'helpdesk-team-perf-time-label', 'Ø'));
times.appendChild(avgEl);
const minEl = h('div', 'helpdesk-team-perf-time');
minEl.appendChild(h('span', 'helpdesk-team-perf-time-value', formatResolution(perf.min_resolution_hours)));
minEl.appendChild(h('span', 'helpdesk-team-perf-time-label', 'Min'));
times.appendChild(minEl);
const maxEl = h('div', 'helpdesk-team-perf-time');
maxEl.appendChild(h('span', 'helpdesk-team-perf-time-value', formatResolution(perf.max_resolution_hours)));
maxEl.appendChild(h('span', 'helpdesk-team-perf-time-label', 'Max'));
times.appendChild(maxEl);
timeStats.appendChild(times);
grid.appendChild(timeStats);
// Top customers
const customersEl = buildRankedList(perf.top_customers, container.dataset.labelTopCustomers || 'Top customers');
if (customersEl) grid.appendChild(customersEl);
// Top categories
const categoriesEl = buildRankedList(perf.top_categories, container.dataset.labelTopCategories || 'Top categories');
if (categoriesEl) grid.appendChild(categoriesEl);
section.appendChild(grid);
return section;
}
@@ -210,7 +277,7 @@ if (container) {
return b.resolved_in_period - a.resolved_in_period;
});
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceRow(m));
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceWidget(m));
}
async function fetchData(refresh = false) {