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>
165 lines
6.1 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|