fix: deep audit — wire TDS v1.2 components, fix kanban/tabs/bulk/avatar

- ticket.js: fix showTab() early return preventing attachments/deps from loading
- ticket.js: fix performStatusChange() overwriting lt-status-* classes
- dashboard.js: fix updateSelectionCount() using is-visible instead of style.display
- dashboard.js: fix populateKanbanCards() to use #kanban-col-* IDs (TDS v1.2)
- dashboard.js: fix setViewMode() removing references to old non-TDS elements
- dashboard.js: remove mobile-bottom-nav injection (no CSS existed for it)
- dashboard.css: add full lt-kanban-card component styles with priority accents
- dashboard.css: add mobile sidebar overlay, filter toggle, ticket preview popup CSS
- DashboardView.php: replace priority badges with lt-chip component
- TicketView.php: add lt-avatar with initials to comment author display
- ApiKeysView.php: enhance API usage section with lt-code-block component + curl example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 19:58:14 -04:00
parent 79c2d2b513
commit 9bdeaf7731
6 changed files with 260 additions and 136 deletions
+115 -91
View File
@@ -72,31 +72,6 @@ function initMobileSidebar() {
sidebar.insertBefore(closeBtn, sidebar.firstChild);
}
// Create mobile bottom navigation
if (!document.getElementById('mobileBottomNav')) {
const nav = document.createElement('nav');
nav.id = 'mobileBottomNav';
nav.className = 'mobile-bottom-nav';
nav.innerHTML = `
<a href="/">
<span class="nav-icon">[ ~ ]</span>
<span class="nav-label">HOME</span>
</a>
<button type="button" data-action="open-mobile-sidebar">
<span class="nav-icon">[ / ]</span>
<span class="nav-label">FILTER</span>
</button>
<a href="/ticket/create">
<span class="nav-icon">[ + ]</span>
<span class="nav-label">NEW</span>
</a>
<button type="button" data-action="open-settings-modal">
<span class="nav-icon">[ * ]</span>
<span class="nav-label">CFG</span>
</button>
`;
document.body.appendChild(nav);
}
}
// Restore sidebar state on page load
@@ -143,7 +118,30 @@ document.addEventListener('DOMContentLoaded', function() {
const action = target.dataset.action;
switch (action) {
// Bulk operations
// Navigation
case 'navigate':
if (target.dataset.url) window.location.href = target.dataset.url;
break;
case 'view-ticket':
if (target.dataset.ticketId) window.location.href = '/ticket/' + target.dataset.ticketId;
break;
// Bulk action triggers (show modals)
case 'bulk-status':
if (typeof showBulkStatusModal === 'function') showBulkStatusModal();
break;
case 'bulk-assign':
if (typeof showBulkAssignModal === 'function') showBulkAssignModal();
break;
case 'bulk-priority':
if (typeof showBulkPriorityModal === 'function') showBulkPriorityModal();
break;
case 'bulk-delete':
if (typeof bulkDelete === 'function') bulkDelete();
break;
case 'clear-selection':
clearSelection();
break;
// Bulk operation perform actions
case 'perform-bulk-assign':
performBulkAssign();
break;
@@ -168,26 +166,63 @@ document.addEventListener('DOMContentLoaded', function() {
case 'close-bulk-delete-modal':
closeBulkDeleteModal();
break;
// Checkbox selection
case 'toggle-select-all':
toggleSelectAll();
break;
case 'update-selection':
updateSelectionCount();
break;
case 'toggle-row-checkbox':
toggleRowCheckbox(e, target);
break;
// Quick actions
case 'quick-status':
quickStatusChange(target.dataset.ticketId, target.dataset.status);
break;
case 'perform-quick-status':
performQuickStatusChange(target.dataset.ticketId);
break;
case 'close-quick-status-modal':
closeQuickStatusModal();
break;
case 'quick-assign':
quickAssign(target.dataset.ticketId);
break;
case 'perform-quick-assign':
performQuickAssign(target.dataset.ticketId);
break;
case 'close-quick-assign-modal':
closeQuickAssignModal();
break;
// View mode toggle
case 'set-view-mode':
if (target.dataset.mode === 'card') populateKanbanCards();
break;
// Settings
case 'open-settings':
case 'open-settings-modal':
if (typeof openSettingsModal === 'function') openSettingsModal();
break;
// Refresh
case 'manual-refresh':
window.location.reload();
break;
// Export
case 'toggle-export-menu':
toggleExportMenu(e);
break;
case 'export-tickets':
exportSelectedTickets(target.dataset.format);
break;
// Advanced search
case 'open-advanced-search':
if (typeof openAdvancedSearch === 'function') openAdvancedSearch();
break;
// Mobile navigation
case 'open-mobile-sidebar':
if (typeof openMobileSidebar === 'function') openMobileSidebar();
break;
case 'open-settings-modal':
if (typeof openSettingsModal === 'function') openSettingsModal();
break;
// Filter badge actions
case 'remove-filter':
removeFilter(target.dataset.filterType, target.dataset.filterValue);
@@ -682,14 +717,14 @@ function updateSelectionCount() {
const exportDropdown = document.getElementById('exportDropdown');
const exportCount = document.getElementById('exportCount');
if (toolbar && countDisplay) {
toolbar.classList.toggle('is-visible', count > 0);
if (count > 0) countDisplay.textContent = count;
if (toolbar) {
toolbar.style.display = count > 0 ? 'flex' : 'none';
if (count > 0 && countDisplay) countDisplay.textContent = count;
}
// Show/hide export dropdown based on selection
if (exportDropdown) {
exportDropdown.classList.toggle('is-visible', count > 0);
exportDropdown.style.display = count > 0 ? 'inline-flex' : 'none';
if (count > 0 && exportCount) exportCount.textContent = count;
}
}
@@ -1278,27 +1313,10 @@ function performQuickAssign(ticketId) {
* Set the view mode (table or card)
*/
function setViewMode(mode) {
const tableView = document.querySelector('.ascii-frame-outer');
const cardView = document.getElementById('cardView');
const tableBtn = document.getElementById('tableViewBtn');
const cardBtn = document.getElementById('cardViewBtn');
if (!tableView || !cardView) return;
// TDS v1.2 uses lt.tabs for show/hide; we just need to populate kanban cards
if (mode === 'card') {
tableView.classList.add('is-hidden');
cardView.classList.remove('is-hidden');
tableBtn.classList.remove('active');
cardBtn.classList.add('active');
populateKanbanCards();
} else {
tableView.classList.remove('is-hidden');
cardView.classList.add('is-hidden');
tableBtn.classList.add('active');
cardBtn.classList.remove('active');
}
// Store preference
localStorage.setItem('ticketViewMode', mode);
}
@@ -1306,18 +1324,17 @@ function setViewMode(mode) {
* Populate Kanban cards from table data
*/
function populateKanbanCards() {
const rows = document.querySelectorAll('tbody tr');
const rows = document.querySelectorAll('#tickets-table tbody tr');
// TDS v1.2 kanban columns use id="kanban-col-{slug}" with .kanban-cards child
const columns = {
'Open': document.querySelector('.kanban-column[data-status="Open"] .kanban-cards'),
'Pending': document.querySelector('.kanban-column[data-status="Pending"] .kanban-cards'),
'In Progress': document.querySelector('.kanban-column[data-status="In Progress"] .kanban-cards'),
'Closed': document.querySelector('.kanban-column[data-status="Closed"] .kanban-cards')
'Open': document.getElementById('kanban-col-open'),
'Pending': document.getElementById('kanban-col-pending'),
'In Progress': document.getElementById('kanban-col-inprogress'),
'Closed': document.getElementById('kanban-col-closed'),
};
// Clear existing cards
Object.values(columns).forEach(col => {
if (col) col.innerHTML = '';
});
Object.values(columns).forEach(col => { if (col) col.innerHTML = ''; });
const counts = { 'Open': 0, 'Pending': 0, 'In Progress': 0, 'Closed': 0 };
const isAdmin = document.getElementById('selectAllCheckbox') !== null;
@@ -1325,53 +1342,60 @@ function populateKanbanCards() {
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length < 6) return; // Skip empty rows
if (cells.length < 6) return;
const ticketId = cells[0 + offset]?.querySelector('.ticket-link')?.textContent.trim() || '';
const priority = cells[1 + offset]?.textContent.trim() || '';
const title = cells[2 + offset]?.textContent.trim() || '';
const category = cells[3 + offset]?.textContent.trim() || '';
const status = cells[5 + offset]?.textContent.trim() || '';
const ticketId = cells[0 + offset]?.querySelector('.ticket-link')?.textContent.trim() || '';
const priorityEl = cells[1 + offset]?.querySelector('[class*="lt-p"]');
const priority = priorityEl ? priorityEl.textContent.trim().replace('P','') : cells[1 + offset]?.textContent.trim() || '4';
const title = cells[2 + offset]?.textContent.trim() || '';
const category = cells[3 + offset]?.textContent.trim() || '';
const statusEl = cells[5 + offset]?.querySelector('.lt-status');
const status = statusEl ? statusEl.textContent.trim() : cells[5 + offset]?.textContent.trim() || '';
const assignedTo = cells[7 + offset]?.textContent.trim() || 'Unassigned';
// Get initials for assignee
const initials = assignedTo === 'Unassigned' ? '?' :
assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
const initials = assignedTo === 'Unassigned' ? '?'
: assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
const column = columns[status];
if (column) {
counts[status]++;
if (!column || !ticketId) return;
const card = document.createElement('div');
card.className = `kanban-card priority-${priority}`;
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
card.innerHTML = `
<div class="card-header">
<span class="card-id">#${lt.escHtml(ticketId)}</span>
<span class="lt-priority lt-p${priority}"></span>
</div>
<div class="card-title">${lt.escHtml(title)}</div>
<div class="card-footer">
<span class="card-category">${lt.escHtml(category)}</span>
<span class="card-assignee" title="${lt.escHtml(assignedTo)}">${lt.escHtml(initials)}</span>
</div>
`;
column.appendChild(card);
}
counts[status] = (counts[status] || 0) + 1;
const pNum = parseInt(priority, 10) || 4;
const card = document.createElement('div');
card.className = 'lt-kanban-card lt-kanban-card--p' + pNum;
card.setAttribute('role', 'button');
card.setAttribute('tabindex', '0');
card.onclick = () => window.location.href = '/ticket/' + encodeURIComponent(ticketId);
card.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') card.click(); };
card.innerHTML =
'<div class="lt-kanban-card-header">' +
'<span class="lt-text-xs lt-text-cyan">#' + lt.escHtml(ticketId) + '</span>' +
'<span class="lt-p' + pNum + ' lt-text-xs">P' + pNum + '</span>' +
'</div>' +
'<div class="lt-kanban-card-title">' + lt.escHtml(title) + '</div>' +
'<div class="lt-kanban-card-footer">' +
'<span class="lt-text-xs lt-text-muted">' + lt.escHtml(category) + '</span>' +
'<span class="lt-kanban-assignee lt-text-xs" title="' + lt.escHtml(assignedTo) + '">' + lt.escHtml(initials) + '</span>' +
'</div>';
column.appendChild(card);
});
// Update column counts
Object.keys(counts).forEach(status => {
const header = document.querySelector(`.kanban-column[data-status="${status}"] .column-count`);
if (header) header.textContent = counts[status];
document.querySelectorAll('.column-count[data-status]').forEach(el => {
const s = el.dataset.status;
el.textContent = '(' + (counts[s] || 0) + ')';
});
}
// Restore view mode on page load
// Restore view mode on page load — click the kanban tab button to trigger lt.tabs
document.addEventListener('DOMContentLoaded', function() {
const savedMode = localStorage.getItem('ticketViewMode');
if (savedMode === 'card') {
setViewMode('card');
const cardBtn = document.getElementById('cardViewBtn');
if (cardBtn) cardBtn.click();
else populateKanbanCards();
}
});