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
+7 -6
View File
@@ -338,7 +338,7 @@ const TicketCreateView = {
// 2. Customer User pre-population with first match
try {
const companies = await App.api('/api/customer-companies/search?q=cliente');
const companies = await App.api('/api/customer-companies/search?q=');
if (companies.length > 0 && customerIdInput && companySearchInput) {
const defaultCompany = companies[0];
customerIdInput.value = defaultCompany.customer_id;
@@ -388,11 +388,7 @@ const TicketCreateView = {
userSearchInput.addEventListener('input', () => {
clearTimeout(userDebounce);
const q = userSearchInput.value.trim();
if (q.length < 2) {
userSuggestionsDiv.style.display = 'none';
customerUserIdInput.value = '';
return;
}
// Do not block empty query to allow all results on focus
userDebounce = setTimeout(async () => {
try {
@@ -432,6 +428,11 @@ const TicketCreateView = {
}
}, 300);
});
userSearchInput.addEventListener('focus', () => {
userSearchInput.value = '';
customerUserIdInput.value = '';
userSearchInput.dispatchEvent(new Event('input'));
});
}
// Owner Autocomplete (dynamic backend search)
+204 -24
View File
@@ -6,9 +6,33 @@ const TicketDetailView = {
ticketId: null,
htmlMode: false,
originalValues: {},
noteAttachments: [],
updateNoteAttachmentList() {
const listEl = document.getElementById('note-file-list');
if (!listEl) return;
listEl.innerHTML = this.noteAttachments.map((att, idx) => `
<div class="upload-file-item">
<span>📎</span>
<strong>${App.escapeHtml(att.filename)}</strong>
<span style="font-size:0.75rem; color:var(--text-muted); margin-left:var(--space-xs);">(${Math.round(att.content.length * 0.75 / 1024)} KB)</span>
<button type="button" class="btn-remove" data-idx="${idx}">✕</button>
</div>
`).join('');
// Bind remove buttons
listEl.querySelectorAll('.btn-remove').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx, 10);
this.noteAttachments.splice(idx, 1);
this.updateNoteAttachmentList();
});
});
},
async render(id) {
this.ticketId = id;
this.noteAttachments = [];
this.htmlMode = localStorage.getItem('otrs_turbo_html_mode') === 'true';
const container = document.getElementById('view-container');
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
@@ -28,10 +52,10 @@ const TicketDetailView = {
return sum + (isNaN(val) ? 0 : val);
}, 0);
// Display total time in the topbar
// Display ticket number in the topbar
const titleEl = document.getElementById('page-title');
if (titleEl) {
titleEl.innerHTML = `Ticket #${id} <span style="font-size:0.85rem; font-weight:normal; color:var(--text-secondary); margin-left:var(--space-md); background:var(--bg-tertiary); padding:4px 10px; border-radius:var(--radius-sm); border:1px solid var(--border-light); display:inline-flex; align-items:center; gap:4px;">⏱ Tempo Consultivato: <strong>${totalTime} min</strong></span>`;
titleEl.innerHTML = `Ticket #${id}`;
}
this.originalValues = {
@@ -59,7 +83,13 @@ const TicketDetailView = {
<div class="ticket-header">
<div class="ticket-header-info">
<div class="ticket-number">#${ticket.tn}</div>
<h2 class="ticket-detail-title">${App.escapeHtml(ticket.title || '(senza titolo)')}</h2>
<h2 class="ticket-detail-title">
${data.otrsWebUrl ? `
<a href="${data.otrsWebUrl}" target="_blank" style="color:inherit; text-decoration:none; border-bottom:1px dashed transparent; transition:border-bottom 0.1s ease;" onmouseover="this.style.borderBottom='1px dashed var(--text-primary)'" onmouseout="this.style.borderBottom='transparent'" title="Apri in OTRS">
${App.escapeHtml(ticket.title || '(senza titolo)')}
</a>
` : App.escapeHtml(ticket.title || '(senza titolo)')}
</h2>
<div class="ticket-meta-badges">
<span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span>
<span class="badge badge-priority" data-priority="${App.priorityIndex(ticket.priority_name)}">${ticket.priority_name}</span>
@@ -124,15 +154,19 @@ const TicketDetailView = {
<input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" />
<div id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div>
</div>
</div>
<div class="quick-edit-field">
<label class="quick-edit-label">Tempo (minuti)</label>
<input type="number" step="any" min="0" class="form-input" id="qe-time-unit" placeholder="minuti" style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
</div>
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);justify-content:flex-end;">
<button class="btn btn-ghost btn-sm" id="qe-reset">Reset</button>
<button class="btn btn-primary btn-sm" id="qe-save" disabled>Salva Modifiche</button>
</div>
</div>
</div>
<!-- Add Note Form -->
<div class="add-note-form" style="margin-bottom: var(--space-lg);">
<!-- Add Note Form -->
<div class="add-note-form" style="margin-bottom: var(--space-lg);">
<div class="card-title">Aggiungi Nota</div>
<div style="display:flex; gap:var(--space-md); margin-bottom:var(--space-md);">
<input type="text" class="note-subject-input" id="note-subject" placeholder="Oggetto (opzionale)" style="flex:1; margin-bottom:0;" />
@@ -140,12 +174,21 @@ const TicketDetailView = {
<div id="note-body-container" style="background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); overflow: hidden; margin-bottom: var(--space-md);">
<div id="note-body-editor" style="min-height: 180px; font-family: inherit; font-size: 0.95rem; border: none; color: var(--text-primary);"></div>
</div>
<div style="display:flex; gap:var(--space-md); justify-content:flex-end; align-items:center; margin-top:var(--space-md);">
<input type="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="Tempo (minuti)" style="width:140px; margin-bottom:0; height:32px; padding:4px 10px; font-size:0.85rem;" />
<button class="btn btn-primary btn-sm" id="note-send" style="height:32px; display:flex; align-items:center; gap:var(--space-xs);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
Invia Nota
</button>
<!-- Attachments List for note -->
<div class="upload-file-list" id="note-file-list" style="margin-bottom: var(--space-md);"></div>
<div style="display:flex; gap:var(--space-md); justify-content:space-between; align-items:center; margin-top:var(--space-md);">
<div>
<input type="file" id="note-attachments" multiple style="display:none;" />
<button type="button" class="btn btn-ghost btn-sm" id="btn-note-add-attachments" style="height:32px; padding: 4px 10px; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;">📎 Allega file</button>
</div>
<div style="display:flex; gap:var(--space-md); align-items:center;">
<input type="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="Tempo (minuti)" style="width:140px; margin-bottom:0; height:32px; padding:4px 10px; font-size:0.85rem;" />
<button class="btn btn-primary btn-sm" id="note-send" style="height:32px; display:flex; align-items:center; gap:var(--space-xs);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
Invia Nota
</button>
</div>
</div>
</div>
@@ -165,7 +208,7 @@ const TicketDetailView = {
? `<iframe srcdoc="${a.a_body.replace(/"/g, '&quot;')}" style="width:100%; border:none; background:var(--bg-card); border-radius:var(--radius-md); min-height:220px; font-family:inherit; color-scheme: dark;"></iframe>`
: `<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>`;
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id);
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id && att.filename !== 'file-1');
return `
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
@@ -177,8 +220,21 @@ const TicketDetailView = {
${a.channel_name ? `<span style="font-size:0.72rem;color:var(--text-muted);">via ${a.channel_name}</span>` : ''}
</div>
<div style="display:flex; gap: var(--space-sm); align-items:center;">
${a.time_unit ? `<span class="badge" style="background:var(--info-bg);color:var(--info);font-size:0.75rem;padding:2px 8px;border-radius:4px;">⏱ ${parseFloat(a.time_unit)} min</span>` : ''}
<div class="time-edit-container" data-article-id="${a.article_id}" style="display:inline-flex; align-items:center; gap:4px; position:relative;">
${a.time_unit ? `
<span class="badge" style="background:var(--info-bg);color:var(--info);font-size:0.75rem;padding:2px 8px;border-radius:4px;">⏱ <span class="time-value-display">${parseFloat(a.time_unit)}</span> min</span>
` : ''}
<button class="retrodate-btn-edit btn-edit-article-time" style="background:none; border:none; cursor:pointer; font-size:0.75rem; padding: 2px 4px; display:inline-flex; align-items:center;" title="${a.time_unit ? 'Modifica Tempo' : 'Aggiungi Tempo'}">
${a.time_unit ? '✏️' : '⏱+✏️'}
</button>
<div class="time-editor-popover" style="display:none; position:absolute; right:0; top: 100%; background:var(--bg-card); border:1px solid var(--border-subtle); padding:var(--space-xs); border-radius:var(--radius-sm); z-index:100; box-shadow:var(--shadow-md); margin-top:4px;">
<input type="number" step="any" min="0" class="form-input input-article-time" value="${a.time_unit ? parseFloat(a.time_unit) : 0}" style="width:70px; height:24px; padding:2px 6px; font-size:0.8rem; margin-bottom:4px; display:block;" />
<div style="display:flex; gap:4px; justify-content:flex-end;">
<button class="btn btn-primary btn-sm btn-save-article-time" style="height:20px; padding:0 6px; font-size:0.75rem; line-height:20px;">✓</button>
<button class="btn btn-ghost btn-sm btn-cancel-article-time" style="height:20px; padding:0 6px; font-size:0.75rem; line-height:20px;">✗</button>
</div>
</div>
</div>
<div class="retrodate-container" data-article-id="${a.article_id}">
<span class="article-time">${App.formatDateTime(a.create_time)}</span>
<button class="retrodate-btn-edit btn-edit-article-date" title="Retrodata Articolo">✏️</button>
@@ -361,6 +417,7 @@ const TicketDetailView = {
const fields = document.querySelectorAll('.quick-edit-select, #qe-customer-user-id, #qe-customer-id');
const saveBtn = document.getElementById('qe-save');
const resetBtn = document.getElementById('qe-reset');
const timeUnitInput = document.getElementById('qe-time-unit');
const checkChanges = () => {
let hasChanges = false;
@@ -372,10 +429,20 @@ const TicketDetailView = {
el.classList.toggle('changed', changed);
if (changed) hasChanges = true;
});
const timeVal = parseFloat(timeUnitInput ? timeUnitInput.value : '0') || 0;
if (timeVal > 0) {
hasChanges = true;
}
saveBtn.disabled = !hasChanges;
};
fields.forEach(el => el.addEventListener('change', checkChanges));
if (timeUnitInput) {
timeUnitInput.addEventListener('input', checkChanges);
timeUnitInput.addEventListener('change', checkChanges);
}
// Customer User Autocomplete inside Quick Edit
const customerSearchInput = document.getElementById('qe-customer-search');
@@ -388,14 +455,7 @@ const TicketDetailView = {
customerSearchInput.addEventListener('input', () => {
clearTimeout(customerDebounce);
const q = customerSearchInput.value.trim();
if (q.length < 2) {
customerSuggestionsDiv.style.display = 'none';
customerUserIdInput.value = '';
customerIdInput.value = '';
customerUserIdInput.dispatchEvent(new Event('change'));
customerIdInput.dispatchEvent(new Event('change'));
return;
}
// Do not block empty query to allow all results on focus
customerDebounce = setTimeout(async () => {
try {
@@ -432,6 +492,14 @@ const TicketDetailView = {
}
}, 300);
});
customerSearchInput.addEventListener('focus', () => {
customerSearchInput.value = '';
customerUserIdInput.value = '';
customerIdInput.value = '';
customerUserIdInput.dispatchEvent(new Event('change'));
customerIdInput.dispatchEvent(new Event('change'));
customerSearchInput.dispatchEvent(new Event('input'));
});
}
// Close suggestions on click outside
@@ -452,6 +520,9 @@ const TicketDetailView = {
const last = this.originalValues['customer_last'] || '';
customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || '');
}
if (timeUnitInput) {
timeUnitInput.value = '';
}
saveBtn.disabled = true;
});
@@ -479,6 +550,13 @@ const TicketDetailView = {
}
});
if (timeUnitInput && timeUnitInput.value.trim()) {
const timeVal = parseFloat(timeUnitInput.value.trim());
if (!isNaN(timeVal) && timeVal > 0) {
updates.time_unit = timeVal;
}
}
if (Object.keys(updates).length === 0) return;
try {
@@ -537,9 +615,15 @@ const TicketDetailView = {
const res = await App.api(`/api/tickets/${this.ticketId}/articles`, {
method: 'POST',
body: JSON.stringify({ subject, body, time_unit }),
body: JSON.stringify({
subject,
body,
time_unit,
attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined
}),
});
this.noteAttachments = []; // Clear attachments
Toast.success(res.message || 'Nota aggiunta!');
App.updateDailyTimer();
this.render(this.ticketId);
@@ -550,6 +634,32 @@ const TicketDetailView = {
}
});
// Note Attachments Upload Handlers
const noteAttachBtn = document.getElementById('btn-note-add-attachments');
const noteFileInput = document.getElementById('note-attachments');
if (noteAttachBtn && noteFileInput) {
noteAttachBtn.addEventListener('click', () => noteFileInput.click());
noteFileInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files);
for (const file of files) {
const reader = new FileReader();
reader.onload = () => {
const base64Data = reader.result.split(',')[1];
this.noteAttachments.push({
filename: file.name,
content: base64Data,
content_type: file.type
});
this.updateNoteAttachmentList();
};
reader.readAsDataURL(file);
}
noteFileInput.value = ''; // Reset input
});
}
// Retrodate Ticket Event Listeners
const btnEditTicketDate = document.getElementById('btn-edit-ticket-date');
const ticketDateEditor = document.getElementById('ticket-date-editor');
@@ -638,6 +748,76 @@ const TicketDetailView = {
});
}
});
// Time Edit Event Listeners (Issue 21)
document.querySelectorAll('.time-edit-container[data-article-id]').forEach(container => {
const articleId = container.dataset.articleId;
const btnEdit = container.querySelector('.btn-edit-article-time');
const popover = container.querySelector('.time-editor-popover');
const btnSave = container.querySelector('.btn-save-article-time');
const btnCancel = container.querySelector('.btn-cancel-article-time');
const input = container.querySelector('.input-article-time');
if (btnEdit) {
btnEdit.addEventListener('click', (e) => {
e.stopPropagation();
// Close all other open time edit popovers
document.querySelectorAll('.time-editor-popover').forEach(p => {
if (p !== popover) p.style.display = 'none';
});
const isOpen = popover.style.display === 'block';
popover.style.display = isOpen ? 'none' : 'block';
if (!isOpen) {
input.focus();
input.select();
}
});
}
if (btnCancel) {
btnCancel.addEventListener('click', (e) => {
e.stopPropagation();
popover.style.display = 'none';
});
}
if (btnSave) {
btnSave.addEventListener('click', async (e) => {
e.stopPropagation();
const val = parseFloat(input.value);
if (isNaN(val) || val < 0) {
Toast.warning('Inserisci un valore numerico valido maggiore o uguale a 0.');
return;
}
try {
btnSave.disabled = true;
await App.api(`/api/tickets/articles/${articleId}/time`, {
method: 'PUT',
body: JSON.stringify({ time_unit: val })
});
Toast.success('Tempo consuntivato aggiornato con successo!');
popover.style.display = 'none';
this.render(this.ticketId);
} catch (err) {
Toast.error('Errore durante l\'aggiornamento del tempo: ' + err.message);
btnSave.disabled = false;
}
});
}
// Stop propagation so clicking inside doesn't close it
if (popover) {
popover.addEventListener('click', (e) => e.stopPropagation());
}
});
// Close popovers clicking outside
document.addEventListener('click', () => {
document.querySelectorAll('.time-editor-popover').forEach(p => {
p.style.display = 'none';
});
});
// HTML Mode Toggle Listener
const htmlToggle = document.getElementById('html-toggle');
if (htmlToggle) {
+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