Files
otrs-turbo/public/js/views/ticketList.js
T

813 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
if (!container.querySelector('.ticket-table-wrapper') && !container.querySelector('.ticket-table')) {
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
}
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) {
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 = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">Errore caricamento ticket</div>
<div class="empty-state-sub">${App.escapeHtml(err.message)}</div>
</div>
`;
}
},
renderContent(container, data) {
const tickets = data.tickets || [];
const { total, page, per_page, total_pages } = data;
container.innerHTML = `
<!-- Batch Actions Bar -->
<div class="batch-bar" id="batch-bar">
<div style="display:flex; align-items:center; gap:var(--space-sm);">
<span class="batch-count" id="batch-count">0 selezionati</span>
<button class="btn btn-ghost btn-xs" id="batch-select-all">Seleziona visibili</button>
<button class="btn btn-ghost btn-xs" id="batch-copy-tns" style="margin-left: 8px;">Copia numeri</button>
</div>
<div class="filter-group">
<span class="filter-label">Stato</span>
<select class="filter-select" id="batch-state">
<option value="">—</option>
${(App.lookups.states || []).map(s => `<option value="${s.id}">${s.name}</option>`).join('')}
</select>
</div>
<div class="filter-group" style="position:relative;">
<span class="filter-label">Coda</span>
<input type="text" class="form-input filter-select" id="batch-queue-search" placeholder="Cerca coda..." autocomplete="off" style="width:160px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
<input type="hidden" id="batch-queue" />
<div id="batch-queue-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%; left: 0; width: 280px; z-index: 1001;"></div>
</div>
<div class="filter-group">
<span class="filter-label">Owner</span>
<select class="filter-select" id="batch-owner">
<option value="">—</option>
${(App.lookups.users || []).map(u => `<option value="${u.id}">${u.first_name} ${u.last_name}</option>`).join('')}
</select>
</div>
<div class="filter-group">
<span class="filter-label">Responsabile</span>
<select class="filter-select" id="batch-responsible">
<option value="">—</option>
${(App.lookups.users || []).map(u => `<option value="${u.id}">${u.first_name} ${u.last_name}</option>`).join('')}
</select>
</div>
${(App.lookups.types || []).length > 0 ? `
<div class="filter-group">
<span class="filter-label">Tipo</span>
<select class="filter-select" id="batch-type">
<option value="">—</option>
${App.lookups.types.map(t => `<option value="${t.id}">${t.name}</option>`).join('')}
</select>
</div>
` : ''}
<button class="btn btn-primary btn-sm" id="batch-apply">Applica</button>
<button class="btn btn-primary btn-sm" id="batch-add-group" disabled style="background: var(--accent-primary); border-color: var(--accent-primary); margin-left: 8px;">Aggiungi a gruppo</button>
<button class="btn btn-primary btn-sm" id="batch-merge" disabled style="background: rgba(160, 65, 71, 0.32); border-color: var(--accent-primary); color: var(--text-primary); margin-left: 8px;">Unisci Selezionati</button>
<button class="btn btn-primary btn-sm" id="batch-open-tabs" disabled style="background: var(--accent-primary); border-color: var(--accent-primary); margin-left: 8px;">Apri ticket in schede</button>
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
</div>
${Filters.renderBar(App.lookups)}
<!-- Pagination Top -->
${this.renderPagination(page, per_page, total, total_pages, true)}
<!-- Ticket Table -->
<div class="ticket-table-wrapper">
<table class="ticket-table" id="ticket-table">
<thead>
<tr>
<th style="width: 75px;" class="sortable ${this.sortBy === 'tn' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="tn">N°</th>
<th style="width: 125px;" class="sortable ${this.sortBy === 'create_time' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="create_time">Creato</th>
<th style="width: 90px;" class="sortable ${this.sortBy === 'state' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="state">Stato</th>
<th class="sortable ${this.sortBy === 'title' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="title">Titolo</th>
<th style="width: 130px;" class="sortable ${this.sortBy === 'queue' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="queue">Coda</th>
<th style="width: 120px;">Owner</th>
<th style="width: 140px;">Cliente</th>
<th style="width: 80px;" class="sortable ${this.sortBy === 'priority' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="priority">Priorità</th>
</tr>
</thead>
<tbody>
${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 `
<tr data-ticket-id="${t.id}" class="${this.selectedIds.has(String(t.id)) ? 'selected' : ''} ${this.selectedOrder[0] === String(t.id) ? 'first-selected' : ''}" style="cursor:pointer;">
<td>
<span class="ticket-tn" style="display:inline-flex; align-items:center;">
<span class="copy-ticket-btn" data-tn="${t.tn}" style="cursor: pointer; font-size: 0.82rem; display: inline-flex; align-items: center; margin-right: 4px;" onclick="event.stopPropagation();" title="Copia numero ticket">📋</span>
<a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a>
${data.otrsBaseUrl ? `
<a href="${data.otrsBaseUrl}index.pl?Action=AgentTicketZoom;TicketID=${t.id}" target="_blank" title="Apri in OTRS" onclick="event.stopPropagation()" style="display:inline-flex; align-items:center; text-decoration:none;">
<span style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:#1070ca; color:#fff; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; vertical-align:middle; line-height:16px;">O</span>
</a>
` : ''}
<button class="open-tab-btn" data-id="${t.id}" data-tn="${t.tn}" data-title="${App.escapeHtml(t.title || '')}" onclick="App.openTab(${t.id}, '${t.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button>
</span>
</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDateTime(t.create_time)}</td>
<td><span class="badge badge-state" data-state-type="${(t.state_type || '').toLowerCase()}">${t.state_name}</span></td>
<td class="ticket-title-cell"><span class="copy-ticket-btn" data-tn="${t.title}" style="cursor: pointer; font-size: 0.82rem; display: inline-flex; align-items: center; margin-right: 4px;" onclick="event.stopPropagation();" title="Copia titolo ticket">📋</span><a href="#/tickets/${t.id}" class="ticket-title-link" onclick="event.stopPropagation()">${App.escapeHtml(t.title || '(senza titolo)')}</a></td>
<td class="queue-cell"><span class="badge badge-queue">${App.escapeHtml(shortQueue)}</span><div class="queue-tooltip">${App.escapeHtml(t.queue_name)}</div></td>
<td style="color:var(--text-secondary);font-size:0.82rem;">${t.owner_first || ''} ${t.owner_last || ''}</td>
<td style="color:var(--text-secondary);font-size:0.82rem;">${t.customer_first ? `${t.customer_first} ${t.customer_last}` : (t.customer_user_id || '—')}</td>
<td><span class="badge badge-priority" data-priority="${App.priorityIndex(t.priority_name)}">${App.priorityIndex(t.priority_name)}</span></td>
</tr>
`;
}).join('') : `
<tr>
<td colspan="8">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">Nessun ticket trovato</div>
<div class="empty-state-sub">Prova a cambiare i filtri o crea un nuovo ticket.</div>
</div>
</td>
</tr>
`}
</tbody>
</table>
</div>
<!-- Pagination Bottom -->
${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 = `
<div style="display:inline-flex; align-items:center; gap:var(--space-xs); font-size:0.8rem; color:var(--text-secondary); margin-right:var(--space-md);">
<span>Righe:</span>
<select class="form-select ticket-per-page-select" style="padding: 2px 24px 2px 6px; font-size: 0.75rem; height: 26px; min-width: 65px; margin: 0; background-position: right 6px center; border-color: var(--border-light);">
<option value="10" ${per_page === 10 ? 'selected' : ''}>10</option>
<option value="20" ${per_page === 20 ? 'selected' : ''}>20</option>
<option value="50" ${per_page === 50 ? 'selected' : ''}>50</option>
<option value="100" ${per_page === 100 ? 'selected' : ''}>100</option>
<option value="200" ${per_page === 200 ? 'selected' : ''}>200</option>
</select>
</div>
`;
if (total_pages > 1) {
return `
<div class="pagination" style="${marginStyle}">
<div class="pagination-info">
Mostrando ${((page - 1) * per_page) + 1}${Math.min(page * per_page, total)} di ${total} ticket
</div>
<div class="pagination-controls">
${limitSelectHtml}
<button class="pagination-btn" data-page="1" ${page <= 1 ? 'disabled' : ''}>«</button>
<button class="pagination-btn" data-page="${page - 1}" ${page <= 1 ? 'disabled' : ''}></button>
${this.renderPageButtons(page, total_pages)}
<button class="pagination-btn" data-page="${page + 1}" ${page >= total_pages ? 'disabled' : ''}></button>
<button class="pagination-btn" data-page="${total_pages}" ${page >= total_pages ? 'disabled' : ''}>»</button>
</div>
</div>
`;
} else {
return `
<div class="pagination" style="${marginStyle}">
<div class="pagination-info">${total} ticket totali</div>
<div class="pagination-controls">
${limitSelectHtml}
</div>
</div>
`;
}
},
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(`<button class="pagination-btn ${i === current ? 'active' : ''}" data-page="${i}">${i}</button>`);
}
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 add to group
const batchAddGroup = document.getElementById('batch-add-group');
if (batchAddGroup) {
batchAddGroup.addEventListener('click', () => this.addToGroup());
}
// 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 = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun utente trovato</div>';
batchCustomerSuggestionsDiv.style.display = 'block';
return;
}
batchCustomerSuggestionsDiv.innerHTML = users.map(u => `
<div class="autocomplete-suggestion-item" data-login="${App.escapeHtml(u.login)}" data-customer-id="${App.escapeHtml(u.customer_id || '')}" data-name="${App.escapeHtml(u.first_name + ' ' + u.last_name)}">
<strong>${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)}</strong>
<span style="font-size:0.75rem; color:var(--text-muted); margin-left:var(--space-xs); font-weight:normal;">(Login: ${App.escapeHtml(u.login)} | Azienda: ${App.escapeHtml(u.customer_id || '—')})</span>
</div>
`).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 = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessuna coda trovata</div>';
batchQueueSuggestionsDiv.style.display = 'block';
return;
}
batchQueueSuggestionsDiv.innerHTML = queues.map(queue => `
<div class="autocomplete-suggestion-item" data-id="${queue.id}" data-name="${App.escapeHtml(queue.name)}">
<strong>${App.escapeHtml(queue.name)}</strong>
</div>
`).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 add to group button
const addGroupBtn = document.getElementById('batch-add-group');
if (addGroupBtn) {
addGroupBtn.disabled = this.selectedIds.size === 0;
}
// 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 = [];
} catch (err) {
Toast.error('Errore durante l\'unione: ' + err.message);
this.updateBatchBar();
}
},
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;
}
// Show group selection prompt/dialog
const groupOptions = groups.map(g => `<option value="${g.id}">${App.escapeHtml(g.nome)}</option>`).join('');
const dialogHtml = `
<div id="batch-group-modal" style="position: fixed; inset: 0; z-index: 8000; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center;">
<div style="background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); width: 400px; max-width: 90vw; padding: var(--space-lg); box-shadow: var(--shadow-lg);">
<h4 style="margin: 0 0 var(--space-md) 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);">Aggiungi a Gruppo</h4>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: var(--space-md);">Seleziona il gruppo a cui aggiungere i <strong>${this.selectedIds.size}</strong> ticket selezionati:</p>
<select id="batch-group-select" class="form-select" style="width: 100%; margin-bottom: var(--space-lg);">
${groupOptions}
</select>
<div style="display: flex; gap: var(--space-sm); justify-content: flex-end;">
<button class="btn btn-ghost btn-sm" id="batch-group-cancel">Annulla</button>
<button class="btn btn-primary btn-sm" id="batch-group-confirm">Aggiungi</button>
</div>
</div>
</div>
`;
// Append modal to body
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) {
// Conflict (already in group) or other errors
errorsCount++;
}
}
closeModal();
if (addedCount > 0) {
Toast.success(`${addedCount} ticket aggiunti al gruppo!`);
this.selectedIds.clear();
this.selectedOrder = [];
this.render();
} else if (errorsCount > 0) {
Toast.warning('I ticket selezionati appartengono già a questo gruppo.');
}
});
} catch (err) {
Toast.error('Errore durante il recupero dei gruppi: ' + err.message);
}
},
};