diff --git a/api/notifications.php b/api/notifications.php index 8f3d95d..de4ca10 100644 --- a/api/notifications.php +++ b/api/notifications.php @@ -65,28 +65,57 @@ $assignRows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); $stmt->close(); // Query 2: Comments on tickets I own or watch (events from other users) -// Comments are logged as action_type='create', entity_type='comment', with ticket_id in details JSON. -$commentSql = "SELECT DISTINCT +// Comments are logged as action_type='create', entity_type='comment', ticket_id stored in details JSON. +// Avoid JSON_EXTRACT (not universally supported) — fetch recent entries then filter in PHP. + +// Step A: ticket IDs the current user owns or watches +$myTicketIds = []; +$myTicketsSql = "SELECT DISTINCT ticket_id FROM tickets WHERE assigned_to = ? OR created_by = ?"; +$stmt = $conn->prepare($myTicketsSql); +$stmt->bind_param('ii', $userId, $userId); +$stmt->execute(); +$mtResult = $stmt->get_result(); +while ($mtRow = $mtResult->fetch_assoc()) { $myTicketIds[(int)$mtRow['ticket_id']] = true; } +$stmt->close(); + +$watchedSql = "SELECT ticket_id FROM ticket_watchers WHERE user_id = ?"; +$stmt = $conn->prepare($watchedSql); +$stmt->bind_param('i', $userId); +$stmt->execute(); +$wResult = $stmt->get_result(); +while ($wRow = $wResult->fetch_assoc()) { $myTicketIds[(int)$wRow['ticket_id']] = true; } +$stmt->close(); + +// Step B: fetch recent comment audit events not by the current user +$commentSql = "SELECT al.log_id, al.action_type, al.entity_type, al.entity_id, al.details, al.created_at, COALESCE(u.display_name, u.username, 'System') AS actor_name FROM audit_log al LEFT JOIN users u ON al.user_id = u.user_id - INNER JOIN tickets t ON t.ticket_id = CAST(JSON_UNQUOTE(JSON_EXTRACT(al.details, '$.ticket_id')) AS UNSIGNED) - LEFT JOIN ticket_watchers tw ON tw.ticket_id = t.ticket_id AND tw.user_id = ? WHERE al.action_type = 'create' AND al.entity_type = 'comment' AND al.user_id != ? AND al.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) - AND (t.assigned_to = ? OR t.created_by = ? OR tw.user_id IS NOT NULL) ORDER BY al.created_at DESC - LIMIT 15"; + LIMIT 50"; $stmt = $conn->prepare($commentSql); -$stmt->bind_param('iiii', $userId, $userId, $userId, $userId); +$stmt->bind_param('i', $userId); $stmt->execute(); -$commentRows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); +$rawCommentRows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); $stmt->close(); +// Step C: filter to only comments on tickets the current user owns/watches +$commentRows = []; +foreach ($rawCommentRows as $rawRow) { + $d = json_decode($rawRow['details'] ?? '{}', true) ?? []; + $tid = (int)($d['ticket_id'] ?? 0); + if ($tid > 0 && isset($myTicketIds[$tid])) { + $commentRows[] = $rawRow; + if (count($commentRows) >= 15) break; + } +} + // Query 3: Status changes on watched tickets (from other users) $statusSql = "SELECT DISTINCT al.log_id, al.action_type, al.entity_type, al.entity_id, al.details, al.created_at, @@ -98,7 +127,7 @@ $statusSql = "SELECT DISTINCT AND al.entity_type = 'ticket' AND al.user_id != ? AND al.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) - AND al.details LIKE '%\"field\":\"status\"%' + AND al.details LIKE '%"status":%' ORDER BY al.created_at DESC LIMIT 10"; diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 0f4503b..590b7ae 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -198,6 +198,12 @@ document.addEventListener('DOMContentLoaded', function() { 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(); diff --git a/middleware/SecurityHeadersMiddleware.php b/middleware/SecurityHeadersMiddleware.php index b8567ee..f86206a 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}' 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';"); + 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' https://cdn.jsdelivr.net;"); // Prevent clickjacking by disallowing framing header("X-Frame-Options: DENY"); diff --git a/views/DashboardView.php b/views/DashboardView.php index 89314be..6f50cac 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -586,7 +586,7 @@ include __DIR__ . '/layout_header.php'; $newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc'; $sortClass = ($currentSort === $col) ? 'sort-' . $currentDir : ''; $ariaSort = ($currentSort === $col) ? 'aria-sort="' . ($currentDir === 'asc' ? 'ascending' : 'descending') . '"' : ''; - $sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir]); + $sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir, 'page' => 1]); $sortUrl = htmlspecialchars('?' . http_build_query($sortParams), ENT_QUOTES, 'UTF-8'); ?>