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>
99 lines
3.0 KiB
JavaScript
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();
|
|
}
|