7 Commits

Author SHA1 Message Date
jared ac05b212b2 Fix performAdvancedSearch ReferenceError, settings save, sort reset, notifications 500, CSP
DashboardView.php: wrap performAdvancedSearch in a closure so it is
resolved at event-fire time rather than listener-registration time
(advanced-search.js loads later via pageScripts so the bare identifier
reference caused ReferenceError).

DashboardView.php: reset sort URL to page=1 so sorting all pages
instead of staying on the current page.

dashboard.js: add missing save-settings and close-settings cases to
the click delegation handler (were removed in a prior session under
the assumption they were in dashboard.js, but they were not).

notifications.php: replace JSON_EXTRACT-based comment join (not
universally supported) with a two-step PHP filter: fetch owner/watcher
ticket IDs first, then filter raw comment rows in PHP. Also fix the
status change LIKE pattern to match the actual logTicketUpdate format
{"status": {"from": ..., "to": ...}}.

SecurityHeadersMiddleware.php: add https://cdn.jsdelivr.net to
connect-src so Chart.js source maps load without CSP violations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 10:53:06 -04:00
jared df6c4de196 Fix notification comment query, status title, and is-hidden visibility
notifications.php: comment notifications never fired because the query
used action_type='comment'/entity_type='ticket' but logCommentCreate
logs action_type='create'/entity_type='comment'. Fix query to match
actual log format and extract ticket_id from details JSON.

notifications.php: status change notification titles always showed
"? → ?" because code read details.old_value/new_value but logTicketUpdate
stores the delta as {"status": {"from": ..., "to": ...}}.

base.css: move .is-hidden to base.css (global) — it was only defined in
ticket.css, so on the dashboard the ticket-preview popup had no hide
rule applied and was visible in the DOM at all times.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 10:47:39 -04:00
jared 2ccf4f2261 Clarify comment: @mention highlight skips markdown-rendered elements
markdown.js already calls renderMarkdownElements() on DOMContentLoaded
for all [data-markdown] elements; ticket.js only processes plain-text
comments to avoid double-rendering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 10:44:14 -04:00
jared dcbe6fb383 Fix double-firing event handlers, non-bubbling keyboard status event, and saved filter status type
- Remove duplicate edit-comment/delete-comment cases from TicketView.php inline
  script — ticket.js already handles them. Double-call of editComment() would
  immediately open then close the edit form (second call sees .editing → cancels)
- Fix keyboard shortcut 1-4 status change: dispatchEvent(new Event('change'))
  was non-bubbling (default), so the document-level change delegation in TicketView
  never received it. Now uses { bubbles: true } so updateTicketStatus() fires correctly
- Fix saved filter status type: getCurrentFilterCriteria() was saving status as a
  joined string "Open,Pending" but pill-click handler called .join() expecting an array
  (TypeError swallowed by try/catch → status filter silently not applied). Now saves
  as array; applySavedFilterCriteria handles both arrays and legacy strings
- Pill-click handler also updated to handle both array and string status formats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 10:40:16 -04:00
jared 914c33ecf3 Fix CSP-blocked chart scripts, undefined CSS classes, and double-firing click handlers
- Add nonce to charts and ticket-preview drawer inline <script> blocks in
  DashboardView.php (both were CSP-blocked — charts never rendered)
- Add .lt-modal-xs (280px) to base.css — used by quickStatus/quickAssign
  modals but was undefined, causing them to use full modal width
- Fix showConfirmModal in utils.js: class="text-center" → "lt-text-center"
  (undefined class); escape newlines as <br> so multi-line messages render
- Remove duplicate click-handler cases from DashboardView.php inline script
  that were already handled by dashboard.js, preventing double-firing
  (export-tickets, open-settings, remove-filter, etc. were all called twice)
- Fix manual-refresh action to use lt.autoRefresh.now() instead of bare
  window.location.reload() so modal/focus guards are respected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 23:00:35 -04:00
jared d588590989 fix: ticket preview popup wrong position and persists after interactions
- position:fixed popup was adding window.scrollX/scrollY to viewport coords
  from getBoundingClientRect(), making it appear far below link when scrolled
