/**
* 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 = '
';
try {
// Fetch lookups for filter dropdowns
await App.ensureLookups();
// Fetch agent settings for tickets_per_page
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);
}
// 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
Owner
Responsabile
${(App.lookups.types || []).length > 0 ? `
Tipo
` : ''}
${Filters.renderBar(App.lookups)}
${this.renderPagination(page, per_page, total, total_pages, true)}
| N° |
Creato |
Stato |
Titolo |
Coda |
Owner |
Cliente |
Priorità |
${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 `
|
📋
${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)} |
`;
}).join('') : `
📭
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;';
const limitSelectHtml = `
Righe:
`;
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 open tabs
const batchOpenTabsBtn = document.getElementById('batch-open-tabs');
if (batchOpenTabsBtn) {
batchOpenTabsBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (this.selectedIds.size === 0) return;
let count = 0;
this.selectedIds.forEach(id => {
const row = document.querySelector(`.ticket-table tbody tr[data-ticket-id="${id}"]`);
if (row) {
const tnLink = row.querySelector('.ticket-tn-link');
const titleLink = row.querySelector('.ticket-title-link');
const tn = tnLink ? tnLink.textContent.trim() : '';
const title = titleLink ? titleLink.textContent.trim() : '';
App.openTab(parseInt(id, 10), tn, title);
count++;
}
});
if (count > 0) {
Toast.success(`${count} ticket aperti in nuove schede!`);
this.selectedIds.clear();
this.selectedOrder = [];
document.querySelectorAll('.ticket-table tbody tr').forEach(tr => {
tr.classList.remove('selected');
tr.classList.remove('first-selected');
});
this.updateBatchBar();
}
});
}
// Batch Copy Ticket Numbers
const batchCopyTns = document.getElementById('batch-copy-tns');
if (batchCopyTns) {
batchCopyTns.addEventListener('click', (e) => {
e.stopPropagation();
const selectedTns = [];
this.selectedIds.forEach(id => {
const row = document.querySelector(`.ticket-table tbody 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) {
const textToCopy = selectedTns.join('\n');
navigator.clipboard.writeText(textToCopy).then(() => {
Toast.success(`${selectedTns.length} numeri ticket copiati!`);
}).catch(err => {
Toast.error('Errore durante la copia: ' + err.message);
});
}
});
}
// Individual copy buttons
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);
});
}
});
});
// 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 batchQueueSearch = document.getElementById('batch-queue-search');
if (batchQueueSearch) batchQueueSearch.value = '';
const batchQueue = document.getElementById('batch-queue');
if (batchQueue) batchQueue.value = '';
const batchOwner = document.getElementById('batch-owner');
if (batchOwner) batchOwner.value = '';
const batchResponsible = document.getElementById('batch-responsible');
if (batchResponsible) batchResponsible.value = '';
const batchType = document.getElementById('batch-type');
if (batchType) batchType.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'));
});
}
// Batch Queue Autocomplete
const batchQueueSearchInput = document.getElementById('batch-queue-search');
const batchQueueSuggestionsDiv = document.getElementById('batch-queue-suggestions');
const batchQueueIdInput = document.getElementById('batch-queue');
let batchQueueDebounce;
if (batchQueueSearchInput) {
batchQueueSearchInput.addEventListener('input', () => {
clearTimeout(batchQueueDebounce);
const q = batchQueueSearchInput.value.trim();
batchQueueDebounce = setTimeout(async () => {
try {
const queues = await App.api(`/api/queues/search?q=${encodeURIComponent(q)}`);
if (queues.length === 0) {
batchQueueSuggestionsDiv.innerHTML = 'Nessuna coda trovata
';
batchQueueSuggestionsDiv.style.display = 'block';
return;
}
batchQueueSuggestionsDiv.innerHTML = queues.map(queue => `
${App.escapeHtml(queue.name)}
`).join('');
batchQueueSuggestionsDiv.style.display = 'block';
// Bind click
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);
}
}, 300);
});
batchQueueSearchInput.addEventListener('focus', () => {
batchQueueSearchInput.value = '';
if (batchQueueIdInput) batchQueueIdInput.value = '';
batchQueueSearchInput.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';
}
if (batchQueueSearchInput && e.target !== batchQueueSearchInput && e.target !== batchQueueSuggestionsDiv) {
batchQueueSuggestionsDiv.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();
});
});
// Page size change
document.querySelectorAll('.ticket-per-page-select').forEach(select => {
select.addEventListener('change', async () => {
const newLimit = parseInt(select.value, 10);
try {
await App.api('/api/dashboard/settings', {
method: 'POST',
body: JSON.stringify({ tickets_per_page: newLimit }),
});
Toast.success(`Righe per pagina aggiornate a ${newLimit}!`);
this.perPage = newLimit;
this.currentPage = 1;
this.render();
} catch (err) {
Toast.error('Errore durante il salvataggio dell\'impostazione: ' + err.message);
}
});
});
},
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;
}
}
// Enable/disable open tabs button
const openTabsBtn = document.getElementById('batch-open-tabs');
if (openTabsBtn) {
if (this.selectedIds.size > 0) {
openTabsBtn.disabled = false;
} else {
openTabsBtn.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 batchResponsible = document.getElementById('batch-responsible')?.value;
const batchType = document.getElementById('batch-type')?.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 (batchType) updates.type_id = parseInt(batchType);
if (batchOwner) updates.user_id = parseInt(batchOwner);
if (batchResponsible) {
updates.responsible_user_id = parseInt(batchResponsible);
} else if (batchOwner) {
updates.responsible_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 = await App.confirm('Unione Ticket', `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();
}
},
};