import { showAsyncFlash } from '/js/components/app-async-flash.js'; import { getJson, postForm } from '/js/core/app-http.js'; import { createListPageModule } from '/js/core/app-list-page-module.js'; import { escapeHtml } from '/js/pages/app-list-utils.js'; createListPageModule({ configId: 'helpdesk-updates', moduleId: 'helpdesk-updates', missingGridMessage: 'Helpdesk updates grid init failed: Grid.js missing', initErrorMessage: 'Helpdesk updates grid init failed', setup: ({ config, gridjs, appBase, initListPage }) => { const labels = config.labels || {}; const canManage = config.canManage || false; const listResult = initListPage({ grid: { gridjs, container: '#helpdesk-updates-grid', dataUrl: config.dataUrl || 'helpdesk/updates-data', appBase, columns: [ { name: labels.ticketNo || 'Ticket', sort: false, formatter: (cell) => { const no = escapeHtml(String(cell?.no || '')); const url = String(cell?.url || '').trim(); if (no === '') return gridjs.html(''); if (url === '') return gridjs.html(`${no}`); return gridjs.html(`${no}`); }, }, { name: labels.ticketDescription || 'Description', sort: false, formatter: (cell) => { const text = String(cell || '').trim(); if (text === '') { return gridjs.html(''); } const escaped = escapeHtml(text); return gridjs.html(`${escaped}`); }, }, { name: labels.ticketState || 'Ticket status', sort: false, formatter: (cell) => { const label = escapeHtml(cell?.label || ''); const variant = cell?.variant || 'neutral'; return gridjs.html(`${label}`); }, }, { name: labels.assignment || 'Assignment', sort: false, formatter: (cell) => { const label = escapeHtml(cell?.label || ''); const variant = cell?.variant || 'neutral'; return gridjs.html(`${label}`); }, }, { name: labels.actions || '', sort: false, formatter: (cell) => { if (!cell) return ''; let buttons = ''; if (canManage) { // raw-html-ok: all values passed through escapeHtml, cell data is from our own API const data = escapeHtml(JSON.stringify(cell)); const buttonLabel = cell.status === 'open' ? escapeHtml(labels.assign || 'Assign') : escapeHtml(labels.edit || 'Edit'); const icon = cell.status === 'open' ? 'bi-link-45deg' : 'bi-pencil'; buttons += ``; } const giteaUrl = String(cell.gitea_path || '').trim(); if (giteaUrl !== '') { const giteaLabel = escapeHtml(labels.giteaLink || 'Gitea'); buttons += ``; } if (buttons === '') return ''; return gridjs.html(`${buttons}`); }, }, { name: labels.category || 'Type', sort: false, formatter: (cell) => { const label = escapeHtml(cell?.label || ''); const variant = cell?.variant || 'neutral'; return gridjs.html(`${label}`); }, }, { name: labels.debitor || 'Customer', sort: false, formatter: (cell) => { const name = escapeHtml(String(cell?.name || '')); const url = String(cell?.url || '').trim(); if (name === '') return gridjs.html(''); const style = 'display:inline-block;max-width:20ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom'; if (url === '') return gridjs.html(`${name}`); return gridjs.html(`${name}`); }, }, { name: labels.domain || 'Domain', sort: false }, { name: labels.createdOn || 'Created', sort: false }, ], paginationLimit: 50, language: config.gridLang || {}, mapData: (data) => { const rows = data.rows || data.data || []; return rows.map((row) => [ { no: row.ticket_no || '', url: row.ticket_url || '' }, row.ticket_description || '', { label: row.ticket_state_label || '', variant: row.ticket_state_variant || 'neutral' }, { label: row.status_label || '', variant: row.status_variant || 'neutral' }, { ticket_no: row.ticket_no || '', debitor_no: row.debitor_no || '', debitor_name: row.debitor_name || '', category_code: row.category_code || '', domain_no: row.domain_no || '', domain_url: row.domain_url || '', gitea_path: row.gitea_path || '', notes: row.notes || '', status: row.status || 'open', }, { label: row.category_label || '', variant: row.category_variant || 'neutral' }, { name: row.debitor_name || '', url: row.debitor_url || '' }, row.domain_url || '', row.created_on || '', ]); }, search: config.gridSearch || null, filterSchema: config.filterSchema || [], urlSync: true, }, filters: { mode: 'drawer', chipMeta: config.filterChipMeta || {}, watchInputs: ['#helpdesk-updates-search-input'], }, }); const gridConfig = listResult?.gridConfig || null; if (!gridConfig) { return null; } const refreshButton = document.getElementById('updates-refresh-button'); if (refreshButton) { refreshButton.addEventListener('click', async () => { refreshButton.disabled = true; refreshButton.setAttribute('aria-busy', 'true'); try { const refreshUrl = new URL(config.dataUrl, appBase); refreshUrl.searchParams.set('refresh', '1'); await getJson(refreshUrl.toString()); gridConfig?.grid?.forceRender(); } catch { showAsyncFlash('error', labels.refreshError || labels.assignError || 'Refresh failed'); } finally { refreshButton.disabled = false; refreshButton.removeAttribute('aria-busy'); } }); } if (!canManage) { return gridConfig; } const dialog = document.getElementById('update-assign-dialog'); const form = document.getElementById('update-assign-form'); const contextEl = document.getElementById('update-assign-context'); const domainSelect = document.getElementById('update-assign-domain'); const giteaInput = document.getElementById('update-assign-gitea'); const notesInput = document.getElementById('update-assign-notes'); const ticketNoInput = document.getElementById('update-assign-ticket-no'); const debitorNoInput = document.getElementById('update-assign-debitor-no'); const debitorNameInput = document.getElementById('update-assign-debitor-name'); const categoryCodeInput = document.getElementById('update-assign-category-code'); const domainUrlInput = document.getElementById('update-assign-domain-url'); const submitButton = document.getElementById('update-assign-submit'); if (!dialog || !form || !domainSelect || !domainUrlInput || !submitButton) { return gridConfig; } const closeDialog = () => { try { dialog.close(); } catch { /* no-op */ } document.body.classList.remove('modal-is-open'); }; dialog.querySelectorAll('[data-update-assign-close]').forEach((button) => { button.addEventListener('click', (event) => { event.preventDefault(); closeDialog(); }); }); dialog.addEventListener('cancel', (event) => { event.preventDefault(); closeDialog(); }); dialog.addEventListener('click', (event) => { if (event.target === dialog) { closeDialog(); } }); domainSelect.addEventListener('change', () => { const selected = domainSelect.options[domainSelect.selectedIndex]; domainUrlInput.value = selected?.dataset?.url || ''; }); const populateDomainSelect = (domains, preselectedNo) => { domainSelect.textContent = ''; if (!Array.isArray(domains) || domains.length === 0) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = labels.noDomains || 'No domains found'; domainSelect.appendChild(opt); } else { const placeholder = document.createElement('option'); placeholder.value = ''; placeholder.textContent = labels.selectDomain || 'Select domain...'; domainSelect.appendChild(placeholder); for (const domain of domains) { const opt = document.createElement('option'); opt.value = domain.value || ''; opt.textContent = domain.label || domain.value || ''; opt.dataset.url = domain.url || ''; if (preselectedNo && domain.value === preselectedNo) { opt.selected = true; domainUrlInput.value = domain.url || ''; } domainSelect.appendChild(opt); } } domainSelect.disabled = false; }; document.addEventListener('click', async (event) => { const button = event.target.closest('[data-update-assign]'); if (!button) { return; } let data; try { data = JSON.parse(button.dataset.updateAssign); } catch { return; } ticketNoInput.value = data.ticket_no || ''; debitorNoInput.value = data.debitor_no || ''; debitorNameInput.value = data.debitor_name || ''; categoryCodeInput.value = data.category_code || ''; giteaInput.value = data.gitea_path || ''; notesInput.value = data.notes || ''; domainUrlInput.value = data.domain_url || ''; const categoryDisplay = data.category_code === 'UPDATE-HF' ? (labels.hotfix || 'Hotfix') : (labels.update || 'Update'); contextEl.textContent = `${data.ticket_no} - ${data.debitor_name} - ${categoryDisplay}`; domainSelect.textContent = ''; const loadingOpt = document.createElement('option'); loadingOpt.value = ''; loadingOpt.textContent = labels.loadingDomains || 'Loading...'; domainSelect.appendChild(loadingOpt); domainSelect.disabled = true; document.body.classList.add('modal-is-open'); dialog.showModal(); try { const domainsUrl = new URL(config.domainsDataUrl, appBase); domainsUrl.searchParams.set('debitor_no', data.debitor_no); const domains = await getJson(domainsUrl.toString()); populateDomainSelect(domains, data.domain_no); } catch { populateDomainSelect([], null); } }); form.addEventListener('submit', async (event) => { event.preventDefault(); submitButton.disabled = true; submitButton.setAttribute('aria-busy', 'true'); const formData = new URLSearchParams(new FormData(form)); if (config.csrfKey) { formData.set(config.csrfKey, config.csrfToken || ''); } try { const result = await postForm(new URL(config.assignUrl, appBase).toString(), formData); if (result?.ok === true) { closeDialog(); gridConfig?.grid?.forceRender(); showAsyncFlash('success', labels.assignSuccess || labels.saved || 'Saved'); } else { const errorMsg = result?.errors?.general || labels.assignError || 'Failed to assign'; showAsyncFlash('error', errorMsg); } } catch (error) { const errorMsg = error?.payload?.errors?.general || labels.assignError || 'Failed to assign'; showAsyncFlash('error', errorMsg); } finally { submitButton.disabled = false; submitButton.removeAttribute('aria-busy'); } }); return gridConfig; }, });