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