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