feat(js): add app-http contracts and migrate helpdesk runtime layer
This commit is contained in:
@@ -1,105 +1,97 @@
|
||||
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 { createListPageModule } from '/js/core/app-list-page-module.js';
|
||||
import { escapeHtml, withCurrentListReturn } from '/js/pages/app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('helpdesk-domains');
|
||||
if (config) {
|
||||
const gridjs = window.gridjs;
|
||||
if (!gridjs) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk domains grid init failed: Grid.js missing', { module: 'helpdesk-domains', component: 'grid' });
|
||||
} else {
|
||||
const appBase = getAppBase();
|
||||
createListPageModule({
|
||||
configId: 'helpdesk-domains',
|
||||
moduleId: 'helpdesk-domains',
|
||||
missingGridMessage: 'Helpdesk domains grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Helpdesk domains grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const labels = config.labels || {};
|
||||
|
||||
const domainBaseUrl = config.domainBaseUrl || '';
|
||||
const domainUrlIndex = 7;
|
||||
|
||||
const gridOptions = {
|
||||
gridjs,
|
||||
container: '#helpdesk-domains-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/domains-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
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>`);
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#helpdesk-domains-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/domains-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
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,
|
||||
formatter: (cell) => {
|
||||
const name = escapeHtml(cell?.name || '');
|
||||
const detailUrl = String(cell?.url || '').trim();
|
||||
if (detailUrl === '') {
|
||||
return gridjs.html(name);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(detailUrl));
|
||||
return gridjs.html(`<a href="${href}">${name}</a>`);
|
||||
{
|
||||
name: labels.customerName || 'Customer Name',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const name = escapeHtml(cell?.name || '');
|
||||
const detailUrl = String(cell?.url || '').trim();
|
||||
if (detailUrl === '') {
|
||||
return gridjs.html(name);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(detailUrl));
|
||||
return gridjs.html(`<a href="${href}">${name}</a>`);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: labels.url || 'URL', sort: true },
|
||||
{ name: labels.contractType || 'Contract Type', sort: true },
|
||||
{
|
||||
name: labels.state || 'State',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const state = escapeHtml(cell?.state || '');
|
||||
const variant = cell?.variant || 'neutral';
|
||||
return gridjs.html(`<span class="badge" data-variant="${escapeHtml(variant)}">${state}</span>`);
|
||||
{ name: labels.url || 'URL', sort: true },
|
||||
{ name: labels.contractType || 'Contract Type', sort: true },
|
||||
{
|
||||
name: labels.state || 'State',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const state = escapeHtml(cell?.state || '');
|
||||
const variant = cell?.variant || 'neutral';
|
||||
return gridjs.html(`<span class="badge" data-variant="${escapeHtml(variant)}">${state}</span>`);
|
||||
},
|
||||
},
|
||||
{ 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, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang || {},
|
||||
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: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[domainUrlIndex]?.data || '',
|
||||
},
|
||||
{ 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, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang || {},
|
||||
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: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[domainUrlIndex]?.data || '',
|
||||
},
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: config.filterChipMeta || {},
|
||||
watchInputs: ['#helpdesk-domains-search-input'],
|
||||
},
|
||||
});
|
||||
}) || {};
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk domains grid init failed', { module: 'helpdesk-domains', component: 'grid' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
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 { createListPageModule } from '/js/core/app-list-page-module.js';
|
||||
import { bindBulkVisibility } from '/js/components/app-bulk-selection.js';
|
||||
import { confirmDialog } from '/js/core/app-confirm-dialog.js';
|
||||
import { postForm } from '/js/core/app-http.js';
|
||||
import { escapeHtml, withCurrentListReturn } from '/js/pages/app-list-utils.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();
|
||||
createListPageModule({
|
||||
configId: 'helpdesk-handovers',
|
||||
moduleId: 'helpdesk-handovers',
|
||||
missingGridMessage: 'Helpdesk handovers grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Helpdesk handovers grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const labels = config.labels || {};
|
||||
const canManage = config.canManage || false;
|
||||
|
||||
@@ -100,7 +98,7 @@ if (config) {
|
||||
};
|
||||
}
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
const listResult = initListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
@@ -109,56 +107,51 @@ if (config) {
|
||||
},
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk handovers grid init failed', { module: 'helpdesk-handovers', component: 'grid' });
|
||||
const gridConfig = listResult?.gridConfig || null;
|
||||
if (!canManage || !gridConfig) {
|
||||
return gridConfig;
|
||||
}
|
||||
|
||||
if (canManage && gridConfig) {
|
||||
bindBulkVisibility({
|
||||
containerSelector: '#helpdesk-handovers-grid',
|
||||
buttonSelector: '[data-handovers-bulk]',
|
||||
});
|
||||
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 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();
|
||||
}
|
||||
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 data = await postForm(new URL('helpdesk/handovers/bulk/delete', appBase).toString(), body);
|
||||
if (data?.ok !== true) {
|
||||
throw new Error('Bulk delete failed');
|
||||
}
|
||||
gridConfig.selection?.clear?.();
|
||||
gridConfig.grid?.forceRender();
|
||||
} catch {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,73 +1,66 @@
|
||||
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 { createListPageModule } from '/js/core/app-list-page-module.js';
|
||||
import { escapeHtml, withCurrentListReturn } from '/js/pages/app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('helpdesk-search');
|
||||
if (config) {
|
||||
const gridjs = window.gridjs;
|
||||
if (!gridjs) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk search grid init failed: Grid.js missing', { module: 'helpdesk-search', component: 'grid' });
|
||||
} else {
|
||||
const appBase = getAppBase();
|
||||
createListPageModule({
|
||||
configId: 'helpdesk-search',
|
||||
moduleId: 'helpdesk-search',
|
||||
missingGridMessage: 'Helpdesk search grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Helpdesk search grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const labels = config.labels || {};
|
||||
|
||||
const gridOptions = {
|
||||
gridjs,
|
||||
container: '#helpdesk-search-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/search-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.debtorNo || 'Debtor No.',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const debtorNo = escapeHtml(cell?.no || '');
|
||||
const detailUrl = String(cell?.url || '').trim();
|
||||
if (detailUrl === '') {
|
||||
return gridjs.html(debtorNo);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(detailUrl));
|
||||
return gridjs.html(`<a href="${href}">${debtorNo}</a>`);
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#helpdesk-search-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/search-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.debtorNo || 'Debtor No.',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const debtorNo = escapeHtml(cell?.no || '');
|
||||
const detailUrl = String(cell?.url || '').trim();
|
||||
if (detailUrl === '') {
|
||||
return gridjs.html(debtorNo);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(detailUrl));
|
||||
return gridjs.html(`<a href="${href}">${debtorNo}</a>`);
|
||||
},
|
||||
},
|
||||
{ name: labels.name || 'Name', sort: true },
|
||||
{ name: labels.city || 'City', sort: true },
|
||||
{ name: labels.phone || 'Phone', sort: false },
|
||||
{ name: labels.email || 'E-Mail', sort: false },
|
||||
{ name: 'detail_url', hidden: true },
|
||||
],
|
||||
sortColumns: ['No', 'Name', 'City', null, null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang || {},
|
||||
mapData: (data) => (data.data || []).map((row) => [
|
||||
{ no: row.No || '', url: row.detail_url || '' },
|
||||
row.Name || '',
|
||||
row.City || '',
|
||||
row.Phone_No || '',
|
||||
row.E_Mail || '',
|
||||
row.detail_url || '',
|
||||
]),
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[5]?.data || '',
|
||||
},
|
||||
{ name: labels.name || 'Name', sort: true },
|
||||
{ name: labels.city || 'City', sort: true },
|
||||
{ name: labels.phone || 'Phone', sort: false },
|
||||
{ name: labels.email || 'E-Mail', sort: false },
|
||||
{ name: 'detail_url', hidden: true },
|
||||
],
|
||||
sortColumns: ['No', 'Name', 'City', null, null, null],
|
||||
paginationLimit: 10,
|
||||
language: config.gridLang || {},
|
||||
mapData: (data) => (data.data || []).map((row) => [
|
||||
{ no: row.No || '', url: row.detail_url || '' },
|
||||
row.Name || '',
|
||||
row.City || '',
|
||||
row.Phone_No || '',
|
||||
row.E_Mail || '',
|
||||
row.detail_url || '',
|
||||
]),
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[5]?.data || '',
|
||||
},
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: config.filterChipMeta || {},
|
||||
watchInputs: ['#helpdesk-search-input'],
|
||||
},
|
||||
});
|
||||
}) || {};
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk search grid init failed', { module: 'helpdesk-search', component: 'grid' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,81 +1,73 @@
|
||||
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 { createListPageModule } from '/js/core/app-list-page-module.js';
|
||||
import { escapeHtml, withCurrentListReturn } from '/js/pages/app-list-utils.js';
|
||||
|
||||
const config = readPageConfig('helpdesk-software-products');
|
||||
if (config) {
|
||||
const gridjs = window.gridjs;
|
||||
if (!gridjs) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk software products grid init failed: Grid.js missing', { module: 'helpdesk-software-products', component: 'grid' });
|
||||
} else {
|
||||
const appBase = getAppBase();
|
||||
createListPageModule({
|
||||
configId: 'helpdesk-software-products',
|
||||
moduleId: 'helpdesk-software-products',
|
||||
missingGridMessage: 'Helpdesk software products grid init failed: Grid.js missing',
|
||||
initErrorMessage: 'Helpdesk software products grid init failed',
|
||||
setup: ({ config, gridjs, appBase, initListPage }) => {
|
||||
const labels = config.labels || {};
|
||||
|
||||
const editUrlIndex = 4;
|
||||
|
||||
const gridOptions = {
|
||||
gridjs,
|
||||
container: '#helpdesk-software-products-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/software-products-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.code || 'Code',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const code = escapeHtml(cell?.code || '');
|
||||
const editUrl = String(cell?.url || '').trim();
|
||||
if (editUrl === '') {
|
||||
return gridjs.html(code);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(editUrl));
|
||||
return gridjs.html(`<a href="${href}">${code}</a>`);
|
||||
const { gridConfig } = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#helpdesk-software-products-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/software-products-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: labels.code || 'Code',
|
||||
sort: true,
|
||||
formatter: (cell) => {
|
||||
const code = escapeHtml(cell?.code || '');
|
||||
const editUrl = String(cell?.url || '').trim();
|
||||
if (editUrl === '') {
|
||||
return gridjs.html(code);
|
||||
}
|
||||
const href = escapeHtml(withCurrentListReturn(editUrl));
|
||||
return gridjs.html(`<a href="${href}">${code}</a>`);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: labels.bcDescription || 'BC Description', sort: true },
|
||||
{ name: labels.name || 'Product Name', sort: true },
|
||||
{
|
||||
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.bcDescription || 'BC Description', sort: true },
|
||||
{ name: labels.name || 'Product Name', sort: true },
|
||||
{
|
||||
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: 'edit_url', hidden: true },
|
||||
],
|
||||
sortColumns: ['code', 'bc_description', 'name', 'active', null],
|
||||
paginationLimit: 20,
|
||||
language: config.gridLang || {},
|
||||
mapData: (data) => (data.data || []).map((row) => [
|
||||
{ code: row.code || '', url: row.edit_url || '' },
|
||||
row.bc_description || '',
|
||||
row.name || '',
|
||||
{ label: row.active_label || '', variant: row.active_variant || 'neutral' },
|
||||
row.edit_url || '',
|
||||
]),
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[editUrlIndex]?.data || '',
|
||||
},
|
||||
{ name: 'edit_url', hidden: true },
|
||||
],
|
||||
sortColumns: ['code', 'bc_description', 'name', 'active', null],
|
||||
paginationLimit: 20,
|
||||
language: config.gridLang || {},
|
||||
mapData: (data) => (data.data || []).map((row) => [
|
||||
{ code: row.code || '', url: row.edit_url || '' },
|
||||
row.bc_description || '',
|
||||
row.name || '',
|
||||
{ label: row.active_label || '', variant: row.active_variant || 'neutral' },
|
||||
row.edit_url || '',
|
||||
]),
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
rowInteraction: { linkColumn: 0 },
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => rowData?.cells?.[editUrlIndex]?.data || '',
|
||||
},
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: config.filterChipMeta || {},
|
||||
watchInputs: ['#helpdesk-software-products-search-input'],
|
||||
},
|
||||
});
|
||||
}) || {};
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk software products grid init failed', { module: 'helpdesk-software-products', component: 'grid' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return gridConfig;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,138 +1,132 @@
|
||||
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 } from '/js/pages/app-list-utils.js';
|
||||
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';
|
||||
|
||||
const config = readPageConfig('helpdesk-updates');
|
||||
if (config) {
|
||||
const gridjs = window.gridjs;
|
||||
if (!gridjs) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk updates grid init failed: Grid.js missing', { module: 'helpdesk-updates', component: 'grid' });
|
||||
} else {
|
||||
const appBase = getAppBase();
|
||||
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 gridOptions = {
|
||||
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();
|
||||
const desc = escapeHtml(String(cell?.description || ''));
|
||||
const tip = desc ? ` data-tooltip="${desc}" data-tooltip-pos="top"` : '';
|
||||
if (no === '') return gridjs.html('<span class="text-muted">—</span>');
|
||||
if (url === '') return gridjs.html(`<span${tip}>${no}</span>`);
|
||||
return gridjs.html(`<a href="${escapeHtml(url)}"${tip}>${no}</a>`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.ticketState || 'Ticket status',
|
||||
sort: false,
|
||||
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.assignment || 'Assignment',
|
||||
sort: false,
|
||||
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.actions || '',
|
||||
sort: false,
|
||||
formatter: (cell) => {
|
||||
if (!cell) return '';
|
||||
let buttons = '';
|
||||
// Assign/Edit button (only if user can manage)
|
||||
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 += `<button type="button" class="secondary outline small" data-update-assign='${data}' data-tooltip="${buttonLabel}" data-tooltip-pos="top" aria-label="${buttonLabel}"><i class="bi ${icon}"></i></button>`;
|
||||
}
|
||||
// Gitea link button (if URL exists)
|
||||
const giteaUrl = String(cell.gitea_path || '').trim();
|
||||
if (giteaUrl !== '') {
|
||||
const giteaLabel = escapeHtml(labels.giteaLink || 'Gitea');
|
||||
buttons += `<a href="${escapeHtml(giteaUrl)}" target="_blank" rel="noopener" role="button" class="secondary outline small" data-tooltip="${giteaLabel}" data-tooltip-pos="top" aria-label="${giteaLabel}"><i class="bi bi-box-arrow-up-right"></i></a>`;
|
||||
}
|
||||
if (buttons === '') return '';
|
||||
return gridjs.html(`<span class="app-inline-actions">${buttons}</span>`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.category || 'Type',
|
||||
sort: false,
|
||||
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.debitor || 'Customer',
|
||||
sort: false,
|
||||
formatter: (cell) => {
|
||||
const name = escapeHtml(String(cell?.name || ''));
|
||||
const url = String(cell?.url || '').trim();
|
||||
if (name === '') return gridjs.html('<span class="text-muted">—</span>');
|
||||
const style = 'display:inline-block;max-width:20ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom';
|
||||
if (url === '') return gridjs.html(`<span style="${style}" title="${name}">${name}</span>`);
|
||||
return gridjs.html(`<a href="${escapeHtml(url)}" style="${style}" title="${name}">${name}</a>`);
|
||||
},
|
||||
},
|
||||
{ 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 || [];
|
||||
// Column order: Ticket | Ticket-Status | Assignment | Actions | Type | Customer | Domain | Created
|
||||
return rows.map((row) => [
|
||||
{ no: row.ticket_no || '', url: row.ticket_url || '', description: row.ticket_description || '' },
|
||||
{ label: row.ticket_state_label || '', variant: row.ticket_state_variant || 'neutral' },
|
||||
{ label: row.status_label || '', variant: row.status_variant || 'neutral' },
|
||||
const listResult = initListPage({
|
||||
grid: {
|
||||
gridjs,
|
||||
container: '#helpdesk-updates-grid',
|
||||
dataUrl: config.dataUrl || 'helpdesk/updates-data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
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',
|
||||
name: labels.ticketNo || 'Ticket',
|
||||
sort: false,
|
||||
formatter: (cell) => {
|
||||
const no = escapeHtml(String(cell?.no || ''));
|
||||
const url = String(cell?.url || '').trim();
|
||||
const desc = escapeHtml(String(cell?.description || ''));
|
||||
const tip = desc ? ` data-tooltip="${desc}" data-tooltip-pos="top"` : '';
|
||||
if (no === '') return gridjs.html('<span class="text-muted">—</span>');
|
||||
if (url === '') return gridjs.html(`<span${tip}>${no}</span>`);
|
||||
return gridjs.html(`<a href="${escapeHtml(url)}"${tip}>${no}</a>`);
|
||||
},
|
||||
},
|
||||
{ label: row.category_label || '', variant: row.category_variant || 'neutral' },
|
||||
{ name: row.debitor_name || '', url: row.debitor_url || '' },
|
||||
row.domain_url || '',
|
||||
row.created_on || '',
|
||||
]);
|
||||
{
|
||||
name: labels.ticketState || 'Ticket status',
|
||||
sort: false,
|
||||
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.assignment || 'Assignment',
|
||||
sort: false,
|
||||
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.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 += `<button type="button" class="secondary outline small" data-update-assign='${data}' data-tooltip="${buttonLabel}" data-tooltip-pos="top" aria-label="${buttonLabel}"><i class="bi ${icon}"></i></button>`;
|
||||
}
|
||||
const giteaUrl = String(cell.gitea_path || '').trim();
|
||||
if (giteaUrl !== '') {
|
||||
const giteaLabel = escapeHtml(labels.giteaLink || 'Gitea');
|
||||
buttons += `<a href="${escapeHtml(giteaUrl)}" target="_blank" rel="noopener" role="button" class="secondary outline small" data-tooltip="${giteaLabel}" data-tooltip-pos="top" aria-label="${giteaLabel}"><i class="bi bi-box-arrow-up-right"></i></a>`;
|
||||
}
|
||||
if (buttons === '') return '';
|
||||
return gridjs.html(`<span class="app-inline-actions">${buttons}</span>`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: labels.category || 'Type',
|
||||
sort: false,
|
||||
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.debitor || 'Customer',
|
||||
sort: false,
|
||||
formatter: (cell) => {
|
||||
const name = escapeHtml(String(cell?.name || ''));
|
||||
const url = String(cell?.url || '').trim();
|
||||
if (name === '') return gridjs.html('<span class="text-muted">—</span>');
|
||||
const style = 'display:inline-block;max-width:20ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom';
|
||||
if (url === '') return gridjs.html(`<span style="${style}" title="${name}">${name}</span>`);
|
||||
return gridjs.html(`<a href="${escapeHtml(url)}" style="${style}" title="${name}">${name}</a>`);
|
||||
},
|
||||
},
|
||||
{ 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 || '', description: 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,
|
||||
},
|
||||
search: config.gridSearch || null,
|
||||
filterSchema: config.filterSchema || [],
|
||||
urlSync: true,
|
||||
};
|
||||
|
||||
const { gridConfig } = initStandardListPage({
|
||||
grid: gridOptions,
|
||||
filters: {
|
||||
mode: 'drawer',
|
||||
chipMeta: config.filterChipMeta || {},
|
||||
@@ -140,28 +134,23 @@ if (config) {
|
||||
},
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
warnOnce('UI_INIT_FAIL', 'Helpdesk updates grid init failed', { module: 'helpdesk-updates', component: 'grid' });
|
||||
const gridConfig = listResult?.gridConfig || null;
|
||||
if (!gridConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Refresh button — bypasses session cache and reloads grid data from BC
|
||||
const refreshButton = document.getElementById('updates-refresh-button');
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', async () => {
|
||||
refreshButton.disabled = true;
|
||||
refreshButton.setAttribute('aria-busy', 'true');
|
||||
try {
|
||||
// Ping the data endpoint with refresh=1 to invalidate the cache
|
||||
const refreshUrl = new URL(config.dataUrl, appBase);
|
||||
refreshUrl.searchParams.set('refresh', '1');
|
||||
await fetch(refreshUrl.toString(), {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'fetch' },
|
||||
});
|
||||
// Now force the grid to re-render (next fetch will get fresh data)
|
||||
await getJson(refreshUrl.toString());
|
||||
gridConfig?.grid?.forceRender();
|
||||
} catch {
|
||||
// Ignore — grid will still show last data
|
||||
showAsyncFlash('error', labels.refreshError || labels.assignError || 'Refresh failed');
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
refreshButton.removeAttribute('aria-busy');
|
||||
@@ -169,157 +158,155 @@ if (config) {
|
||||
});
|
||||
}
|
||||
|
||||
// Assignment dialog logic
|
||||
if (canManage) {
|
||||
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) {
|
||||
const closeDialog = () => {
|
||||
try { dialog.close(); } catch { /* no-op */ }
|
||||
document.body.classList.remove('modal-is-open');
|
||||
};
|
||||
|
||||
// Close buttons
|
||||
dialog.querySelectorAll('[data-update-assign-close]').forEach((button) => {
|
||||
button.addEventListener('click', (e) => { e.preventDefault(); closeDialog(); });
|
||||
});
|
||||
dialog.addEventListener('cancel', (e) => { e.preventDefault(); closeDialog(); });
|
||||
dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); });
|
||||
|
||||
// Domain select change -> update hidden domain_url
|
||||
domainSelect.addEventListener('change', () => {
|
||||
const selected = domainSelect.options[domainSelect.selectedIndex];
|
||||
domainUrlInput.value = selected?.dataset?.url || '';
|
||||
});
|
||||
|
||||
/**
|
||||
* Populate the domain <select> with options loaded from the API.
|
||||
* Uses textContent and DOM methods instead of innerHTML to avoid XSS.
|
||||
*/
|
||||
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 d of domains) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.value || '';
|
||||
opt.textContent = d.label || d.value || '';
|
||||
opt.dataset.url = d.url || '';
|
||||
if (preselectedNo && d.value === preselectedNo) {
|
||||
opt.selected = true;
|
||||
domainUrlInput.value = d.url || '';
|
||||
}
|
||||
domainSelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
domainSelect.disabled = false;
|
||||
};
|
||||
|
||||
// Open dialog on assign button click (delegated)
|
||||
document.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('[data-update-assign]');
|
||||
if (!button) return;
|
||||
|
||||
let data;
|
||||
try { data = JSON.parse(button.dataset.updateAssign); } catch { return; }
|
||||
|
||||
// Populate form
|
||||
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} \u00b7 ${data.debitor_name} \u00b7 ${categoryDisplay}`;
|
||||
|
||||
// Show loading state in domain select
|
||||
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 res = await fetch(domainsUrl.toString(), {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'fetch' },
|
||||
});
|
||||
const domains = await res.json();
|
||||
populateDomainSelect(domains, data.domain_no);
|
||||
} catch {
|
||||
populateDomainSelect([], null);
|
||||
}
|
||||
});
|
||||
|
||||
// Form submit
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.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 res = await fetch(new URL(config.assignUrl, appBase).toString(), {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
|
||||
if (res.ok && result?.ok) {
|
||||
closeDialog();
|
||||
gridConfig?.grid?.forceRender();
|
||||
} else {
|
||||
const errorMsg = result?.errors?.general || labels.assignError || 'Failed to assign';
|
||||
window.alert(errorMsg);
|
||||
}
|
||||
} catch {
|
||||
window.alert(labels.assignError || 'Failed to assign');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.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;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user