Files
breadcrumb-the-shire/modules/helpdesk/web/js/helpdesk-team.js
fs 397f9fe659 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>
2026-04-05 19:09:15 +02:00

198 lines
6.9 KiB
JavaScript

/**
* 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 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');
let currentPeriod = 90;
function showState(state) {
if (elLoading) elLoading.hidden = state !== 'loading';
if (elError) elError.hidden = state !== 'error';
if (elEmpty) elEmpty.hidden = state !== 'empty';
if (elContent) elContent.hidden = state !== 'success';
if (elLoading) elLoading.setAttribute('aria-busy', state === 'loading' ? 'true' : 'false');
}
function el(tag, className, text) {
const e = document.createElement(tag);
if (className) e.className = className;
if (text !== undefined) e.textContent = text;
return e;
}
function formatAge(hours) {
if (hours < 24) return hours + 'h';
const days = Math.floor(hours / 24);
return days + 'd';
}
/**
* Build a single agent card for the Current tab.
*/
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 card = el('article', 'helpdesk-team-card');
if (isQueue) card.classList.add('helpdesk-team-card-queue');
// 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);
// 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);
// 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');
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 card;
}
/**
* Build a single agent card for the Performance tab.
*/
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(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));
// Queue (unassigned) first if present
const queue = visible.find(m => m.support_user === '');
const agents = visible.filter(m => m.support_user !== '');
if (queue) elCurrentCards.appendChild(buildAgentCard(queue, maxOpen));
for (const m of agents) elCurrentCards.appendChild(buildAgentCard(m, maxOpen));
}
function renderPerformanceTab(members) {
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 maxResolved = Math.max(1, ...sorted.map(m => m.resolved_in_period));
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceCard(m, maxResolved));
}
async function fetchData(refresh = false) {
showState('loading');
const params = new URLSearchParams({ periodDays: String(currentPeriod) });
if (refresh) params.set('refresh', '1');
try {
const res = await fetch(dataUrl + '?' + params.toString(), { credentials: 'same-origin' });
const json = await res.json();
if (!json.ok) {
showState('error');
return;
}
if (!json.members || json.members.length === 0) {
showState('empty');
return;
}
renderCurrentTab(json.members);
renderPerformanceTab(json.members);
showState('success');
} catch {
showState('error');
}
}
if (periodSelector) {
periodSelector.addEventListener('change', (e) => {
const radio = e.target.closest('input[name="team-period"]');
if (!radio) return;
const period = parseInt(radio.value, 10);
if (!period || period === currentPeriod) return;
currentPeriod = period;
fetchData();
});
}
if (elRefresh) {
elRefresh.addEventListener('click', () => fetchData(true));
}
fetchData();
}