diff --git a/api/add_comment.php b/api/add_comment.php index 62c95ef..937d2ab 100644 --- a/api/add_comment.php +++ b/api/add_comment.php @@ -62,7 +62,14 @@ try { throw new Exception("Invalid JSON data received"); } - $ticketId = $data['ticket_id']; + $ticketId = isset($data['ticket_id']) ? (int)$data['ticket_id'] : 0; + if ($ticketId <= 0) { + http_response_code(400); + ob_end_clean(); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Invalid ticket ID']); + exit; + } // Initialize models $commentModel = new CommentModel($conn); diff --git a/api/assign_ticket.php b/api/assign_ticket.php index 9ee2b39..88ed712 100644 --- a/api/assign_ticket.php +++ b/api/assign_ticket.php @@ -6,10 +6,17 @@ require_once dirname(__DIR__) . '/models/UserModel.php'; // Get request data $data = json_decode(file_get_contents('php://input'), true); -$ticketId = $data['ticket_id'] ?? null; +if (!is_array($data)) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Invalid JSON body']); + exit; +} + +$ticketId = isset($data['ticket_id']) ? (int)$data['ticket_id'] : null; $assignedTo = $data['assigned_to'] ?? null; if (!$ticketId) { + http_response_code(400); echo json_encode(['success' => false, 'error' => 'Ticket ID required']); exit; } @@ -18,6 +25,21 @@ $ticketModel = new TicketModel($conn); $auditLogModel = new AuditLogModel($conn); $userModel = new UserModel($conn); +// Verify ticket exists +$ticket = $ticketModel->getTicketById($ticketId); +if (!$ticket) { + http_response_code(404); + echo json_encode(['success' => false, 'error' => 'Ticket not found']); + exit; +} + +// Authorization: only admins or the ticket creator/assignee can reassign +if (!$isAdmin && $ticket['created_by'] !== $userId && $ticket['assigned_to'] !== $userId) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'Permission denied']); + exit; +} + if ($assignedTo === null || $assignedTo === '') { // Unassign ticket $success = $ticketModel->unassignTicket($ticketId, $userId); @@ -40,4 +62,9 @@ if ($assignedTo === null || $assignedTo === '') { } } -echo json_encode(['success' => $success]); +if (!$success) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Failed to update ticket assignment']); +} else { + echo json_encode(['success' => true]); +} diff --git a/api/bulk_operation.php b/api/bulk_operation.php index 25a9e5d..e94b21e 100644 --- a/api/bulk_operation.php +++ b/api/bulk_operation.php @@ -14,6 +14,7 @@ header('Content-Type: application/json'); // Check authentication if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) { + http_response_code(401); echo json_encode(['success' => false, 'error' => 'Not authenticated']); exit; } @@ -32,6 +33,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Check admin status - bulk operations are admin-only $isAdmin = $_SESSION['user']['is_admin'] ?? false; if (!$isAdmin) { + http_response_code(403); echo json_encode(['success' => false, 'error' => 'Admin access required']); exit; } diff --git a/api/clone_ticket.php b/api/clone_ticket.php index 8a0a9ab..f6e062a 100644 --- a/api/clone_ticket.php +++ b/api/clone_ticket.php @@ -17,7 +17,9 @@ try { require_once dirname(__DIR__) . '/models/AuditLogModel.php'; // Check authentication - session_start(); + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Authentication required']); @@ -50,8 +52,14 @@ try { exit; } - $sourceTicketId = $data['ticket_id']; + $sourceTicketId = (int)$data['ticket_id']; + if ($sourceTicketId <= 0) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Invalid ticket ID']); + exit; + } $userId = $_SESSION['user']['user_id']; + $isAdmin = $_SESSION['user']['is_admin'] ?? false; // Get database connection $conn = Database::getConnection(); @@ -66,6 +74,15 @@ try { exit; } + // Authorization: non-admins cannot clone internal tickets unless they created/are assigned + if (!$isAdmin && ($sourceTicket['visibility'] ?? 'public') === 'internal') { + if ($sourceTicket['created_by'] != $userId && $sourceTicket['assigned_to'] != $userId) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'Permission denied']); + exit; + } + } + // Prepare cloned ticket data $clonedTicketData = [ 'title' => '[CLONE] ' . $sourceTicket['title'], diff --git a/api/update_ticket.php b/api/update_ticket.php index e53f724..0d58e0d 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -79,6 +79,17 @@ try { ]; } + // Authorization: admins can edit any ticket; others only their own or assigned + if (!$this->isAdmin + && $currentTicket['created_by'] != $this->userId + && $currentTicket['assigned_to'] != $this->userId + ) { + return [ + 'success' => false, + 'error' => 'Permission denied' + ]; + } + // Merge current data with updates, keeping existing values for missing fields $updateData = [ 'ticket_id' => $id, diff --git a/api/upload_attachment.php b/api/upload_attachment.php index 218d2b2..e613859 100644 --- a/api/upload_attachment.php +++ b/api/upload_attachment.php @@ -155,7 +155,7 @@ if (empty($originalFilename)) { // Save to database try { - $attachmentModel = new AttachmentModel(); + $attachmentModel = new AttachmentModel($conn); $attachmentId = $attachmentModel->addAttachment( $ticketId, $uniqueFilename, diff --git a/assets/css/base.css b/assets/css/base.css index 0e112de..e2b3739 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -128,9 +128,9 @@ --header-height: 58px; --container-max: 1600px; - /* --- Transitions --- */ - --transition-fast: all 0.15s ease; - --transition-default: all 0.3s ease; + /* --- Transitions — scoped to GPU-safe properties (no box-shadow/filter) --- */ + --transition-fast: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease; + --transition-default: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease; /* --- Z-index ladder --- */ --z-base: 1; diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index b3eea24..e0de4fb 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -7,7 +7,8 @@ /* Terminal Colors */ --terminal-green: #00ff41; - --terminal-green-dim: #00cc33; + /* Note: --terminal-green-dim is NOT overridden here — base.css defines it as + rgba(0,255,65,0.15) for button hover backgrounds. Use --text-secondary for dim text. */ --terminal-amber: #ffb000; --terminal-cyan: #00ffff; --terminal-red: #ff4444; @@ -56,8 +57,8 @@ --spacing-md: 1.5rem; --spacing-lg: 2rem; - /* Transitions */ - --transition-default: all 0.3s ease; + /* Transitions — scoped to GPU-safe properties only (no box-shadow, no filter) */ + --transition-default: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease; /* Z-Index Scale - Centralized stacking context management */ --z-base: 1; @@ -116,7 +117,9 @@ body::before { display: none; } -/* Suppress body::after binary text watermark (also position:fixed → GPU layer) */ +/* Suppress body::after binary text watermark (also position:fixed → GPU layer). + Do NOT add a second body::after rule below — the last rule wins in the cascade, + overriding this display:none and re-creating the fixed GPU compositing layer. */ body::after { display: none; } @@ -139,30 +142,6 @@ body::after { 100% { transform: translate(0); } } - -/* Subtle data stream effect in corner */ -body::after { - content: '10101010'; - position: fixed; - bottom: 10px; - right: 10px; - font-family: var(--font-mono); - font-size: 0.6rem; - color: var(--terminal-green); - opacity: 0.1; - pointer-events: none; - letter-spacing: 2px; - animation: data-stream 3s linear infinite; -} - -@keyframes data-stream { - 0% { content: '10101010'; opacity: 0.1; } - 25% { content: '01010101'; opacity: 0.15; } - 50% { content: '11001100'; opacity: 0.1; } - 75% { content: '00110011'; opacity: 0.15; } - 100% { content: '10101010'; opacity: 0.1; } -} - /* ===== ENHANCED TERMINAL ANIMATIONS ===== */ /* Typing cursor effect for focused inputs */ @@ -182,12 +161,8 @@ textarea:focus, select:focus { outline: 2px solid var(--terminal-amber); outline-offset: 2px; - animation: focus-pulse 2s ease-in-out infinite; -} - -@keyframes focus-pulse { - 0%, 100% { box-shadow: var(--glow-amber), inset 0 0 10px rgba(0, 0, 0, 0.5); } - 50% { box-shadow: var(--glow-amber-intense), inset 0 0 10px rgba(0, 0, 0, 0.5); } + /* Static glow on focus — no animation to avoid CPU repaints on every frame */ + box-shadow: var(--glow-amber); } /* Focus visible for keyboard navigation */ @@ -313,29 +288,6 @@ tbody tr { .btn { transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - background: rgba(255, 255, 255, 0.1); - border-radius: 50%; - transform: translate(-50%, -50%); - transition: width 0.4s, height 0.4s; -} - -.btn:active::before { - width: 200%; - height: 200%; -} - -.btn:active { - transform: scale(0.98); } /* Terminal cursor blink for active/selected elements */ @@ -1584,6 +1536,7 @@ h1 { } /* ===== BUTTON STYLES - TERMINAL EDITION ===== */ +/* Base: apply terminal font/reset to all buttons */ .btn, .btn-base, button { @@ -1597,25 +1550,25 @@ button { text-transform: uppercase; font-weight: bold; position: relative; + display: inline-block; + white-space: nowrap; will-change: transform; transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease; } +/* Bracket notation only for explicitly-styled .btn class buttons */ .btn::before, -.btn-base::before, -button::before { +.btn-base::before { content: '[ '; } .btn::after, -.btn-base::after, -button::after { +.btn-base::after { content: ' ]'; } .btn:hover, -.btn-base:hover, -button:hover { +.btn-base:hover { background: rgba(0, 255, 65, 0.15); color: var(--terminal-amber); border-color: var(--terminal-amber); @@ -1623,8 +1576,7 @@ button:hover { } .btn:active, -.btn-base:active, -button:active { +.btn-base:active { transform: translateY(0); } @@ -2083,7 +2035,7 @@ select { border: 2px solid var(--terminal-green); border-radius: 0; padding: 8px 12px; - transition: all 0.3s ease; + transition: border-color 0.2s ease, background-color 0.2s ease; box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); } @@ -2288,7 +2240,7 @@ input[type="checkbox"]:checked { text-align: left; font-size: 0.9rem; text-shadow: var(--glow-amber); - transition: all 0.2s ease; + transition: background-color 0.2s ease; } .banner-toggle:hover { @@ -2532,7 +2484,7 @@ input[type="checkbox"]:checked { cursor: pointer; font-size: 0.85rem; font-family: var(--font-mono); - transition: all 0.2s ease; + transition: color 0.2s ease, padding-left 0.2s ease; } .filter-group label:hover { @@ -2548,7 +2500,7 @@ input[type="checkbox"]:checked { width: 16px; height: 16px; border: 2px solid var(--terminal-green); - transition: all 0.2s ease; + transition: opacity 0.15s ease; } .filter-group input[type="checkbox"]:checked { @@ -2568,7 +2520,7 @@ input[type="checkbox"]:checked { border: 2px solid var(--terminal-green); font-family: var(--font-mono); cursor: pointer; - transition: all 0.2s ease; + transition: background-color 0.2s ease, color 0.2s ease; } .dashboard-sidebar .btn:hover { @@ -2802,7 +2754,7 @@ input[type="checkbox"]:checked { border-radius: 0; display: inline-block; border: 1px solid transparent; - transition: background 0.2s ease, border-color 0.2s ease, text-shadow 0.2s ease; + transition: background-color 0.2s ease, border-color 0.2s ease; text-shadow: var(--glow-green); } @@ -3250,9 +3202,25 @@ body.dark-mode select option { font-size: 0.4rem !important; } - /* Remove all pseudo-element decorations except essential ones */ - *::before, - *::after { + /* Remove purely decorative frame corner glyphs on tiny screens */ + .ascii-frame-outer .bottom-left-corner, + .ascii-frame-outer .bottom-right-corner { + display: none; + } + .lt-frame::before, .lt-frame::after, + .lt-frame-inner::before, .lt-frame-inner::after, + .lt-card::before, .lt-card::after, + .lt-stat-card::before, .lt-stat-card::after, + .lt-divider::before, .lt-divider::after, + .lt-section-header::before, .lt-section-header::after, + .lt-subsection-header::before, .lt-subsection-header::after, + .lt-sidebar-header::before, + .lt-modal::before, .lt-modal::after, + .lt-modal-title::before, + .lt-timeline::before, + h1::before, h3::before, h3::after, + .lt-page-title::before, + .lt-brand-title::before { content: none !important; } @@ -3278,11 +3246,6 @@ body.dark-mode select option { padding: 0.25rem; } - /* Re-enable essential pseudo-elements */ - .search-form::before { - content: '$ ' !important; - } - /* Simplify modals */ .settings-modal, .modal-content { @@ -3350,7 +3313,7 @@ body.dark-mode select option { z-index: var(--z-toast); opacity: 0; transform: translateX(400px); - transition: all 0.3s ease; + transition: opacity 0.3s ease, transform 0.3s ease; max-width: 400px; word-wrap: break-word; } @@ -3403,7 +3366,7 @@ body.dark-mode select option { padding: 0.25rem 0.5rem; cursor: pointer; font-family: var(--font-mono); - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; border-radius: 0; margin-left: 1rem; } @@ -3579,7 +3542,7 @@ body.dark-mode select option { padding: 0.25rem 0.75rem; cursor: pointer; font-family: var(--font-mono); - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; } .close-settings:hover { @@ -3863,7 +3826,7 @@ body.dark-mode select option { padding: 0.75rem 1.5rem; border: 2px solid; cursor: pointer; - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; background: transparent; } @@ -4035,7 +3998,7 @@ code.inline-code { color: var(--terminal-cyan); text-decoration: none; border-bottom: 1px dotted var(--terminal-cyan); - transition: all 0.3s ease; + transition: color 0.2s ease, border-bottom-color 0.2s ease; } [data-markdown] a:hover { @@ -4126,7 +4089,7 @@ code.inline-code { padding: 0.25rem 0.75rem; cursor: pointer; font-family: var(--font-mono); - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; } .close-advanced-search:hover { @@ -4211,7 +4174,7 @@ code.inline-code { font-family: var(--font-mono); font-size: 1rem; cursor: pointer; - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; font-weight: bold; } @@ -4229,7 +4192,7 @@ code.inline-code { font-family: var(--font-mono); font-size: 1rem; cursor: pointer; - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; } .btn-search-secondary:hover { @@ -4246,7 +4209,7 @@ code.inline-code { font-family: var(--font-mono); font-size: 1rem; cursor: pointer; - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease; } .btn-search-reset:hover { @@ -4377,7 +4340,7 @@ tr:hover .quick-actions { .stat-label { font-size: 0.8rem; - color: var(--terminal-green-dim, #008822); + color: var(--text-secondary); margin-top: 0.25rem; text-transform: uppercase; letter-spacing: 0.05em; @@ -4456,7 +4419,7 @@ tr:hover .quick-actions { color: var(--terminal-green); text-decoration: none; font-family: var(--font-mono); - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease; } .export-dropdown-content a:hover { @@ -4498,7 +4461,7 @@ tr:hover .quick-actions { text-decoration: none; font-family: var(--font-mono); font-size: 0.85rem; - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease; border-bottom: 1px solid var(--bg-tertiary); } @@ -4545,7 +4508,7 @@ table td:nth-child(4) { padding: 0.1rem 0.3rem; border-radius: 0; background: rgba(0, 255, 255, 0.1); - transition: all 0.2s ease; + transition: color 0.15s ease, background-color 0.15s ease; } .ticket-link-ref:hover { @@ -4859,7 +4822,7 @@ table td:nth-child(4) { font-size: 1.2rem; min-width: 44px; min-height: 44px; - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease; } .view-btn::before, @@ -4957,7 +4920,7 @@ table td:nth-child(4) { border: 1px solid var(--terminal-green); padding: 0.75rem; cursor: pointer; - transition: all 0.2s ease; + transition: border-color 0.15s ease, transform 0.15s ease; font-family: var(--font-mono); } diff --git a/assets/css/ticket.css b/assets/css/ticket.css index 31581e3..7bf1d31 100644 --- a/assets/css/ticket.css +++ b/assets/css/ticket.css @@ -345,6 +345,7 @@ textarea[data-field="description"]:not(:disabled)::after { color: var(--terminal-amber); border-color: var(--terminal-amber); background: rgba(255, 176, 0, 0.1); + box-shadow: 0 0 6px rgba(255, 176, 0, 0.4); animation: pulse-warning 2s ease-in-out infinite; } @@ -352,17 +353,18 @@ textarea[data-field="description"]:not(:disabled)::after { color: var(--priority-1); border-color: var(--priority-1); background: rgba(255, 77, 77, 0.15); + box-shadow: 0 0 8px rgba(255, 77, 77, 0.5); animation: pulse-critical 1s ease-in-out infinite; } @keyframes pulse-warning { - 0%, 100% { box-shadow: 0 0 5px rgba(255, 176, 0, 0.3); } - 50% { box-shadow: 0 0 15px rgba(255, 176, 0, 0.6); } + 0%, 100% { opacity: 0.75; } + 50% { opacity: 1; } } @keyframes pulse-critical { - 0%, 100% { box-shadow: 0 0 5px rgba(255, 77, 77, 0.3); } - 50% { box-shadow: 0 0 20px rgba(255, 77, 77, 0.8); } + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } } /* Tab transition animations */ @@ -507,7 +509,7 @@ textarea[data-field="description"]:not(:disabled)::after { border-radius: 0; background: var(--bg-primary); color: var(--text-primary); - transition: all 0.3s ease; + transition: border-color 0.2s ease; } input.editable { @@ -547,7 +549,7 @@ textarea.editable { font-weight: 500; background: var(--bg-primary); color: var(--text-primary); - transition: all 0.3s ease; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; } .btn.primary { @@ -626,7 +628,7 @@ textarea.editable { margin-bottom: 1rem; position: relative; box-shadow: none; - transition: all 0.3s ease; + transition: border-color 0.2s ease; animation: comment-appear 0.4s ease-out; } @@ -643,7 +645,6 @@ textarea.editable { .comment:hover { border-color: var(--terminal-amber); - background: linear-gradient(135deg, var(--bg-primary) 0%, rgba(255, 176, 0, 0.03) 100%); } .comment:hover::before, @@ -760,13 +761,16 @@ textarea.editable { padding: 0.25rem 0.5rem; font-size: 0.85rem; cursor: pointer; - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; font-family: var(--font-mono); line-height: 1; } -.comment-action-btn:hover { +.comment-action-btn:hover, +.comment-action-btn:focus-visible { background: rgba(0, 255, 65, 0.1); + outline: 2px solid var(--terminal-amber); + outline-offset: 2px; } .comment-action-btn.edit-btn:hover { @@ -1055,7 +1059,7 @@ textarea.editable { font-size: 1em; font-family: var(--font-mono); color: var(--terminal-green); - transition: all 0.3s ease; + transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; position: relative; margin-right: -2px; } @@ -1070,9 +1074,12 @@ textarea.editable { color: var(--terminal-green); } -.tab-btn:hover { +.tab-btn:hover, +.tab-btn:focus-visible { background: rgba(0, 255, 65, 0.05); color: var(--terminal-amber); + outline: 2px solid var(--terminal-amber); + outline-offset: -2px; } .tab-btn.active { @@ -1155,7 +1162,7 @@ textarea.editable { right: 0; bottom: 0; background-color: var(--bg-secondary); - transition: .4s; + transition: background-color 0.4s ease; } .slider:before { @@ -1166,7 +1173,7 @@ textarea.editable { left: 4px; bottom: 4px; background-color: white; - transition: .4s; + transition: transform 0.4s ease; } .slider.round { @@ -1353,7 +1360,7 @@ body.dark-mode .timeline-date { letter-spacing: 0.5px; border: 2px solid transparent; cursor: pointer; - transition: all 0.2s ease; + transition: opacity 0.15s ease, border-color 0.15s ease; } .status-select:hover { @@ -1604,7 +1611,7 @@ body.dark-mode .editable { padding: 2rem; text-align: center; cursor: pointer; - transition: all 0.3s ease; + transition: border-color 0.2s ease, background-color 0.2s ease; background: var(--bg-primary); } @@ -1691,7 +1698,7 @@ body.dark-mode .editable { padding: 0.75rem 1rem; border: 1px solid var(--terminal-green); background: var(--bg-primary); - transition: all 0.2s ease; + transition: border-color 0.15s ease, background-color 0.15s ease; } .attachment-item:hover { @@ -1782,7 +1789,7 @@ body.dark-mode .editable { border-radius: 0; font-weight: 600; cursor: pointer; - transition: all 0.2s ease; + transition: background-color 0.15s ease; } .mention:hover { @@ -1816,7 +1823,7 @@ body.dark-mode .editable { cursor: pointer; font-family: var(--font-mono); color: var(--terminal-green); - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease; display: flex; align-items: center; gap: 0.5rem; @@ -1857,7 +1864,7 @@ body.dark-mode .editable { font-family: var(--font-mono); font-size: 0.85rem; cursor: pointer; - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; min-width: 32px; } @@ -1910,7 +1917,7 @@ body.dark-mode .editable { color: var(--terminal-green); text-decoration: none; font-family: var(--font-mono); - transition: all 0.2s ease; + transition: background-color 0.15s ease, color 0.15s ease; } .export-dropdown-content a:hover { diff --git a/assets/js/advanced-search.js b/assets/js/advanced-search.js index 7ff8086..4cb1ac8 100644 --- a/assets/js/advanced-search.js +++ b/assets/js/advanced-search.js @@ -148,7 +148,7 @@ async function saveCurrentFilter() { 'My Filter', async (filterName) => { if (!filterName || filterName.trim() === '') { - toast.warning('Filter name cannot be empty', 2000); + lt.toast.warning('Filter name cannot be empty', 2000); return; } diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 0fce760..c0c9a63 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -58,7 +58,7 @@ function initMobileSidebar() { const toggleBtn = document.createElement('button'); toggleBtn.id = 'mobileFilterToggle'; toggleBtn.className = 'mobile-filter-toggle'; - toggleBtn.innerHTML = '☰ Filters & Search Options'; + toggleBtn.innerHTML = '[ = ] Filters & Search Options'; toggleBtn.onclick = openMobileSidebar; dashboardMain.insertBefore(toggleBtn, dashboardMain.firstChild); } @@ -79,20 +79,20 @@ function initMobileSidebar() { nav.className = 'mobile-bottom-nav'; nav.innerHTML = ` - - + + - - + + `; document.body.appendChild(nav); @@ -566,11 +566,11 @@ function quickSave() { } } else { - toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000); + lt.toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000); } }) .catch(error => { - toast.error('Error updating ticket: ' + error.message, 5000); + lt.toast.error('Error updating ticket: ' + error.message, 5000); }); } @@ -671,11 +671,11 @@ function loadTemplate() { document.getElementById('priority').value = template.default_priority; } } else { - toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000); + lt.toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000); } }) .catch(error => { - toast.error('Error loading template: ' + error.message, 4000); + lt.toast.error('Error loading template: ' + error.message, 4000); }); } @@ -752,7 +752,7 @@ function bulkClose() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { - toast.warning('No tickets selected', 2000); + lt.toast.warning('No tickets selected', 2000); return; } @@ -782,17 +782,17 @@ function performBulkCloseAction(ticketIds) { .then(data => { if (data.success) { if (data.failed > 0) { - toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000); + lt.toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { - toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000); + lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000); } setTimeout(() => window.location.reload(), 1500); } else { - toast.error('Error: ' + (data.error || 'Unknown error'), 5000); + lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { - toast.error('Bulk close failed: ' + error.message, 5000); + lt.toast.error('Bulk close failed: ' + error.message, 5000); }); } @@ -800,7 +800,7 @@ function showBulkAssignModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { - toast.warning('No tickets selected', 2000); + lt.toast.warning('No tickets selected', 2000); return; } @@ -834,12 +834,14 @@ function showBulkAssignModal() { .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); - }); + if (select) { + 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(() => lt.toast.error('Error loading users')); @@ -856,7 +858,7 @@ function performBulkAssign() { const ticketIds = getSelectedTicketIds(); if (!userId) { - toast.warning('Please select a user', 2000); + lt.toast.warning('Please select a user', 2000); return; } @@ -878,17 +880,17 @@ function performBulkAssign() { if (data.success) { closeBulkAssignModal(); if (data.failed > 0) { - toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000); + lt.toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { - toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000); + lt.toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000); } setTimeout(() => window.location.reload(), 1500); } else { - toast.error('Error: ' + (data.error || 'Unknown error'), 5000); + lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { - toast.error('Bulk assign failed: ' + error.message, 5000); + lt.toast.error('Bulk assign failed: ' + error.message, 5000); }); } @@ -896,7 +898,7 @@ function showBulkPriorityModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { - toast.warning('No tickets selected', 2000); + lt.toast.warning('No tickets selected', 2000); return; } @@ -941,7 +943,7 @@ function performBulkPriority() { const ticketIds = getSelectedTicketIds(); if (!priority) { - toast.warning('Please select a priority', 2000); + lt.toast.warning('Please select a priority', 2000); return; } @@ -963,17 +965,17 @@ function performBulkPriority() { if (data.success) { closeBulkPriorityModal(); if (data.failed > 0) { - toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000); + lt.toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { - toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000); + lt.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); + lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { - toast.error('Bulk priority update failed: ' + error.message, 5000); + lt.toast.error('Bulk priority update failed: ' + error.message, 5000); }); } @@ -1013,7 +1015,7 @@ function showBulkStatusModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { - toast.warning('No tickets selected', 2000); + lt.toast.warning('No tickets selected', 2000); return; } @@ -1057,7 +1059,7 @@ function performBulkStatusChange() { const ticketIds = getSelectedTicketIds(); if (!status) { - toast.warning('Please select a status', 2000); + lt.toast.warning('Please select a status', 2000); return; } @@ -1079,17 +1081,17 @@ function performBulkStatusChange() { closeBulkStatusModal(); if (data.success) { if (data.failed > 0) { - toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000); + lt.toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000); } else { - toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000); + lt.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); + lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } }) .catch(error => { - toast.error('Bulk status change failed: ' + error.message, 5000); + lt.toast.error('Bulk status change failed: ' + error.message, 5000); }); } @@ -1098,7 +1100,7 @@ function showBulkDeleteModal() { const ticketIds = getSelectedTicketIds(); if (ticketIds.length === 0) { - toast.warning('No tickets selected', 2000); + lt.toast.warning('No tickets selected', 2000); return; } @@ -1106,7 +1108,7 @@ function showBulkDeleteModal() {