diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 1255031..d3d7947 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -361,3 +361,39 @@ kbd { letter-spacing: 0.08em; color: var(--text-muted); } + +/* ── Flatpickr TDS theme overrides ──────────────────────────────── */ +.flatpickr-calendar { + background: var(--bg-secondary, #0a0e14) !important; + border: 1px solid var(--border-color, rgba(0,255,65,0.25)) !important; + box-shadow: 0 8px 24px rgba(0,0,0,0.6) !important; + border-radius: 0 !important; + font-family: var(--font-mono, monospace) !important; +} +.flatpickr-day { + color: var(--text-secondary, #8fa3b1) !important; + border-radius: 0 !important; +} +.flatpickr-day.today { + border-color: var(--accent-cyan, #00d4ff) !important; + color: var(--accent-cyan, #00d4ff) !important; +} +.flatpickr-day.selected, .flatpickr-day.selected:hover { + background: var(--accent-orange, #ff8c00) !important; + border-color: var(--accent-orange, #ff8c00) !important; + color: #000 !important; +} +.flatpickr-day:hover { + background: rgba(0,212,255,0.1) !important; + color: var(--text-primary, #e8f4f8) !important; +} +.flatpickr-months, .flatpickr-weekdays { + background: var(--bg-tertiary, #1a1f2e) !important; +} +.flatpickr-current-month, .flatpickr-weekday { + color: var(--text-muted, #5a7a8a) !important; + font-family: var(--font-mono, monospace) !important; +} +.flatpickr-prev-month svg path, .flatpickr-next-month svg path { + fill: var(--text-muted) !important; +} diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index cab45e6..f7dcfff 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -535,7 +535,7 @@ function performBulkCloseAction(ticketIds) { } else { lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000); } - setTimeout(() => window.location.reload(), 1500); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } @@ -631,7 +631,7 @@ function performBulkAssign() { } else { lt.toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000); } - setTimeout(() => window.location.reload(), 1500); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } @@ -707,7 +707,7 @@ function performBulkPriority() { } else { lt.toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000); } - setTimeout(() => window.location.reload(), 1500); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } @@ -810,7 +810,7 @@ function performBulkStatusChange() { } else { lt.toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000); } - setTimeout(() => window.location.reload(), 1500); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } @@ -869,7 +869,7 @@ function performBulkDelete() { closeBulkDeleteModal(); if (data.success) { lt.toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000); - setTimeout(() => window.location.reload(), 1500); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1500); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000); } @@ -993,7 +993,7 @@ function performQuickStatusChange(ticketId) { closeQuickStatusModal(); if (data.success) { lt.toast.success(`Status updated to ${newStatus}`, 3000); - setTimeout(() => window.location.reload(), 1000); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1000); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000); } @@ -1079,7 +1079,7 @@ function performQuickAssign(ticketId) { closeQuickAssignModal(); if (data.success) { lt.toast.success('Assignment updated', 3000); - setTimeout(() => window.location.reload(), 1000); + showTableSkeleton(5); setTimeout(() => window.location.reload(), 1000); } else { lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000); } @@ -1455,12 +1455,30 @@ function hideLoadingOverlay(element) { * 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(); } diff --git a/middleware/SecurityHeadersMiddleware.php b/middleware/SecurityHeadersMiddleware.php index e2b51f2..b8567ee 100644 --- a/middleware/SecurityHeadersMiddleware.php +++ b/middleware/SecurityHeadersMiddleware.php @@ -28,7 +28,7 @@ class SecurityHeadersMiddleware { // Content Security Policy - restricts where resources can be loaded from // Using nonces for scripts to prevent XSS attacks while allowing inline scripts with valid nonces // All inline event handlers have been refactored to use addEventListener with data-action attributes - header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';"); + header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';"); // Prevent clickjacking by disallowing framing header("X-Frame-Options: DENY"); diff --git a/views/DashboardView.php b/views/DashboardView.php index 3140587..7cdc52a 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -15,13 +15,18 @@ $nonce = SecurityHeadersMiddleware::getNonce(); $pageTitle = 'Dashboard'; $activeNav = 'dashboard'; $_v = $GLOBALS['config']['ASSET_VERSION'] ?? '1'; -$pageStyles = ["/assets/css/dashboard.css?v={$_v}"]; +$pageStyles = [ + "https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css", + "/assets/css/dashboard.css?v={$_v}", +]; $pageScripts = [ "/assets/js/markdown.js?v={$_v}", "/assets/js/dashboard.js?v={$_v}", "/assets/js/advanced-search.js?v={$_v}", "/assets/js/keyboard-shortcuts.js?v={$_v}", "/assets/js/settings.js?v={$_v}", + "https://cdn.jsdelivr.net/npm/flatpickr", + "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js", ]; // ── Pagination helpers ──────────────────────────────────────────────────────── @@ -168,6 +173,135 @@ include __DIR__ . '/layout_header.php'; + +
+
+ +
Priority Distribution
+
+ +
+
+
+ +
Status Breakdown
+
+ +
+
+
+ +
Category Breakdown
+
+ +
+
+
+ +