Files
otrs-turbo/public/js/views/ticketList.js
T
2026-07-05 10:55:40 +02:00

304 lines
12 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(),
searchTimeout: null,
async render() {
const container = document.getElementById('view-container');
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
try {
// Fetch lookups for filter dropdowns
await App.ensureLookups();
// Build query params
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 = `
${Filters.renderBar(App.lookups)}
<!-- Batch Actions Bar -->
<div class="batch-bar" id="batch-bar">
<span class="batch-count" id="batch-count">0 selezionati</span>
<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">
<span class="filter-label">Coda</span>
<select class="filter-select" id="batch-queue">
<option value="">—</option>
${(App.lookups.queues || []).map(q => `<option value="${q.id}">${q.name}</option>`).join('')}
</select>
</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>
<button class="btn btn-primary btn-sm" id="batch-apply">Applica</button>
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
</div>
<!-- Ticket Table -->
<div class="ticket-table-wrapper">
<table class="ticket-table" id="ticket-table">
<thead>
<tr>
<th class="checkbox-cell">
<input type="checkbox" id="select-all" title="Seleziona tutti" />
</th>
<th class="sortable ${this.sortBy === 'tn' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="tn">N°</th>
<th class="sortable ${this.sortBy === 'title' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="title">Titolo</th>
<th class="sortable ${this.sortBy === 'state' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="state">Stato</th>
<th class="sortable ${this.sortBy === 'priority' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="priority">Priorità</th>
<th class="sortable ${this.sortBy === 'queue' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="queue">Coda</th>
<th>Owner</th>
<th class="sortable ${this.sortBy === 'create_time' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="create_time">Creato</th>
<th class="sortable ${this.sortBy === 'change_time' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="change_time">Modificato</th>
</tr>
</thead>
<tbody>
${tickets.length > 0 ? tickets.map(t => `
<tr data-ticket-id="${t.id}" class="${this.selectedIds.has(String(t.id)) ? 'selected' : ''}">
<td class="checkbox-cell" onclick="event.stopPropagation()">
<input type="checkbox" class="ticket-checkbox" value="${t.id}" ${this.selectedIds.has(String(t.id)) ? 'checked' : ''} />
</td>
<td><span class="ticket-tn">${t.tn}</span></td>
<td class="ticket-title-cell">${App.escapeHtml(t.title || '(senza titolo)')}</td>
<td><span class="badge badge-state" data-state-type="${(t.state_type || '').toLowerCase()}">${t.state_name}</span></td>
<td><span class="badge badge-priority" data-priority="${App.priorityIndex(t.priority_name)}">${t.priority_name}</span></td>
<td><span class="badge badge-queue">${t.queue_name}</span></td>
<td style="color:var(--text-secondary);font-size:0.82rem;">${t.owner_first || ''} ${t.owner_last || ''}</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDate(t.create_time)}</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDate(t.change_time)}</td>
</tr>
`).join('') : `
<tr>
<td colspan="9">
<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 -->
${total_pages > 1 ? `
<div class="pagination">
<div class="pagination-info">
Mostrando ${((page - 1) * per_page) + 1}${Math.min(page * per_page, total)} di ${total} ticket
</div>
<div class="pagination-controls">
<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>
` : `
<div class="pagination">
<div class="pagination-info">${total} ticket totali</div>
<div></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.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 → detail
document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(row => {
row.addEventListener('click', (e) => {
if (e.target.type === 'checkbox' || e.target.closest('.checkbox-cell')) return;
window.location.hash = `#/tickets/${row.dataset.ticketId}`;
});
});
// Checkbox selection
const selectAll = document.getElementById('select-all');
if (selectAll) {
selectAll.addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.ticket-checkbox');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = cb.value;
if (e.target.checked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
cb.closest('tr').classList.toggle('selected', e.target.checked);
});
this.updateBatchBar();
});
}
document.querySelectorAll('.ticket-checkbox').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = e.target.value;
if (e.target.checked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
e.target.closest('tr').classList.toggle('selected', e.target.checked);
this.updateBatchBar();
});
});
// Batch apply
const batchApply = document.getElementById('batch-apply');
if (batchApply) {
batchApply.addEventListener('click', () => this.applyBatch());
}
// Batch cancel
const batchCancel = document.getElementById('batch-cancel');
if (batchCancel) {
batchCancel.addEventListener('click', () => {
this.selectedIds.clear();
document.querySelectorAll('.ticket-checkbox').forEach(cb => {
cb.checked = false;
cb.closest('tr').classList.remove('selected');
});
const selectAll = document.getElementById('select-all');
if (selectAll) selectAll.checked = false;
this.updateBatchBar();
});
}
// Pagination
document.querySelectorAll('.pagination-btn[data-page]').forEach(btn => {
btn.addEventListener('click', () => {
this.currentPage = parseInt(btn.dataset.page);
this.selectedIds.clear();
this.render();
});
});
},
updateBatchBar() {
const bar = document.getElementById('batch-bar');
const count = document.getElementById('batch-count');
if (this.selectedIds.size > 0) {
bar.classList.add('visible');
count.textContent = `${this.selectedIds.size} selezionat${this.selectedIds.size === 1 ? 'o' : 'i'}`;
} else {
bar.classList.remove('visible');
}
},
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;
if (batchState) updates.ticket_state_id = parseInt(batchState);
if (batchQueue) updates.queue_id = parseInt(batchQueue);
if (batchOwner) updates.user_id = parseInt(batchOwner);
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.render();
} catch (err) {
Toast.error('Errore aggiornamento batch: ' + err.message);
}
},
};