Files
breadcrumb-the-shire/modules/helpdesk/templates/helpdesk-ticket-detail.phtml
fs 22966fdacf 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>
2026-04-22 17:03:25 +02:00

137 lines
5.8 KiB
PHTML

<?php
/**
* Ticket detail body — shared by the full-page ticket view and the drawer
* fragment. Does NOT render the titlebar or flash area (those belong to the
* page shell, not the content body).
*
* Required variables:
* $ticketNo, $ticketCustomerNo, $ticket (array|null), $connectionError (bool),
* $ticketCommunicationUrl (string)
*/
$ticketNo = $ticketNo ?? '';
$ticketCustomerNo = $ticketCustomerNo ?? '';
$ticket = is_array($ticket ?? null) ? $ticket : null;
$connectionError = $connectionError ?? false;
$ticketCommunicationUrl = $ticketCommunicationUrl ?? '';
?>
<div class="app-details-container"
data-app-component="helpdesk-ticket"
data-ticket-no="<?php e($ticketNo); ?>"
data-ticket-communication-url="<?php e($ticketCommunicationUrl); ?>"
data-file-download-url="<?php e(lurl('helpdesk/ticket-file-data')); ?>"
>
<section>
<?php if ($connectionError): ?>
<div class="notice" data-variant="error" role="alert">
<?php e(t('Could not connect to Business Central.')); ?>
</div>
<?php elseif ($ticket !== null): ?>
<!-- Communication feed — main content, loaded async -->
<div class="helpdesk-ticket-detail-content">
<section>
<div id="ticket-communication-loading">
<?php for ($i = 0; $i < 6; $i++): ?>
<div class="helpdesk-skeleton helpdesk-skeleton-aside-bubble <?php e($i % 2 === 1 ? 'helpdesk-skeleton-aside-bubble-alt' : ($i % 3 === 0 ? 'helpdesk-skeleton-aside-bubble-short' : '')); ?>"></div>
<?php endfor; ?>
</div>
<div id="ticket-communication-content" hidden>
<div id="ticket-communication-feed"></div>
</div>
</section>
</div>
<?php endif; ?>
</section>
<?php if (!$connectionError && $ticket !== null): ?>
<?php
$createdRaw = (string) ($ticket['Created_On'] ?? '');
$createdFormatted = $createdRaw !== '' && $createdRaw !== '0001-01-01T00:00:00Z'
? (new DateTimeImmutable($createdRaw))->format('d.m.Y H:i')
: '—';
$lastActivityRaw = (string) ($ticket['Last_Activity_Date'] ?? '');
$lastActivityFormatted = $lastActivityRaw !== '' && $lastActivityRaw !== '0001-01-01T00:00:00Z'
? (new DateTimeImmutable($lastActivityRaw))->format('d.m.Y H:i')
: '—';
$ticketState = (string) ($ticket['Ticket_State'] ?? '');
$ageLabel = '—';
if ($createdRaw !== '' && $createdRaw !== '0001-01-01T00:00:00Z') {
$createdDt = new DateTimeImmutable($createdRaw);
$diff = $createdDt->diff(new DateTimeImmutable('now'));
if ($diff->days >= 1) {
$ageLabel = $diff->days . ' ' . ($diff->days === 1 ? t('day') : t('days'));
} else {
$hours = $diff->h + ($diff->days * 24);
$ageLabel = max(1, $hours) . ' ' . t('hours');
}
}
?>
<aside id="app-details-aside-section">
<div class="app-details-aside-section helpdesk-ticket-aside">
<h3 class="helpdesk-support-section-title"><?php e(t('Ticket details')); ?></h3>
<dl class="helpdesk-ticket-meta">
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Status')); ?></dt>
<dd><?php e($ticketState); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Description')); ?></dt>
<dd><?php e((string) ($ticket['Description'] ?? '—')); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Company')); ?></dt>
<dd>
<?php if ($ticketCustomerNo !== ''): ?>
<a href="<?php e(lurl('helpdesk/debitor/' . rawurlencode($ticketCustomerNo))); ?>"><?php e((string) ($ticket['Company_Contact_Name'] ?? $ticketCustomerNo)); ?></a>
<?php else: ?>
<?php e((string) ($ticket['Company_Contact_Name'] ?? '—')); ?>
<?php endif; ?>
</dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Contact')); ?></dt>
<dd><?php e((string) ($ticket['Current_Contact_Name'] ?? '—')); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Category')); ?></dt>
<dd><?php e((string) ($ticket['Category_1_Description'] ?? '—')); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Support')); ?></dt>
<dd><?php e((string) ($ticket['Support_User_Name'] ?? '—')); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Created')); ?></dt>
<dd><?php e($createdFormatted); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Last activity')); ?></dt>
<dd><?php e($lastActivityFormatted); ?></dd>
</div>
<div class="helpdesk-ticket-meta-item">
<dt><?php e(t('Age')); ?></dt>
<dd><?php e($ageLabel); ?></dd>
</div>
</dl>
</div>
</aside>
<?php endif; ?>
</div>
<!-- i18n strings for JS rendering -->
<script type="application/json" id="helpdesk-ticket-i18n"><?php gridJsonForJs([
'No communication found.' => t('No communication found.'),
'Could not load communication history.' => t('Could not load communication history.'),
'Live communication unavailable, showing activity fallback.' => t('Live communication unavailable, showing activity fallback.'),
'Ticket' => t('Ticket'),
'Unknown user' => t('Unknown user'),
'Unknown time' => t('Unknown time'),
'changed status' => t('changed status'),
'Forwarded' => t('Forwarded'),
'attached a file' => t('attached a file'),
'logged an activity' => t('logged an activity'),
'on' => t('on'),
]); ?></script>