/** * Ticket Detail View * Shows full ticket info with quick-edit dropdowns, article timeline, and add-note form. */ 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) => `
Caricamento ticket...
${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; } 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, attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined }), }); this.noteAttachments = []; // Clear attachments Toast.success(res.message || 'Nota aggiunta!'); App.updateDailyTimer(); this.render(this.ticketId); } catch (err) { Toast.error('Errore: ' + err.message); noteSendBtn.disabled = false; noteSendBtn.innerHTML = 'Invia Nota'; } }); // 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'); const ticketDateText = document.getElementById('ticket-date-text'); const btnSaveTicketDate = document.getElementById('btn-save-ticket-date'); const btnCancelTicketDate = document.getElementById('btn-cancel-ticket-date'); const inputTicketDate = document.getElementById('input-ticket-date'); if (btnEditTicketDate) { btnEditTicketDate.addEventListener('click', () => { btnEditTicketDate.style.display = 'none'; ticketDateText.style.display = 'none'; ticketDateEditor.style.display = 'flex'; }); btnCancelTicketDate.addEventListener('click', () => { ticketDateEditor.style.display = 'none'; btnEditTicketDate.style.display = 'inline-flex'; ticketDateText.style.display = 'inline'; }); btnSaveTicketDate.addEventListener('click', async () => { const val = inputTicketDate.value; if (!val) { Toast.warning('Scegli una data/ora valida.'); return; } try { btnSaveTicketDate.disabled = true; await App.api(`/api/tickets/${this.ticketId}/retrodata-ticket`, { method: 'POST', body: JSON.stringify({ create_time: val }) }); Toast.success('Data creazione ticket aggiornata!'); this.render(this.ticketId); } catch (err) { Toast.error('Errore retrodatazione ticket: ' + err.message); btnSaveTicketDate.disabled = false; } }); } // Retrodate Articles Event Listeners document.querySelectorAll('.retrodate-container[data-article-id]').forEach(container => { const articleId = container.dataset.articleId; const btnEdit = container.querySelector('.btn-edit-article-date'); const editor = container.querySelector('.retrodate-editor'); const timeText = container.querySelector('.article-time'); const btnSave = container.querySelector('.btn-save-article-date'); const btnCancel = container.querySelector('.btn-cancel-article-date'); const input = container.querySelector('.input-article-date'); if (btnEdit) { btnEdit.addEventListener('click', () => { btnEdit.style.display = 'none'; timeText.style.display = 'none'; editor.style.display = 'flex'; }); btnCancel.addEventListener('click', () => { editor.style.display = 'none'; btnEdit.style.display = 'inline-flex'; timeText.style.display = 'inline'; }); btnSave.addEventListener('click', async () => { const val = input.value; if (!val) { Toast.warning('Scegli una data/ora valida.'); return; } try { btnSave.disabled = true; await App.api(`/api/tickets/articles/${articleId}/retrodata-article`, { method: 'POST', body: JSON.stringify({ create_time: val }) }); Toast.success('Data creazione articolo aggiornata!'); this.render(this.ticketId); } catch (err) { Toast.error('Errore retrodatazione articolo: ' + err.message); btnSave.disabled = false; } }); } }); // 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'; }); }); // Delete Article Event Listeners document.querySelectorAll('.btn-delete-article').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const articleId = btn.dataset.articleId; const ok = await App.confirm( "Elimina Articolo", "Sei sicuro di voler eliminare questa nota/articolo? Tutti i file allegati e i tempi ad esso associati verranno rimossi permanentemente.", { confirmText: 'Elimina', cancelText: 'Annulla' } ); if (ok) { try { btn.disabled = true; await App.api(`/api/tickets/articles/${articleId}`, { method: 'DELETE' }); Toast.success('Articolo/nota eliminato con successo!'); this.render(this.ticketId); } catch (err) { Toast.error('Errore durante l\'eliminazione: ' + err.message); btn.disabled = false; } } }); }); // HTML Mode Toggle Listener const htmlToggle = document.getElementById('html-toggle'); if (htmlToggle) { htmlToggle.addEventListener('change', (e) => { const isChecked = e.target.checked; localStorage.setItem('otrs_turbo_html_mode', isChecked); this.htmlMode = isChecked; this.render(this.ticketId); }); } }, };