Files
breadcrumb-the-shire/modules/helpdesk/web/js/pages/helpdesk-handovers-index.js
fs e7c60468c9 feat(helpdesk): add domain linking, bulk actions, and revision polish to handovers
Adds domain selection (cascaded from debitor) to handover creation and edit,
bulk delete with confirmation dialog, and various UI improvements to the
handover wizard and list page. Includes migration for domain columns,
domain-select AJAX endpoint, and updated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:21:12 +02:00

165 lines
6.1 KiB
JavaScript

import { initStandardListPage } from '/js/core/app-grid-factory.js';
import { readPageConfig } from '/js/core/app-page-config.js';
import { warnOnce } from '/js/core/app-dom.js';
import { escapeHtml, getAppBase, withCurrentListReturn } from '/js/pages/app-list-utils.js';
import { bindBulkVisibility } from '/js/components/app-bulk-selection.js';
import { confirmDialog } from '/js/core/app-confirm-dialog.js';
const config = readPageConfig('helpdesk-handovers');
if (config) {
const gridjs = window.gridjs;
if (!gridjs) {
warnOnce('UI_INIT_FAIL', 'Helpdesk handovers grid init failed: Grid.js missing', { module: 'helpdesk-handovers', component: 'grid' });
} else {
const appBase = getAppBase();
const labels = config.labels || {};
const canManage = config.canManage || false;
// Column layout: Domain | Status | Customer | Product | Created | Created by | (edit_url hidden)
// With selection: checkbox is prepended by grid factory, shifting indices +1
const editUrlIndex = canManage ? 7 : 6;
const domainColumnIndex = canManage ? 1 : 0;
const gridOptions = {
gridjs,
container: '#helpdesk-handovers-grid',
dataUrl: config.dataUrl || 'helpdesk/handovers-data',
appBase,
columns: [
{
name: labels.domain || 'Domain',
sort: true,
formatter: (cell) => {
const domain = escapeHtml(String(cell?.domain || ''));
const editUrl = String(cell?.url || '').trim();
if (domain === '') {
return gridjs.html('<span class="text-muted">—</span>');
}
if (editUrl === '') {
return gridjs.html(domain);
}
const href = escapeHtml(withCurrentListReturn(editUrl));
return gridjs.html(`<a href="${href}">${domain}</a>`);
},
},
{
name: labels.status || 'Status',
sort: true,
formatter: (cell) => {
const label = escapeHtml(cell?.label || '');
const variant = cell?.variant || 'neutral';
return gridjs.html(`<span class="badge" data-variant="${escapeHtml(variant)}">${label}</span>`);
},
},
{ name: labels.debitorName || 'Customer', sort: true },
{ name: labels.productDisplay || 'Software product', sort: true },
{ name: labels.createdAt || 'Created', sort: true },
{ name: labels.createdBy || 'Created by', sort: true },
{ name: 'edit_url', hidden: true },
],
sortColumns: ['domain_url', 'status', 'debitor_name', 'product_code', 'created_at', 'created_by', null],
paginationLimit: 20,
language: config.gridLang || {},
mapData: (data) => (data.data || []).map((row) => [
{ domain: row.domain_url || '', url: row.edit_url || '', id: row.id || '' },
{ label: row.status_label || '', variant: row.status_variant || 'neutral' },
row.debitor_name || '',
row.product_display || '',
row.created_at || '',
row.created_by_name || '',
row.edit_url || '',
]),
search: config.gridSearch || null,
filterSchema: config.filterSchema || [],
urlSync: true,
rowInteraction: { linkColumn: domainColumnIndex },
rowDblClick: {
getUrl: (rowData) => rowData?.cells?.[editUrlIndex]?.data || '',
},
};
if (canManage && gridjs.plugins?.selection?.RowSelection) {
gridOptions.selection = {
enabled: true,
id: 'selectRow',
component: gridjs.plugins.selection.RowSelection,
selectAllLabel: labels.selectAll || 'Select all',
props: {
id: (row) => row.cell(domainColumnIndex).data?.id || '',
},
getSelectedIds: ({ container }) =>
Array.from(container.querySelectorAll('.gridjs-tr-selected'))
.map((tr) => {
const link = tr.querySelector('a[href]');
if (!link) return '';
const href = link.getAttribute('href') || '';
const match = href.match(/\/edit\/(\d+)/);
return match ? match[1] : '';
})
.filter(Boolean),
};
}
const { gridConfig } = initStandardListPage({
grid: gridOptions,
filters: {
mode: 'drawer',
chipMeta: config.filterChipMeta || {},
watchInputs: ['#helpdesk-handovers-search-input'],
},
});
if (!gridConfig || !gridConfig.grid) {
warnOnce('UI_INIT_FAIL', 'Helpdesk handovers grid init failed', { module: 'helpdesk-handovers', component: 'grid' });
}
if (canManage && gridConfig) {
bindBulkVisibility({
containerSelector: '#helpdesk-handovers-grid',
buttonSelector: '[data-handovers-bulk]',
});
const bulkButtons = Array.from(document.querySelectorAll('[data-handovers-bulk]'));
bulkButtons.forEach((button) => {
button.addEventListener('click', async () => {
const action = button.dataset.handoversBulk || '';
const ids = gridConfig.selection?.getSelectedIds?.() || [];
if (!ids.length || action !== 'delete') return;
const confirmed = await confirmDialog.confirm({
message: labels.bulkDeleteConfirm || 'Delete selected handovers?',
actionKind: 'delete',
});
if (!confirmed) return;
const body = new URLSearchParams({
ids: ids.join(','),
[config.csrfKey]: config.csrfToken,
});
try {
const res = await fetch(new URL('helpdesk/handovers/bulk/delete', appBase).toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'X-Requested-With': 'fetch',
},
body,
});
const data = await res.json().catch(() => null);
if (res.ok && data?.ok) {
gridConfig.selection?.clear?.();
gridConfig.grid?.forceRender();
} else {
window.location.reload();
}
} catch {
window.location.reload();
}
});
});
}
}
}