From 70bfbbdc302730233aafd5d42c42cc5df7a970b2 Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 5 Apr 2026 19:22:09 +0200 Subject: [PATCH] refactor(helpdesk): redesign team dashboard with section widgets and ticket tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace custom card layout with the existing debitor dashboard pattern: each agent is a section with helpdesk-support-section-title (avatar initials + name + count badge + line) and a ticket table underneath (Ticket No, Customer, Description, Age). Critical rows highlighted. Queue section uses same pattern without avatar. Performance tab uses compact rows with avatar + name + resolved badge. No custom card or pill styles — consistent with existing dashboard widgets. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/helpdesk/web/css/helpdesk.css | 204 ++++++++--------------- modules/helpdesk/web/js/helpdesk-team.js | 160 ++++++++---------- 2 files changed, 145 insertions(+), 219 deletions(-) diff --git a/modules/helpdesk/web/css/helpdesk.css b/modules/helpdesk/web/css/helpdesk.css index 263498e..601711c 100644 --- a/modules/helpdesk/web/css/helpdesk.css +++ b/modules/helpdesk/web/css/helpdesk.css @@ -126,160 +126,104 @@ } } - /* Team workload — queue section */ - .helpdesk-team-queue { - border: 1px dashed var(--app-muted-border-color); - border-radius: var(--app-radius, 0.375rem); - padding: calc(var(--app-spacing) * 0.6) calc(var(--app-spacing) * 0.75); - margin-bottom: calc(var(--app-spacing) * 0.75); - background: color-mix(in srgb, var(--app-muted-border-color) 10%, transparent); - } - - .helpdesk-team-queue-header { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: calc(var(--app-spacing) * 0.4); - } - - .helpdesk-team-queue-title { - font-weight: 600; - font-size: var(--text-sm, 0.875rem); - color: var(--app-muted-color); - text-transform: uppercase; - letter-spacing: 0.04em; - } - - .helpdesk-team-queue-count { - font-size: var(--text-sm, 0.875rem); - color: var(--app-muted-color); - } - - /* Team workload — agent rows */ - .helpdesk-team-agents { - display: grid; - gap: calc(var(--app-spacing) * 0.5); - } - - .helpdesk-team-agent { - display: flex; - gap: calc(var(--app-spacing) * 0.65); - padding: calc(var(--app-spacing) * 0.5) calc(var(--app-spacing) * 0.6); - border: 1px solid var(--app-muted-border-color); - border-radius: var(--app-radius, 0.375rem); - } - - .helpdesk-team-avatar { - width: 2.25rem; - height: 2.25rem; - border-radius: 50%; - display: flex; + /* Team workload — inline avatar for section titles */ + .helpdesk-team-avatar-inline { + display: inline-flex; align-items: center; justify-content: center; - font-size: 0.75rem; + width: 1.6rem; + height: 1.6rem; + border-radius: 50%; + font-size: 0.65rem; font-weight: 700; letter-spacing: 0.02em; flex-shrink: 0; background: color-mix(in srgb, var(--app-muted-border-color) 80%, transparent); color: var(--app-muted-color); + vertical-align: middle; } - .helpdesk-team-avatar-sm { - width: 1.75rem; - height: 1.75rem; - font-size: 0.65rem; - } - - .helpdesk-team-agent-content { - flex: 1; - min-width: 0; - } - - .helpdesk-team-agent-header { - display: flex; - align-items: center; - gap: calc(var(--app-spacing) * 0.4); - margin-bottom: calc(var(--app-spacing) * 0.25); - } - - .helpdesk-team-agent-name { - font-weight: 600; - font-size: var(--text-sm, 0.875rem); - } - - .helpdesk-team-agent-count { + /* Count badge in section title */ + .helpdesk-team-title-count { font-variant-numeric: tabular-nums; font-weight: 600; - font-size: 0.7rem; + font-size: 0.75rem; padding: 0.1em 0.5em; border-radius: 999px; - background: color-mix(in srgb, var(--app-contrast, #000) 8%, transparent); + background: color-mix(in srgb, var(--app-contrast, #000) 7%, transparent); color: var(--app-muted-color); } - .helpdesk-team-agent-count-warning { + .helpdesk-team-title-count-warning { background: color-mix(in srgb, var(--app-warning, #f59e0b) 15%, transparent); color: var(--app-warning, #f59e0b); } - .helpdesk-team-agent-count-success { + /* Ticket table inside agent widget */ + .helpdesk-team-ticket-table { + width: 100%; + font-size: var(--text-sm, 0.875rem); + } + + .helpdesk-team-ticket-table th { + text-align: left; + font-weight: 500; + color: var(--app-muted-color); + padding: calc(var(--app-spacing) * 0.25) calc(var(--app-spacing) * 0.4); + border-bottom: 1px solid var(--app-muted-border-color); + } + + .helpdesk-team-ticket-table td { + padding: calc(var(--app-spacing) * 0.3) calc(var(--app-spacing) * 0.4); + border-bottom: 1px solid color-mix(in srgb, var(--app-muted-border-color) 40%, transparent); + } + + .helpdesk-team-ticket-table tbody tr:last-child td { + border-bottom: none; + } + + .helpdesk-team-ticket-no { + font-variant-numeric: tabular-nums; + font-weight: 500; + } + + .helpdesk-team-ticket-critical td { + color: var(--app-warning, #f59e0b); + } + + .helpdesk-team-ticket-critical .helpdesk-team-ticket-no { + font-weight: 600; + } + + /* Performance tab — compact rows */ + .helpdesk-team-perf-row { + display: flex; + align-items: center; + gap: calc(var(--app-spacing) * 0.5); + padding: calc(var(--app-spacing) * 0.35) 0; + border-bottom: 1px solid color-mix(in srgb, var(--app-muted-border-color) 40%, transparent); + font-size: var(--text-sm, 0.875rem); + } + + .helpdesk-team-perf-row:last-child { + border-bottom: none; + } + + .helpdesk-team-perf-name { + flex: 1; + font-weight: 500; + } + + .helpdesk-team-perf-count { + font-variant-numeric: tabular-nums; + font-weight: 600; + font-size: 0.75rem; + padding: 0.1em 0.5em; + border-radius: 999px; background: color-mix(in srgb, var(--app-success, #198754) 12%, transparent); color: var(--app-success, #198754); } - /* Ticket pills */ - .helpdesk-team-pills { - display: flex; - flex-wrap: wrap; - gap: calc(var(--app-spacing) * 0.25); - } - - .helpdesk-team-pill { - display: inline-flex; - align-items: center; - gap: 0.3em; - font-size: 0.75rem; - font-variant-numeric: tabular-nums; - padding: 0.15em 0.5em; - border-radius: var(--app-radius, 0.375rem); - background: color-mix(in srgb, var(--app-muted-border-color) 35%, transparent); - color: var(--app-color); - cursor: default; - line-height: 1.5; - } - - .helpdesk-team-pill-critical { - background: color-mix(in srgb, var(--app-warning, #f59e0b) 12%, transparent); - } - - .helpdesk-team-pill-no { - font-weight: 600; - } - - .helpdesk-team-pill-sep { - opacity: 0.35; - } - - .helpdesk-team-pill-customer { - opacity: 0.6; - } - - .helpdesk-team-pill-age { - opacity: 0.6; - } - - .helpdesk-team-pill-critical .helpdesk-team-pill-no, - .helpdesk-team-pill-critical .helpdesk-team-pill-age { - color: var(--app-warning, #f59e0b); - opacity: 1; - font-weight: 600; - } - - /* Compact variant for performance tab */ - .helpdesk-team-agent-compact { - padding: calc(var(--app-spacing) * 0.4) calc(var(--app-spacing) * 0.6); - } - .helpdesk-team-performance-panel > .helpdesk-segment-control { margin-bottom: calc(var(--app-spacing) * 0.75); } diff --git a/modules/helpdesk/web/js/helpdesk-team.js b/modules/helpdesk/web/js/helpdesk-team.js index 13277c0..69c65a3 100644 --- a/modules/helpdesk/web/js/helpdesk-team.js +++ b/modules/helpdesk/web/js/helpdesk-team.js @@ -1,7 +1,7 @@ /** - * Team workload dashboard — visual workload map with avatar cards. - * Current tab: queue section + agent cards with ticket pills. - * Performance tab: compact agent cards with resolved counts. + * 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]'); @@ -36,9 +36,6 @@ if (container) { return e; } - /** - * Generate two-letter initials from a display name. - */ function initials(name) { const parts = name.trim().split(/\s+/); if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); @@ -50,97 +47,87 @@ if (container) { return Math.floor(hours / 24) + 'd'; } - /** - * Build a ticket pill: "T-1234 · K-10610 · 12h" - */ - function buildTicketPill(t) { - const pill = h('span', 'helpdesk-team-pill'); - if (t.critical) pill.classList.add('helpdesk-team-pill-critical'); - - pill.appendChild(h('span', 'helpdesk-team-pill-no', t.no)); - if (t.customer) { - pill.appendChild(h('span', 'helpdesk-team-pill-sep', '·')); - pill.appendChild(h('span', 'helpdesk-team-pill-customer', t.customer)); - } - pill.appendChild(h('span', 'helpdesk-team-pill-sep', '·')); - pill.appendChild(h('span', 'helpdesk-team-pill-age', formatAge(t.age_hours))); - - if (t.description) pill.title = t.description; - return pill; + 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' }); } /** - * Build the queue section (unassigned tickets). + * Build a ticket table row: No | Customer | Last activity | Age */ - function buildQueueSection(member) { - const section = h('div', 'helpdesk-team-queue'); + function buildTicketRow(t) { + const tr = h('tr', t.critical ? 'helpdesk-team-ticket-critical' : ''); - const header = h('div', 'helpdesk-team-queue-header'); - header.appendChild(h('span', 'helpdesk-team-queue-title', labelQueue)); - header.appendChild(h('span', 'helpdesk-team-queue-count', String(member.open_tickets) + ' ' + labelTickets)); - section.appendChild(header); + const noCell = h('td', 'helpdesk-team-ticket-no', t.no); + tr.appendChild(noCell); + tr.appendChild(h('td', '', t.customer || '—')); + tr.appendChild(h('td', '', t.description || '—')); + const ageCell = h('td', '', formatAge(t.age_hours)); + tr.appendChild(ageCell); - const pills = h('div', 'helpdesk-team-pills'); - for (const t of member.open_ticket_details || []) { - pills.appendChild(buildTicketPill(t)); + return tr; + } + + /** + * Build a section widget for one agent — mirrors debitor dashboard section pattern. + */ + function buildAgentWidget(m, isQueue) { + const section = h('section'); + + // 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)); + + const badge = h('span', 'helpdesk-team-title-count', count); + if (m.critical_tickets > 0) badge.classList.add('helpdesk-team-title-count-warning'); + title.appendChild(badge); + section.appendChild(title); + + // Ticket table + const tickets = m.open_ticket_details || []; + if (tickets.length > 0) { + const table = h('table', 'helpdesk-team-ticket-table'); + const thead = h('thead'); + const headRow = h('tr'); + headRow.appendChild(h('th', '', 'Ticket')); + headRow.appendChild(h('th', '', 'Kunde')); + headRow.appendChild(h('th', '', 'Beschreibung')); + headRow.appendChild(h('th', '', 'Alter')); + 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); } - section.appendChild(pills); return section; } /** - * Build an agent card with avatar and ticket pills. + * Build a compact row for the performance tab. */ - function buildAgentCard(m) { - const card = h('article', 'helpdesk-team-agent'); - - // Avatar - card.appendChild(h('div', 'helpdesk-team-avatar', initials(m.display_name))); - - // Content area - const content = h('div', 'helpdesk-team-agent-content'); - - // Name row - const nameRow = h('div', 'helpdesk-team-agent-header'); - nameRow.appendChild(h('span', 'helpdesk-team-agent-name', m.display_name)); - const countBadge = h('span', 'helpdesk-team-agent-count', String(m.open_tickets)); - if (m.critical_tickets > 0) countBadge.classList.add('helpdesk-team-agent-count-warning'); - nameRow.appendChild(countBadge); - content.appendChild(nameRow); - - // Ticket pills - const tickets = m.open_ticket_details || []; - if (tickets.length > 0) { - const pills = h('div', 'helpdesk-team-pills'); - for (const t of tickets) pills.appendChild(buildTicketPill(t)); - content.appendChild(pills); - } - - card.appendChild(content); - return card; - } - - /** - * Build a compact performance card with avatar and resolved count. - */ - function buildPerformanceCard(m) { + function buildPerformanceRow(m) { const isQueue = m.support_user === ''; + const section = h('div', 'helpdesk-team-perf-row'); - const card = h('article', 'helpdesk-team-agent helpdesk-team-agent-compact'); + 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))); - const avatar = h('div', 'helpdesk-team-avatar helpdesk-team-avatar-sm', isQueue ? '?' : initials(m.display_name)); - if (isQueue) avatar.classList.add('helpdesk-team-avatar-muted'); - card.appendChild(avatar); - - const content = h('div', 'helpdesk-team-agent-content'); - const nameRow = h('div', 'helpdesk-team-agent-header'); - nameRow.appendChild(h('span', 'helpdesk-team-agent-name', isQueue ? labelQueue : m.display_name)); - nameRow.appendChild(h('span', 'helpdesk-team-agent-count helpdesk-team-agent-count-success', String(m.resolved_in_period))); - content.appendChild(nameRow); - - card.appendChild(content); - return card; + return section; } function renderCurrentTab(members) { @@ -150,11 +137,8 @@ if (container) { 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(buildQueueSection(queue)); - - const grid = h('div', 'helpdesk-team-agents'); - for (const m of agents) grid.appendChild(buildAgentCard(m)); - elCurrentCards.appendChild(grid); + if (queue) elCurrentCards.appendChild(buildAgentWidget(queue, true)); + for (const m of agents) elCurrentCards.appendChild(buildAgentWidget(m, false)); } function renderPerformanceTab(members) { @@ -169,9 +153,7 @@ if (container) { return b.resolved_in_period - a.resolved_in_period; }); - const grid = h('div', 'helpdesk-team-agents'); - for (const m of sorted) grid.appendChild(buildPerformanceCard(m)); - elPerformanceCards.appendChild(grid); + for (const m of sorted) elPerformanceCards.appendChild(buildPerformanceRow(m)); } async function fetchData(refresh = false) {