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 = '
';
+ dupes.forEach(dup => {
+ html += `-
+ #${lt.escHtml(String(dup.ticket_id))}
+ ${lt.escHtml(dup.title)}
+ ${lt.escHtml(String(dup.similarity))}% · ${lt.escHtml(dup.status)}
+
+
`;
+ });
+ 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
+
+
+
╚╝
@@ -833,6 +840,31 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
+
@@ -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();
});
}