diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 2a8299e..522c625 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -204,6 +204,108 @@ kbd { /* ── lt-msg variants ─────────────────────────────────────────── */ .lt-mb-md { margin-bottom: 1rem; } +/* ── Kanban cards ────────────────────────────────────────────── */ +.lt-kanban-card { + padding: 0.6rem 0.75rem; + border: 1px solid rgba(0, 255, 65, 0.2); + margin-bottom: 0.4rem; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease; + display: flex; + flex-direction: column; + gap: 0.35rem; +} +.lt-kanban-card:hover, +.lt-kanban-card:focus-visible { + border-color: var(--lt-text-primary, #00ff41); + background: rgba(0, 255, 65, 0.04); + outline: none; +} +.lt-kanban-card--p1 { border-left: 2px solid var(--lt-danger, #ff4d4d); } +.lt-kanban-card--p2 { border-left: 2px solid var(--lt-amber, #ffb000); } +.lt-kanban-card--p3 { border-left: 2px solid var(--lt-cyan, #00ffff); } +.lt-kanban-card--p4 { border-left: 2px solid rgba(0, 255, 65, 0.4); } +.lt-kanban-card--p5 { border-left: 2px solid rgba(0, 255, 65, 0.2); } + +.lt-kanban-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} +.lt-kanban-card-title { + font-size: 0.78rem; + line-height: 1.35; + font-weight: 600; + word-break: break-word; +} +.lt-kanban-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 0.1rem; +} +.lt-kanban-assignee { + width: 1.5rem; + height: 1.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(0, 255, 65, 0.35); + font-size: 0.6rem; + font-weight: 700; + flex-shrink: 0; +} + +/* ── Mobile sidebar overlay ──────────────────────────────────── */ +.mobile-sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 199; +} +.mobile-sidebar-overlay.active { display: block; } + +/* ── Mobile filter toggle button ─────────────────────────────── */ +.mobile-filter-toggle { + display: none; + width: 100%; + padding: 0.5rem 0.75rem; + margin-bottom: 0.5rem; + background: transparent; + border: 1px solid rgba(0, 255, 65, 0.3); + color: var(--lt-text-primary, #00ff41); + font-family: inherit; + font-size: 0.75rem; + letter-spacing: 0.05em; + cursor: pointer; + text-align: left; +} + +/* ── Ticket preview popup ────────────────────────────────────── */ +.ticket-preview-popup { + position: fixed; + z-index: 9999; + background: var(--lt-surface, #0a0e14); + border: 1px solid rgba(0, 255, 65, 0.4); + padding: 0.75rem; + min-width: 280px; + max-width: 360px; + font-size: 0.75rem; + pointer-events: auto; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); +} +.ticket-preview-popup .preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.4rem; +} +.ticket-preview-popup .preview-id { color: var(--lt-cyan, #00ffff); font-weight: 700; } +.ticket-preview-popup .preview-title { font-weight: 600; margin-bottom: 0.4rem; } +.ticket-preview-popup .preview-meta { opacity: 0.7; display: flex; flex-direction: column; gap: 0.1rem; } +.ticket-preview-popup .preview-footer { margin-top: 0.4rem; opacity: 0.5; font-size: 0.65rem; } + /* ── Responsive ──────────────────────────────────────────────── */ @media (max-width: 768px) { .lt-page-header { @@ -214,10 +316,10 @@ kbd { .lt-stats-grid { grid-template-columns: repeat(2, 1fr); } + .mobile-filter-toggle { display: block; } + .lt-sidebar.mobile-open { transform: translateX(0); } } @media (max-width: 480px) { - .lt-stats-grid { - grid-template-columns: 1fr; - } + .lt-stats-grid { grid-template-columns: 1fr; } } diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index c301979..2018137 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -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 = ` - - - - - - - - - - - `; - 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 = ` -
Include the API key in your requests using the Authorization header:
- -API keys provide programmatic access to create and manage tickets. Keep keys secure and rotate them regularly.
++ Example — create a ticket via cURL:
++
API keys provide programmatic access to create and manage tickets. Keep keys secure and rotate them regularly.