/** * Ricerca Spudorata View * Full-featured text-first ticket search with local in-memory caching and client-side filtering. */ const RicercaSpudorataView = { searchTerm: '', cachedTickets: null, currentPage: 1, perPage: 50, sortBy: 'create_time', sortDir: 'DESC', selectedIds: new Set(), selectedOrder: [], hasSearched: false, isFetching: false, invalidateCache() { this.cachedTickets = null; this.hasSearched = false; }, async render() { await App.ensureLookups(); Filters.currentMode = 'spudorata'; Filters.load(); const container = document.getElementById('view-container'); if (!container) return; const urlParams = new URLSearchParams((window.location.hash.split('?')[1]) || ''); const qParam = urlParams.get('q'); if (qParam && qParam !== this.searchTerm) { this.renderContainer(container); this.bindEvents(); const input = document.getElementById('spudorata-search-input'); if (input) input.value = qParam; this.executeSearch(qParam); return; } // Instant render from memory cache if already searched if (this.hasSearched && this.cachedTickets) { this.renderContainer(container); this.bindEvents(); this.renderLocalResults(); return; } this.renderContainer(container); this.bindEvents(); setTimeout(() => { const input = document.getElementById('spudorata-search-input'); if (input) { input.focus(); input.select(); } }, 50); }, renderContainer(container) { container.innerHTML = `
πŸš€

Ricerca Testuale Spudorata

Esegui la ricerca nel testo sui ticket. I filtri applicati successivamente operano istantaneamente in cache locale senza fare nuove richieste al server.

${this.hasSearched ? ` ` : ''}
0 selezionati
Stato
Coda
Owner
Responsabile
${(App.lookups.types || []).length > 0 ? `
Tipo
` : ''}
${Filters.renderBar(App.lookups)}
${this.hasSearched ? '' : `
πŸ”
Nessuna ricerca effettuata
Scrivi una parola nel campo in alto e clicca su "Cerca" per caricare i risultati in cache locale.
`}
`; }, async executeSearch(query) { if (!query) { Toast.error('Inserisci un termine di ricerca'); return; } this.searchTerm = query; this.isFetching = true; const resultsArea = document.getElementById('spudorata-results-area'); if (resultsArea) { resultsArea.innerHTML = `

Ricerca testuale nel server in corso per "${App.escapeHtml(query)}"...

`; } try { const data = await App.api(`/api/tickets?search=${encodeURIComponent(query)}&per_page=2000`); this.cachedTickets = data.tickets || []; this.otrsBaseUrl = data.otrsBaseUrl || ''; this.hasSearched = true; this.currentPage = 1; this.selectedIds.clear(); this.selectedOrder = []; Toast.success(`${this.cachedTickets.length} ticket caricati in cache locale per "${query}"`); this.renderContainer(document.getElementById('view-container')); this.bindEvents(); this.renderLocalResults(); } catch (err) { console.error(err); Toast.error('Errore durante la ricerca testuale: ' + err.message); if (resultsArea) { resultsArea.innerHTML = `
⚠️
Errore durante la ricerca
${App.escapeHtml(err.message)}
`; } } finally { this.isFetching = false; } }, renderLocalResults() { const resultsArea = document.getElementById('spudorata-results-area'); if (!resultsArea || !this.hasSearched) return; if (!this.cachedTickets || this.cachedTickets.length === 0) { resultsArea.innerHTML = `
πŸ“­
Nessun risultato trovato
Nessun ticket corrisponde alla ricerca "${App.escapeHtml(this.searchTerm)}".
`; return; } const filtered = this.getFilteredTickets(); const total = filtered.length; const totalPages = Math.ceil(total / this.perPage) || 1; if (this.currentPage > totalPages) { this.currentPage = 1; } const cacheInfo = document.getElementById('spudorata-cache-info'); if (cacheInfo) { cacheInfo.innerHTML = ` ⚑ Cache locale: ${total} visibil${total === 1 ? 'o' : 'i'} su ${this.cachedTickets.length} caricati `; } const sorted = this.sortTickets(filtered); const startIndex = (this.currentPage - 1) * this.perPage; const paginatedTickets = sorted.slice(startIndex, startIndex + this.perPage); resultsArea.innerHTML = ` ${this.renderPagination(this.currentPage, this.perPage, total, totalPages, true)}
${paginatedTickets.length > 0 ? paginatedTickets.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('') : ` `}
NΒ° Creato Stato Titolo Coda Owner Cliente PrioritΓ 
πŸ“‹ ${this.otrsBaseUrl ? ` O ` : ''} ${t.tn} ${App.formatDateTime(t.create_time)} ${App.escapeHtml(t.state_name)} ${App.escapeHtml(t.title)} ${App.escapeHtml(shortQueue)} ${t.owner_first ? `${App.escapeHtml(t.owner_first)} ${App.escapeHtml(t.owner_last || '')}` : 'β€”'} ${t.customer_first ? `${App.escapeHtml(t.customer_first)} ${App.escapeHtml(t.customer_last || '')}` : (t.customer_id || 'β€”')} ${App.escapeHtml(t.priority_name)}
Nessun ticket corrisponde ai filtri selezionati sulla cache locale.
${this.renderPagination(this.currentPage, this.perPage, total, totalPages, false)} `; this.bindTableEvents(); this.updateBatchBar(); }, getFilteredTickets() { if (!this.cachedTickets) return []; let list = [...this.cachedTickets]; const s = Filters.state; if (s.sub_search) { const q = String(s.sub_search).toLowerCase().trim(); if (q) { list = list.filter(t => { const fields = [ t.tn, t.title, t.queue_name, t.state_name, t.priority_name, t.owner_first, t.owner_last, `${t.owner_first || ''} ${t.owner_last || ''}`, t.customer_first, t.customer_last, `${t.customer_first || ''} ${t.customer_last || ''}`, t.customer_id, t.customer_user_id ]; return fields.some(f => f && String(f).toLowerCase().includes(q)); }); } } if (s.state_id) { const selectedStates = String(s.state_id).split(',').filter(Boolean); list = list.filter(t => selectedStates.includes(String(t.ticket_state_id))); } if (s.queue_id) { const selectedQueues = String(s.queue_id).split(',').filter(Boolean); list = list.filter(t => selectedQueues.includes(String(t.queue_id))); } if (s.priority_id) { const selectedPriorities = String(s.priority_id).split(',').filter(Boolean); list = list.filter(t => selectedPriorities.includes(String(t.ticket_priority_id))); } if (s.user_id) { const selectedUsers = String(s.user_id).split(',').filter(Boolean); list = list.filter(t => selectedUsers.includes(String(t.user_id))); } if (s.responsible_user_id) { list = list.filter(t => String(t.responsible_user_id) === String(s.responsible_user_id)); } if (s.type_id) { list = list.filter(t => String(t.type_id) === String(s.type_id)); } if (s.customer_user_id) { list = list.filter(t => String(t.customer_user_id || '').toLowerCase() === String(s.customer_user_id).toLowerCase()); } if (s.customer_id) { list = list.filter(t => String(t.customer_id || '').toLowerCase() === String(s.customer_id).toLowerCase()); } if (s.date_from) { const fromDate = new Date(s.date_from); list = list.filter(t => new Date(t.create_time) >= fromDate); } if (s.date_to) { const toDate = new Date(s.date_to + 'T23:59:59'); list = list.filter(t => new Date(t.create_time) <= toDate); } return list; }, sortTickets(tickets) { const list = [...tickets]; const key = this.sortBy; const isAsc = this.sortDir === 'ASC'; list.sort((a, b) => { let valA, valB; if (key === 'queue') { valA = a.queue_name || ''; valB = b.queue_name || ''; } else if (key === 'state') { valA = a.state_name || ''; valB = b.state_name || ''; } else if (key === 'priority') { valA = a.ticket_priority_id || 0; valB = b.ticket_priority_id || 0; } else { valA = a[key] || ''; valB = b[key] || ''; } if (typeof valA === 'string') { return isAsc ? valA.localeCompare(valB, 'it', { sensitivity: 'base' }) : valB.localeCompare(valA, 'it', { sensitivity: 'base' }); } return isAsc ? valA - valB : valB - valA; }); return list; }, renderPagination(current, perPage, total, totalPages, isTop = false) { const limitSelectHtml = `
Righe:
`; return ` `; }, renderPageNumbers(current, totalPages) { const pages = []; const start = Math.max(1, current - 2); const end = Math.min(totalPages, current + 2); for (let i = start; i <= end; i++) { pages.push(``); } return pages.join(''); }, bindEvents() { const searchInput = document.getElementById('spudorata-search-input'); const searchBtn = document.getElementById('spudorata-search-btn'); const clearBtn = document.getElementById('spudorata-input-clear'); const resetCacheBtn = document.getElementById('spudorata-reset-cache-btn'); if (searchInput) { searchInput.addEventListener('input', () => { if (clearBtn) clearBtn.style.display = searchInput.value.trim() ? 'flex' : 'none'; }); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.executeSearch(searchInput.value.trim()); } }); } if (searchBtn) { searchBtn.addEventListener('click', () => { if (searchInput) this.executeSearch(searchInput.value.trim()); }); } if (clearBtn) { clearBtn.addEventListener('click', () => { if (searchInput) searchInput.value = ''; clearBtn.style.display = 'none'; searchInput.focus(); }); } if (resetCacheBtn) { resetCacheBtn.addEventListener('click', () => { this.invalidateCache(); if (searchInput) searchInput.value = ''; Filters.reset(); this.renderContainer(document.getElementById('view-container')); this.bindEvents(); }); } Filters.bindEvents(() => { this.currentPage = 1; this.renderLocalResults(); }); const batchApply = document.getElementById('batch-apply'); if (batchApply) batchApply.addEventListener('click', () => this.applyBatch()); const batchAddGroup = document.getElementById('batch-add-group'); if (batchAddGroup) batchAddGroup.addEventListener('click', () => this.addToGroup()); const batchMerge = document.getElementById('batch-merge'); if (batchMerge) batchMerge.addEventListener('click', () => this.mergeBatch()); const batchOpenTabs = document.getElementById('batch-open-tabs'); if (batchOpenTabs) batchOpenTabs.addEventListener('click', () => this.openBatchInTabs()); const batchCopyTns = document.getElementById('batch-copy-tns'); if (batchCopyTns) batchCopyTns.addEventListener('click', () => this.copySelectedTns()); const batchSelectAll = document.getElementById('batch-select-all'); if (batchSelectAll) batchSelectAll.addEventListener('click', () => this.selectAllVisible()); const batchCancel = document.getElementById('batch-cancel'); if (batchCancel) batchCancel.addEventListener('click', () => this.cancelBatchSelection()); const batchQueueSearchInput = document.getElementById('batch-queue-search'); const batchQueueSuggestionsDiv = document.getElementById('batch-queue-suggestions'); const batchQueueIdInput = document.getElementById('batch-queue'); if (batchQueueSearchInput) { batchQueueSearchInput.addEventListener('input', async () => { const q = batchQueueSearchInput.value.trim(); try { const queues = await App.api(`/api/queues/search?q=${encodeURIComponent(q)}`); if (queues.length === 0) { batchQueueSuggestionsDiv.innerHTML = '
Nessuna coda trovata
'; } else { batchQueueSuggestionsDiv.innerHTML = queues.map(queue => `
${App.escapeHtml(queue.name)}
`).join(''); } batchQueueSuggestionsDiv.style.display = 'block'; batchQueueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { item.addEventListener('click', () => { batchQueueSearchInput.value = item.dataset.name; if (batchQueueIdInput) batchQueueIdInput.value = item.dataset.id; batchQueueSuggestionsDiv.style.display = 'none'; }); }); } catch (err) { console.error(err); } }); App.setupAutocompleteKeyboardNavigation(batchQueueSearchInput, batchQueueSuggestionsDiv, (item) => { if (item && item.dataset.id) { batchQueueSearchInput.value = item.dataset.name; if (batchQueueIdInput) batchQueueIdInput.value = item.dataset.id; batchQueueSuggestionsDiv.style.display = 'none'; } }); batchQueueSearchInput.addEventListener('focus', () => { batchQueueSearchInput.value = ''; if (batchQueueIdInput) batchQueueIdInput.value = ''; batchQueueSearchInput.dispatchEvent(new Event('input')); }); } document.addEventListener('click', (e) => { if (batchQueueSearchInput && e.target !== batchQueueSearchInput && e.target !== batchQueueSuggestionsDiv) { if (batchQueueSuggestionsDiv) batchQueueSuggestionsDiv.style.display = 'none'; } }); }, bindTableEvents() { document.querySelectorAll('#spudorata-results-area 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.renderLocalResults(); }); }); document.querySelectorAll('.spudorata-per-page-select').forEach(sel => { sel.addEventListener('change', (e) => { const val = parseInt(e.target.value, 10); if (!isNaN(val) && val > 0) { this.perPage = val; this.currentPage = 1; this.renderLocalResults(); } }); }); document.querySelectorAll('.spudorata-page-btn').forEach(btn => { btn.addEventListener('click', () => { const page = parseInt(btn.dataset.page, 10); if (!isNaN(page) && page > 0) { this.currentPage = page; this.renderLocalResults(); } }); }); document.querySelectorAll('#spudorata-results-area tr[data-ticket-id]').forEach(row => { row.addEventListener('click', (e) => { if (e.target.closest('.ticket-tn-link') || e.target.closest('.ticket-title-link') || e.target.closest('.copy-ticket-btn') || e.target.closest('.open-tab-btn')) { 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); } document.querySelectorAll('#spudorata-results-area 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(); }); }); document.querySelectorAll('.copy-ticket-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const tn = btn.dataset.tn; if (tn) { navigator.clipboard.writeText(tn).then(() => { Toast.success(`Numero ticket ${tn} copiato!`); }).catch(err => Toast.error('Errore durante la copia: ' + err.message)); } }); }); }, updateBatchBar() { const countEl = document.getElementById('batch-count'); const addGroupBtn = document.getElementById('batch-add-group'); const mergeBtn = document.getElementById('batch-merge'); const openTabsBtn = document.getElementById('batch-open-tabs'); const count = this.selectedIds.size; if (countEl) countEl.textContent = `${count} selezionat${count === 1 ? 'o' : 'i'}`; if (addGroupBtn) addGroupBtn.disabled = count === 0; if (mergeBtn) mergeBtn.disabled = count < 2; if (openTabsBtn) openTabsBtn.disabled = count === 0; }, selectAllVisible() { document.querySelectorAll('#spudorata-results-area 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); } }); document.querySelectorAll('#spudorata-results-area 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(); }, cancelBatchSelection() { this.selectedIds.clear(); this.selectedOrder = []; document.querySelectorAll('#spudorata-results-area tr').forEach(tr => { tr.classList.remove('selected'); tr.classList.remove('first-selected'); }); this.updateBatchBar(); }, copySelectedTns() { const selectedTns = []; this.selectedIds.forEach(id => { const row = document.querySelector(`#spudorata-results-area tr[data-ticket-id="${id}"]`); if (row) { const tnLink = row.querySelector('.ticket-tn-link'); if (tnLink) selectedTns.push(tnLink.textContent.trim()); } }); if (selectedTns.length > 0) { navigator.clipboard.writeText(selectedTns.join('\n')).then(() => { Toast.success(`${selectedTns.length} numeri ticket copiati!`); }).catch(err => Toast.error('Errore durante la copia: ' + err.message)); } }, openBatchInTabs() { let count = 0; this.selectedOrder.forEach(id => { const row = document.querySelector(`#spudorata-results-area tr[data-ticket-id="${id}"]`); if (row) { const tn = row.querySelector('.ticket-tn-link')?.textContent.trim() || id; const title = row.querySelector('.ticket-title-link')?.textContent.trim() || ''; App.openTab(parseInt(id), tn, title); count++; } }); if (count > 0) { Toast.success(`${count} ticket aperti in nuove schede!`); this.cancelBatchSelection(); } }, async applyBatch() { if (this.selectedIds.size === 0) { Toast.error('Seleziona almeno un ticket per applicare le modifiche'); return; } const updates = {}; const stateVal = document.getElementById('batch-state')?.value; const queueVal = document.getElementById('batch-queue')?.value; const ownerVal = document.getElementById('batch-owner')?.value; const responsibleVal = document.getElementById('batch-responsible')?.value; const typeVal = document.getElementById('batch-type')?.value; if (stateVal) updates.ticket_state_id = parseInt(stateVal); if (queueVal) updates.queue_id = parseInt(queueVal); if (ownerVal) updates.user_id = parseInt(ownerVal); if (responsibleVal) updates.responsible_user_id = parseInt(responsibleVal); if (typeVal) updates.type_id = parseInt(typeVal); if (Object.keys(updates).length === 0) { Toast.error('Seleziona almeno un campo da modificare dalla barra'); return; } try { const ticket_ids = Array.from(this.selectedIds).map(id => parseInt(id, 10)); const res = await App.api('/api/tickets/batch/update', { method: 'PATCH', body: JSON.stringify({ ticket_ids, updates }) }); Toast.success(res.message || 'Ticket aggiornati con successo'); if (this.cachedTickets) { this.cachedTickets.forEach(t => { if (this.selectedIds.has(String(t.id))) { if (updates.ticket_state_id) { t.ticket_state_id = updates.ticket_state_id; const foundState = App.lookups.states.find(s => s.id === updates.ticket_state_id); if (foundState) t.state_name = foundState.name; } if (updates.queue_id) { t.queue_id = updates.queue_id; const foundQueue = App.lookups.queues.find(q => q.id === updates.queue_id); if (foundQueue) t.queue_name = foundQueue.name; } if (updates.user_id) { t.user_id = updates.user_id; const foundUser = App.lookups.users.find(u => u.id === updates.user_id); if (foundUser) { t.owner_first = foundUser.first_name; t.owner_last = foundUser.last_name; } } if (updates.responsible_user_id) { t.responsible_user_id = updates.responsible_user_id; } if (updates.type_id) { t.type_id = updates.type_id; } } }); } this.cancelBatchSelection(); this.renderLocalResults(); } catch (err) { Toast.error('Errore aggiornamento batch: ' + err.message); } }, async addToGroup() { if (this.selectedIds.size === 0) return; try { const groups = await App.api('/api/groups'); if (!groups || groups.length === 0) { Toast.warning('non sono presenti gruppi'); return; } const groupOptions = groups.map(g => ``).join(''); const dialogHtml = `

Aggiungi a Gruppo

Seleziona il gruppo a cui aggiungere i ${this.selectedIds.size} ticket selezionati:

`; const modalContainer = document.createElement('div'); modalContainer.innerHTML = dialogHtml; document.body.appendChild(modalContainer); const closeModal = () => modalContainer.remove(); document.getElementById('batch-group-cancel').addEventListener('click', closeModal); document.getElementById('batch-group-confirm').addEventListener('click', async () => { const groupId = document.getElementById('batch-group-select').value; if (!groupId) return; const confirmBtn = document.getElementById('batch-group-confirm'); confirmBtn.disabled = true; confirmBtn.textContent = 'Aggiunta...'; let addedCount = 0; let errorsCount = 0; for (const ticketId of this.selectedIds) { try { await App.api(`/api/groups/${groupId}/tickets`, { method: 'POST', body: JSON.stringify({ ticket_identifier: ticketId }) }); addedCount++; } catch (err) { errorsCount++; } } closeModal(); if (addedCount > 0) { Toast.success(`${addedCount} ticket aggiunti al gruppo!`); this.cancelBatchSelection(); } else { Toast.error('Impossibile aggiungere i ticket al gruppo selezionato.'); } }); } catch (err) { Toast.error('Errore caricamento gruppi: ' + err.message); } }, async mergeBatch() { if (this.selectedOrder.length < 2) return; const targetId = this.selectedOrder[0]; const sourceIds = this.selectedOrder.slice(1); const targetRow = document.querySelector(`#spudorata-results-area tr[data-ticket-id="${targetId}"]`); const targetTn = targetRow ? targetRow.querySelector('.ticket-tn-link').textContent.trim() : targetId; const sourceTns = sourceIds.map(id => { const row = document.querySelector(`#spudorata-results-area tr[data-ticket-id="${id}"]`); return row ? row.querySelector('.ticket-tn-link').textContent.trim() : `#${id}`; }); const confirmed = await App.confirm('Unione Ticket', `Sei sicuro di voler unire i ticket ${sourceTns.join(', ')} nel ticket principale #${targetTn}?`); if (!confirmed) return; try { 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'); if (this.cachedTickets) { this.cachedTickets.forEach(t => { if (sourceIds.includes(String(t.id))) { t.ticket_state_id = 9; t.state_name = 'merged'; t.state_type = 'merged'; } }); } this.cancelBatchSelection(); this.renderLocalResults(); } catch (err) { Toast.error('Errore durante l\'unione: ' + err.message); } } };