/** * Ticket List View * Full-featured ticket list with filters, sorting, batch actions, and pagination. */ const TicketListView = { currentPage: 1, perPage: 50, sortBy: 'create_time', sortDir: 'DESC', selectedIds: new Set(), selectedOrder: [], searchTimeout: null, async render() { const container = document.getElementById('view-container'); container.innerHTML = '

Caricamento ticket...

'; try { // Fetch lookups for filter dropdowns await App.ensureLookups(); // Build query params const isMyTickets = window.location.hash.startsWith('#/tickets/my'); Filters.currentMode = isMyTickets ? 'my' : 'general'; Filters.load(); // Load state for current mode if (isMyTickets && !Filters.state.user_id) { const activeAgentId = localStorage.getItem('activeAgentId') || '1'; Filters.state.user_id = activeAgentId; Filters.save(); } const params = Filters.toQueryParams(); params.set('page', this.currentPage); params.set('per_page', this.perPage); params.set('sort_by', this.sortBy); params.set('sort_dir', this.sortDir); const searchInput = document.getElementById('global-search'); if (searchInput && searchInput.value.trim()) { params.set('search', searchInput.value.trim()); } const data = await App.api(`/api/tickets?${params.toString()}`); this.renderContent(container, data); this.bindEvents(data); } catch (err) { container.innerHTML = `
⚠️
Errore caricamento ticket
${App.escapeHtml(err.message)}
`; } }, renderContent(container, data) { const tickets = data.tickets || []; const { total, page, per_page, total_pages } = data; container.innerHTML = `
0 selezionati
Stato
Coda
Owner
Cliente
${Filters.renderBar(App.lookups)} ${this.renderPagination(page, per_page, total, total_pages, true)}
${tickets.length > 0 ? tickets.map(t => { const displayQueue = t.queue_name.includes('::') ? t.queue_name.split('::').pop() : t.queue_name; const shortQueue = displayQueue.length > 15 ? displayQueue.substring(0, 12) + '...' : displayQueue; return ` `; }).join('') : ` `}
Creato Stato Titolo Coda Owner Cliente Priorità
${t.tn} ${data.otrsBaseUrl ? ` O ` : ''} ${App.formatDateTime(t.create_time)} ${t.state_name} ${App.escapeHtml(t.title || '(senza titolo)')} ${App.escapeHtml(shortQueue)}
${App.escapeHtml(t.queue_name)}
${t.owner_first || ''} ${t.owner_last || ''} ${t.customer_first ? `${t.customer_first} ${t.customer_last}` : (t.customer_user_id || '—')} ${App.priorityIndex(t.priority_name)}
📭
Nessun ticket trovato
Prova a cambiare i filtri o crea un nuovo ticket.
${this.renderPagination(page, per_page, total, total_pages, false)} `; }, renderPagination(page, per_page, total, total_pages, isTop) { const marginStyle = isTop ? 'margin-bottom: var(--space-md); margin-top: 0;' : 'margin-top: var(--space-md); margin-bottom: 0;'; if (total_pages > 1) { return ` `; } else { return ` `; } }, renderPageButtons(current, total) { const pages = []; const start = Math.max(1, current - 2); const end = Math.min(total, current + 2); for (let i = start; i <= end; i++) { pages.push(``); } return pages.join(''); }, bindEvents(data) { // Filter events Filters.bindEvents(() => { this.currentPage = 1; this.selectedIds.clear(); this.selectedOrder = []; this.render(); }); // Sort events document.querySelectorAll('.ticket-table th.sortable').forEach(th => { th.addEventListener('click', () => { const sortKey = th.dataset.sort; if (this.sortBy === sortKey) { this.sortDir = this.sortDir === 'ASC' ? 'DESC' : 'ASC'; } else { this.sortBy = sortKey; this.sortDir = 'DESC'; } this.currentPage = 1; this.render(); }); }); // Row click → Toggle selection document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(row => { row.addEventListener('click', (e) => { // If click was on a link, let the browser perform navigation if (e.target.closest('.ticket-tn-link') || e.target.closest('.ticket-title-link')) { return; } const id = String(row.dataset.ticketId); if (this.selectedIds.has(id)) { this.selectedIds.delete(id); this.selectedOrder = this.selectedOrder.filter(x => x !== id); } else { this.selectedIds.add(id); this.selectedOrder.push(id); } // Update classes on all rows document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(tr => { const trId = String(tr.dataset.ticketId); if (this.selectedIds.has(trId)) { tr.classList.add('selected'); } else { tr.classList.remove('selected'); } if (this.selectedOrder[0] === trId) { tr.classList.add('first-selected'); } else { tr.classList.remove('first-selected'); } }); this.updateBatchBar(); }); }); // Batch apply const batchApply = document.getElementById('batch-apply'); if (batchApply) { batchApply.addEventListener('click', () => this.applyBatch()); } // Batch merge (Issue #7) const batchMerge = document.getElementById('batch-merge'); if (batchMerge) { batchMerge.addEventListener('click', () => this.mergeBatch()); } // Batch select all visible const batchSelectAll = document.getElementById('batch-select-all'); if (batchSelectAll) { batchSelectAll.addEventListener('click', () => { document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(row => { const id = String(row.dataset.ticketId); if (!this.selectedIds.has(id)) { this.selectedIds.add(id); this.selectedOrder.push(id); } }); // Update classes document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(tr => { const trId = String(tr.dataset.ticketId); tr.classList.add('selected'); if (this.selectedOrder[0] === trId) { tr.classList.add('first-selected'); } else { tr.classList.remove('first-selected'); } }); this.updateBatchBar(); }); } // Batch cancel const batchCancel = document.getElementById('batch-cancel'); if (batchCancel) { batchCancel.addEventListener('click', () => { this.selectedIds.clear(); this.selectedOrder = []; document.querySelectorAll('.ticket-table tbody tr').forEach(tr => { tr.classList.remove('selected'); tr.classList.remove('first-selected'); }); const batchCustomerSearch = document.getElementById('batch-customer-search'); const batchCustomerUserId = document.getElementById('batch-customer-user-id'); const batchCustomerId = document.getElementById('batch-customer-id'); if (batchCustomerSearch) batchCustomerSearch.value = ''; if (batchCustomerUserId) batchCustomerUserId.value = ''; if (batchCustomerId) batchCustomerId.value = ''; const batchState = document.getElementById('batch-state'); if (batchState) batchState.value = ''; const batchQueue = document.getElementById('batch-queue'); if (batchQueue) batchQueue.value = ''; const batchOwner = document.getElementById('batch-owner'); if (batchOwner) batchOwner.value = ''; this.updateBatchBar(); }); } // Batch Customer User Autocomplete const batchCustomerSearchInput = document.getElementById('batch-customer-search'); const batchCustomerSuggestionsDiv = document.getElementById('batch-customer-suggestions'); const batchCustomerUserIdInput = document.getElementById('batch-customer-user-id'); const batchCustomerIdInput = document.getElementById('batch-customer-id'); let batchCustomerDebounce; if (batchCustomerSearchInput) { batchCustomerSearchInput.addEventListener('input', () => { clearTimeout(batchCustomerDebounce); const q = batchCustomerSearchInput.value.trim(); // Do not block empty query to allow all results on focus batchCustomerDebounce = setTimeout(async () => { try { const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); if (users.length === 0) { batchCustomerSuggestionsDiv.innerHTML = '
Nessun utente trovato
'; batchCustomerSuggestionsDiv.style.display = 'block'; return; } batchCustomerSuggestionsDiv.innerHTML = users.map(u => `
${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)} (Login: ${App.escapeHtml(u.login)} | Azienda: ${App.escapeHtml(u.customer_id || '—')})
`).join(''); batchCustomerSuggestionsDiv.style.display = 'block'; // Bind click batchCustomerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { if (item.dataset.login) { item.addEventListener('click', () => { batchCustomerSearchInput.value = item.dataset.name; if (batchCustomerUserIdInput) batchCustomerUserIdInput.value = item.dataset.login; if (batchCustomerIdInput) batchCustomerIdInput.value = item.dataset.customerId || ''; batchCustomerSuggestionsDiv.style.display = 'none'; }); } }); } catch (err) { console.error(err); } }, 300); }); batchCustomerSearchInput.addEventListener('focus', () => { batchCustomerSearchInput.value = ''; if (batchCustomerUserIdInput) batchCustomerUserIdInput.value = ''; if (batchCustomerIdInput) batchCustomerIdInput.value = ''; batchCustomerSearchInput.dispatchEvent(new Event('input')); }); } // Close suggestions on click outside document.addEventListener('click', (e) => { if (batchCustomerSearchInput && e.target !== batchCustomerSearchInput && e.target !== batchCustomerSuggestionsDiv) { batchCustomerSuggestionsDiv.style.display = 'none'; } }); // Pagination document.querySelectorAll('.pagination-btn[data-page]').forEach(btn => { btn.addEventListener('click', () => { this.currentPage = parseInt(btn.dataset.page); this.selectedIds.clear(); this.selectedOrder = []; this.render(); }); }); }, updateBatchBar() { const count = document.getElementById('batch-count'); if (count) { count.textContent = `${this.selectedIds.size} selezionat${this.selectedIds.size === 1 ? 'o' : 'i'}`; } // Enable/disable merge button const mergeBtn = document.getElementById('batch-merge'); if (mergeBtn) { if (this.selectedIds.size >= 2) { mergeBtn.disabled = false; } else { mergeBtn.disabled = true; } } }, async applyBatch() { if (this.selectedIds.size === 0) return; const updates = {}; const batchState = document.getElementById('batch-state')?.value; const batchQueue = document.getElementById('batch-queue')?.value; const batchOwner = document.getElementById('batch-owner')?.value; const batchCustomerSearch = document.getElementById('batch-customer-search')?.value.trim(); let batchCustomerUserId = document.getElementById('batch-customer-user-id')?.value; let batchCustomerId = document.getElementById('batch-customer-id')?.value; if (!batchCustomerUserId && batchCustomerSearch) { batchCustomerUserId = batchCustomerSearch; if (!batchCustomerId) { batchCustomerId = batchCustomerSearch; } } if (batchState) updates.ticket_state_id = parseInt(batchState); if (batchQueue) updates.queue_id = parseInt(batchQueue); if (batchOwner) updates.user_id = parseInt(batchOwner); if (batchCustomerUserId) updates.customer_user_id = batchCustomerUserId; if (batchCustomerId) updates.customer_id = batchCustomerId; if (Object.keys(updates).length === 0) { Toast.warning('Seleziona almeno un campo da modificare'); return; } try { const res = await App.api('/api/tickets/batch/update', { method: 'PATCH', body: JSON.stringify({ ticket_ids: Array.from(this.selectedIds), updates, }), }); Toast.success(res.message || `${this.selectedIds.size} ticket aggiornati`); this.selectedIds.clear(); this.selectedOrder = []; this.render(); } catch (err) { Toast.error('Errore aggiornamento batch: ' + err.message); } }, async mergeBatch() { if (this.selectedOrder.length < 2) return; const targetId = this.selectedOrder[0]; const sourceIds = this.selectedOrder.slice(1); // Get target ticket number const targetRow = document.querySelector(`.ticket-table tbody tr[data-ticket-id="${targetId}"]`); const targetTn = targetRow ? targetRow.querySelector('.ticket-tn-link').textContent.trim() : targetId; // Collect source ticket numbers const sourceTns = []; sourceIds.forEach(id => { const row = document.querySelector(`.ticket-table tbody tr[data-ticket-id="${id}"]`); if (row) { sourceTns.push(row.querySelector('.ticket-tn-link').textContent.trim()); } else { sourceTns.push(`#${id}`); } }); const confirmed = confirm(`Sei sicuro di voler unire i ticket ${sourceTns.join(', ')} nel ticket principale #${targetTn}? Questa azione sposterà tutti gli articoli e tempi consultivati.`); if (!confirmed) return; try { const mergeBtn = document.getElementById('batch-merge'); if (mergeBtn) { mergeBtn.disabled = true; mergeBtn.textContent = 'Unione in corso...'; } const res = await App.api('/api/tickets/merge', { method: 'POST', body: JSON.stringify({ targetId: parseInt(targetId), sourceIds: sourceIds.map(x => parseInt(x)) }) }); Toast.success(res.message || 'Ticket uniti con successo'); this.selectedIds.clear(); this.selectedOrder = []; this.render(); } catch (err) { Toast.error('Errore durante l\'unione: ' + err.message); this.updateBatchBar(); } }, };