/** * Helpdesk domain detail page — read-only dashboard. * * Fetches customer, contract, handover, and related domain data * from a single async endpoint and renders all sections. * * All dynamic values are escaped via the esc() helper before insertion * into innerHTML. This matches the pattern used in helpdesk-detail.js * and other helpdesk JS modules in this codebase. */ import { getJson } from '/js/core/app-http.js'; import { resolveHost } from '/js/core/app-dom.js'; import { renderEmptyState } from './helpdesk-empty-state.js'; function esc(str) { const d = document.createElement('div'); d.textContent = String(str ?? ''); return d.innerHTML; } function formatDate(dateStr) { if (!dateStr) return '—'; const d = new Date(dateStr); if (isNaN(d.getTime())) return String(dateStr); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } const STATUS_LABEL_MAP = { draft: 'Draft', in_progress: 'In progress', completed: 'Completed', archived: 'Archived', }; const EMPTY_API = Object.freeze({ destroy: () => {} }); const resolveContainer = (root) => { const host = resolveHost(root); if (host instanceof HTMLElement && host.matches('.app-details-container[data-app-component="helpdesk-domain-detail"]')) { return host; } return host.querySelector('.app-details-container[data-app-component="helpdesk-domain-detail"]'); }; function init(container) { const requestController = new AbortController(); const domainNo = container.dataset.domainNo || ''; const customerNo = container.dataset.customerNo || ''; const detailUrl = container.dataset.detailUrl || ''; const debitorBaseUrl = container.dataset.debitorBaseUrl || ''; const handoverEditBaseUrl = container.dataset.handoverEditBaseUrl || ''; const domainBaseUrl = container.dataset.domainBaseUrl || ''; const ticketBaseUrl = container.dataset.ticketBaseUrl || ''; if (!domainNo || !detailUrl) return; const i18nEl = document.getElementById('helpdesk-domain-i18n'); const i18n = i18nEl ? JSON.parse(i18nEl.textContent) : {}; const t = (key) => i18n[key] || key; fetchDomainDetails(); async function fetchDomainDetails() { const loadingEl = document.getElementById('domain-loading'); const contentEl = document.getElementById('domain-content'); try { const params = new URLSearchParams({ domainNo, customerNo }); const data = await getJson(`${detailUrl}?${params.toString()}`, { signal: requestController.signal, }); if (!data.ok) { showError(data.error || t('Network error — please try again')); return; } renderCustomerInfo(data.customer); renderContractInfo(data.contract); renderSecurityLevel(data.security_level, data.security_level_variant); renderHandoversList(data.handovers || []); renderUpdatesList(data.updates || []); renderRelatedDomains(data.related_domains || []); if (loadingEl) loadingEl.hidden = true; if (contentEl) contentEl.hidden = false; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { return; } showError(t('Network error — please try again')); } } function showError(message) { const loadingEl = document.getElementById('domain-loading'); if (loadingEl) { // safe-html: esc() escapes all dynamic content loadingEl.innerHTML = ''; loadingEl.removeAttribute('aria-busy'); } } function renderCustomerInfo(customer) { const el = document.getElementById('domain-customer-info'); if (!el) return; if (!customer) { el.textContent = ''; const p = document.createElement('p'); p.className = 'text-muted'; p.textContent = t('No contract information available'); el.appendChild(p); return; } const name = customer.Name || ''; const no = customer.No || ''; const address = customer.Address || ''; const city = customer.City || ''; const postCode = customer.Post_Code || ''; const phone = customer.Phone_No || ''; const email = customer.E_Mail || ''; const salesperson = customer.Salesperson_Code || ''; const supportType = customer.Support_Type || ''; const debitorLink = no && debitorBaseUrl ? '' + esc(name || no) + '' : esc(name || no); // safe-html: all dynamic values are escaped via esc() before insertion el.innerHTML = '
' + '
' + esc(t('Name')) + '
' + debitorLink + '
' + '
' + esc(t('Customer No.')) + '
' + esc(no) + '
' + '
' + esc(t('Phone')) + '
' + (phone ? '' + esc(phone) + '' : '—') + '
' + (salesperson ? '
' + esc(t('Salesperson')) + '
' + esc(salesperson) + '
' : '') + (supportType ? '
' + esc(t('Support type')) + '
' + esc(supportType) + '
' : '') + '
'; } function renderContractInfo(contract) { const section = document.getElementById('domain-contract-section'); const el = document.getElementById('domain-contract-info'); if (!el || !section) return; if (!contract || !contract.contract_type) { section.hidden = true; return; } section.hidden = false; // safe-html: all dynamic values are escaped via esc() before insertion el.innerHTML = '
' + '
' + esc(t('Contract type')) + '
' + esc(contract.contract_type) + '
' + '
' + esc(t('Description')) + '
' + esc(contract.contract_description || '—') + '
' + '
' + esc(t('Contract No.')) + '
' + esc(contract.contract_no || '—') + '
' + (contract.header_state ? '
' + esc(t('Contract state')) + '
' + esc(contract.header_state) + '
' : '') + '
'; } function renderSecurityLevel(level, variant) { const badge = document.getElementById('domain-security-badge'); const select = document.querySelector('[data-security-level-select]'); if (!badge && !select) return; const levelLabels = { niedrig: t('Low'), normal: t('Normal'), hoch: t('High'), kritisch: t('Critical') }; const label = levelLabels[level] ?? level; if (badge) { badge.textContent = label; badge.dataset.variant = variant || 'neutral'; } if (select) { select.value = level || 'normal'; } } function renderHandoversList(handovers) { const el = document.getElementById('domain-handovers-list'); const countEl = document.getElementById('domain-handover-count'); if (!el) return; if (countEl) countEl.textContent = String(handovers.length); if (!handovers || handovers.length === 0) { el.innerHTML = renderEmptyState({ // safe-html: renderEmptyState escapes internally message: t('No handovers for this domain yet'), size: 'compact', icon: true, }); return; } const statusVariantMap = { draft: 'neutral', in_progress: 'info', completed: 'success', archived: 'neutral', }; const returnPath = 'helpdesk/domain/' + encodeURIComponent(domainNo); let items = ''; for (const h of handovers) { const editUrl = handoverEditBaseUrl + encodeURIComponent(h.id) + '?return=' + encodeURIComponent(returnPath); const productLabel = h.product_name || h.product_bc_description || h.product_code || '—'; const statusVariant = statusVariantMap[h.status] || 'neutral'; const statusLabel = t(STATUS_LABEL_MAP[h.status] || h.status); const meta = [h.created_by_name, formatDate(h.created_at)].filter(Boolean).join(' · '); // safe-html: all dynamic values are escaped via esc() items += '' + '
' + '' + esc(productLabel) + '' + '' + esc(statusLabel) + '' + '
' + (meta ? '
' + esc(meta) + '
' : '') + '
'; } // safe-html: all content built from esc()-escaped values el.innerHTML = '
' + items + '
'; } function renderUpdatesList(updates) { const section = document.getElementById('domain-updates-section'); const el = document.getElementById('domain-updates-list'); const countEl = document.getElementById('domain-update-count'); if (!el || !section) return; if (!updates || updates.length === 0) { section.hidden = true; return; } section.hidden = false; if (countEl) countEl.textContent = String(updates.length); // Build update items using DOM methods for safety const listDiv = document.createElement('div'); listDiv.className = 'helpdesk-domain-handover-list'; for (const u of updates) { const ticketNo = u.ticket_no || ''; const ticketUrl = ticketNo && ticketBaseUrl ? ticketBaseUrl + encodeURIComponent(ticketNo) : ''; const categoryLabel = u.category_label || u.category_code || ''; const categoryVariant = u.category_variant || 'neutral'; const giteaUrl = u.gitea_path || ''; const item = document.createElement('div'); item.className = 'helpdesk-domain-handover-item'; const main = document.createElement('div'); main.className = 'helpdesk-domain-handover-item-main'; const productSpan = document.createElement('span'); productSpan.className = 'helpdesk-domain-handover-item-product'; if (ticketUrl) { const link = document.createElement('a'); link.href = ticketUrl; link.textContent = ticketNo; productSpan.appendChild(link); } else { productSpan.textContent = ticketNo; } main.appendChild(productSpan); const badge = document.createElement('span'); badge.className = 'badge'; badge.dataset.variant = categoryVariant; badge.textContent = categoryLabel; main.appendChild(badge); item.appendChild(main); const metaParts = []; if (giteaUrl) { // Show last path segment as clickable link const parts = giteaUrl.replace(/\/+$/, '').split('/'); const shortLabel = parts[parts.length - 1] || giteaUrl; const giteaLink = document.createElement('a'); giteaLink.href = giteaUrl; giteaLink.target = '_blank'; giteaLink.rel = 'noopener'; giteaLink.textContent = shortLabel; metaParts.push(giteaLink); } const dateStr = formatDate(u.created_at); if (dateStr && dateStr !== '\u2014') { const dateSpan = document.createElement('span'); dateSpan.textContent = dateStr; metaParts.push(dateSpan); } if (metaParts.length) { const metaDiv = document.createElement('div'); metaDiv.className = 'helpdesk-domain-handover-item-meta'; metaParts.forEach((part, i) => { if (i > 0) metaDiv.appendChild(document.createTextNode(' \u00b7 ')); metaDiv.appendChild(part); }); item.appendChild(metaDiv); } listDiv.appendChild(item); } el.textContent = ''; el.appendChild(listDiv); } function renderRelatedDomains(domains) { const section = document.getElementById('domain-related-section'); const el = document.getElementById('domain-related-domains'); if (!el || !section) return; if (!domains || domains.length === 0) { section.hidden = true; return; } section.hidden = false; const stateVariantMap = { 'Aktiv': 'success', 'In Vorbereitung': 'warning', }; let items = ''; for (const d of domains) { const url = d.URL || ''; const no = d.No || ''; const state = d.State || ''; const variant = stateVariantMap[state] || 'neutral'; const detailLink = domainBaseUrl + encodeURIComponent(no); // safe-html: all dynamic values are escaped via esc() items += '' + '' + esc(url || no) + '' + '' + esc(state) + '' + ''; } // safe-html: all content built from esc()-escaped values el.innerHTML = items; } return { destroy: () => { requestController.abort(); }, }; } export function initHelpdeskDomainDetail(root = document) { const container = resolveContainer(root); if (!container) { return EMPTY_API; } return init(container); }