feat(helpdesk): separate queue from agents in team dashboard with wait time
Queue detection: any member with 'warteschlange' in support_user name (or empty) is treated as queue, not as a regular agent. Current tab: queue cards shown at top with a wait time summary (Ø/Min/Max from open ticket ages). Agents listed below. Historical tab: queue cards shown before agents, same performance widget pattern. No backend changes — purely client-side separation using existing data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -301,5 +301,6 @@
|
||||
"Current": "Aktuell",
|
||||
"Performance": "Leistung",
|
||||
"Historical": "Historisch",
|
||||
"Queue": "Warteschlange"
|
||||
"Queue": "Warteschlange",
|
||||
"Wait time": "Wartezeit"
|
||||
}
|
||||
|
||||
@@ -301,5 +301,6 @@
|
||||
"Current": "Current",
|
||||
"Performance": "Performance",
|
||||
"Historical": "Historical",
|
||||
"Queue": "Queue"
|
||||
"Queue": "Queue",
|
||||
"Wait time": "Wait time"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ $periodLabels = [
|
||||
data-label-resolution-time="<?php e(t('Resolution time')); ?>"
|
||||
data-label-top-customers="<?php e(t('Top customers')); ?>"
|
||||
data-label-top-categories="<?php e(t('Top categories')); ?>"
|
||||
data-label-wait-time="<?php e(t('Wait time')); ?>"
|
||||
>
|
||||
<section>
|
||||
<?php
|
||||
|
||||
@@ -220,6 +220,15 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Team workload — queue wait time summary */
|
||||
.helpdesk-team-queue-wait {
|
||||
padding: calc(var(--app-spacing) * 0.5) calc(var(--app-spacing) * 0.75);
|
||||
margin-bottom: calc(var(--app-spacing) * 0.5);
|
||||
border: 1px dashed var(--app-muted-border-color);
|
||||
border-radius: var(--app-radius, 0.375rem);
|
||||
background: color-mix(in srgb, var(--app-muted-border-color) 10%, transparent);
|
||||
}
|
||||
|
||||
/* Team workload — inline avatar for section titles */
|
||||
.helpdesk-team-avatar-inline {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -10,6 +10,7 @@ if (container) {
|
||||
const labelQueue = container.dataset.labelQueue || 'Queue';
|
||||
const labelTickets = container.dataset.labelTickets || 'Tickets';
|
||||
const labelCritical = container.dataset.labelCritical || 'Critical (>48h open)';
|
||||
const labelWaitTime = container.dataset.labelWaitTime || 'Wait time';
|
||||
const ticketBaseUrl = container.dataset.ticketBaseUrl || '';
|
||||
const debitorBaseUrl = container.dataset.debitorBaseUrl || '';
|
||||
|
||||
@@ -38,6 +39,13 @@ if (container) {
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect queue entries — Support_User_Name contains 'warteschlange'.
|
||||
*/
|
||||
function isQueueMember(m) {
|
||||
return m.support_user === '' || m.support_user.includes('warteschlange');
|
||||
}
|
||||
|
||||
function initials(name) {
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
@@ -298,14 +306,48 @@ if (container) {
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a queue summary with wait time Ø/Min/Max from open ticket ages.
|
||||
*/
|
||||
function buildQueueWaitSummary(queueMembers) {
|
||||
const allAges = queueMembers.flatMap(m => (m.open_ticket_details || []).map(t => t.age_hours));
|
||||
if (allAges.length === 0) return null;
|
||||
|
||||
const stats = computeResolution(allAges);
|
||||
const wrap = h('div', 'helpdesk-team-queue-wait');
|
||||
wrap.appendChild(h('span', 'helpdesk-team-perf-stat-label', labelWaitTime));
|
||||
|
||||
const list = h('ol', 'helpdesk-team-perf-ranked helpdesk-team-perf-ranked-plain');
|
||||
const items = [
|
||||
['Ø', formatResolution(stats.avg)],
|
||||
['Min', formatResolution(stats.min)],
|
||||
['Max', formatResolution(stats.max)],
|
||||
];
|
||||
for (const [label, value] of items) {
|
||||
const li = h('li');
|
||||
li.appendChild(h('span', 'helpdesk-team-perf-ranked-name', label));
|
||||
li.appendChild(h('span', 'helpdesk-team-perf-ranked-count', value));
|
||||
list.appendChild(li);
|
||||
}
|
||||
wrap.appendChild(list);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderCurrentTab(members) {
|
||||
if (!elCurrentCards) return;
|
||||
elCurrentCards.replaceChildren();
|
||||
|
||||
const queue = members.find(m => m.support_user === '' && m.open_tickets > 0);
|
||||
const agents = members.filter(m => m.support_user !== '' && (m.open_tickets > 0 || m.critical_tickets > 0));
|
||||
const queueMembers = members.filter(m => isQueueMember(m) && m.open_tickets > 0);
|
||||
const agents = members.filter(m => !isQueueMember(m) && (m.open_tickets > 0 || m.critical_tickets > 0));
|
||||
|
||||
if (queue) elCurrentCards.appendChild(buildAgentWidget(queue, true));
|
||||
// Queue section(s) at top
|
||||
for (const q of queueMembers) elCurrentCards.appendChild(buildAgentWidget(q, true));
|
||||
|
||||
// Wait time summary across all queues
|
||||
const waitSummary = buildQueueWaitSummary(queueMembers);
|
||||
if (waitSummary) elCurrentCards.appendChild(waitSummary);
|
||||
|
||||
// Agent cards
|
||||
for (const m of agents) elCurrentCards.appendChild(buildAgentWidget(m, false));
|
||||
|
||||
// Legend
|
||||
@@ -324,15 +366,13 @@ if (container) {
|
||||
if (!elPerformanceCards) return;
|
||||
elPerformanceCards.replaceChildren();
|
||||
|
||||
const sorted = [...members]
|
||||
.filter(m => m.resolved_in_period > 0)
|
||||
.sort((a, b) => {
|
||||
if (a.support_user === '') return 1;
|
||||
if (b.support_user === '') return -1;
|
||||
return b.resolved_in_period - a.resolved_in_period;
|
||||
});
|
||||
const queueResolved = members.filter(m => isQueueMember(m) && m.resolved_in_period > 0);
|
||||
const agentsResolved = members
|
||||
.filter(m => !isQueueMember(m) && m.resolved_in_period > 0)
|
||||
.sort((a, b) => b.resolved_in_period - a.resolved_in_period);
|
||||
|
||||
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceWidget(m));
|
||||
for (const q of queueResolved) elPerformanceCards.appendChild(buildPerformanceWidget(q));
|
||||
for (const m of agentsResolved) elPerformanceCards.appendChild(buildPerformanceWidget(m));
|
||||
}
|
||||
|
||||
async function fetchData(refresh = false) {
|
||||
|
||||
Reference in New Issue
Block a user