Fix duplicate users in bulk/quick assign modals; add combobox search

Root cause: DashboardView.php and dashboard.js both had a global
document.addEventListener('click') handler handling the same bulk-assign
and quick-assign actions. Every click fired both handlers, creating two
modals and two API fetches that both appended to the same select element.

Fix: Remove duplicate cases (bulk-*, navigate, view-ticket, quick-*,
set-view-mode, toggle-*, clear-selection) from DashboardView.php's inline
handler. dashboard.js already handles all of these correctly.

Also replace <select> with lt.combobox in both bulk-assign and
quick-assign modals so large user lists are searchable instead of a
long scrolling dropdown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 20:13:10 -04:00
parent fdc6d3d463
commit 55c6fc81db
2 changed files with 57 additions and 43 deletions
+54 -31
View File
@@ -545,6 +545,8 @@ function performBulkCloseAction(ticketIds) {
});
}
var _bulkAssignUserId = null; // set by combobox onSelect
function showBulkAssignModal() {
const ticketIds = getSelectedTicketIds();
@@ -553,7 +555,8 @@ function showBulkAssignModal() {
return;
}
// Create modal HTML
_bulkAssignUserId = null;
const modalHtml = `
<div class="lt-modal-overlay" id="bulkAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkAssignModalTitle">
<div class="lt-modal">
@@ -562,10 +565,15 @@ function showBulkAssignModal() {
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body">
<label for="bulkAssignUser">Assign to:</label>
<select id="bulkAssignUser" class="lt-select">
<option value="">Select User...</option>
</select>
<label class="lt-label">Assign to:</label>
<div class="lt-combobox" id="bulkAssignCombobox">
<div class="lt-combobox-input-wrap">
<input type="text" class="lt-combobox-input" id="bulkAssignUserInput"
placeholder="Search users…" autocomplete="off" aria-label="Search users">
</div>
<ul class="lt-combobox-list" role="listbox" aria-hidden="true"></ul>
</div>
<span class="lt-field-hint lt-text-muted">Type to search — supports large user lists</span>
</div>
<div class="lt-modal-footer">
<button data-action="perform-bulk-assign" class="lt-btn lt-btn-primary">ASSIGN</button>
@@ -578,19 +586,18 @@ function showBulkAssignModal() {
document.body.insertAdjacentHTML('beforeend', modalHtml);
lt.modal.open('bulkAssignModal');
// Fetch users for the dropdown
lt.api.get('/api/get_users.php')
.then(data => {
if (data.success && data.users) {
const select = document.getElementById('bulkAssignUser');
if (select) {
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
});
}
const input = document.getElementById('bulkAssignUserInput');
if (!input) return;
const items = data.users.map(u => ({
value: String(u.user_id),
label: u.display_name || u.username
}));
lt.combobox.init(input, items, {
onSelect: function(item) { _bulkAssignUserId = item.value; }
});
}
})
.catch(() => lt.toast.error('Error loading users'));
@@ -603,11 +610,11 @@ function closeBulkAssignModal() {
}
function performBulkAssign() {
const userId = document.getElementById('bulkAssignUser').value;
const userId = _bulkAssignUserId;
const ticketIds = getSelectedTicketIds();
if (!userId) {
lt.toast.warning('Please select a user', 2000);
lt.toast.warning('Please select a user from the list', 2000);
return;
}
@@ -997,10 +1004,14 @@ function performQuickStatusChange(ticketId) {
});
}
var _quickAssignUserId = undefined; // undefined = no change; null = unassign; string = user_id
/**
* Quick assign from dashboard
*/
function quickAssign(ticketId) {
_quickAssignUserId = undefined;
const modalHtml = `
<div class="lt-modal-overlay" id="quickAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="quickAssignModalTitle">
<div class="lt-modal lt-modal-xs">
@@ -1009,14 +1020,18 @@ function quickAssign(ticketId) {
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body">
<p class="lt-mb-xs">Ticket #${lt.escHtml(ticketId)}</p>
<label for="quickAssignSelect">Assign to:</label>
<select id="quickAssignSelect" class="lt-select">
<option value="">Unassigned</option>
</select>
<p class="lt-mb-xs lt-text-muted lt-text-xs">Ticket #${lt.escHtml(String(ticketId))}</p>
<label class="lt-label">Assign to:</label>
<div class="lt-combobox" id="quickAssignCombobox">
<div class="lt-combobox-input-wrap">
<input type="text" class="lt-combobox-input" id="quickAssignInput"
placeholder="Search users…" autocomplete="off" aria-label="Search users">
</div>
<ul class="lt-combobox-list" role="listbox" aria-hidden="true"></ul>
</div>
</div>
<div class="lt-modal-footer">
<button data-action="perform-quick-assign" data-ticket-id="${ticketId}" class="lt-btn lt-btn-primary">ASSIGN</button>
<button data-action="perform-quick-assign" data-ticket-id="${lt.escHtml(String(ticketId))}" class="lt-btn lt-btn-primary">ASSIGN</button>
<button data-action="close-quick-assign-modal" class="lt-btn lt-btn-ghost">CANCEL</button>
</div>
</div>
@@ -1026,16 +1041,20 @@ function quickAssign(ticketId) {
document.body.insertAdjacentHTML('beforeend', modalHtml);
lt.modal.open('quickAssignModal');
// Load users
lt.api.get('/api/get_users.php')
.then(data => {
if (data.success && data.users) {
const select = document.getElementById('quickAssignSelect');
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
const input = document.getElementById('quickAssignInput');
if (!input) return;
const items = [
{ value: '', label: 'Unassigned' },
...data.users.map(u => ({
value: String(u.user_id),
label: u.display_name || u.username
}))
];
lt.combobox.init(input, items, {
onSelect: function(item) { _quickAssignUserId = item.value || null; }
});
}
})
@@ -1049,7 +1068,11 @@ function closeQuickAssignModal() {
}
function performQuickAssign(ticketId) {
const assignedTo = document.getElementById('quickAssignSelect').value || null;
if (_quickAssignUserId === undefined) {
lt.toast.warning('Please select a user from the list', 2000);
return;
}
const assignedTo = _quickAssignUserId;
lt.api.post('/api/assign_ticket.php', { ticket_id: ticketId, assigned_to: assignedTo })
.then(data => {
+3 -12
View File
@@ -857,7 +857,9 @@ document.querySelectorAll('.lt-stat-card').forEach(function (card) {
});
});
// Event delegation for click actions
// 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).
document.addEventListener('click', function (e) {
var target = e.target.closest('[data-action]');
if (!target) return;
@@ -870,19 +872,8 @@ document.addEventListener('click', function (e) {
case 'open-advanced-search': openAdvancedSearch(); break;
case 'close-advanced-search': closeAdvancedSearch(); break;
case 'reset-advanced-search': resetAdvancedSearch(); break;
case 'set-view-mode': setViewMode(target.getAttribute('data-mode')); break;
case 'navigate': window.location.href = target.getAttribute('data-url'); break;
case 'toggle-export-menu': e.stopPropagation(); toggleExportMenu(e); break;
case 'export-tickets': e.preventDefault(); exportSelectedTickets(target.getAttribute('data-format')); break;
case 'bulk-status': showBulkStatusModal(); break;
case 'bulk-assign': showBulkAssignModal(); break;
case 'bulk-priority': showBulkPriorityModal(); break;
case 'clear-selection': clearSelection(); break;
case 'toggle-select-all': toggleSelectAll(); break;
case 'toggle-row-checkbox': toggleRowCheckbox(e, target); break;
case 'view-ticket': e.stopPropagation(); window.location.href = '/ticket/' + target.getAttribute('data-ticket-id'); break;
case 'quick-status': e.stopPropagation(); quickStatusChange(target.getAttribute('data-ticket-id'), target.getAttribute('data-status')); break;
case 'quick-assign': e.stopPropagation(); quickAssign(target.getAttribute('data-ticket-id')); 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;