feat(js): add app-http contracts and migrate helpdesk runtime layer

This commit is contained in:
2026-04-20 22:31:13 +02:00
parent ec2f03e0cf
commit f189ef9df6
34 changed files with 2023 additions and 1134 deletions

View File

@@ -1,66 +1,82 @@
/**
* 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 { getJson } from '/js/core/app-http.js';
import { resolveHost } from '/js/core/app-dom.js';
import {
renderCommunicationError,
renderCommunicationFeed,
renderCommunicationHint,
} from './helpdesk-communication.js';
const container = document.querySelector('.app-details-container[data-ticket-no]');
if (container) {
init(container);
}
const EMPTY_API = Object.freeze({ destroy: () => {} });
const resolveContainer = (root) => {
const host = resolveHost(root);
if (host instanceof HTMLElement && host.matches('.app-details-container[data-ticket-no]')) {
return host;
}
return host.querySelector('.app-details-container[data-ticket-no]');
};
export function initHelpdeskTicket(root = document) {
const container = resolveContainer(root);
if (!container) {
return EMPTY_API;
}
function init(container) {
const ticketNo = container.dataset.ticketNo || '';
const communicationUrl = container.dataset.ticketCommunicationUrl || '';
const fileDownloadUrl = container.dataset.fileDownloadUrl || '';
if (!ticketNo || !communicationUrl) {
return EMPTY_API;
}
if (!ticketNo || !communicationUrl) return;
const listenerController = new AbortController();
const requestController = new AbortController();
const on = (target, type, handler, options = {}) => {
if (!target || typeof target.addEventListener !== 'function') {
return;
}
target.addEventListener(type, handler, { ...options, signal: listenerController.signal });
};
// i18n
const i18nEl = document.getElementById('helpdesk-ticket-i18n');
const i18n = i18nEl ? JSON.parse(i18nEl.textContent) : {};
const i18n = i18nEl ? JSON.parse(i18nEl.textContent || '{}') : {};
const t = (key) => i18n[key] || key;
function showLoading(id) {
const toggleHidden = (id, hidden) => {
const el = document.getElementById(id);
if (el) el.hidden = false;
}
if (el) {
el.hidden = hidden;
}
};
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 loadCommunication = async (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 (!feedEl || !loadingEl || !contentEl) {
return;
}
if (contentEl) contentEl.hidden = true;
showLoading('ticket-communication-loading');
contentEl.hidden = true;
loadingEl.hidden = false;
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();
if (refresh) {
params.set('refresh', '1');
}
hideLoading('ticket-communication-loading');
showContent('ticket-communication-content');
const data = await getJson(`${communicationUrl}?${params.toString()}`, {
signal: requestController.signal,
});
if (!data.ok) {
toggleHidden('ticket-communication-loading', true);
toggleHidden('ticket-communication-content', false);
if (!data || data.ok !== true) {
feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.'));
return;
}
@@ -82,17 +98,30 @@ function init(container) {
});
feedEl.innerHTML = html;
} catch {
hideLoading('ticket-communication-loading');
showContent('ticket-communication-content');
feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.'));
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return;
}
toggleHidden('ticket-communication-loading', true);
toggleHidden('ticket-communication-content', false);
const feedEl = document.getElementById('ticket-communication-feed');
if (feedEl) {
feedEl.innerHTML = renderCommunicationError(t('Could not load communication history.'));
}
}
}
};
const refreshButton = container.querySelector('button[name="ticket-refresh"]');
if (refreshButton) {
refreshButton.addEventListener('click', () => loadCommunication(true));
}
on(refreshButton, 'click', () => {
void loadCommunication(true);
});
loadCommunication();
void loadCommunication();
return {
destroy: () => {
listenerController.abort();
requestController.abort();
},
};
}