/** * 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(); }