Use table-layout:fixed with colgroup defining column widths in ch units. Every agent card table gets the same colgroup, ensuring identical column alignment across all cards. Ticket (9ch), Customer (22ch), Category (16ch), Activity (11ch) are fixed; Description takes remaining space with ellipsis. All cells get overflow:hidden + text-overflow:ellipsis. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
261 lines
9.2 KiB
JavaScript
261 lines
9.2 KiB
JavaScript
/**
|
|
* Team workload dashboard.
|
|
* Current tab: per-agent widget with section title + ticket table.
|
|
* Performance tab: compact agent rows with resolved counts.
|
|
*/
|
|
|
|
const container = document.querySelector('.app-details-container[data-team-workload-url]');
|
|
if (container) {
|
|
const dataUrl = container.dataset.teamWorkloadUrl;
|
|
const labelQueue = container.dataset.labelQueue || 'Queue';
|
|
const labelTickets = container.dataset.labelTickets || 'Tickets';
|
|
const labelCritical = container.dataset.labelCritical || 'Critical (>48h open)';
|
|
const ticketBaseUrl = container.dataset.ticketBaseUrl || '';
|
|
const debitorBaseUrl = container.dataset.debitorBaseUrl || '';
|
|
|
|
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 h(tag, cls, text) {
|
|
const e = document.createElement(tag);
|
|
if (cls) e.className = cls;
|
|
if (text !== undefined) e.textContent = text;
|
|
return e;
|
|
}
|
|
|
|
function initials(name) {
|
|
const parts = name.trim().split(/\s+/);
|
|
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
return name.substring(0, 2).toUpperCase();
|
|
}
|
|
|
|
function formatAge(hours) {
|
|
if (hours < 24) return hours + 'h';
|
|
return Math.floor(hours / 24) + 'd';
|
|
}
|
|
|
|
function formatDate(isoString) {
|
|
if (!isoString) return '—';
|
|
const d = new Date(isoString);
|
|
if (isNaN(d.getTime())) return '—';
|
|
return d.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
function formatDateTime(isoString) {
|
|
if (!isoString) return '—';
|
|
const d = new Date(isoString);
|
|
if (isNaN(d.getTime())) return '—';
|
|
return d.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
/**
|
|
* Build a ticket table row: No (link) | Customer (link) | Description | Last activity
|
|
*/
|
|
function buildTicketRow(t) {
|
|
const tr = h('tr', t.critical ? 'helpdesk-team-ticket-critical' : '');
|
|
|
|
// Ticket number as link
|
|
const noCell = h('td', 'helpdesk-team-ticket-no');
|
|
if (ticketBaseUrl && t.no) {
|
|
const a = h('a', '', t.no);
|
|
a.href = ticketBaseUrl + encodeURIComponent(t.no);
|
|
noCell.appendChild(a);
|
|
} else {
|
|
noCell.textContent = t.no || '—';
|
|
}
|
|
tr.appendChild(noCell);
|
|
|
|
// Customer name (link to debitor when customer_no available)
|
|
const customerCell = h('td', 'helpdesk-team-ticket-customer');
|
|
const customerDisplay = t.customer_name || t.customer_no || '—';
|
|
if (debitorBaseUrl && t.customer_no) {
|
|
const a = h('a', '', customerDisplay);
|
|
a.href = debitorBaseUrl + encodeURIComponent(t.customer_no);
|
|
customerCell.appendChild(a);
|
|
} else {
|
|
customerCell.textContent = customerDisplay;
|
|
}
|
|
tr.appendChild(customerCell);
|
|
|
|
// Description (ellipsis)
|
|
tr.appendChild(h('td', 'helpdesk-team-ticket-desc', t.description || '—'));
|
|
|
|
// Category
|
|
tr.appendChild(h('td', '', t.category || '—'));
|
|
|
|
// Last activity date
|
|
tr.appendChild(h('td', 'helpdesk-team-ticket-date', formatDateTime(t.last_activity)));
|
|
|
|
return tr;
|
|
}
|
|
|
|
/**
|
|
* Build a section widget for one agent — mirrors debitor dashboard section pattern.
|
|
*/
|
|
function buildAgentWidget(m, isQueue) {
|
|
const section = h('section', 'helpdesk-team-widget');
|
|
|
|
// Section title: "MM Name (count)" with line — same as helpdesk-support-section-title
|
|
const title = h('h3', 'helpdesk-support-section-title');
|
|
const name = isQueue ? labelQueue : m.display_name;
|
|
const count = String(m.open_tickets);
|
|
|
|
if (!isQueue) {
|
|
const avatar = h('span', 'helpdesk-team-avatar-inline', initials(m.display_name));
|
|
title.appendChild(avatar);
|
|
}
|
|
title.appendChild(document.createTextNode(name));
|
|
|
|
title.appendChild(h('span', 'helpdesk-team-title-count', count));
|
|
section.appendChild(title);
|
|
|
|
// Ticket table
|
|
const tickets = m.open_ticket_details || [];
|
|
if (tickets.length > 0) {
|
|
const table = h('table', 'helpdesk-team-ticket-table');
|
|
|
|
// Colgroup for consistent column widths across all agent cards
|
|
const colgroup = document.createElement('colgroup');
|
|
colgroup.appendChild(h('col', 'helpdesk-team-col-ticket'));
|
|
colgroup.appendChild(h('col', 'helpdesk-team-col-customer'));
|
|
colgroup.appendChild(h('col', 'helpdesk-team-col-description'));
|
|
colgroup.appendChild(h('col', 'helpdesk-team-col-category'));
|
|
colgroup.appendChild(h('col', 'helpdesk-team-col-activity'));
|
|
table.appendChild(colgroup);
|
|
|
|
const thead = h('thead');
|
|
const headRow = h('tr');
|
|
headRow.appendChild(h('th', '', container.dataset.labelTicket || 'Ticket'));
|
|
headRow.appendChild(h('th', '', container.dataset.labelCustomer || 'Customer'));
|
|
headRow.appendChild(h('th', '', container.dataset.labelDescription || 'Description'));
|
|
headRow.appendChild(h('th', '', container.dataset.labelCategory || 'Category'));
|
|
headRow.appendChild(h('th', '', container.dataset.labelLastActivity || 'Last activity'));
|
|
thead.appendChild(headRow);
|
|
table.appendChild(thead);
|
|
|
|
const tbody = h('tbody');
|
|
for (const t of tickets) tbody.appendChild(buildTicketRow(t));
|
|
table.appendChild(tbody);
|
|
section.appendChild(table);
|
|
}
|
|
|
|
return section;
|
|
}
|
|
|
|
/**
|
|
* Build a compact row for the performance tab.
|
|
*/
|
|
function buildPerformanceRow(m) {
|
|
const isQueue = m.support_user === '';
|
|
const section = h('div', 'helpdesk-team-perf-row');
|
|
|
|
if (!isQueue) {
|
|
section.appendChild(h('span', 'helpdesk-team-avatar-inline', initials(m.display_name)));
|
|
}
|
|
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)));
|
|
|
|
return section;
|
|
}
|
|
|
|
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));
|
|
|
|
if (queue) elCurrentCards.appendChild(buildAgentWidget(queue, true));
|
|
for (const m of agents) elCurrentCards.appendChild(buildAgentWidget(m, false));
|
|
|
|
// Legend
|
|
const hasCritical = members.some(m => m.critical_tickets > 0);
|
|
if (hasCritical) {
|
|
const legend = h('div', 'helpdesk-team-legend');
|
|
const item = h('span', 'helpdesk-team-legend-item');
|
|
item.appendChild(h('span', 'helpdesk-team-legend-dot helpdesk-team-legend-dot-warning'));
|
|
item.appendChild(h('span', '', labelCritical));
|
|
legend.appendChild(item);
|
|
elCurrentCards.appendChild(legend);
|
|
}
|
|
}
|
|
|
|
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;
|
|
});
|
|
|
|
for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceRow(m));
|
|
}
|
|
|
|
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) {
|
|
console.error('[helpdesk-team] API error:', json.error || json);
|
|
if (elError) {
|
|
const p = elError.querySelector('p');
|
|
if (p && json.error) p.textContent = json.error;
|
|
}
|
|
showState('error');
|
|
return;
|
|
}
|
|
if (!json.members || json.members.length === 0) { showState('empty'); return; }
|
|
|
|
renderCurrentTab(json.members);
|
|
renderPerformanceTab(json.members);
|
|
showState('success');
|
|
} catch (err) {
|
|
console.error('[helpdesk-team]', err);
|
|
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();
|
|
}
|