/** * Toggle sidebar visibility on desktop */ function toggleSidebar() { const sidebar = document.getElementById('dashboardSidebar'); const layout = document.getElementById('dashboardLayout'); if (sidebar && layout) { const isCollapsed = sidebar.classList.toggle('collapsed'); layout.classList.toggle('sidebar-collapsed', isCollapsed); // Store state in localStorage localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false'); } } /** * Mobile sidebar functions */ function openMobileSidebar() { const sidebar = document.getElementById('dashboardSidebar'); const overlay = document.getElementById('mobileSidebarOverlay'); if (sidebar) { sidebar.classList.add('mobile-open'); } if (overlay) { overlay.classList.add('active'); } document.body.style.overflow = 'hidden'; } function closeMobileSidebar() { const sidebar = document.getElementById('dashboardSidebar'); const overlay = document.getElementById('mobileSidebarOverlay'); if (sidebar) { sidebar.classList.remove('mobile-open'); } if (overlay) { overlay.classList.remove('active'); } document.body.style.overflow = ''; } // Initialize mobile elements function initMobileSidebar() { const sidebar = document.getElementById('dashboardSidebar'); const dashboardMain = document.querySelector('.dashboard-main'); // Create overlay if it doesn't exist if (!document.getElementById('mobileSidebarOverlay')) { const overlay = document.createElement('div'); overlay.id = 'mobileSidebarOverlay'; overlay.className = 'mobile-sidebar-overlay'; overlay.onclick = closeMobileSidebar; document.body.appendChild(overlay); } // Create mobile filter toggle button if (dashboardMain && !document.getElementById('mobileFilterToggle')) { const toggleBtn = document.createElement('button'); toggleBtn.id = 'mobileFilterToggle'; toggleBtn.className = 'mobile-filter-toggle'; toggleBtn.innerHTML = '[ = ] Filters & Search Options'; toggleBtn.onclick = openMobileSidebar; dashboardMain.insertBefore(toggleBtn, dashboardMain.firstChild); } // Create close button inside sidebar if (sidebar && !sidebar.querySelector('.mobile-sidebar-close')) { const closeBtn = document.createElement('button'); closeBtn.className = 'mobile-sidebar-close'; closeBtn.innerHTML = '×'; closeBtn.onclick = closeMobileSidebar; sidebar.insertBefore(closeBtn, sidebar.firstChild); } } // Restore sidebar state on page load document.addEventListener('DOMContentLoaded', function() { const savedState = localStorage.getItem('sidebarCollapsed'); const sidebar = document.getElementById('dashboardSidebar'); const layout = document.getElementById('dashboardLayout'); if (savedState === 'true' && sidebar && layout) { sidebar.classList.add('collapsed'); layout.classList.add('sidebar-collapsed'); } }); // Main initialization document.addEventListener('DOMContentLoaded', function() { // Initialize mobile sidebar for dashboard initMobileSidebar(); // Check if we're on the dashboard page const hasTable = document.querySelector('table'); const isTicketPage = window.location.pathname.includes('/ticket/') || window.location.href.includes('ticket.php') || document.querySelector('.ticket-details') !== null; const isDashboard = hasTable && !isTicketPage; if (isDashboard) { // Dashboard-specific initialization initSidebarFilters(); } // Initialize for all pages initSettingsModal(); // Event delegation for dynamically created modals document.addEventListener('click', function(e) { const target = e.target.closest('[data-action]'); if (!target) return; const action = target.dataset.action; switch (action) { // 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': showBulkDeleteModal(); break; case 'clear-selection': clearSelection(); break; // Bulk operation perform actions case 'perform-bulk-assign': performBulkAssign(); break; case 'close-bulk-assign-modal': closeBulkAssignModal(); break; case 'perform-bulk-priority': performBulkPriority(); break; case 'close-bulk-priority-modal': closeBulkPriorityModal(); break; case 'perform-bulk-status': performBulkStatusChange(); break; case 'close-bulk-status-modal': closeBulkStatusModal(); break; case 'perform-bulk-delete': performBulkDelete(); break; 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': setViewMode(target.dataset.mode); break; // Settings case 'open-settings': case 'open-settings-modal': if (typeof openSettingsModal === 'function') openSettingsModal(); break; case 'close-settings': if (typeof closeSettingsModal === 'function') closeSettingsModal(); break; case 'save-settings': if (typeof saveSettings === 'function') saveSettings(); break; // Refresh — use lt.autoRefresh.now() so modal/focus guards are respected case 'manual-refresh': if (window.lt && lt.autoRefresh) lt.autoRefresh.now(); else 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; // Filter badge actions case 'remove-filter': removeFilter(target.dataset.filterType, target.dataset.filterValue); break; case 'clear-all-filters': clearAllFilters(); break; } }); }); /** * Remove a single filter and reload page */ function removeFilter(filterType, filterValue) { const params = new URLSearchParams(window.location.search); if (filterType === 'status') { const currentStatuses = (params.get('status') || '').split(',').filter(s => s.trim()); const newStatuses = currentStatuses.filter(s => s !== filterValue); if (newStatuses.length > 0) { params.set('status', newStatuses.join(',')); } else { params.delete('status'); } } else if (filterType === 'priority') { const currentPriorities = (params.get('priority') || '').split(',').filter(p => p.trim()); const newPriorities = currentPriorities.filter(p => p !== filterValue); if (newPriorities.length > 0) { params.set('priority', newPriorities.join(',')); } else { params.delete('priority'); } } else if (filterType === 'search') { params.delete('search'); } else { params.delete(filterType); } // Reset to page 1 when changing filters params.delete('page'); window.location.search = params.toString(); } /** * Clear all filters and reload page */ function clearAllFilters() { const params = new URLSearchParams(window.location.search); // Remove all filter parameters params.delete('status'); params.delete('priority'); params.delete('category'); params.delete('type'); params.delete('assigned_to'); params.delete('search'); params.delete('date_from'); params.delete('date_to'); params.delete('page'); // Keep sort parameters const sortParams = new URLSearchParams(); if (params.has('sort')) sortParams.set('sort', params.get('sort')); if (params.has('dir')) sortParams.set('dir', params.get('dir')); window.location.search = sortParams.toString(); } function initTableSorting() { // Use the TDS lt.sortTable helper which manages aria-sort attributes correctly. // Falls back to no-op if the table isn't present on this page. if (window.lt && lt.sortTable && document.getElementById('tickets-table')) { lt.sortTable.init('tickets-table'); } } function initSidebarFilters() { const applyFiltersBtn = document.getElementById('apply-filters-btn'); const clearFiltersBtn = document.getElementById('clear-filters-btn'); if (applyFiltersBtn) { applyFiltersBtn.addEventListener('click', () => { const params = new URLSearchParams(window.location.search); // Collect selected statuses const selectedStatuses = Array.from( document.querySelectorAll('.filter-group input[name="status"]:checked') ).map(cb => cb.value); // Collect selected categories const selectedCategories = Array.from( document.querySelectorAll('.filter-group input[name="category"]:checked') ).map(cb => cb.value); // Collect selected types const selectedTypes = Array.from( document.querySelectorAll('.filter-group input[name="type"]:checked') ).map(cb => cb.value); // Update URL parameters if (selectedStatuses.length > 0) { params.set('status', selectedStatuses.join(',')); } else { params.delete('status'); } if (selectedCategories.length > 0) { params.set('category', selectedCategories.join(',')); } else { params.delete('category'); } if (selectedTypes.length > 0) { params.set('type', selectedTypes.join(',')); } else { params.delete('type'); } // Reset to page 1 when filters change params.set('page', '1'); // Reload with new parameters window.location.search = params.toString(); }); } if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', () => { const params = new URLSearchParams(window.location.search); // Remove filter parameters params.delete('status'); params.delete('category'); params.delete('type'); params.set('page', '1'); // Reload with cleared filters window.location.search = params.toString(); }); } } function initSettingsModal() { const settingsIcon = document.querySelector('.settings-icon'); if (settingsIcon) { settingsIcon.addEventListener('click', function(e) { e.preventDefault(); // openSettingsModal is defined in settings.js if (typeof openSettingsModal === 'function') { openSettingsModal(); } }); } } function sortTable(table, column) { const headers = table.querySelectorAll('th'); headers.forEach(header => { header.classList.remove('sort-asc', 'sort-desc'); }); const rows = Array.from(table.querySelectorAll('tbody tr')); const currentDirection = table.dataset.sortColumn == column ? (table.dataset.sortDirection === 'asc' ? 'desc' : 'asc') : 'asc'; table.dataset.sortColumn = column; table.dataset.sortDirection = currentDirection; rows.sort((a, b) => { const aValue = a.children[column].textContent.trim(); const bValue = b.children[column].textContent.trim(); // Check if this is a date column — prefer data-ts attribute over text (which may be relative) const headerText = headers[column].textContent.toLowerCase(); if (headerText === 'created' || headerText === 'updated') { const cellA = a.children[column]; const cellB = b.children[column]; const dateA = new Date(cellA.dataset.ts || aValue); const dateB = new Date(cellB.dataset.ts || bValue); return currentDirection === 'asc' ? dateA - dateB : dateB - dateA; } // Special handling for "Assigned To" column if (headerText === 'assigned to') { const aUnassigned = aValue === 'Unassigned'; const bUnassigned = bValue === 'Unassigned'; // Both unassigned - equal if (aUnassigned && bUnassigned) return 0; // Put unassigned at the end regardless of sort direction if (aUnassigned) return 1; if (bUnassigned) return -1; // Otherwise sort names normally return currentDirection === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } // Numeric comparison const numA = parseFloat(aValue); const numB = parseFloat(bValue); if (!isNaN(numA) && !isNaN(numB)) { return currentDirection === 'asc' ? numA - numB : numB - numA; } // String comparison return currentDirection === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); }); const currentHeader = headers[column]; currentHeader.classList.add(currentDirection === 'asc' ? 'sort-asc' : 'sort-desc'); const tbody = table.querySelector('tbody'); rows.forEach(row => tbody.appendChild(row)); } // Old settings modal functions removed - now using settings.js with new settings modal /** * Bulk Actions Functions (Admin only) */ function toggleSelectAll() { const selectAll = document.getElementById('selectAllCheckbox'); if (!selectAll) return; const checkboxes = document.querySelectorAll('.ticket-checkbox'); checkboxes.forEach(checkbox => { checkbox.checked = selectAll.checked; }); updateSelectionCount(); } /** * Toggle checkbox when clicking anywhere in the checkbox cell */ function toggleRowCheckbox(event, cell) { // Prevent double-toggle if clicking directly on the checkbox if (event.target.type === 'checkbox') return; event.stopPropagation(); const cb = cell.querySelector('.ticket-checkbox'); if (cb) { cb.checked = !cb.checked; updateSelectionCount(); } } function updateSelectionCount() { const checkboxes = document.querySelectorAll('.ticket-checkbox:checked'); const count = checkboxes.length; const toolbar = document.querySelector('.bulk-actions-inline'); const countDisplay = document.getElementById('selected-count'); const exportDropdown = document.getElementById('exportDropdown'); const exportCount = document.getElementById('exportCount'); 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.style.display = count > 0 ? 'inline-flex' : 'none'; if (count > 0 && exportCount) exportCount.textContent = count; } } function getSelectedTicketIds() { const checkboxes = document.querySelectorAll('.ticket-checkbox:checked'); return Array.from(checkboxes).map(cb => parseInt(cb.value)); } function clearSelection() { document.querySelectorAll('.ticket-checkbox').forEach(cb => cb.checked = false); const selectAll = document.getElementById('selectAllCheckbox'); if (selectAll) selectAll.checked = false; updateSelectionCount(); } function bulkClose() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { lt.toast.warning('No tickets selected', 2000); return; } showConfirmModal( `Close ${ticketIds.length} Ticket(s)?`, 'Are you sure you want to close these tickets?', 'warning', () => performBulkCloseAction(ticketIds) ); } function performBulkCloseAction(ticketIds) { lt.api.post('/api/bulk_operation.php', { operation_type: 'bulk_close', ticket_ids: ticketIds }) .then(data => { if (data.success) { if (data.failed > 0) { lt.toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000); } showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { lt.toast.error('Bulk close failed: ' + error.message, 5000); }); } var _bulkAssignUserId = null; // set by combobox onSelect function showBulkAssignModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { lt.toast.warning('No tickets selected', 2000); return; } _bulkAssignUserId = null; const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); lt.modal.open('bulkAssignModal'); lt.api.get('/api/get_users.php') .then(data => { if (data.success && data.users) { const input = document.getElementById('bulkAssignUserInput'); if (!input) return; const items = data.users.map(u => ({ value: String(u.user_id), label: u.display_name || u.username })); lt.combobox.init(input, items, { onSelect: function(item) { _bulkAssignUserId = item.value; } }); } }) .catch(() => lt.toast.error('Error loading users')); } function closeBulkAssignModal() { lt.modal.close('bulkAssignModal'); const modal = document.getElementById('bulkAssignModal'); if (modal) setTimeout(() => modal.remove(), 300); } function performBulkAssign() { const userId = _bulkAssignUserId; const ticketIds = getSelectedTicketIds(); if (!userId) { lt.toast.warning('Please select a user from the list', 2000); return; } lt.api.post('/api/bulk_operation.php', { operation_type: 'bulk_assign', ticket_ids: ticketIds, parameters: { assigned_to: parseInt(userId) } }) .then(data => { if (data.success) { closeBulkAssignModal(); if (data.failed > 0) { lt.toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { lt.toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000); } showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { lt.toast.error('Bulk assign failed: ' + error.message, 5000); }); } function showBulkPriorityModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { lt.toast.warning('No tickets selected', 2000); return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); lt.modal.open('bulkPriorityModal'); } function closeBulkPriorityModal() { lt.modal.close('bulkPriorityModal'); const modal = document.getElementById('bulkPriorityModal'); if (modal) setTimeout(() => modal.remove(), 300); } function performBulkPriority() { const priorityEl = document.getElementById('bulkPriority'); if (!priorityEl) return; const priority = priorityEl.value; const ticketIds = getSelectedTicketIds(); if (!priority) { lt.toast.warning('Please select a priority', 2000); return; } lt.api.post('/api/bulk_operation.php', { operation_type: 'bulk_priority', ticket_ids: ticketIds, parameters: { priority: parseInt(priority) } }) .then(data => { if (data.success) { closeBulkPriorityModal(); if (data.failed > 0) { lt.toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { lt.toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000); } showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { lt.toast.error('Bulk priority update failed: ' + error.message, 5000); }); } // Make table rows clickable document.addEventListener('DOMContentLoaded', function() { const tableRows = document.querySelectorAll('tbody tr'); tableRows.forEach(row => { // Skip if row already has click handler if (row.dataset.clickable) return; row.dataset.clickable = 'true'; row.addEventListener('click', function(e) { // Don't navigate if clicking on a link, button, checkbox, or select if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.closest('a') || e.target.closest('button')) { return; } // Find the ticket link in the row const ticketLink = row.querySelector('.ticket-link'); if (ticketLink) { window.location.href = ticketLink.href; } }); }); }); // Bulk Status Change function showBulkStatusModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { lt.toast.warning('No tickets selected', 2000); return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); lt.modal.open('bulkStatusModal'); } function closeBulkStatusModal() { lt.modal.close('bulkStatusModal'); const modal = document.getElementById('bulkStatusModal'); if (modal) setTimeout(() => modal.remove(), 300); } function performBulkStatusChange() { const bulkStatusEl = document.getElementById('bulkStatus'); if (!bulkStatusEl) return; const status = bulkStatusEl.value; const ticketIds = getSelectedTicketIds(); if (!status) { lt.toast.warning('Please select a status', 2000); return; } lt.api.post('/api/bulk_operation.php', { operation_type: 'bulk_status', ticket_ids: ticketIds, parameters: { status: status } }) .then(data => { closeBulkStatusModal(); if (data.success) { if (data.failed > 0) { lt.toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { lt.toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000); } showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { lt.toast.error('Bulk status change failed: ' + error.message, 5000); }); } // Bulk Delete function showBulkDeleteModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { lt.toast.warning('No tickets selected', 2000); return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); lt.modal.open('bulkDeleteModal'); } function closeBulkDeleteModal() { lt.modal.close('bulkDeleteModal'); const modal = document.getElementById('bulkDeleteModal'); if (modal) setTimeout(() => modal.remove(), 300); } function performBulkDelete() { const ticketIds = getSelectedTicketIds(); lt.api.post('/api/bulk_operation.php', { operation_type: 'bulk_delete', ticket_ids: ticketIds }) .then(data => { closeBulkDeleteModal(); if (data.success) { lt.toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000); showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { lt.toast.error('Bulk delete failed: ' + error.message, 5000); }); } // ============================================ // TERMINAL-STYLE MODAL UTILITIES // ============================================ /** * Show a terminal-style input modal * @param {string} title - Modal title * @param {string} label - Input field label * @param {string} placeholder - Input placeholder text * @param {function} onSubmit - Callback with input value when submitted * @param {function} onCancel - Optional callback when cancelled */ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = null) { const modalId = 'inputModal' + Date.now(); const inputId = modalId + '_input'; // Escape user-provided content to prevent XSS const safeTitle = lt.escHtml(title); const safeLabel = lt.escHtml(label); const safePlaceholder = lt.escHtml(placeholder); const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = document.getElementById(modalId); const input = document.getElementById(inputId); lt.modal.open(modalId); setTimeout(() => input.focus(), 100); const cleanup = (cb) => { lt.modal.close(modalId); setTimeout(() => modal.remove(), 300); if (cb) cb(); }; const handleSubmit = () => cleanup(() => onSubmit && onSubmit(input.value.trim())); document.getElementById(`${modalId}_submit`).addEventListener('click', handleSubmit); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleSubmit(); }); document.getElementById(`${modalId}_cancel`).addEventListener('click', () => cleanup(onCancel)); modal.querySelector('[data-modal-close]').addEventListener('click', () => cleanup(onCancel)); } // ======================================== // QUICK ACTIONS // ======================================== /** * Quick status change from dashboard */ function quickStatusChange(ticketId, currentStatus) { const statuses = ['Open', 'Pending', 'In Progress', 'Closed']; const otherStatuses = statuses.filter(s => s !== currentStatus); const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); lt.modal.open('quickStatusModal'); } function closeQuickStatusModal() { lt.modal.close('quickStatusModal'); const modal = document.getElementById('quickStatusModal'); if (modal) setTimeout(() => modal.remove(), 300); } function performQuickStatusChange(ticketId) { const quickStatusEl = document.getElementById('quickStatusSelect'); if (!quickStatusEl) return; const newStatus = quickStatusEl.value; lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, status: newStatus }) .then(data => { closeQuickStatusModal(); if (data.success) { lt.toast.success(`Status updated to ${newStatus}`, 3000); showTableSkeleton(5); setTimeout(() => window.location.reload(), 1000); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000); } }) .catch(error => { closeQuickStatusModal(); lt.toast.error('Error updating status', 4000); }); } var _quickAssignUserId = undefined; // undefined = no change; null = unassign; string = user_id /** * Quick assign from dashboard */ function quickAssign(ticketId) { _quickAssignUserId = undefined; const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); lt.modal.open('quickAssignModal'); lt.api.get('/api/get_users.php') .then(data => { if (data.success && data.users) { const input = document.getElementById('quickAssignInput'); if (!input) return; const items = [ { value: '', label: 'Unassigned' }, ...data.users.map(u => ({ value: String(u.user_id), label: u.display_name || u.username })) ]; lt.combobox.init(input, items, { onSelect: function(item) { _quickAssignUserId = item.value || null; } }); } }) .catch(() => lt.toast.error('Error loading users')); } function closeQuickAssignModal() { lt.modal.close('quickAssignModal'); const modal = document.getElementById('quickAssignModal'); if (modal) setTimeout(() => modal.remove(), 300); } function performQuickAssign(ticketId) { if (_quickAssignUserId === undefined) { lt.toast.warning('Please select a user from the list', 2000); return; } const assignedTo = _quickAssignUserId; lt.api.post('/api/assign_ticket.php', { ticket_id: ticketId, assigned_to: assignedTo }) .then(data => { closeQuickAssignModal(); if (data.success) { lt.toast.success('Assignment updated', 3000); showTableSkeleton(5); setTimeout(() => window.location.reload(), 1000); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000); } }) .catch(error => { closeQuickAssignModal(); lt.toast.error('Error updating assignment', 4000); }); } // ======================================== // KANBAN / CARD VIEW // ======================================== /** * Set the view mode (table or card) */ function setViewMode(mode) { // TDS v1.2 uses lt.tabs for show/hide; we just need to populate kanban cards if (mode === 'card') { populateKanbanCards(); } localStorage.setItem('ticketViewMode', mode); } /** * Populate Kanban cards from table data */ function populateKanbanCards() { 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.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 = ''; }); const counts = { 'Open': 0, 'Pending': 0, 'In Progress': 0, 'Closed': 0 }; const isAdmin = document.getElementById('selectAllCheckbox') !== null; const offset = isAdmin ? 1 : 0; rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 6) return; 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'; const initials = assignedTo === 'Unassigned' ? '?' : assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); const column = columns[status]; if (!column || !ticketId) return; 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.dataset.ticketId = ticketId; card.dataset.status = status; card.addEventListener('click', (e) => { // Don't navigate if drag just ended (drag adds/removes is-dragging briefly) if (card.dataset.dragged) { delete card.dataset.dragged; return; } window.location.href = '/ticket/' + encodeURIComponent(ticketId); }); card.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') card.click(); }; card.innerHTML = '
' + '#' + lt.escHtml(ticketId) + '' + 'P' + pNum + '' + '
' + '
' + lt.escHtml(title) + '
' + ''; column.appendChild(card); }); // Update column counts document.querySelectorAll('.column-count[data-status]').forEach(el => { const s = el.dataset.status; el.textContent = '(' + (counts[s] || 0) + ')'; }); // ── Kanban drag-and-drop via lt.sortable ────────────────────── if (window.lt && lt.sortable) { const colStatusMap = { 'kanban-col-open': 'Open', 'kanban-col-pending': 'Pending', 'kanban-col-inprogress': 'In Progress', 'kanban-col-closed': 'Closed', }; function handleKanbanSort(newItems, movedCard) { if (!movedCard) return; const newColEl = movedCard.parentNode; const newColId = newColEl ? newColEl.id : null; const newStatus = colStatusMap[newColId]; const oldStatus = movedCard.dataset.status; const ticketId = movedCard.dataset.ticketId; if (!newStatus || !ticketId || newStatus === oldStatus) return; movedCard.dataset.status = newStatus; movedCard.dataset.dragged = '1'; // Optimistically update column counts const dec = document.querySelector(`.column-count[data-status="${oldStatus}"]`); const inc = document.querySelector(`.column-count[data-status="${newStatus}"]`); if (dec) dec.textContent = '(' + Math.max(0, (parseInt(dec.textContent.replace(/\D/g,''),10)||1) - 1) + ')'; if (inc) inc.textContent = '(' + ((parseInt(inc.textContent.replace(/\D/g,''),10)||0) + 1) + ')'; // POST status update fetch('/api/update_ticket.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN || '' }, body: JSON.stringify({ ticket_id: parseInt(ticketId, 10), status: newStatus }) }) .then(r => r.json()) .then(data => { if (data.success) { lt.toast.success('Ticket #' + ticketId + ' → ' + newStatus, 2500); movedCard.dataset.status = newStatus; } else { lt.toast.error('Status update failed: ' + (data.error || 'Unknown error')); // Revert: put card back in original column const origCol = document.getElementById(Object.keys(colStatusMap).find(k => colStatusMap[k] === oldStatus)); if (origCol) origCol.appendChild(movedCard); movedCard.dataset.status = oldStatus; } }) .catch(() => { lt.toast.error('Network error — status not saved'); }); } Object.keys(columns).forEach(status => { const col = columns[status]; if (col) lt.sortable.init(col, { group: 'kanban', onSort: handleKanbanSort }); }); } } // Restore view mode on page load — lt.tabs already restores the active panel visually // via lt_activeTab_; we just need to populate kanban cards if that panel is active document.addEventListener('DOMContentLoaded', function() { try { const savedTab = localStorage.getItem('lt_activeTab_' + location.pathname); if (savedTab === 'tab-kanban') { populateKanbanCards(); } } catch (_) {} }); // ======================================== // INLINE TICKET PREVIEW // ======================================== let previewTimeout = null; let currentPreview = null; function initTicketPreview() { // Create preview element const preview = document.createElement('div'); preview.id = 'ticketPreview'; preview.className = 'ticket-preview-popup is-hidden'; document.body.appendChild(preview); currentPreview = preview; // Add event listeners to ticket links document.querySelectorAll('.ticket-link').forEach(link => { link.addEventListener('mouseenter', showTicketPreview); link.addEventListener('mouseleave', hideTicketPreview); }); // Keep preview visible when hovering over it preview.addEventListener('mouseenter', () => { if (previewTimeout) { clearTimeout(previewTimeout); previewTimeout = null; } }); preview.addEventListener('mouseleave', hideTicketPreview); } function showTicketPreview(event) { const link = event.target.closest('.ticket-link'); if (!link) return; // Clear any pending hide if (previewTimeout) { clearTimeout(previewTimeout); } // Delay before showing previewTimeout = setTimeout(() => { const row = link.closest('tr'); if (!row) return; // Extract data from the table row const cells = row.querySelectorAll('td'); const isAdmin = document.getElementById('selectAllCheckbox') !== null; const offset = isAdmin ? 1 : 0; const ticketId = 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 type = cells[4 + offset]?.textContent.trim() || ''; const status = cells[5 + offset]?.textContent.trim() || ''; const createdBy = cells[6 + offset]?.textContent.trim() || ''; const assignedTo = cells[7 + offset]?.textContent.trim() || ''; // Build preview content currentPreview.innerHTML = `
#${lt.escHtml(ticketId)} ${lt.escHtml(status)}
${lt.escHtml(title)}
Priority: P${lt.escHtml(priority)}
Category: ${lt.escHtml(category)}
Type: ${lt.escHtml(type)}
Assigned: ${lt.escHtml(assignedTo)}
`; // Position the preview — element is position:fixed so coords are // viewport-relative; getBoundingClientRect() already returns viewport coords, // do NOT add scrollX/scrollY const rect = link.getBoundingClientRect(); const previewWidth = 320; const previewHeight = 200; let left = rect.left; let top = rect.bottom + 5; // Adjust if going off-screen if (left + previewWidth > window.innerWidth) { left = window.innerWidth - previewWidth - 20; } if (top + previewHeight > window.innerHeight) { top = rect.top - previewHeight - 5; } if (left < 0) left = 4; if (top < 0) top = 4; currentPreview.style.left = left + 'px'; currentPreview.style.top = top + 'px'; currentPreview.classList.remove('is-hidden'); }, 300); } function hideTicketPreview() { if (previewTimeout) { clearTimeout(previewTimeout); } previewTimeout = setTimeout(() => { if (currentPreview) { currentPreview.classList.add('is-hidden'); } }, 100); } // Initialize preview on page load document.addEventListener('DOMContentLoaded', function() { const hasTable = document.querySelector('table'); const isTicketPage = window.location.pathname.includes('/ticket/'); if (hasTable && !isTicketPage) { initTicketPreview(); } }); // Hide preview when a modal opens, user scrolls, or page is about to navigate document.addEventListener('click', function(e) { if (e.target.closest('[data-modal-open], [data-action="open-advanced-search"], .lt-pagination a, .lt-pagination button')) { hideTicketPreview(); if (currentPreview) currentPreview.classList.add('is-hidden'); } }, true); document.addEventListener('scroll', function() { if (currentPreview && !currentPreview.classList.contains('is-hidden')) { currentPreview.classList.add('is-hidden'); if (previewTimeout) { clearTimeout(previewTimeout); previewTimeout = null; } } }, { passive: true }); /** * Toggle export dropdown menu */ function toggleExportMenu(event) { event.stopPropagation(); const dropdown = document.getElementById('exportDropdown'); const content = document.getElementById('exportDropdownContent'); if (dropdown && content) { dropdown.classList.toggle('open'); } } // Close export dropdown when clicking outside document.addEventListener('click', function(event) { const dropdown = document.getElementById('exportDropdown'); if (dropdown && !dropdown.contains(event.target)) { dropdown.classList.remove('open'); } }); /** * Export selected tickets to CSV or JSON */ function exportSelectedTickets(format) { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { lt.toast.warning('No tickets selected', 2000); return; } // Build URL with selected ticket IDs const params = new URLSearchParams(); params.set('format', format); params.set('ticket_ids', ticketIds.join(',')); // Trigger download window.location.href = '/api/export_tickets.php?' + params.toString(); // Close dropdown const dropdown = document.getElementById('exportDropdown'); if (dropdown) dropdown.classList.remove('open'); } /** * Show TDS spinner overlay on an element. * Uses lt-spinner + lt-loading-text from base.css. */ function showLoadingOverlay(element, message = 'Loading...') { const existing = element.querySelector('.lt-loading-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.className = 'lt-loading-overlay'; const spinner = document.createElement('div'); spinner.className = 'lt-spinner'; const text = document.createElement('div'); text.className = 'lt-loading-text'; text.textContent = message; overlay.appendChild(spinner); overlay.appendChild(text); element.classList.add('has-lt-overlay'); element.appendChild(overlay); } /** * Hide TDS spinner overlay */ function hideLoadingOverlay(element) { const overlay = element.querySelector('.lt-loading-overlay'); if (overlay) { overlay.remove(); element.classList.remove('has-lt-overlay'); } } // ======================================== // AUTO-REFRESH (lt.autoRefresh integration) // ======================================== /** * Reload the dashboard, but skip if a modal is open or user is typing. * Registered with lt.autoRefresh so it runs every 5 minutes automatically. */ /** * Replace table body rows with skeleton placeholders before a page reload. * Gives visual feedback that a reload is in progress. */ function showTableSkeleton(rowCount) { rowCount = rowCount || 5; const tbody = document.querySelector('#tickets-table tbody'); if (!tbody) return; let html = ''; for (let i = 0; i < rowCount; i++) { html += '' + '
'.repeat(6) + ''; } tbody.innerHTML = html; } function dashboardAutoRefresh() { // Don't interrupt the user if a modal is open if (document.querySelector('.lt-modal-overlay[aria-hidden="false"]')) return; // Don't interrupt if focus is in a text input const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; showTableSkeleton(6); window.location.reload(); } document.addEventListener('DOMContentLoaded', function() { // Only run auto-refresh on the dashboard, not on ticket pages if (!window.location.pathname.includes('/ticket/')) { lt.autoRefresh.start(dashboardAutoRefresh, 5 * 60 * 1000); } }); // ======================================== // RELATIVE TIMESTAMPS // ======================================== /** * Convert all .ts-cell[data-ts] elements to relative time using lt.time.ago(). * Runs once on DOMContentLoaded and refreshes every 60s so "2m ago" stays current. * The original full timestamp is preserved in the title attribute for hover. */ function initRelativeTimes() { document.querySelectorAll('.ts-cell[data-ts]').forEach(el => { el.textContent = lt.time.ago(el.dataset.ts); }); } document.addEventListener('DOMContentLoaded', initRelativeTimes); setInterval(initRelativeTimes, 60000); // Export for use in other scripts window.showLoadingOverlay = showLoadingOverlay; window.hideLoadingOverlay = hideLoadingOverlay;