/**
* 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() {
Filters.currentMode = 'spudorata';
Filters.load();
const container = document.getElementById('view-container');
container.innerHTML = `
Inizializzazione Ricerca Spudorata...
`;
await App.ensureLookups();
try {
const settings = await App.api('/api/dashboard/settings');
if (settings && settings.tickets_per_page) {
this.perPage = settings.tickets_per_page;
}
} catch (err) {
console.warn('Failed to load agent settings:', err);
}
this.renderContainer(container);
this.bindEvents();
const urlParams = new URLSearchParams((window.location.hash.split('?')[1]) || '');
const qParam = urlParams.get('q');
if (qParam && !this.hasSearched) {
const input = document.getElementById('spudorata-search-input');
if (input) input.value = qParam;
this.executeSearch(qParam);
} else if (this.hasSearched && this.cachedTickets) {
this.renderLocalResults();
}
},
renderContainer(container) {
container.innerHTML = `
π
Ricerca Spudorata Testuale
Esegui la ricerca nel testo sui ticket. I filtri applicati successivamente operano istantaneamente in cache locale senza fare nuove richieste al server.
β
Cerca
${this.hasSearched ? `
Azzera cache
` : ''}
0 selezionati
Seleziona visibili
Copia numeri
Stato
β
${(App.lookups.states || []).map(s => `${s.name} `).join('')}
Owner
β
${(App.lookups.users || []).map(u => `${u.first_name} ${u.last_name} `).join('')}
Responsabile
β
${(App.lookups.users || []).map(u => `${u.first_name} ${u.last_name} `).join('')}
${(App.lookups.types || []).length > 0 ? `
Tipo
β
${App.lookups.types.map(t => `${t.name} `).join('')}
` : ''}
Applica
Aggiungi a gruppo
Unisci Selezionati
Apri ticket in schede
Annulla
${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.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)}
NΒ°
Creato
Stato
Titolo
Coda
Owner
Cliente
PrioritΓ
${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 `
π
${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)}
`;
}).join('') : `
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) {
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 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); }
});
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-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 ticketIds = Array.from(this.selectedIds).map(id => parseInt(id));
const res = await App.api('/api/tickets/batch', {
method: 'POST',
body: JSON.stringify({ ticketIds, 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;
}
}
}
});
}
this.cancelBatchSelection();
this.renderLocalResults();
} 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);
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);
}
}
};