- Off-screen check compared against innerHeight + scrollY instead of innerHeight
- Added clamp to prevent negative coords (popup clipped off top/left edge)
- Hide preview on scroll, modal open, and pagination clicks (capture phase)
  so stale popup doesn't linger after user navigates away

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:51:39 -04:00
jared b7b6884bb0 fix: add missing CSS classes + clean up remaining inline styles
- Add .lt-modal-sm (max 360px) and .lt-modal-header--danger variant used
  in JS-generated bulk delete confirmation modal (no CSS = unstyled header)
- Add .lt-badge-sm for compact inline badges (comment counts, group tags)
- Add .lt-kv-row { display:contents } with .lt-kv-label/.lt-kv-value rules
  (was missing from previous commit — added in base.css)
- Replace style="text-align:center" with .lt-text-center in JS modal body
- Replace style="flex-direction:column" with .lt-flex-col on .lt-btn-group

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:50:13 -04:00
11 changed files with 131 additions and 60 deletions
+52 -15
View File
@@ -65,27 +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)
$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(al.entity_id AS UNSIGNED)
LEFT JOIN ticket_watchers tw ON tw.ticket_id = t.ticket_id AND tw.user_id = ?
WHERE al.action_type = 'comment'
AND al.entity_type = 'ticket'
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,
@@ -97,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";
@@ -123,16 +153,23 @@ $all = array_slice($all, 0, 30);
$notifications = [];
foreach ($all as $row) {
$details = json_decode($row['details'] ?? '{}', true) ?? [];
$ticketId = (int)$row['entity_id'];
// Comment rows: entity_id is the comment_id; real ticket_id is in details
$actionType = ($row['action_type'] === 'create' && $row['entity_type'] === 'comment')
? 'comment'
: $row['action_type'];
$ticketId = ($actionType === 'comment')
? (int)($details['ticket_id'] ?? 0)
: (int)$row['entity_id'];
$isRead = $lastSeen && $row['created_at'] <= $lastSeen;
// Build human-readable title
$title = match($row['action_type']) {
$title = match($actionType) {
'assign' => "{$row['actor_name']} assigned ticket #{$ticketId} to you",
'comment' => "{$row['actor_name']} commented on ticket #{$ticketId}",
'update' => (function() use ($row, $details, $ticketId) {
$from = $details['old_value'] ?? '?';
$to = $details['new_value'] ?? '?';
// logTicketUpdate stores delta as {"status": {"from": "Open", "to": "In Progress"}}
$from = $details['status']['from'] ?? ($details['old_value'] ?? '?');
$to = $details['status']['to'] ?? ($details['new_value'] ?? '?');
return "{$row['actor_name']} changed status on #{$ticketId}: {$from}{$to}";
})(),
default => "{$row['actor_name']} updated ticket #{$ticketId}",
@@ -149,7 +186,7 @@ foreach ($all as $row) {
'title' => $title,
'created_at' => $row['created_at'],
'is_read' => $isRead,
'action' => $row['action_type'],
'action' => $actionType,
'url' => "/ticket/{$ticketId}",
];
}
+16
View File
@@ -1211,6 +1211,7 @@ select option:checked {
letter-spacing: 0.1em;
border: 1px solid currentColor;
}
.lt-badge-sm { font-size: 0.5rem; padding: 0.05rem 0.3rem; }
.lt-badge-green { color: var(--accent-green); }
.lt-badge-amber { color: var(--accent-amber); }
.lt-badge-red { color: var(--accent-red); }
@@ -1316,6 +1317,20 @@ select option:checked {
.lt-modal-close:active { color: var(--accent-red); opacity: 0.7; }
.lt-modal-close:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
/* Modal size modifiers */
.lt-modal-xs { width: min(280px, 92vw); }
.lt-modal-sm { width: min(360px, 92vw); }
/* Modal header danger variant */
.lt-modal-header--danger {
background: rgba(255, 77, 77, 0.08);
border-bottom-color: var(--accent-red);
}
.lt-modal-header--danger .lt-modal-title {
color: var(--accent-red);
text-shadow: var(--glow-red);
}
.lt-modal-body {
padding: var(--space-lg);
overflow-y: auto;
@@ -2326,6 +2341,7 @@ select option:checked {
.lt-text-upper { text-transform: uppercase; letter-spacing: 0.1em; }
.lt-hidden { display: none !important; }
.is-hidden { display: none !important; }
/* Skip navigation link — visible only on focus */
.lt-skip-link {
+1 -2
View File
@@ -203,8 +203,7 @@ body.edit-mode .editable-metadata {
}
/* ── Visibility groups toggle ────────────────────────────────── */
.ticket-visibility-groups.is-hidden,
.is-hidden { display: none !important; }
.ticket-visibility-groups.is-hidden { display: none !important; }
/* ── Page header utility ─────────────────────────────────────── */
.lt-page-header {
+5 -3
View File
@@ -181,7 +181,7 @@ function getCurrentFilterCriteria() {
const statusSelect = document.getElementById('adv-status');
const selectedStatuses = Array.from(statusSelect.selectedOptions).map(opt => opt.value);
if (selectedStatuses.length > 0) criteria.status = selectedStatuses.join(',');
if (selectedStatuses.length > 0) criteria.status = selectedStatuses; // keep as array — pill handler uses .join(',')
const priorityMin = document.getElementById('adv-priority-min').value;
if (priorityMin) criteria.priority_min = priorityMin;
@@ -256,9 +256,11 @@ function applySavedFilterCriteria(criteria) {
document.getElementById('adv-updated-from').value = criteria.updated_from || '';
document.getElementById('adv-updated-to').value = criteria.updated_to || '';
// Status
// Status — criteria.status may be an array (new saves) or a comma-joined string (old saves)
const statusSelect = document.getElementById('adv-status');
const statuses = criteria.status ? criteria.status.split(',') : [];
const statuses = criteria.status
? (Array.isArray(criteria.status) ? criteria.status : criteria.status.split(','))
: [];
Array.from(statusSelect.options).forEach(option => {
option.selected = statuses.includes(option.value);
});
+33 -8
View File
@@ -198,9 +198,16 @@ document.addEventListener('DOMContentLoaded', function() {
case 'open-settings-modal':
if (typeof openSettingsModal === 'function') openSettingsModal();
break;
// Refresh
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':
window.location.reload();
if (window.lt && lt.autoRefresh) lt.autoRefresh.now();
else window.location.reload();
break;
// Export
case 'toggle-export-menu':
@@ -840,7 +847,7 @@ function showBulkDeleteModal() {
<span class="lt-modal-title" id="bulkDeleteModalTitle">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body" style="text-align:center">
<div class="lt-modal-body lt-text-center">
<p class="lt-text-danger lt-text-sm">This action cannot be undone!</p>
<p class="lt-text-cyan">You are about to permanently delete ${ticketIds.length} ticket(s).<br>All associated comments and history will be lost.</p>
</div>
@@ -1332,21 +1339,25 @@ function showTicketPreview(event) {
<div class="preview-footer">Created by ${lt.escHtml(createdBy)}</div>
`;
// Position the preview
// Position the preview — element is position:fixed so coords are
// viewport-relative; getBoundingClientRect() already returns viewport coords,
// do NOT add scrollX/scrollY
const rect = link.getBoundingClientRect();
const previewWidth = 320;
const previewHeight = 200;
let left = rect.left + window.scrollX;
let top = rect.bottom + window.scrollY + 5;
let left = rect.left;
let top = rect.bottom + 5;
// Adjust if going off-screen
if (left + previewWidth > window.innerWidth) {
left = window.innerWidth - previewWidth - 20;
}
if (top + previewHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - previewHeight - 5;
if (top + previewHeight > window.innerHeight) {
top = rect.top - previewHeight - 5;
}
if (left < 0) left = 4;
if (top < 0) top = 4;
currentPreview.style.left = left + 'px';
currentPreview.style.top = top + 'px';
@@ -1374,6 +1385,20 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Hide preview when a modal opens, user scrolls, or page is about to navigate
document.addEventListener('click', function(e) {
if (e.target.closest('[data-modal-open], [data-action="open-advanced-search"], .lt-pagination a, .lt-pagination button')) {
hideTicketPreview();
if (currentPreview) currentPreview.classList.add('is-hidden');
}
}, true);
document.addEventListener('scroll', function() {
if (currentPreview && !currentPreview.classList.contains('is-hidden')) {
currentPreview.classList.add('is-hidden');
if (previewTimeout) { clearTimeout(previewTimeout); previewTimeout = null; }
}
}, { passive: true });
/**
* Toggle export dropdown menu
*/
+1 -1
View File
@@ -104,7 +104,7 @@ document.addEventListener('DOMContentLoaded', function() {
const option = Array.from(statusSelect.options).find(opt => opt.value === targetStatus);
if (option && !option.disabled) {
statusSelect.value = targetStatus;
statusSelect.dispatchEvent(new Event('change'));
statusSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
+1 -1
View File
@@ -1181,7 +1181,7 @@ function highlightMentions(text) {
document.addEventListener('DOMContentLoaded', function() {
initMentionAutocomplete();
// Highlight existing mentions in comments
// Highlight @mentions in plain-text comments (markdown.js handles [data-markdown] elements)
document.querySelectorAll('.comment-text').forEach(el => {
if (!el.hasAttribute('data-markdown')) {
el.innerHTML = highlightMentions(el.innerHTML);
+3 -3
View File
@@ -26,7 +26,7 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
const color = colors[type] || colors.warning;
const icon = icons[type] || icons.warning;
const safeTitle = lt.escHtml(title);
const safeMessage = lt.escHtml(message);
const safeMessage = lt.escHtml(message).replace(/\n/g, '<br>');
document.body.insertAdjacentHTML('beforeend', `
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="${modalId}_title">
@@ -35,8 +35,8 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
<span class="lt-modal-title" id="${modalId}_title">${icon} ${safeTitle}</span>
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body text-center">
<p class="modal-message">${safeMessage}</p>
<div class="lt-modal-body lt-text-center">
<p>${safeMessage}</p>
</div>
<div class="lt-modal-footer">
<button class="lt-btn lt-btn-primary" id="${modalId}_confirm">CONFIRM</button>
+1 -1
View File
@@ -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");
+14 -19
View File
@@ -205,7 +205,7 @@ include __DIR__ . '/layout_header.php';
</div>
</div>
</div>
<script>
<script nonce="<?= $nonce ?>">
// ── Dashboard charts (Chart.js, loaded from CDN) ─────────────────
(function() {
function waitForChart(cb, tries) {
@@ -431,7 +431,7 @@ include __DIR__ . '/layout_header.php';
</fieldset>
<?php endif ?>
<div class="lt-btn-group" style="flex-direction:column">
<div class="lt-btn-group lt-flex-col">
<button type="button" id="apply-filters-btn" class="lt-btn lt-btn-primary lt-btn-sm">APPLY</button>
<button type="button" id="clear-filters-btn" class="lt-btn lt-btn-ghost lt-btn-sm">CLEAR ALL</button>
</div>
@@ -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');
?>
<th scope="col" class="<?= $sortClass ?>"
@@ -1086,7 +1086,7 @@ if (window.lt) {
var c = JSON.parse(btn.dataset.criteria);
var params = new URLSearchParams();
if (c.search) params.set('search', c.search);
if (c.status && c.status.length) params.set('status', c.status.join(','));
if (c.status && c.status.length) params.set('status', Array.isArray(c.status) ? c.status.join(',') : c.status);
if (c.priority_min) params.set('priority_min', c.priority_min);
if (c.priority_max) params.set('priority_max', c.priority_max);
if (c.assigned_to) params.set('assigned_to', c.assigned_to);
@@ -1124,27 +1124,19 @@ document.querySelectorAll('.lt-stat-card').forEach(function (card) {
});
});
// Event delegation for click actions — only handles cases NOT covered by dashboard.js
// bulk-*, navigate, view-ticket, quick-*, set-view-mode, clear-selection, toggle-*
// are all handled by dashboard.js to avoid double-firing (duplicate handlers = duplicate users in selects).
// Event delegation handles ONLY cases NOT covered by dashboard.js
// (bulk-*, navigate, view-ticket, quick-*, set-view-mode, clear-selection, toggle-select-all,
// toggle-row-checkbox, remove-filter, clear-all-filters, open/close/save-settings,
// open/toggle-export-menu, export-tickets, open-advanced-search are in dashboard.js)
document.addEventListener('click', function (e) {
var target = e.target.closest('[data-action]');
if (!target) return;
switch (target.getAttribute('data-action')) {
case 'open-settings': openSettingsModal(); break;
case 'close-settings': closeSettingsModal(); break;
case 'save-settings': saveSettings(); break;
case 'manual-refresh': if (lt.autoRefresh) lt.autoRefresh.now(); break;
case 'toggle-sidebar': if (typeof toggleSidebar==='function') toggleSidebar(); break;
case 'open-advanced-search': openAdvancedSearch(); break;
case 'close-advanced-search': closeAdvancedSearch(); break;
case 'reset-advanced-search': resetAdvancedSearch(); break;
case 'toggle-export-menu': e.stopPropagation(); toggleExportMenu(e); break;
case 'export-tickets': e.preventDefault(); exportSelectedTickets(target.getAttribute('data-format')); break;
case 'save-filter': saveCurrentFilter(); break;
case 'delete-filter': deleteSavedFilter(); break;
case 'remove-filter': removeFilter(target.getAttribute('data-filter-type'), target.getAttribute('data-filter-value')); break;
case 'clear-all-filters': window.location.href = '/'; break;
}
});
@@ -1158,9 +1150,12 @@ document.addEventListener('change', function (e) {
}
});
// Advanced search form submit
// Advanced search form submit — use wrapper so performAdvancedSearch is resolved at event time
// (advanced-search.js loads later via pageScripts in layout_footer.php)
var advForm = document.getElementById('advancedSearchForm');
if (advForm) advForm.addEventListener('submit', performAdvancedSearch);
if (advForm) advForm.addEventListener('submit', function(e) {
if (typeof performAdvancedSearch === 'function') performAdvancedSearch(e);
});
// ── Flatpickr date pickers on advanced search date fields ────────
(function initFlatpickr() {
@@ -1206,7 +1201,7 @@ if (advForm) advForm.addEventListener('submit', performAdvancedSearch);
</aside>
<div class="lt-drawer-right-overlay" id="ticketPreviewDrawerOverlay"></div>
<script>
<script nonce="<?= $nonce ?>">
// ── Ticket Preview Drawer ──────────────────────────────────────────
(function() {
var drawer = document.getElementById('ticketPreviewDrawer');
+4 -7
View File
@@ -1104,17 +1104,14 @@ document.addEventListener('DOMContentLoaded', function () {
}
});
// Click delegation for comment actions
// Click delegation — only handles actions NOT covered by ticket.js
// (edit-comment, delete-comment, remove-dependency, delete-attachment, select-mention,
// save/cancel-edit-comment, reply-comment, close-reply, submit-reply are in ticket.js)
document.addEventListener('click', function (e) {
var target = e.target.closest('[data-action]');
if (!target) return;
var action = target.getAttribute('data-action');
var commentId = target.getAttribute('data-comment-id');
if (action === 'edit-comment' && commentId) {
if (typeof editComment === 'function') editComment(parseInt(commentId, 10));
} else if (action === 'delete-comment' && commentId) {
if (typeof deleteComment === 'function') deleteComment(parseInt(commentId, 10));
} else if (action === 'dismiss-priority-banner') {
if (action === 'dismiss-priority-banner') {
var banner = target.closest('[data-alert-id]');
if (banner) {
try { sessionStorage.setItem('lt_dismissed_' + banner.dataset.alertId, '1'); } catch(ex) {}