feat(helpdesk): ticket detail drawer — third drawer consumer

Migrates the ticket detail view (communication feed + ticket metadata aside)
into the core detail drawer pattern. Click a ticket number in the customer's
support tab → drawer slides in with the full ticket thread; arrow icons step
through the current filter page; ↗ opens the full page.

- `modules/helpdesk/templates/helpdesk-ticket-detail.phtml` extracts the
  body (communication feed container + metadata aside + i18n JSON) so the
  full page and the fragment render from the same source.
- `ticket-fragment($id).php` / `(none).phtml` reuse BcODataGateway::getTicket
  and enforce the same ABILITY_ACCESS guard as the full page. Connection
  errors render inline without aborting the fragment.
- Route `helpdesk/ticket-fragment/{id}` declared in module.php.
- `helpdesk-detail.js` wires the tickets grid: ticket-no formatter emits
  `data-drawer-trigger`, `linkColumn` disabled, `initDetailDrawer` called
  with `onContentLoaded → initHelpdeskTicket(contentEl)` so the async
  communication feed loads inside the drawer just like on the full page.
- `fetchUrl`/`fullUrl` written as single-expression arrows so the
  DetailDrawerFragmentContractTest can discover the consumer and validate
  the fragment endpoint files exist — the test now covers all three drawer
  consumers (address book, admin users, helpdesk tickets).

The full-page ticket view is unchanged for users who deep-link directly;
only the router-to-body wiring moved through the partial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 17:03:25 +02:00
parent 9a78a81af8
commit 22966fdacf
6 changed files with 223 additions and 142 deletions

View File

@@ -10,6 +10,8 @@ import { initListSection } from '/js/core/app-list-page-module.js';
import { getJson } from '/js/core/app-http.js';
import { resolveHost } from '/js/core/app-dom.js';
import { readPageConfig } from '/js/core/app-page-config.js';
import { initDetailDrawer } from '/js/components/app-detail-drawer.js';
import { initHelpdeskTicket } from './helpdesk-ticket.js';
import {
renderCommunicationError,
renderCommunicationFeed,
@@ -776,7 +778,7 @@ function init(container) {
{
name: labels.ticketNo || 'Ticket No.',
sort: true,
formatter: (cell) => gridjs.html(`<a href="${cell.url}">${cell.no}</a>`),
formatter: (cell) => gridjs.html(`<a class="app-grid-link-cell" href="${cell.url}" data-drawer-trigger data-ticket-no="${cell.no || ''}">${cell.no}</a>`),
},
{ name: labels.description || 'Description', sort: false },
{
@@ -816,10 +818,13 @@ function init(container) {
return url || '';
},
},
rowInteraction: { linkColumn: 0 },
rowInteraction: { linkColumn: false },
rowDataset: (row) => ({
uuid: row?.cells?.[0]?.data?.no || '',
}),
};
initListSection({
const section = initListSection({
moduleId: 'helpdesk-detail-tickets',
initErrorMessage: 'Helpdesk tickets grid init failed',
initOptions: {
@@ -832,6 +837,26 @@ function init(container) {
},
});
ticketsGridInitialized = true;
initDetailDrawer({
gridConfig: section?.gridConfig || null,
triggerSelector: '[data-drawer-trigger][data-ticket-no]',
rowUuidAttr: 'uuid',
fetchUrl: (ticketNo) => new URL(`helpdesk/ticket-fragment/${encodeURIComponent(ticketNo)}?from=${encodeURIComponent(customerNo || '')}`, appBase).toString(),
fullUrl: (ticketNo) => new URL(`helpdesk/ticket/${encodeURIComponent(ticketNo)}?from=${encodeURIComponent(customerNo || '')}`, appBase).toString(),
hashPrefix: 'ticket',
onContentLoaded: (contentEl) => {
try { initHelpdeskTicket(contentEl); } catch (err) { console.warn('[helpdesk-detail] initHelpdeskTicket failed', err); }
},
labels: {
close: labels.drawerClose || 'Close',
previous: labels.drawerPrev || 'Previous',
next: labels.drawerNext || 'Next',
openFull: labels.drawerOpenFull || 'Open full page',
loading: labels.drawerLoading || 'Loading',
error: labels.drawerError || 'Failed to load',
},
});
} finally {
ticketsGridInitializing = false;
}