/** * 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. */ const container = document.querySelector('.app-details-container[data-team-workload-url]'); if (container) { const dataUrl = container.dataset.teamWorkloadUrl; const labelNotAssigned = container.dataset.labelNotAssigned || '—'; 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 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 setText(id, value) { const el = document.getElementById(id); if (el) el.textContent = value; } function agentName(m) { return m.support_user === '' ? labelNotAssigned : m.display_name; } /** * Create a table cell with an inline proportional bar behind the value. * Stripe-style: the bar gives instant visual comparison across rows. */ function barCell(value, max, variant) { const td = document.createElement('td'); const pct = max > 0 ? Math.round((value / max) * 100) : 0; const wrap = document.createElement('span'); wrap.className = 'helpdesk-team-bar-cell'; const bar = document.createElement('span'); bar.className = 'helpdesk-team-bar'; if (variant) bar.classList.add('helpdesk-team-bar-' + variant); bar.style.width = pct + '%'; const label = document.createElement('span'); label.className = 'helpdesk-team-bar-value'; label.textContent = String(value); if (value === 0) label.classList.add('helpdesk-team-muted'); wrap.appendChild(bar); wrap.appendChild(label); td.appendChild(wrap); return td; } /** * 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); } return td; } /** * Create a plain text cell, muted when value is 0. */ 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 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(); const maxOpen = Math.max(1, ...members.map(m => m.open_tickets)); for (const m of members) { const tr = document.createElement('tr'); const isUnassigned = 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); } } function renderPerformanceTab(kpis, members) { setText('team-kpi-resolved', kpis.resolved_in_period ?? 0); if (!elPerformanceBody) return; elPerformanceBody.replaceChildren(); const sorted = [...members].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) { 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); } 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; } renderAll(json); 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(); }