feat: Chart.js donut/bar charts, Flatpickr dates, skeleton loaders, CSP update

- DashboardView: Charts row with 3 panels (priority donut, status donut, category bar)
  using Chart.js from CDN; data passed inline from PHP stats; TDS color palette
- DashboardView: Flatpickr date picker on advanced search date fields with TDS theme overrides
- dashboard.js: showTableSkeleton() shows lt-skeleton-row during filter-triggered reloads
  and auto-refresh; called before all location.reload() with delay
- dashboard.css: Flatpickr TDS theme overrides (dark BG, monospace font, TDS accent colors)
- SecurityHeadersMiddleware: Added cdn.jsdelivr.net to script-src and style-src CSP
  to allow Chart.js and Flatpickr from CDN

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 17:45:02 -04:00
parent c15defc09b
commit 04b019a8e1
4 changed files with 219 additions and 9 deletions
+25 -7
View File
@@ -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 += '<tr class="lt-skeleton-row" aria-hidden="true">' +
'<td><div class="lt-skeleton" style="height:0.8rem;width:100%"></div></td>'.repeat(6) +
'</tr>';
}
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();
}