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 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 17:25:58 -04:00
parent 3c29c6ee6f
commit c15defc09b
2 changed files with 125 additions and 0 deletions
+65
View File
@@ -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 = '<ul class="duplicate-list lt-text-sm">';
dupes.forEach(dup => {
html += `<li class="lt-flex lt-flex-gap-sm lt-flex-align-center" style="padding:0.3rem 0">
<a href="/ticket/${lt.escHtml(String(dup.ticket_id))}" class="lt-text-cyan lt-text-xs" target="_blank">#${lt.escHtml(String(dup.ticket_id))}</a>
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${lt.escHtml(dup.title)}</span>
<span class="lt-text-muted lt-text-xs">${lt.escHtml(String(dup.similarity))}% · ${lt.escHtml(dup.status)}</span>
<button type="button" class="lt-btn lt-btn-ghost lt-btn-xs mark-dup-btn"
data-dup-id="${lt.escHtml(String(dup.ticket_id))}"
title="Link this ticket as a duplicate of #${lt.escHtml(String(dup.ticket_id))}">
Mark duplicate
</button>
</li>`;
});
html += '</ul>';
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');