From eed1e848df28a9ef2dc2b3af7015b6557520272c Mon Sep 17 00:00:00 2001 From: fs Date: Sun, 5 Apr 2026 16:32:53 +0200 Subject: [PATCH] feat(helpdesk): redesign ticket detail with communication as main content and metadata aside MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap layout: communication feed becomes the primary main content area (full width), ticket metadata moves to aside panel with 2 stacked KPI tiles (Status, Age) and a Stripe-style key-value definition list (Description, Company link, Contact, Category, Support, Debtor No.). Remove activity timeline entirely — communication feed already includes activity fallback when SOAP is unavailable. Add skeleton bubble loading for async communication. Simplify JS from 211 to 89 lines. Remove unused ticketLogUrl data attribute and i18n keys. Co-Authored-By: Claude Opus 4.6 --- .../helpdesk/pages/helpdesk/ticket($id).php | 1 - .../pages/helpdesk/ticket(default).phtml | 190 +++++++++--------- modules/helpdesk/web/js/helpdesk-ticket.js | 172 +++------------- 3 files changed, 116 insertions(+), 247 deletions(-) diff --git a/modules/helpdesk/pages/helpdesk/ticket($id).php b/modules/helpdesk/pages/helpdesk/ticket($id).php index f6f65fb..b0ac040 100644 --- a/modules/helpdesk/pages/helpdesk/ticket($id).php +++ b/modules/helpdesk/pages/helpdesk/ticket($id).php @@ -40,7 +40,6 @@ if ($ticket === null && !$connectionError) { $ticketCustomerNo = $fromCustomerNo; $ticketDescription = (string) ($ticket['Description'] ?? $ticketNo); -$ticketLogUrl = lurl('helpdesk/ticket-log-data'); $ticketCommunicationUrl = lurl('helpdesk/ticket-communication-data'); Buffer::set('title', $ticketDescription . ' — ' . t('Helpdesk')); diff --git a/modules/helpdesk/pages/helpdesk/ticket(default).phtml b/modules/helpdesk/pages/helpdesk/ticket(default).phtml index ee3c127..abc2f07 100644 --- a/modules/helpdesk/pages/helpdesk/ticket(default).phtml +++ b/modules/helpdesk/pages/helpdesk/ticket(default).phtml @@ -7,7 +7,6 @@ * @var bool $connectionError * @var string $ticketCustomerNo * @var string $ticketDescription - * @var string $ticketLogUrl * @var string $ticketCommunicationUrl */ @@ -17,7 +16,6 @@ $ticket = is_array($ticket ?? null) ? $ticket : null; $connectionError = $connectionError ?? false; $ticketCustomerNo = $ticketCustomerNo ?? ''; $ticketDescription = $ticketDescription ?? ''; -$ticketLogUrl = $ticketLogUrl ?? ''; $ticketCommunicationUrl = $ticketCommunicationUrl ?? ''; $backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($ticketCustomerNo)) : lurl('helpdesk'); @@ -25,7 +23,6 @@ $backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($t ?>
@@ -58,122 +55,117 @@ $backUrl = $ticketCustomerNo !== '' ? lurl('helpdesk/debitor/' . rawurlencode($t
- format('d.m.Y H:i') - : '—'; - $lastActivityRaw = (string) ($ticket['Last_Activity_Date'] ?? ''); - $lastActivityFormatted = $lastActivityRaw !== '' && $lastActivityRaw !== '0001-01-01T00:00:00Z' - ? (new DateTimeImmutable($lastActivityRaw))->format('d.m.Y H:i') - : '—'; - $ticketState = (string) ($ticket['Ticket_State'] ?? ''); - $stateVariant = match ($ticketState) { - 'Offen', 'Open' => 'primary', - 'In Bearbeitung', 'In Progress' => 'warning', - 'Erledigt', 'Resolved', 'Closed', 'Geschlossen' => 'success', - default => 'neutral', - }; - ?> - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
- - - - - - - - - - - - - - - - - - - -
-
-
- - -
-
-
-
-
+ +
+
+

+
+ +
+
-
-
-
-
-
+ -
+
+ + + format('d.m.Y H:i') + : '—'; + $lastActivityRaw = (string) ($ticket['Last_Activity_Date'] ?? ''); + $lastActivityFormatted = $lastActivityRaw !== '' && $lastActivityRaw !== '0001-01-01T00:00:00Z' + ? (new DateTimeImmutable($lastActivityRaw))->format('d.m.Y H:i') + : '—'; + $ticketState = (string) ($ticket['Ticket_State'] ?? ''); + + // Compute ticket age + $ageLabel = '—'; + if ($createdRaw !== '' && $createdRaw !== '0001-01-01T00:00:00Z') { + $createdDt = new DateTimeImmutable($createdRaw); + $diff = $createdDt->diff(new DateTimeImmutable('now')); + if ($diff->days >= 1) { + $ageLabel = $diff->days . ' ' . ($diff->days === 1 ? t('day') : t('days')); + } else { + $hours = $diff->h + ($diff->days * 24); + $ageLabel = max(1, $hours) . ' ' . t('hours'); + } + } + ?> + +
diff --git a/modules/helpdesk/web/js/helpdesk-ticket.js b/modules/helpdesk/web/js/helpdesk-ticket.js index f19d5e4..378b8f3 100644 --- a/modules/helpdesk/web/js/helpdesk-ticket.js +++ b/modules/helpdesk/web/js/helpdesk-ticket.js @@ -1,15 +1,14 @@ /** - * Helpdesk ticket detail page — async activity timeline. + * Helpdesk ticket detail page — async communication aside. * - * Loads the ticket activity log (PBI_FP_TicketLog) via fetch and renders - * a chronological timeline of status changes and messages. + * Loads the ticket communication feed via fetch and renders + * it in the aside panel, matching the debitor detail pattern. */ import { renderCommunicationError, renderCommunicationFeed, renderCommunicationHint, } from './helpdesk-communication.js'; -import { renderEmptyState } from './helpdesk-empty-state.js'; const container = document.querySelector('.app-details-container[data-ticket-no]'); if (container) { @@ -18,169 +17,48 @@ if (container) { function init(container) { const ticketNo = container.dataset.ticketNo || ''; - const logUrl = container.dataset.ticketLogUrl || ''; const communicationUrl = container.dataset.ticketCommunicationUrl || ''; - if (!ticketNo || !logUrl) return; + if (!ticketNo || !communicationUrl) return; // i18n const i18nEl = document.getElementById('helpdesk-ticket-i18n'); const i18n = i18nEl ? JSON.parse(i18nEl.textContent) : {}; const t = (key) => i18n[key] || key; - // Date/time formatter - const dateFmt = new Intl.DateTimeFormat('de-DE', { - day: '2-digit', month: '2-digit', year: 'numeric', - }); - const timeFmt = new Intl.DateTimeFormat('de-DE', { - hour: '2-digit', minute: '2-digit', - }); - - function esc(str) { - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; + function showLoading(id) { + const el = document.getElementById(id); + if (el) el.hidden = false; } - function formatDate(raw) { - if (!raw || raw === '0001-01-01' || raw === '0001-01-01T00:00:00Z') return ''; - try { - return dateFmt.format(new Date(raw)); - } catch { - return raw; - } + function hideLoading(id) { + const el = document.getElementById(id); + if (el) el.hidden = true; } - function formatTime(raw) { - if (!raw) return ''; - // BC time format: "15:21:09.76" — parse as today's time - const parts = raw.split(':'); - if (parts.length < 2) return raw; - return parts[0] + ':' + parts[1]; - } - - function typeIcon(type) { - switch (type) { - case 'Nachricht': return 'bi-chat-dots-fill'; - case 'Status': return 'bi-arrow-repeat'; - case 'Weiterleitung': return 'bi-forward-fill'; - case 'Datei': return 'bi-paperclip'; - default: return 'bi-circle-fill'; - } - } - - function typeVariant(type) { - switch (type) { - case 'Nachricht': return 'message'; - case 'Status': return 'status'; - case 'Weiterleitung': return 'forward'; - case 'Datei': return 'file'; - default: return 'other'; - } - } - - function typeLabel(type, stateSubtype) { - switch (type) { - case 'Nachricht': return t('sent a message'); - case 'Status': { - if (stateSubtype) { - return t('changed status') + ' → ' + stateSubtype; - } - return t('changed status'); - } - case 'Weiterleitung': return t('Forwarded'); - case 'Datei': return t('attached a file'); - default: return type || t('Activity'); - } - } - - function renderTimeline(entries) { - if (entries.length === 0) { - return renderEmptyState({ - message: t('No activity found.'), - size: 'compact', - align: 'center', - icon: false, - }); - } - - let html = '
'; - let lastDate = ''; - - for (const entry of entries) { - const date = formatDate(entry.Creation_Date); - const time = formatTime(entry.Creation_Time); - const type = entry.Type || ''; - const user = entry.Created_By || ''; - const variant = typeVariant(type); - const icon = typeIcon(type); - const label = typeLabel(type, entry.State_Subtype || ''); - - // Date separator - if (date !== lastDate) { - html += `
${esc(date)}
`; - lastDate = date; - } - - html += `
`; - html += `
`; - html += '
'; - html += `${esc(user)} ${esc(label)}`; - if (time) { - html += `${esc(time)}`; - } - html += '
'; - html += '
'; - } - - html += '
'; - return html; - } - - function renderError(message) { - return ``; - } - - // --- Load and render --- - - async function loadTimeline() { - const timelineEl = document.getElementById('ticket-timeline'); - const loadingEl = document.getElementById('timeline-loading'); - if (!timelineEl) return; - - try { - const params = new URLSearchParams({ ticketNo }); - const response = await fetch(logUrl + '?' + params.toString(), { credentials: 'same-origin' }); - const data = await response.json(); - - if (loadingEl) loadingEl.hidden = true; - - if (!data.ok) { - timelineEl.innerHTML = renderError(t('Could not load activity log.')); - return; - } - - timelineEl.innerHTML = renderTimeline(data.log || []); - } catch { - if (loadingEl) loadingEl.hidden = true; - timelineEl.innerHTML = renderError(t('Could not load activity log.')); - } + function showContent(id) { + const el = document.getElementById(id); + if (el) el.hidden = false; } async function loadCommunication() { - const communicationEl = document.getElementById('ticket-communication'); + const feedEl = document.getElementById('ticket-communication-feed'); const loadingEl = document.getElementById('ticket-communication-loading'); - if (!communicationEl || !communicationUrl) return; + const contentEl = document.getElementById('ticket-communication-content'); + if (!feedEl || !loadingEl || !contentEl) return; + + showLoading('ticket-communication-loading'); try { const params = new URLSearchParams({ ticketNo }); const response = await fetch(communicationUrl + '?' + params.toString(), { credentials: 'same-origin' }); const data = await response.json(); - if (loadingEl) loadingEl.hidden = true; + hideLoading('ticket-communication-loading'); + showContent('ticket-communication-content'); if (!data.ok) { - communicationEl.innerHTML = renderCommunicationError(t('Could not load communication history.')); + feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.')); return; } @@ -199,13 +77,13 @@ function init(container) { showTicketNo: false, }); - communicationEl.innerHTML = html; + feedEl.innerHTML = html; } catch { - if (loadingEl) loadingEl.hidden = true; - communicationEl.innerHTML = renderCommunicationError(t('Could not load communication history.')); + hideLoading('ticket-communication-loading'); + showContent('ticket-communication-content'); + feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.')); } } - loadTimeline(); loadCommunication(); }