Files
breadcrumb-the-shire/modules/helpdesk/web/js/helpdesk-ticket.js
fs 792f706d9e feat(helpdesk): add ticket file download and inline image preview
New SOAP method getTicketFile() on BcSoapGateway calls BC's
GetTicketFile to retrieve file attachments as Base64, decodes in
memory and returns binary data without writing to disk.

New proxy endpoint helpdesk/ticket-file-data streams files directly
to the browser with correct MIME type. Images served inline, other
files as download attachment.

TicketCommunicationService now attaches file_entry_no and file_name
to file-type events. Frontend renders download links with bi-download
icon and inline image previews for jpg/png/gif attachments.

Works in both ticket detail and debitor detail communication feeds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:49:01 +02:00

99 lines
3.0 KiB
JavaScript

/**
* Helpdesk ticket detail page — async communication aside.
*
* 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';
const container = document.querySelector('.app-details-container[data-ticket-no]');
if (container) {
init(container);
}
function init(container) {
const ticketNo = container.dataset.ticketNo || '';
const communicationUrl = container.dataset.ticketCommunicationUrl || '';
const fileDownloadUrl = container.dataset.fileDownloadUrl || '';
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;
function showLoading(id) {
const el = document.getElementById(id);
if (el) el.hidden = false;
}
function hideLoading(id) {
const el = document.getElementById(id);
if (el) el.hidden = true;
}
function showContent(id) {
const el = document.getElementById(id);
if (el) el.hidden = false;
}
async function loadCommunication(refresh = false) {
const feedEl = document.getElementById('ticket-communication-feed');
const loadingEl = document.getElementById('ticket-communication-loading');
const contentEl = document.getElementById('ticket-communication-content');
if (!feedEl || !loadingEl || !contentEl) return;
if (contentEl) contentEl.hidden = true;
showLoading('ticket-communication-loading');
try {
const params = new URLSearchParams({ ticketNo });
if (refresh) params.set('refresh', '1');
const response = await fetch(communicationUrl + '?' + params.toString(), { credentials: 'same-origin' });
const data = await response.json();
hideLoading('ticket-communication-loading');
showContent('ticket-communication-content');
if (!data.ok) {
feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.'));
return;
}
let html = '';
if (
data.meta
&& data.meta.soap_used === false
&& data.meta.fallback_used === true
&& data.meta.soap_error
) {
html += renderCommunicationHint(t('Live communication unavailable, showing activity fallback.'));
}
html += renderCommunicationFeed(data.entries || [], {
t,
emptyMessage: 'No communication found.',
showTicketNo: false,
fileDownloadUrl,
});
feedEl.innerHTML = html;
} catch {
hideLoading('ticket-communication-loading');
showContent('ticket-communication-content');
feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.'));
}
}
const refreshButton = container.querySelector('button[name="ticket-refresh"]');
if (refreshButton) {
refreshButton.addEventListener('click', () => loadCommunication(true));
}
loadCommunication();
}