From b957272eb8acbec93bedb0004590820e082a8ef7 Mon Sep 17 00:00:00 2001 From: Gabriele Cimaschi Date: Thu, 10 Sep 2026 19:33:27 +0200 Subject: [PATCH] feat: unificata interfaccia note rapide con l'inserimento delle note. --- public/js/views/shortcuts.js | 6 +- public/js/views/ticketDetail.js | 580 +++++++++++--------------------- 2 files changed, 199 insertions(+), 387 deletions(-) diff --git a/public/js/views/shortcuts.js b/public/js/views/shortcuts.js index 6c296d3..af89cd9 100644 --- a/public/js/views/shortcuts.js +++ b/public/js/views/shortcuts.js @@ -74,9 +74,9 @@ const ShortcutsView = { }, { id: 'quick_edit_save', - name: 'Salva Modifica Rapida', - description: 'Salva le modifiche apportate nei campi di Modifica Rapida', - context: 'Pagina Dettaglio Ticket (Modifica Rapida)', + name: 'Salva Modifiche / Invia Nota', + description: 'Salva le modifiche apportate nei campi di Modifica Rapida o invia la nota interna nel dettaglio ticket', + context: 'Pagina Dettaglio Ticket', defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true } }, { diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index 09019b2..3f3d6f4 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -2322,64 +2322,48 @@ const TicketDetailView = { -
- - -
-
- - -
-
-
- - -
- -
- - - -
-
-
Aggiungi Nota
-
- - + +
+
+ 📝 Aggiungi Nota / Messaggio Interno + Opzionale se aggiorni solo i campi sopra
-
-
- -
-
-
-
- -
-
-
- - +
+
-
- - - - + +
+
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + +
@@ -2789,9 +2773,14 @@ const TicketDetailView = { const fields = document.querySelectorAll('.quick-edit-select:not(#qe-queue-search), #qe-queue, #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 noteSubjectInput = document.getElementById('qe-note-subject'); - const noteInput = document.getElementById('qe-note'); + const queueSearchInput = document.getElementById('qe-queue-search'); + const queueIdInput = document.getElementById('qe-queue'); + const customerSearchInput = document.getElementById('qe-customer-search'); + const customerUserIdInput = document.getElementById('qe-customer-user-id'); + const customerIdInput = document.getElementById('qe-customer-id'); + const noteSubjectInput = document.getElementById('note-subject'); + const noteTimeUnitsInput = document.getElementById('note-time-units'); + const noteCreateTimeInput = document.getElementById('note-create-time'); const checkChanges = () => { let hasChanges = false; @@ -2801,374 +2790,180 @@ const TicketDetailView = { const current = el.value; const changed = current !== original; if (el.id === 'qe-queue') { - const searchInput = document.getElementById('qe-queue-search'); - if (searchInput) searchInput.classList.toggle('changed', changed); + if (queueSearchInput) queueSearchInput.classList.toggle('changed', changed); } else { el.classList.toggle('changed', changed); } if (changed) hasChanges = true; }); - const timeVal = parseFloat(timeUnitInput ? timeUnitInput.value : '0') || 0; - if (timeVal > 0) { - hasChanges = true; - } - const noteSubText = noteSubjectInput ? noteSubjectInput.value.trim() : ''; - if (noteSubjectInput) { - noteSubjectInput.classList.toggle('changed', noteSubText.length > 0); - } - if (noteSubText.length > 0) { - hasChanges = true; - } + if (noteSubText.length > 0) hasChanges = true; - const noteText = noteInput ? noteInput.value.trim() : ''; - if (noteInput) { - noteInput.classList.toggle('changed', noteText.length > 0); - } - if (noteText.length > 0) { - hasChanges = true; - } + const noteBodyText = (this.noteQuill ? this.noteQuill.root.innerText : '').trim(); + if (noteBodyText.length > 0) hasChanges = true; - saveBtn.disabled = !hasChanges; + const noteTimeUnitsVal = parseFloat(noteTimeUnitsInput ? noteTimeUnitsInput.value : '0') || 0; + if (noteTimeUnitsVal > 0) hasChanges = true; + + if (noteCreateTimeInput && noteCreateTimeInput.value) hasChanges = true; + + if (this.noteAttachments && this.noteAttachments.length > 0) hasChanges = true; + + if (saveBtn) saveBtn.disabled = !hasChanges; }; fields.forEach(el => { el.addEventListener('change', checkChanges); }); - if (timeUnitInput) { - timeUnitInput.addEventListener('input', checkChanges); - timeUnitInput.addEventListener('change', checkChanges); - } + if (noteSubjectInput) { noteSubjectInput.addEventListener('input', checkChanges); - noteSubjectInput.addEventListener('change', checkChanges); } - if (noteInput) { - noteInput.addEventListener('input', checkChanges); - noteInput.addEventListener('change', checkChanges); + if (noteTimeUnitsInput) { + noteTimeUnitsInput.addEventListener('input', checkChanges); + } + if (noteCreateTimeInput) { + noteCreateTimeInput.addEventListener('change', checkChanges); + } + if (this.noteQuill) { + this.noteQuill.on('text-change', checkChanges); } - // Queue Autocomplete inside Quick Edit - const queueSearchInput = document.getElementById('qe-queue-search'); - const queueSuggestionsDiv = document.getElementById('qe-queue-suggestions'); - const queueIdInput = document.getElementById('qe-queue'); - - let queueDebounce; - if (queueSearchInput) { - queueSearchInput.addEventListener('input', () => { - clearTimeout(queueDebounce); - const q = queueSearchInput.value.trim(); - - queueDebounce = setTimeout(async () => { - try { - const queues = await App.api(`/api/queues/search?q=${encodeURIComponent(q)}`); - if (queueSearchInput.value.trim() !== q) { - return; - } - if (queues.length === 0) { - queueSuggestionsDiv.innerHTML = '
Nessuna coda trovata
'; - queueSuggestionsDiv.style.display = 'block'; - return; - } - - queueSuggestionsDiv.innerHTML = queues.map(q => { - const displayName = q.name.replace(/::/g, ' › '); - return ` -
- ${App.escapeHtml(displayName)} -
- `; - }).join(''); - queueSuggestionsDiv.style.display = 'block'; - - // Bind click/mousedown - queueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { - if (item.dataset.id) { - item.addEventListener('mousedown', (e) => { - e.preventDefault(); - queueSearchInput.value = item.dataset.name; - queueIdInput.value = item.dataset.id; - queueSuggestionsDiv.style.display = 'none'; - queueIdInput.dispatchEvent(new Event('change')); - }); - } - }); - } catch (err) { - console.error(err); - } - }, 150); - }); - - App.setupAutocompleteKeyboardNavigation(queueSearchInput, queueSuggestionsDiv, (item) => { - if (item && item.dataset.id) { - queueSearchInput.value = item.dataset.name; - queueIdInput.value = item.dataset.id; - queueSuggestionsDiv.style.display = 'none'; - queueIdInput.dispatchEvent(new Event('change')); + // Reset quick-edit & note form + if (resetBtn) { + resetBtn.addEventListener('click', () => { + fields.forEach(el => { + el.value = this.originalValues[el.dataset.field] || ''; + el.classList.remove('changed'); + }); + if (queueSearchInput) { + queueSearchInput.classList.remove('changed'); + queueSearchInput.value = this.originalValues['queue_name'] || ''; } - }); - - queueSearchInput.addEventListener('focus', () => { - queueSearchInput.value = ''; - queueIdInput.value = ''; - queueSearchInput.dispatchEvent(new Event('input')); - }); - - queueSearchInput.addEventListener('blur', () => { - setTimeout(() => { queueSuggestionsDiv.style.display = 'none'; }, 150); - }); - } - - // Customer User Autocomplete inside Quick Edit - const customerSearchInput = document.getElementById('qe-customer-search'); - const customerSuggestionsDiv = document.getElementById('qe-customer-suggestions'); - const customerUserIdInput = document.getElementById('qe-customer-user-id'); - const customerIdInput = document.getElementById('qe-customer-id'); - - let customerDebounce; - if (customerSearchInput) { - customerSearchInput.addEventListener('input', () => { - clearTimeout(customerDebounce); - const q = customerSearchInput.value.trim(); - - customerDebounce = setTimeout(async () => { - try { - const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); - console.log('[Frontend LDAP Search Detail] Users returned:', users); - // Prevent race conditions: discard results if the input value has changed - if (customerSearchInput.value.trim() !== q) { - console.log('[Frontend LDAP Search Detail] Discarding stale results for query:', q); - return; - } - if (users.length === 0) { - customerSuggestionsDiv.innerHTML = '
Nessun utente trovato
'; - customerSuggestionsDiv.style.display = 'block'; - return; - } - - customerSuggestionsDiv.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(''); - customerSuggestionsDiv.style.display = 'block'; - - // Use mousedown instead of click to fire before blur event - customerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { - if (item.dataset.login) { - item.addEventListener('mousedown', (e) => { - e.preventDefault(); // prevent input from losing focus before value is set - customerSearchInput.value = item.dataset.name; - customerUserIdInput.value = item.dataset.login; - customerIdInput.value = item.dataset.customerId || ''; - customerSuggestionsDiv.style.display = 'none'; - customerUserIdInput.dispatchEvent(new Event('change')); - customerIdInput.dispatchEvent(new Event('change')); - }); - } - }); - } catch (err) { - console.error(err); - } - }, 300); - }); - customerSearchInput.addEventListener('focus', () => { - if (!customerUserIdInput.value) { - customerSearchInput.dispatchEvent(new Event('input')); + if (customerSearchInput) { + const first = this.originalValues['customer_first'] || ''; + const last = this.originalValues['customer_last'] || ''; + customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || ''); } - }); - customerSearchInput.addEventListener('blur', () => { - setTimeout(() => { customerSuggestionsDiv.style.display = 'none'; }, 150); + if (noteSubjectInput) noteSubjectInput.value = ''; + if (this.noteQuill) this.noteQuill.root.innerHTML = ''; + if (noteTimeUnitsInput) noteTimeUnitsInput.value = ''; + if (noteCreateTimeInput) noteCreateTimeInput.value = ''; + this.noteAttachments = []; + this.updateNoteAttachmentList(); + App.clearDraft(this.ticketId, 'note'); + if (saveBtn) saveBtn.disabled = true; }); } - // Reset quick-edit - resetBtn.addEventListener('click', () => { - fields.forEach(el => { - el.value = this.originalValues[el.dataset.field] || ''; - el.classList.remove('changed'); - }); - if (queueSearchInput) { - queueSearchInput.classList.remove('changed'); - queueSearchInput.value = this.originalValues['queue_name'] || ''; - } - if (customerSearchInput) { - const first = this.originalValues['customer_first'] || ''; - const last = this.originalValues['customer_last'] || ''; - customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || ''); - } - if (timeUnitInput) { - timeUnitInput.value = ''; - } - if (noteSubjectInput) { - noteSubjectInput.value = ''; - noteSubjectInput.classList.remove('changed'); - } - if (noteInput) { - noteInput.value = ''; - noteInput.classList.remove('changed'); - } - saveBtn.disabled = true; - }); + // Unified Save Quick Edit & Send Note action + if (saveBtn) { + saveBtn.addEventListener('click', async () => { + const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : ''; + if (queueSearchInput && !queueIdInput.value) { + Toast.warning('Seleziona una coda valida dall\'elenco.'); + return; + } - // Save quick-edit - saveBtn.addEventListener('click', async () => { - const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : ''; - if (queueSearchInput && !queueIdInput.value) { - Toast.warning('Seleziona una coda valida dall\'elenco.'); + const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : ''; + if (customerSearch && customerUserIdInput && !customerUserIdInput.value) { + customerUserIdInput.value = customerSearch; + if (customerIdInput) customerIdInput.value = customerSearch; + } + + // Check metadata updates + const updates = {}; + fields.forEach(el => { + const field = el.dataset.field; + let val; + if (field === 'customer_id' || field === 'customer_user_id') { + val = el.value || null; + } else { + val = el.value ? parseInt(el.value) : null; + } + + const origVal = this.originalValues[field] || null; + if (val !== origVal) { + updates[field] = val; + } + }); + + // Check note content + let noteBody = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : ''; + if (noteBody === '


') noteBody = ''; + let noteSubject = noteSubjectInput ? noteSubjectInput.value.trim() : ''; + const timeUnitVal = noteTimeUnitsInput ? noteTimeUnitsInput.value.trim() : ''; + const noteCreateTimeVal = noteCreateTimeInput ? noteCreateTimeInput.value : ''; + + const hasMetadataChanges = Object.keys(updates).length > 0; + const hasNoteContent = !!(noteBody || noteSubject || timeUnitVal || this.noteAttachments.length > 0 || noteCreateTimeVal); + + if (!hasMetadataChanges && !hasNoteContent) { + Toast.warning('Nessuna modifica o nota da salvare'); return; } - const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : ''; - if (customerSearch && !customerUserIdInput.value) { - customerUserIdInput.value = customerSearch; - customerIdInput.value = customerSearch; - } - - const updates = {}; - fields.forEach(el => { - const field = el.dataset.field; - let val; - if (field === 'customer_id' || field === 'customer_user_id') { - val = el.value || null; - } else { - val = el.value ? parseInt(el.value) : null; - } - - const origVal = this.originalValues[field] || null; - if (val !== origVal) { - updates[field] = val; - } - }); - - if (timeUnitInput && timeUnitInput.value.trim()) { - const timeVal = parseFloat(timeUnitInput.value.trim()); - if (!isNaN(timeVal) && timeVal > 0) { - updates.time_unit = timeVal; - } - } - - if (noteSubjectInput && noteSubjectInput.value.trim()) { - updates.note_subject = noteSubjectInput.value.trim(); - } - - if (noteInput && noteInput.value.trim()) { - updates.note = noteInput.value.trim(); - } - - if (Object.keys(updates).length === 0) return; - try { saveBtn.disabled = true; - saveBtn.textContent = 'Salvando...'; - const res = await App.api(`/api/tickets/${this.ticketId}`, { - method: 'PATCH', - body: JSON.stringify(updates), - }); - Toast.success(res.message || 'Ticket aggiornato!'); - TicketListView.invalidateCache(); - // Refresh the view - this.render(this.ticketId); - } catch (err) { - Toast.error('Errore: ' + err.message); - saveBtn.disabled = false; - saveBtn.textContent = 'Salva Modifiche'; - } - }); + saveBtn.innerHTML = '
Salvataggio...'; - // Shortcut for Quick Edit save - const quickEditContainer = container.querySelector('.quick-edit'); - if (quickEditContainer) { - quickEditContainer.addEventListener('keydown', (e) => { - if (App.isShortcutTriggered(e, 'quick_edit_save')) { - e.preventDefault(); - if (saveBtn && !saveBtn.disabled) { - saveBtn.click(); + // 1. Perform metadata updates if changed + if (hasMetadataChanges) { + await App.api(`/api/tickets/${this.ticketId}`, { + method: 'PATCH', + body: JSON.stringify(updates), + }); + } + + // 2. Perform article note creation if note fields are populated + if (hasNoteContent) { + if (!noteBody && noteSubject) { + noteBody = `

${App.escapeHtml(noteSubject)}

`; + } + if (noteBody && !noteSubject) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = noteBody; + const plainText = tempDiv.textContent || tempDiv.innerText || ''; + noteSubject = plainText.trim().substring(0, 50) || 'Nota interna'; + } + + const resNote = await App.api(`/api/tickets/${this.ticketId}/articles`, { + method: 'POST', + body: JSON.stringify({ + subject: noteSubject, + body: noteBody, + time_unit: timeUnitVal, + attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined + }), + }); + + this.noteAttachments = []; // Clear attachments after successful send + + if (noteCreateTimeVal && resNote.article_id) { + try { + const formattedDate = noteCreateTimeVal.replace('T', ' '); + await App.api(`/api/tickets/articles/${resNote.article_id}/retrodata-article`, { + method: 'POST', + body: JSON.stringify({ create_time: formattedDate }) + }); + } catch (retErr) { + console.warn('Note retrodate error:', retErr); + } } } - }); - } - - // Send note - const noteSendBtn = document.getElementById('note-send'); - noteSendBtn.addEventListener('click', async () => { - let body = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : ''; - if (body === '


') { - body = ''; - } - let subject = document.getElementById('note-subject').value.trim(); - const time_unit = document.getElementById('note-time-units').value.trim(); - const note_create_time = document.getElementById('note-create-time')?.value; - - // If body is empty but subject is provided, fill body with subject text - if (!body && subject) { - body = `

${App.escapeHtml(subject)}

`; - if (this.noteQuill) { - this.noteQuill.root.innerHTML = body; - } - } - - // If subject is empty but body is provided, fill subject with truncated plain text of body - if (body && !subject) { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = body; - const plainText = tempDiv.textContent || tempDiv.innerText || ''; - subject = plainText.trim().substring(0, 50) || 'Nota interna'; - document.getElementById('note-subject').value = subject; - } - - // If both are still empty, show warning - if (!body && !subject) { - Toast.warning('Inserisci un oggetto o il corpo della nota prima di inviare'); - return; - } - - const noteStateSelect = document.getElementById('note-ticket-state'); - const ticket_state_id = noteStateSelect ? noteStateSelect.value : null; - - try { - noteSendBtn.disabled = true; - noteSendBtn.innerHTML = '
Invio...'; - - const res = await App.api(`/api/tickets/${this.ticketId}/articles`, { - method: 'POST', - body: JSON.stringify({ - subject, - body, - time_unit, - ticket_state_id: ticket_state_id ? parseInt(ticket_state_id) : undefined, - attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined - }), - }); - - this.noteAttachments = []; // Clear attachments - - if (note_create_time && res.article_id) { - try { - const formattedDate = note_create_time.replace('T', ' '); - await App.api(`/api/tickets/articles/${res.article_id}/retrodata-article`, { - method: 'POST', - body: JSON.stringify({ create_time: formattedDate }) - }); - Toast.success('Nota aggiunta e retrodatata!'); - } catch (retErr) { - Toast.warning('Nota aggiunta, ma errore durante la retrodatazione: ' + retErr.message); - } - } else { - Toast.success(res.message || 'Nota aggiunta!'); - } + Toast.success('Modifiche e note salvate con successo!'); App.clearDraft(this.ticketId, 'note'); App.updateDailyTimer(); + TicketListView.invalidateCache(); this.render(this.ticketId); } catch (err) { - Toast.error('Errore: ' + err.message); - noteSendBtn.disabled = false; - noteSendBtn.innerHTML = 'Invia Nota'; + Toast.error('Errore durante il salvataggio: ' + err.message); + saveBtn.disabled = false; } }); + } // Note Attachments Upload Handlers const noteAttachBtn = document.getElementById('btn-note-add-attachments'); @@ -3630,5 +3425,22 @@ const TicketDetailView = { } }); }); + + // Keydown listener for Ticket Detail shortcut (Save Quick Edit / Send Note) + const handleTicketDetailShortcut = (e) => { + if (!document.getElementById('qe-save')) { + document.removeEventListener('keydown', handleTicketDetailShortcut, true); + return; + } + if (App.isShortcutTriggered(e, 'quick_edit_save')) { + e.preventDefault(); + e.stopPropagation(); + const btn = document.getElementById('qe-save'); + if (btn && !btn.disabled) { + btn.click(); + } + } + }; + document.addEventListener('keydown', handleTicketDetailShortcut, true); }, };