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>
This commit is contained in:
@@ -12,7 +12,8 @@ if (config) {
|
||||
const appBase = getAppBase();
|
||||
const labels = config.labels || {};
|
||||
|
||||
const debitorUrlIndex = 6;
|
||||
const domainBaseUrl = config.domainBaseUrl || '';
|
||||
const domainUrlIndex = 7;
|
||||
|
||||
const gridOptions = {
|
||||
gridjs,
|
||||
@@ -20,7 +21,19 @@ if (config) {
|
||||
dataUrl: config.dataUrl || 'helpdesk/domains-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{ name: labels.no || 'No.', sort: true },
|
||||
{
|
||||
name: labels.no || 'No.',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const no = escapeHtml(cell?.no || '');
|
||||
const detailUrl = String(cell?.url || '').trim();
|
||||
if (detailUrl === '') {
|
||||
return gridjs.html(no);
|
||||
}
|
||||
const href = escapeHtml(detailUrl);
|
||||
return gridjs.html(`<a href="${href}">${no}</a>`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.customerName || 'Customer Name',
|
||||
sort: true,
|
||||
@@ -47,25 +60,32 @@ if (config) {
|
||||
},
|
||||
{ name: labels.administration || 'Administration', sort: true },
|
||||
{ name: 'debitor_url', hidden: true },
|
||||
{ name: 'domain_detail_url', hidden: true },
|
||||
],
|
||||
sortColumns: ['No', 'Customer_Name', 'URL', 'contract_type', 'State', 'Administration', null],
|
||||
sortColumns: ['No', 'Customer_Name', 'URL', 'contract_type', 'State', 'Administration', null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang || {},
|
||||
mapData: (data) => (data.data || []).map((row) => [
|
||||
row.No || '',
|
||||
{ name: row.Customer_Name || '', url: row.debitor_url || '' },
|
||||
row.URL || '',
|
||||
row.contract_type || '',
|
||||
{ state: row.State || '', variant: row.state_variant || 'neutral' },
|
||||
row.Administration || '',
|
||||
row.debitor_url || '',
|
||||
]),
|
||||
mapData: (data) => (data.data || []).map((row) => {
|
||||
const domainDetailUrl = domainBaseUrl && row.No
|
||||
? domainBaseUrl + encodeURIComponent(row.No)
|
||||
: '';
|
||||
return [
|
||||
{ no: row.No || '', url: domainDetailUrl },
|
||||
{ name: row.Customer_Name || '', url: row.debitor_url || '' },
|
||||
row.URL || '',
|
||||
row.contract_type || '',
|
||||
{ state: row.State || '', variant: row.state_variant || 'neutral' },
|
||||
row.Administration || '',
|
||||
row.debitor_url || '',
|
||||
domainDetailUrl,
|
||||
];
|
||||
}),
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
rowInteraction: { linkColumn: 1 },
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[debitorUrlIndex]?.data || '',
|
||||
getUrl: (rowData) => rowData?.cells?.[domainUrlIndex]?.data || '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ 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) {
|
||||
@@ -11,8 +13,12 @@ if (config) {
|
||||
} else {
|
||||
const appBase = getAppBase();
|
||||
const labels = config.labels || {};
|
||||
const canManage = config.canManage || false;
|
||||
|
||||
const editUrlIndex = 6;
|
||||
// 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,
|
||||
@@ -21,21 +27,21 @@ if (config) {
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.id || 'ID',
|
||||
name: labels.domain || 'Domain',
|
||||
sort: true,
|
||||
width: '80px',
|
||||
formatter: (cell) => {
|
||||
const id = escapeHtml(String(cell?.id || ''));
|
||||
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(id);
|
||||
return gridjs.html(domain);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(editUrl));
|
||||
return gridjs.html(`<a href="${href}">#${id}</a>`);
|
||||
return gridjs.html(`<a href="${href}">${domain}</a>`);
|
||||
},
|
||||
},
|
||||
{ name: labels.debitorName || 'Customer', sort: true },
|
||||
{ name: labels.productCode || 'Software product', sort: true },
|
||||
{
|
||||
name: labels.status || 'Status',
|
||||
sort: true,
|
||||
@@ -45,18 +51,20 @@ if (config) {
|
||||
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: ['id', 'debitor_name', 'product_code', 'status', 'created_at', 'created_by', null],
|
||||
sortColumns: ['domain_url', 'status', 'debitor_name', 'product_code', 'created_at', 'created_by', null],
|
||||
paginationLimit: 20,
|
||||
language: config.gridLang || {},
|
||||
mapData: (data) => (data.data || []).map((row) => [
|
||||
{ id: row.id || '', url: row.edit_url || '' },
|
||||
row.debitor_name || '',
|
||||
row.product_code || '',
|
||||
{ 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 || '',
|
||||
@@ -64,12 +72,34 @@ if (config) {
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
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: {
|
||||
@@ -82,5 +112,53 @@ if (config) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user