313 lines
13 KiB
JavaScript
313 lines
13 KiB
JavaScript
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();
|
|
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 = '';
|
|
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,
|
|
},
|
|
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;
|
|
},
|
|
});
|