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>
This commit is contained in:
2026-04-05 22:49:01 +02:00
parent 659a6d71c5
commit 792f706d9e
12 changed files with 310 additions and 2 deletions

View File

@@ -1307,6 +1307,42 @@
text-decoration: underline;
}
/* File attachment in chat */
.helpdesk-chat-file {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.helpdesk-chat-file-preview {
display: block;
max-width: 16rem;
margin-left: 1.35rem;
}
.helpdesk-chat-file-preview img {
display: block;
max-width: 100%;
height: auto;
border-radius: var(--app-radius, 0.375rem);
border: 1px solid var(--app-muted-border-color);
}
.helpdesk-chat-file-link {
display: inline-flex;
align-items: center;
gap: 0.35em;
margin-left: 1.35rem;
font-size: var(--text-sm, 0.875rem);
font-weight: 500;
color: var(--app-primary, #0d6efd);
text-decoration: none;
}
.helpdesk-chat-file-link:hover {
text-decoration: underline;
}
/* Ticket detail — communication feed in main content (no max-height) */
.helpdesk-ticket-detail-content .helpdesk-comm-feed {
max-height: none;

View File

@@ -70,6 +70,7 @@ export function renderCommunicationFeed(entries, options = {}) {
emptyMessage = 'No communication found.',
showTicketNo = false,
ticketBaseUrl = '',
fileDownloadUrl = '',
} = options;
const list = Array.isArray(entries) ? entries : [];
@@ -114,6 +115,28 @@ export function renderCommunicationFeed(entries, options = {}) {
continue;
}
// File attachment: render download link and inline image for images
if (typeVariant === 'file' && fileDownloadUrl && entry.file_entry_no) {
const fileName = entry.file_name || 'attachment';
const fileUrl = fileDownloadUrl + '?entryNo=' + encodeURIComponent(entry.file_entry_no) + '&filename=' + encodeURIComponent(fileName);
const ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : '';
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp'];
const isImage = imageExts.includes(ext);
html += `<li class="helpdesk-chat-row helpdesk-chat-row--activity" data-variant="file">`;
html += `<div class="helpdesk-chat-activity helpdesk-chat-file" data-variant="file">`;
html += `<span class="helpdesk-chat-activity-pill">${esc(actor)} ${esc(t('attached a file'))}`;
if (timeLabel) html += ` ${esc(t('on'))} ${esc(timeLabel)}`;
html += `</span>`;
if (isImage) {
html += `<a href="${esc(fileUrl)}" target="_blank" class="helpdesk-chat-file-preview"><img src="${esc(fileUrl)}" alt="${esc(fileName)}" loading="lazy"></a>`;
}
html += `<a href="${esc(fileUrl)}" target="_blank" class="helpdesk-chat-file-link"><i class="bi bi-download"></i> ${esc(fileName)}</a>`;
html += '</div>';
html += '</li>';
continue;
}
const activityAction = activityActionLabel(typeVariant, type, stateSubtype, t);
let activityText = `${actor} ${activityAction}`.trim();
if (timeLabel) {

View File

@@ -27,6 +27,7 @@ function init(container) {
const contactsUrl = container.dataset.contactsUrl || '';
const ticketBaseUrl = container.dataset.ticketBaseUrl || '';
const communicationUrl = container.dataset.communicationUrl || '';
const fileDownloadUrl = container.dataset.fileDownloadUrl || '';
const salesDashboardUrl = container.dataset.salesDashboardUrl || '';
const refreshFromUrl = (() => {
const value = new URLSearchParams(window.location.search).get('refresh');
@@ -1148,6 +1149,7 @@ function init(container) {
emptyMessage: 'No communication found.',
showTicketNo: true,
ticketBaseUrl,
fileDownloadUrl,
});
feedEl.innerHTML = html;
}

View File

@@ -18,6 +18,7 @@ if (container) {
function init(container) {
const ticketNo = container.dataset.ticketNo || '';
const communicationUrl = container.dataset.ticketCommunicationUrl || '';
const fileDownloadUrl = container.dataset.fileDownloadUrl || '';
if (!ticketNo || !communicationUrl) return;
@@ -77,6 +78,7 @@ function init(container) {
t,
emptyMessage: 'No communication found.',
showTicketNo: false,
fileDownloadUrl,
});
feedEl.innerHTML = html;