Fix ticket age, bulk assign, add column visibility toggle

- TicketView: ticket age was measuring from last update not creation;
  fixed to always use created_at
- dashboard.js: bulk assign used non-existent onSelect callback (no
  selection was ever stored); fixed to onChange with selected[0],
  added max:1 to enforce single-select
- base.js: lt.combobox Enter key only fired when focusedIdx >= 0;
  now falls back to first filtered result when no arrow key used
- DashboardView + dashboard.js + dashboard.css: add COLS ▾ button on
  table header that opens a checkbox panel to show/hide optional
  columns (Ticket ID, Category, Type, Created By, Assigned To,
  Created, Updated); state persisted in localStorage, Reset button
  restores all; core columns (Priority, Title, Status, Actions) always
  visible; data-col attributes added to all th/td for CSS targeting

Notifications bell: was functional all along — was broken by the
notifications.php 500 error (now fixed). Avg resolution: correct,
tickets genuinely take ~158 days average on this dataset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 12:31:30 -04:00
parent 6c491c1baa
commit c6037a9ccc
5 changed files with 163 additions and 18 deletions
+46 -13
View File
@@ -568,7 +568,40 @@ include __DIR__ . '/layout_header.php';
<!-- Ticket table frame -->
<div class="lt-frame">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
<div class="lt-section-header">Ticket Queue</div>
<div class="lt-section-header" style="display:flex;align-items:center;justify-content:space-between;gap:0.5rem">
<span>Ticket Queue</span>
<div style="position:relative;display:inline-block">
<button type="button" id="colToggleBtn"
class="lt-btn lt-btn-ghost lt-btn-sm"
aria-haspopup="true" aria-expanded="false"
aria-controls="colTogglePanel"
title="Show/hide columns"
style="font-size:0.65rem;letter-spacing:0.05em">COLS &#x25BE;</button>
<div id="colTogglePanel" class="col-toggle-panel" aria-hidden="true" role="dialog" aria-label="Column visibility">
<div class="col-toggle-title">Visible Columns</div>
<?php
$toggleableCols = [
'ticket_id' => 'Ticket ID',
'category' => 'Category',
'type' => 'Type',
'created_by' => 'Created By',
'assigned_to' => 'Assigned To',
'created_at' => 'Created',
'updated_at' => 'Updated',
];
foreach ($toggleableCols as $colKey => $colName): ?>
<label class="col-toggle-row">
<input type="checkbox" class="lt-checkbox col-toggle-cb"
data-col="<?= $colKey ?>" checked>
<span><?= $colName ?></span>
</label>
<?php endforeach ?>
<div class="col-toggle-footer">
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm lt-w-full" id="colToggleReset">Reset</button>
</div>
</div>
</div>
</div>
<div class="lt-table-wrap">
<table class="lt-table lt-table-responsive" id="tickets-table" aria-label="Ticket queue">
<caption class="lt-sr-only">Ticket queue sorted by <?= htmlspecialchars($currentSort) ?> <?= $currentDir ?></caption>
@@ -596,7 +629,7 @@ include __DIR__ . '/layout_header.php';
];
foreach ($columns as $col => $label):
if ($col === '_actions'): ?>
<th scope="col" class="col-actions">Actions</th>
<th scope="col" class="col-actions" data-col="_actions">Actions</th>
<?php else:
$newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
$sortClass = ($currentSort === $col) ? 'sort-' . $currentDir : '';
@@ -604,7 +637,7 @@ include __DIR__ . '/layout_header.php';
$sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir, 'page' => 1]);
$sortUrl = htmlspecialchars('?' . http_build_query($sortParams), ENT_QUOTES, 'UTF-8');
?>
<th scope="col" class="<?= $sortClass ?>"
<th scope="col" class="<?= $sortClass ?>" data-col="<?= $col ?>"
data-action="navigate" data-url="<?= $sortUrl ?>"
<?= $ariaSort ?>
style="cursor:pointer"><?= $label ?></th>
@@ -646,20 +679,20 @@ include __DIR__ . '/layout_header.php';
aria-label="Select ticket <?= htmlspecialchars($row['ticket_id']) ?>">
</td>
<?php endif ?>
<td data-label="Ticket ID">
<td data-label="Ticket ID" data-col="ticket_id">
<a href="/ticket/<?= htmlspecialchars($row['ticket_id']) ?>"
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
</td>
<td data-label="Priority">
<td data-label="Priority" data-col="priority">
<?php $badgeClass = match($pNum) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
<span class="lt-badge <?= $badgeClass ?>">P<?= $pNum ?></span>
</td>
<td data-label="Title" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<td data-label="Title" data-col="title" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<span data-tooltip="<?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?>" data-tooltip-pos="top"><?= htmlspecialchars($row['title']) ?></span>
</td>
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
<td data-label="Type" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['type']) ?></td>
<td data-label="Status">
<td data-label="Category" data-col="category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
<td data-label="Type" data-col="type" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['type']) ?></td>
<td data-label="Status" data-col="status">
<?php $rowDotClass = match($row['status']) {
'Open' => 'lt-dot-up',
'In Progress' => 'lt-dot-warn',
@@ -670,8 +703,8 @@ include __DIR__ . '/layout_header.php';
<span class="lt-dot <?= $rowDotClass ?>" aria-hidden="true" style="vertical-align:middle;margin-right:0.3rem"></span>
<span class="lt-status lt-status-<?= $rowStatusSlug ?>"><?= htmlspecialchars($row['status']) ?></span>
</td>
<td data-label="Created By" class="lt-text-xs"><?= $creator ?></td>
<td data-label="Assigned To" class="lt-text-xs">
<td data-label="Created By" data-col="created_by" class="lt-text-xs"><?= $creator ?></td>
<td data-label="Assigned To" data-col="assigned_to" class="lt-text-xs">
<?php $assigneeDisplay = $row['assigned_display_name'] ?? $row['assigned_username'] ?? null; ?>
<?php if ($assigneeDisplay): ?>
<span data-tooltip="<?= htmlspecialchars($assigneeDisplay, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($assigneeDisplay) ?></span>
@@ -679,10 +712,10 @@ include __DIR__ . '/layout_header.php';
<span class="lt-text-muted">Unassigned</span>
<?php endif ?>
</td>
<td data-label="Created" class="lt-text-xs lt-text-muted ts-cell"
<td data-label="Created" data-col="created_at" class="lt-text-xs lt-text-muted ts-cell"
data-ts="<?= htmlspecialchars($row['created_at'], ENT_QUOTES, 'UTF-8') ?>"
title="<?= date('Y-m-d H:i T', strtotime($row['created_at'])) ?>"><?= $createdFmt ?></td>
<td data-label="Updated" class="lt-text-xs lt-text-muted ts-cell"
<td data-label="Updated" data-col="updated_at" class="lt-text-xs lt-text-muted ts-cell"
data-ts="<?= htmlspecialchars($row['updated_at'], ENT_QUOTES, 'UTF-8') ?>"
title="<?= date('Y-m-d H:i T', strtotime($row['updated_at'])) ?>"><?= $updatedFmt ?></td>
<td data-label="Actions">
+2 -3
View File
@@ -64,9 +64,8 @@ function formatAction(array $event): string {
}
}
// Calculate ticket age
$lastUpdate = !empty($ticket['updated_at']) ? strtotime($ticket['updated_at']) : strtotime($ticket['created_at']);
$ageSeconds = time() - $lastUpdate;
// Calculate ticket age from creation (not last update)
$ageSeconds = time() - strtotime($ticket['created_at']);
$ageDays = floor($ageSeconds / 86400);
$ageHours = floor(($ageSeconds % 86400) / 3600);
$ageClass = 'lt-text-muted';