// XSS prevention helper function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Get ticket ID from URL (handles both /ticket/123 and ?id=123 formats) function getTicketIdFromUrl() { const pathMatch = window.location.pathname.match(/\/ticket\/(\d+)/); if (pathMatch) return pathMatch[1]; const params = new URLSearchParams(window.location.search); return params.get('id'); } /** * 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); } // Create mobile bottom navigation if (!document.getElementById('mobileBottomNav')) { const nav = document.createElement('nav'); nav.id = 'mobileBottomNav'; nav.className = 'mobile-bottom-nav'; nav.innerHTML = ` Home New `; document.body.appendChild(nav); } } // 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 initStatusFilter(); initTableSorting(); initSidebarFilters(); } // Initialize for all pages initSettingsModal(); // Force dark mode only (terminal aesthetic - no theme switching) document.documentElement.setAttribute('data-theme', 'dark'); document.body.classList.add('dark-mode'); // 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) { // Bulk operations 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; // Quick actions case 'perform-quick-status': performQuickStatusChange(target.dataset.ticketId); break; case 'close-quick-status-modal': closeQuickStatusModal(); break; case 'perform-quick-assign': performQuickAssign(target.dataset.ticketId); break; case 'close-quick-assign-modal': closeQuickAssignModal(); 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); 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() { const tableHeaders = document.querySelectorAll('th'); tableHeaders.forEach((header, index) => { header.style.cursor = 'pointer'; header.addEventListener('click', () => { const table = header.closest('table'); sortTable(table, index); }); }); } 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 const headerText = headers[column].textContent.toLowerCase(); if (headerText === 'created' || headerText === 'updated') { const dateA = new Date(aValue); const dateB = new Date(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 function initStatusFilter() { const filterContainer = document.createElement('div'); filterContainer.className = 'status-filter-container'; const dropdown = document.createElement('div'); dropdown.className = 'status-dropdown'; const dropdownHeader = document.createElement('div'); dropdownHeader.className = 'dropdown-header'; dropdownHeader.textContent = 'Status Filter'; const dropdownContent = document.createElement('div'); dropdownContent.className = 'dropdown-content'; const statuses = ['Open', 'In Progress', 'Closed']; statuses.forEach(status => { const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = status; checkbox.id = `status-${status.toLowerCase().replace(/\s+/g, '-')}`; const urlParams = new URLSearchParams(window.location.search); const currentStatuses = urlParams.get('status') ? urlParams.get('status').split(',') : []; const showAll = urlParams.get('show_all'); // FIXED LOGIC: Determine checkbox state if (showAll === '1') { // If show_all=1 parameter exists, all should be checked checkbox.checked = true; } else if (currentStatuses.length === 0) { // No status parameter - default: Open and In Progress checked, Closed unchecked checkbox.checked = status !== 'Closed'; } else { // Status parameter exists - check if this status is in the list checkbox.checked = currentStatuses.includes(status); } label.appendChild(checkbox); label.appendChild(document.createTextNode(' ' + status)); dropdownContent.appendChild(label); }); const saveButton = document.createElement('button'); saveButton.className = 'btn save-filter'; saveButton.textContent = 'Apply Filter'; saveButton.onclick = () => { const checkedBoxes = dropdownContent.querySelectorAll('input:checked'); const selectedStatuses = Array.from(checkedBoxes).map(cb => cb.value); const params = new URLSearchParams(window.location.search); if (selectedStatuses.length === 0) { // No statuses selected - show default (Open + In Progress) params.delete('status'); params.delete('show_all'); } else if (selectedStatuses.length === 3) { // All statuses selected - show all tickets params.delete('status'); params.set('show_all', '1'); } else { // Some statuses selected - set the parameter params.set('status', selectedStatuses.join(',')); params.delete('show_all'); } params.set('page', '1'); window.location.search = params.toString(); dropdown.classList.remove('active'); }; dropdownHeader.onclick = () => { dropdown.classList.toggle('active'); }; dropdown.appendChild(dropdownHeader); dropdown.appendChild(dropdownContent); dropdownContent.appendChild(saveButton); filterContainer.appendChild(dropdown); const tableActions = document.querySelector('.table-controls .table-actions'); if (tableActions) { tableActions.prepend(filterContainer); } } function quickSave() { if (!window.ticketData) { console.error('No ticket data available'); return; } const statusSelect = document.getElementById('status-select'); const prioritySelect = document.getElementById('priority-select'); if (!statusSelect || !prioritySelect) { console.error('Status or priority select not found'); return; } const data = { ticket_id: parseInt(window.ticketData.id), status: statusSelect.value, priority: parseInt(prioritySelect.value) }; fetch('/api/update_ticket.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify(data) }) .then(response => { return response.text().then(text => { try { return JSON.parse(text); } catch (e) { throw new Error('Invalid JSON response: ' + text); } }); }) .then(result => { if (result.success) { // Update the hamburger menu display const hamburgerStatus = document.getElementById('hamburger-status'); const hamburgerPriority = document.getElementById('hamburger-priority'); if (hamburgerStatus) hamburgerStatus.textContent = statusSelect.value; if (hamburgerPriority) hamburgerPriority.textContent = 'P' + prioritySelect.value; // Update window.ticketData window.ticketData.status = statusSelect.value; window.ticketData.priority = parseInt(prioritySelect.value); // Update main page elements if they exist const statusDisplay = document.getElementById('statusDisplay'); if (statusDisplay) { statusDisplay.className = `status-${statusSelect.value}`; statusDisplay.textContent = statusSelect.value; } // Close hamburger menu after successful save const hamburgerContent = document.querySelector('.hamburger-content'); if (hamburgerContent) { hamburgerContent.classList.remove('open'); document.body.classList.remove('menu-open'); } } else { console.error('Error updating ticket:', result.error || 'Unknown error'); toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000); } }) .catch(error => { console.error('Error updating ticket:', error); toast.error('Error updating ticket: ' + error.message, 5000); }); } // Ticket page functions (if needed) function saveTicket() { const editables = document.querySelectorAll('.editable'); const data = {}; const ticketId = getTicketIdFromUrl(); editables.forEach(field => { if (field.dataset.field) { data[field.dataset.field] = field.value; } }); 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: ticketId, ...data }) }) .then(response => response.json()) .then(data => { if(data.success) { const statusDisplay = document.getElementById('statusDisplay'); if (statusDisplay) { statusDisplay.className = `status-${data.status}`; statusDisplay.textContent = data.status; } } }); } /** * Load template data into the create ticket form */ function loadTemplate() { const templateSelect = document.getElementById('templateSelect'); const templateId = templateSelect.value; if (!templateId) { // Clear form when "No Template" is selected document.getElementById('title').value = ''; document.getElementById('description').value = ''; document.getElementById('priority').value = '4'; document.getElementById('category').value = 'General'; document.getElementById('type').value = 'Issue'; return; } // Fetch template data fetch(`/api/get_template.php?template_id=${templateId}`, { credentials: 'same-origin' }) .then(response => { if (!response.ok) { throw new Error('Failed to fetch template'); } return response.json(); }) .then(data => { if (data.success && data.template) { const template = data.template; // Populate form fields with template data if (template.title_template) { document.getElementById('title').value = template.title_template; } if (template.description_template) { document.getElementById('description').value = template.description_template; } if (template.category) { document.getElementById('category').value = template.category; } if (template.type) { document.getElementById('type').value = template.type; } if (template.default_priority) { document.getElementById('priority').value = template.default_priority; } } else { console.error('Failed to load template:', data.error); toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000); } }) .catch(error => { console.error('Error loading template:', error); toast.error('Error loading template: ' + error.message, 4000); }); } /** * Bulk Actions Functions (Admin only) */ function toggleSelectAll() { const selectAll = document.getElementById('selectAllCheckbox'); 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 && countDisplay) { if (count > 0) { toolbar.style.display = 'flex'; countDisplay.textContent = count; } else { toolbar.style.display = 'none'; } } // Show/hide export dropdown based on selection if (exportDropdown) { if (count > 0) { exportDropdown.style.display = ''; if (exportCount) exportCount.textContent = count; } else { exportDropdown.style.display = 'none'; } } } 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) { 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) { fetch('/api/bulk_operation.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify({ operation_type: 'bulk_close', ticket_ids: ticketIds }) }) .then(response => response.json()) .then(data => { if (data.success) { if (data.failed > 0) { toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000); } setTimeout(() => window.location.reload(), 1500); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { console.error('Error performing bulk close:', error); toast.error('Bulk close failed: ' + error.message, 5000); }); } function showBulkAssignModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { toast.warning('No tickets selected', 2000); return; } // Create modal HTML const modalHtml = `
`; document.body.insertAdjacentHTML('beforeend', modalHtml); // Fetch users for the dropdown fetch('/api/get_users.php', { credentials: 'same-origin' }) .then(response => response.json()) .then(data => { if (data.success && data.users) { const select = document.getElementById('bulkAssignUser'); data.users.forEach(user => { const option = document.createElement('option'); option.value = user.user_id; option.textContent = user.display_name || user.username; select.appendChild(option); }); } }) .catch(error => { console.error('Error loading users:', error); }); } function closeBulkAssignModal() { const modal = document.getElementById('bulkAssignModal'); if (modal) { modal.remove(); } } function performBulkAssign() { const userId = document.getElementById('bulkAssignUser').value; const ticketIds = getSelectedTicketIds(); if (!userId) { toast.warning('Please select a user', 2000); return; } fetch('/api/bulk_operation.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify({ operation_type: 'bulk_assign', ticket_ids: ticketIds, parameters: { assigned_to: parseInt(userId) } }) }) .then(response => response.json()) .then(data => { if (data.success) { closeBulkAssignModal(); if (data.failed > 0) { toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000); } setTimeout(() => window.location.reload(), 1500); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { console.error('Error performing bulk assign:', error); toast.error('Bulk assign failed: ' + error.message, 5000); }); } function showBulkPriorityModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { toast.warning('No tickets selected', 2000); return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); } function closeBulkPriorityModal() { const modal = document.getElementById('bulkPriorityModal'); if (modal) { modal.remove(); } } function performBulkPriority() { const priority = document.getElementById('bulkPriority').value; const ticketIds = getSelectedTicketIds(); if (!priority) { toast.warning('Please select a priority', 2000); return; } fetch('/api/bulk_operation.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify({ operation_type: 'bulk_priority', ticket_ids: ticketIds, parameters: { priority: parseInt(priority) } }) }) .then(response => response.json()) .then(data => { if (data.success) { closeBulkPriorityModal(); if (data.failed > 0) { toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000); } setTimeout(() => window.location.reload(), 1500); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { console.error('Error performing bulk priority update:', error); 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.style.cursor = 'pointer'; 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; } }); // Add hover effect row.addEventListener('mouseenter', function() { this.style.backgroundColor = 'rgba(0, 255, 65, 0.08)'; }); row.addEventListener('mouseleave', function() { this.style.backgroundColor = ''; }); }); }); // Bulk Status Change function showBulkStatusModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { toast.warning('No tickets selected', 2000); return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); } function closeBulkStatusModal() { const modal = document.getElementById('bulkStatusModal'); if (modal) { modal.remove(); } } function performBulkStatusChange() { const status = document.getElementById('bulkStatus').value; const ticketIds = getSelectedTicketIds(); if (!status) { toast.warning('Please select a status', 2000); return; } fetch('/api/bulk_operation.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify({ operation_type: 'bulk_status', ticket_ids: ticketIds, parameters: { status: status } }) }) .then(response => response.json()) .then(data => { closeBulkStatusModal(); if (data.success) { if (data.failed > 0) { toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000); } setTimeout(() => window.location.reload(), 1500); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { console.error('Error performing bulk status change:', error); toast.error('Bulk status change failed: ' + error.message, 5000); }); } // Bulk Delete function showBulkDeleteModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { toast.warning('No tickets selected', 2000); return; } const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); } function closeBulkDeleteModal() { const modal = document.getElementById('bulkDeleteModal'); if (modal) { modal.remove(); } } function performBulkDelete() { const ticketIds = getSelectedTicketIds(); fetch('/api/bulk_operation.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify({ operation_type: 'bulk_delete', ticket_ids: ticketIds }) }) .then(response => response.json()) .then(data => { closeBulkDeleteModal(); if (data.success) { toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000); setTimeout(() => window.location.reload(), 1500); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { console.error('Error performing bulk delete:', error); toast.error('Bulk delete failed: ' + error.message, 5000); }); } // ============================================ // TERMINAL-STYLE MODAL UTILITIES // ============================================ /** * Show a terminal-style confirmation modal * @param {string} title - Modal title * @param {string} message - Message body * @param {string} type - 'warning', 'error', 'info' (affects color) * @param {function} onConfirm - Callback when user confirms * @param {function} onCancel - Optional callback when user cancels */ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel = null) { const modalId = 'confirmModal' + Date.now(); // Color scheme based on type const colors = { warning: 'var(--terminal-amber)', error: 'var(--status-closed)', info: 'var(--terminal-cyan)' }; const color = colors[type] || colors.warning; // Icon based on type const icons = { warning: '⚠', error: '✗', info: 'ℹ' }; const icon = icons[type] || icons.warning; // Escape user-provided content to prevent XSS const safeTitle = escapeHtml(title); const safeMessage = escapeHtml(message); const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = document.getElementById(modalId); const confirmBtn = document.getElementById(`${modalId}_confirm`); const cancelBtn = document.getElementById(`${modalId}_cancel`); confirmBtn.addEventListener('click', () => { modal.remove(); if (onConfirm) onConfirm(); }); cancelBtn.addEventListener('click', () => { modal.remove(); if (onCancel) onCancel(); }); // ESC key to cancel const escHandler = (e) => { if (e.key === 'Escape') { modal.remove(); if (onCancel) onCancel(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } /** * 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 = escapeHtml(title); const safeLabel = escapeHtml(label); const safePlaceholder = escapeHtml(placeholder); const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = document.getElementById(modalId); const input = document.getElementById(inputId); const submitBtn = document.getElementById(`${modalId}_submit`); const cancelBtn = document.getElementById(`${modalId}_cancel`); // Focus input setTimeout(() => input.focus(), 100); const handleSubmit = () => { const value = input.value.trim(); modal.remove(); if (onSubmit) onSubmit(value); }; submitBtn.addEventListener('click', handleSubmit); // Enter key to submit input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { handleSubmit(); } }); cancelBtn.addEventListener('click', () => { modal.remove(); if (onCancel) onCancel(); }); // ESC key to cancel const escHandler = (e) => { if (e.key === 'Escape') { modal.remove(); if (onCancel) onCancel(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } // ======================================== // 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); } function closeQuickStatusModal() { const modal = document.getElementById('quickStatusModal'); if (modal) modal.remove(); } function performQuickStatusChange(ticketId) { const newStatus = document.getElementById('quickStatusSelect').value; 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: ticketId, status: newStatus }) }) .then(response => response.json()) .then(data => { closeQuickStatusModal(); if (data.success) { toast.success(`Status updated to ${newStatus}`, 3000); setTimeout(() => window.location.reload(), 1000); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 4000); } }) .catch(error => { closeQuickStatusModal(); console.error('Error:', error); toast.error('Error updating status', 4000); }); } /** * Quick assign from dashboard */ function quickAssign(ticketId) { const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); // Load users fetch('/api/get_users.php', { credentials: 'same-origin' }) .then(response => response.json()) .then(data => { if (data.success && data.users) { const select = document.getElementById('quickAssignSelect'); data.users.forEach(user => { const option = document.createElement('option'); option.value = user.user_id; option.textContent = user.display_name || user.username; select.appendChild(option); }); } }) .catch(error => console.error('Error loading users:', error)); } function closeQuickAssignModal() { const modal = document.getElementById('quickAssignModal'); if (modal) modal.remove(); } function performQuickAssign(ticketId) { const assignedTo = document.getElementById('quickAssignSelect').value || null; fetch('/api/assign_ticket.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN }, body: JSON.stringify({ ticket_id: ticketId, assigned_to: assignedTo }) }) .then(response => response.json()) .then(data => { closeQuickAssignModal(); if (data.success) { toast.success('Assignment updated', 3000); setTimeout(() => window.location.reload(), 1000); } else { toast.error('Error: ' + (data.error || 'Unknown error'), 4000); } }) .catch(error => { closeQuickAssignModal(); console.error('Error:', error); toast.error('Error updating assignment', 4000); }); } // ======================================== // KANBAN / CARD VIEW // ======================================== /** * 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; if (mode === 'card') { tableView.style.display = 'none'; cardView.style.display = 'block'; tableBtn.classList.remove('active'); cardBtn.classList.add('active'); populateKanbanCards(); } else { tableView.style.display = 'block'; cardView.style.display = 'none'; tableBtn.classList.add('active'); cardBtn.classList.remove('active'); } // Store preference localStorage.setItem('ticketViewMode', mode); } /** * Populate Kanban cards from table data */ function populateKanbanCards() { const rows = document.querySelectorAll('tbody tr'); 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') }; // 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; // Skip empty rows 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 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 column = columns[status]; if (column) { counts[status]++; const card = document.createElement('div'); card.className = `kanban-card priority-${priority}`; card.onclick = () => window.location.href = `/ticket/${ticketId}`; card.innerHTML = `