Files
breadcrumb-the-shire/modules/helpdesk/web/js/helpdesk-team.js
fs 3c0d3bb6f4 fix(helpdesk): consistent table columns across agent cards via colgroup
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>
2026-04-05 19:51:20 +02:00

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