From c15defc09bc5484236e43aa2c8ec4599f8323d6b Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 4 Apr 2026 17:25:58 -0400 Subject: [PATCH] feat: duplicate detection + mark-as-duplicate, lt-toggle preferences in settings - Dependencies tab: auto-loads potential duplicates via /api/check_duplicates.php on first activation; shows 'Mark duplicate' button per result which POSTs to ticket_dependencies with type=duplicates and refreshes the dependencies list - Settings modal: replaced checkboxes with lt-toggle switches for notifications_enabled and sound_effects; loads current user prefs on modal open and saves via /api/user_preferences.php on SAVE button Co-Authored-By: Claude Sonnet 4.6 --- assets/js/ticket.js | 65 ++++++++++++++++++++++++++++++++++++++++++++ views/TicketView.php | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/assets/js/ticket.js b/assets/js/ticket.js index 6c660ef..2e4b282 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -507,6 +507,7 @@ function showTab(tabName) { initializeUploadZone(); } else if (tabName === 'dependencies') { loadDependencies(); + loadPotentialDuplicates(); } } @@ -531,6 +532,70 @@ function loadDependencies() { }); } +// Load potential duplicates from check_duplicates API and show "Mark as duplicate" buttons +let _dupsLoaded = false; +function loadPotentialDuplicates() { + if (_dupsLoaded) return; + _dupsLoaded = true; + + const frame = document.getElementById('potentialDupsFrame'); + const list = document.getElementById('potentialDupsList'); + if (!frame || !list) return; + + const title = window.ticketData?.title || document.querySelector('.title-input')?.textContent?.trim() || ''; + if (!title) return; + + lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title)) + .then(data => { + if (!data.success || !data.duplicates || !data.duplicates.length) return; + + // Filter out this ticket itself + const thisId = String(window.ticketData.id); + const dupes = data.duplicates.filter(d => String(d.ticket_id) !== thisId); + if (!dupes.length) return; + + let html = ''; + list.innerHTML = html; + frame.style.display = ''; + + list.querySelectorAll('.mark-dup-btn').forEach(btn => { + btn.addEventListener('click', () => { + const dupId = btn.dataset.dupId; + const ticketId = window.ticketData.id; + lt.api.post('/api/ticket_dependencies.php', { + ticket_id: ticketId, + depends_on_id: dupId, + dependency_type: 'duplicates' + }).then(res => { + if (res.success) { + btn.textContent = '✓ Linked'; + btn.disabled = true; + btn.classList.add('lt-btn-primary'); + lt.toast.success('Linked as duplicate of #' + dupId); + loadDependencies(); + } else { + lt.toast.error(res.error || 'Failed to link dependency'); + } + }).catch(() => lt.toast.error('Network error')); + }); + }); + }) + .catch(() => {}); // silent — duplicate check is advisory only +} + function showDependencyError(message) { const dependenciesList = document.getElementById('dependenciesList'); const dependentsList = document.getElementById('dependentsList'); diff --git a/views/TicketView.php b/views/TicketView.php index 30b3e01..910b098 100644 --- a/views/TicketView.php +++ b/views/TicketView.php @@ -702,6 +702,13 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr + + +
Current Dependencies
@@ -833,6 +840,31 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
+
+

Preferences

+
+
+ Notifications + + + +
+
+ Sound effects + + + +
+
+

Keyboard Shortcuts

@@ -1028,9 +1060,37 @@ document.addEventListener('DOMContentLoaded', function () { } // Settings save/cancel + // Load user preference toggles on settings modal open + (function() { + fetch('/api/user_preferences.php', { credentials: 'same-origin' }) + .then(function(r) { return r.json(); }) + .then(function(d) { + if (!d.success || !d.preferences) return; + var notifEl = document.getElementById('settingNotificationsEnabled'); + var soundEl = document.getElementById('settingSoundEffects'); + if (notifEl && d.preferences.notifications_enabled !== undefined) + notifEl.checked = d.preferences.notifications_enabled == '1' || d.preferences.notifications_enabled === true; + if (soundEl && d.preferences.sound_effects !== undefined) + soundEl.checked = d.preferences.sound_effects == '1' || d.preferences.sound_effects === true; + }).catch(function() {}); + })(); + var saveSettingsBtn = document.getElementById('saveSettingsBtn'); if (saveSettingsBtn) { saveSettingsBtn.addEventListener('click', function () { + // Save lt-toggle preferences + var notifEl = document.getElementById('settingNotificationsEnabled'); + var soundEl = document.getElementById('settingSoundEffects'); + var prefsToSave = {}; + if (notifEl) prefsToSave.notifications_enabled = notifEl.checked ? '1' : '0'; + if (soundEl) prefsToSave.sound_effects = soundEl.checked ? '1' : '0'; + if (Object.keys(prefsToSave).length) { + fetch('/api/user_preferences.php', { + method: 'POST', credentials: 'same-origin', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN || '' }, + body: JSON.stringify({ preferences: prefsToSave }) + }).catch(function() {}); + } if (typeof saveSettings === 'function') saveSettings(); }); }