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>
157 lines
5.6 KiB
JavaScript
157 lines
5.6 KiB
JavaScript
import { renderEmptyState } from './helpdesk-empty-state.js';
|
|
|
|
function esc(str) {
|
|
const d = document.createElement('div');
|
|
d.textContent = String(str ?? '');
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function activityActionLabel(typeVariant, type, stateSubtype, t) {
|
|
if (typeVariant === 'status') {
|
|
const base = t('changed status');
|
|
return stateSubtype ? `${base} (${stateSubtype})` : base;
|
|
}
|
|
if (typeVariant === 'forward') {
|
|
return t('Forwarded');
|
|
}
|
|
if (typeVariant === 'file') {
|
|
return t('attached a file');
|
|
}
|
|
if (type && String(type).trim() !== '') {
|
|
return type;
|
|
}
|
|
|
|
return t('logged an activity');
|
|
}
|
|
|
|
function entryTimestamp(entry, t) {
|
|
const date = entry.date || '';
|
|
const timeRaw = entry.time || '';
|
|
const time = timeRaw.length >= 5 ? timeRaw.slice(0, 5) : timeRaw;
|
|
if (date && time) return `${date} ${time}`;
|
|
if (entry.timestamp_iso) return entry.timestamp_iso;
|
|
if (date) return date;
|
|
return '—';
|
|
}
|
|
|
|
function participantRole(entry) {
|
|
const explicitRole = String(entry.actor_role || '').trim().toLowerCase();
|
|
if (explicitRole === 'customer' || explicitRole === 'support') {
|
|
return explicitRole;
|
|
}
|
|
|
|
const actor = String(entry.actor || '').trim().toLowerCase();
|
|
if (actor.startsWith('kt') || actor.includes('kontakt') || actor.includes('contact')) {
|
|
return 'customer';
|
|
}
|
|
|
|
return 'support';
|
|
}
|
|
|
|
function ticketHref(baseUrl, ticketNo) {
|
|
const base = String(baseUrl || '').trim();
|
|
const no = String(ticketNo || '').trim();
|
|
if (!base || !no) return '';
|
|
|
|
return `${base}${encodeURIComponent(no)}`;
|
|
}
|
|
|
|
export function renderCommunicationError(message) {
|
|
return `<div class="notice" data-variant="warning" role="alert"><p>${esc(message)}</p></div>`;
|
|
}
|
|
|
|
export function renderCommunicationHint(message) {
|
|
return `<p class="helpdesk-comm-hint">${esc(message)}</p>`;
|
|
}
|
|
|
|
export function renderCommunicationFeed(entries, options = {}) {
|
|
const {
|
|
t = (k) => k,
|
|
emptyMessage = 'No communication found.',
|
|
showTicketNo = false,
|
|
ticketBaseUrl = '',
|
|
fileDownloadUrl = '',
|
|
} = options;
|
|
|
|
const list = Array.isArray(entries) ? entries : [];
|
|
if (list.length === 0) {
|
|
return renderEmptyState({
|
|
message: t(emptyMessage),
|
|
size: 'compact',
|
|
align: 'center',
|
|
icon: false,
|
|
});
|
|
}
|
|
|
|
let html = '<ol class="helpdesk-comm-feed" role="list">';
|
|
for (const entry of list) {
|
|
const actor = entry.actor || 'System';
|
|
const typeVariant = entry.type_variant || 'other';
|
|
const type = entry.type || '';
|
|
const stateSubtype = entry.state_subtype || '';
|
|
const message = entry.message || '';
|
|
const ticketNo = entry.ticket_no || '';
|
|
const timeLabel = entryTimestamp(entry, t);
|
|
const ticketUrl = ticketHref(ticketBaseUrl, ticketNo);
|
|
|
|
if (typeVariant === 'message') {
|
|
const role = participantRole(entry);
|
|
html += `<li class="helpdesk-chat-row helpdesk-chat-row--message helpdesk-chat-row--${esc(role)}" data-variant="${esc(typeVariant)}">`;
|
|
html += '<div class="helpdesk-chat-meta-block">';
|
|
html += `<strong class="helpdesk-chat-actor">${esc(actor)}</strong>`;
|
|
html += '<small class="helpdesk-chat-meta-row">';
|
|
if (showTicketNo && ticketNo) {
|
|
if (ticketUrl) {
|
|
html += `<a class="helpdesk-chat-meta-link" href="${esc(ticketUrl)}">${esc(t('Ticket'))} ${esc(ticketNo)}</a>`;
|
|
} else {
|
|
html += `<span class="helpdesk-chat-meta-item">${esc(t('Ticket'))} ${esc(ticketNo)}</span>`;
|
|
}
|
|
}
|
|
html += `<span class="helpdesk-chat-meta-item helpdesk-chat-time">${esc(timeLabel)}</span>`;
|
|
html += '</small>';
|
|
html += '</div>';
|
|
html += `<p class="helpdesk-chat-bubble">${esc(message)}</p>`;
|
|
html += '</li>';
|
|
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) {
|
|
activityText += ` ${t('on')} ${timeLabel}`;
|
|
}
|
|
if (showTicketNo && ticketNo) {
|
|
activityText += ` · ${t('Ticket')} ${ticketNo}`;
|
|
}
|
|
|
|
html += `<li class="helpdesk-chat-row helpdesk-chat-row--activity" data-variant="${esc(typeVariant)}">`;
|
|
html += `<div class="helpdesk-chat-activity" data-variant="${esc(typeVariant)}"><span class="helpdesk-chat-activity-pill">${esc(activityText)}</span></div>`;
|
|
html += '</li>';
|
|
}
|
|
html += '</ol>';
|
|
|
|
return html;
|
|
}
|