style: riviste parti dell'interfaccia dei ticket. feat: aggiunta possiblità di modificare il tempo inserito nelle note ed aggiunta voce per tempo nel menu azioni rapide

This commit is contained in:
2026-07-07 21:14:24 +02:00
parent 7b0b29e6c6
commit 3f6d2cc3a8
8 changed files with 832 additions and 140 deletions
+64 -34
View File
@@ -21,9 +21,13 @@ const TicketListView = {
// Build query params
const isMyTickets = window.location.hash.startsWith('#/tickets/my');
if (isMyTickets) {
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();
@@ -99,36 +103,52 @@ const TicketListView = {
${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 class="sortable ${this.sortBy === 'tn' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="tn">N°</th>
<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 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>Cliente</th>
<th class="sortable ${this.sortBy === 'create_time' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="create_time">Creato</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 => `
<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"><a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a></span></td>
<td class="ticket-title-cell"><a href="#/tickets/${t.id}" class="ticket-title-link" onclick="event.stopPropagation()">${App.escapeHtml(t.title || '(senza titolo)')}</a></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)}">${App.priorityIndex(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-secondary);font-size:0.82rem;">${t.customer_first ? `${t.customer_first} ${t.customer_last}` : (t.customer_user_id || '—')}</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDateTime(t.create_time)}</td>
</tr>
`).join('') : `
${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;">
<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>
` : ''}
</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"><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="7">
<td colspan="8">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">Nessun ticket trovato</div>
@@ -141,9 +161,16 @@ const TicketListView = {
</table>
</div>
<!-- Pagination -->
${total_pages > 1 ? `
<div class="pagination">
<!-- 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;';
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>
@@ -155,13 +182,15 @@ const TicketListView = {
<button class="pagination-btn" data-page="${total_pages}" ${page >= total_pages ? 'disabled' : ''}>»</button>
</div>
</div>
` : `
<div class="pagination">
`;
} else {
return `
<div class="pagination" style="${marginStyle}">
<div class="pagination-info">${total} ticket totali</div>
<div></div>
</div>
`}
`;
`;
}
},
renderPageButtons(current, total) {
@@ -312,12 +341,7 @@ const TicketListView = {
batchCustomerSearchInput.addEventListener('input', () => {
clearTimeout(batchCustomerDebounce);
const q = batchCustomerSearchInput.value.trim();
if (q.length < 2) {
batchCustomerSuggestionsDiv.style.display = 'none';
if (batchCustomerUserIdInput) batchCustomerUserIdInput.value = '';
if (batchCustomerIdInput) batchCustomerIdInput.value = '';
return;
}
// Do not block empty query to allow all results on focus
batchCustomerDebounce = setTimeout(async () => {
try {
@@ -352,6 +376,12 @@ const TicketListView = {
}
}, 300);
});
batchCustomerSearchInput.addEventListener('focus', () => {
batchCustomerSearchInput.value = '';
if (batchCustomerUserIdInput) batchCustomerUserIdInput.value = '';
if (batchCustomerIdInput) batchCustomerIdInput.value = '';
batchCustomerSearchInput.dispatchEvent(new Event('input'));
});
}
// Close suggestions on click outside