feat: unificata interfaccia note rapide con l'inserimento delle note.

This commit is contained in:
Gabriele Cimaschi committed 2026-09-10 19:33:27 +02:00
1 parent a5d3431008
commit b957272eb8
2 files changed
+199 -387

No files matched your search

+3 -3
View File
@@ -74,9 +74,9 @@ const ShortcutsView = {
}, },
{ {
id: 'quick_edit_save', id: 'quick_edit_save',
name: 'Salva Modifica Rapida', name: 'Salva Modifiche / Invia Nota',
description: 'Salva le modifiche apportate nei campi di Modifica Rapida', description: 'Salva le modifiche apportate nei campi di Modifica Rapida o invia la nota interna nel dettaglio ticket',
context: 'Pagina Dettaglio Ticket (Modifica Rapida)', context: 'Pagina Dettaglio Ticket',
defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true } defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }
}, },
{ {
+196 -384
View File
@@ -2322,64 +2322,48 @@ const TicketDetailView = {
<input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" /> <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 id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div>
</div> </div>
<div class="quick-edit-field full-width">
<label class="quick-edit-label">Oggetto Nota (opzionale)</label>
<input type="text" class="form-input quick-edit-input" id="qe-note-subject" placeholder="Oggetto della nota..." style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
</div>
<div class="quick-edit-field full-width">
<label class="quick-edit-label">Nota / Commento</label>
<textarea class="quick-edit-textarea" id="qe-note" placeholder="Inserisci una nota..."></textarea>
</div>
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);align-items:center;justify-content:flex-end;grid-column:1 / -1;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:var(--space-xs);margin-right:auto;">
<label for="qe-time-unit" style="font-size:0.85rem;font-weight:600;color:var(--text-secondary);white-space:nowrap;">Tempo (minuti):</label>
<input type="number" step="any" min="0" class="form-input" id="qe-time-unit" placeholder="minuti" style="padding:4px 8px; height:32px; font-size:0.85rem; width:110px;" />
</div>
<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>
</div>
<!-- Add Note Form --> <!-- Integrated Note & Article Editor inside Modifica Rapida -->
<div class="add-note-form" style="margin-bottom: var(--space-lg);"> <div style="border-top: 1px solid var(--border-subtle); margin-top: var(--space-lg); padding-top: var(--space-md);">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: var(--space-md); flex-wrap:wrap; gap:var(--space-sm);"> <div style="font-weight: 700; font-size: 0.85rem; color: var(--text-primary); margin-bottom: var(--space-sm); display: flex; align-items: center; justify-content: space-between;">
<div class="card-title" style="margin-bottom:0;">Aggiungi Nota</div> <span>📝 Aggiungi Nota / Messaggio Interno</span>
<div style="display:flex; align-items:center; gap:var(--space-xs);"> <span style="font-size: 0.75rem; font-weight: normal; color: var(--text-muted);">Opzionale se aggiorni solo i campi sopra</span>
<label for="note-ticket-state" style="font-size:0.85rem; font-weight:600; color:var(--text-secondary);">Stato ticket:</label>
<select id="note-ticket-state" class="quick-edit-select" style="height:32px; padding:4px 8px; font-size:0.85rem; margin-bottom:0; width:auto;">
${(App.lookups.states || []).map(s =>
`<option value="${s.id}" ${s.id === ticket.ticket_state_id ? 'selected' : ''}>${App.escapeHtml(s.name)}</option>`
).join('')}
</select>
</div> </div>
</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;" />
</div>
<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>
<!-- 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 style="margin-bottom: var(--space-sm);">
<div> <input type="text" class="note-subject-input" id="note-subject" placeholder="Oggetto della nota (opzionale)" style="width: 100%; margin-bottom: 0; height: 32px; font-size: 0.85rem;" />
<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>
<div style="display:flex; gap:var(--space-md); align-items:center;">
<input type="datetime-local" class="note-subject-input" id="note-create-time" title="Data/ora nota (opzionale per retrodatazione)" style="width:185px; margin-bottom:0; height:32px; padding:4px 8px; font-size:0.82rem;" /> <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-sm);">
<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;" /> <div id="note-body-editor" style="min-height: 160px; font-family: inherit; font-size: 0.95rem; border: none; color: var(--text-primary);"></div>
<button class="btn btn-ghost btn-sm" id="btn-open-email-compose" style="height:32px; display:flex; align-items:center; gap:var(--space-xs); border-color:var(--accent-secondary); color:var(--accent-secondary);" </div>
data-ticket-id="${ticket.id}" data-ticket-tn="${ticket.tn}" data-ticket-title="${App.escapeHtml(ticket.title)}" data-customer-email="${App.escapeHtml(ticket.customer_email || '')}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> <!-- Attachments List for note -->
${emailBtnText} <div class="upload-file-list" id="note-file-list" style="margin-bottom: var(--space-sm);"></div>
</button>
<button class="btn btn-primary btn-sm" id="note-send" style="height:32px; display:flex; align-items:center; gap:var(--space-xs);"> <div style="display:flex; gap:var(--space-md); justify-content:space-between; align-items:center; flex-wrap:wrap;">
<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> <div>
Invia Nota <input type="file" id="note-attachments" multiple style="display:none;" />
</button> <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-sm); align-items:center; flex-wrap:wrap;">
<label for="note-create-time" style="font-size:0.8rem; font-weight:600; color:var(--text-secondary);">Data/Ora:</label>
<input type="datetime-local" class="note-subject-input" id="note-create-time" title="Data/ora nota (opzionale per retrodatazione)" style="width:185px; margin-bottom:0; height:32px; padding:4px 8px; font-size:0.82rem;" />
<label for="note-time-units" style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-left:4px;">Tempo (min):</label>
<input type="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="minuti" style="width:90px; margin-bottom:0; height:32px; padding:4px 8px; font-size:0.85rem;" />
<button class="btn btn-ghost btn-sm" id="btn-open-email-compose" style="height:32px; display:flex; align-items:center; gap:var(--space-xs); border-color:var(--accent-secondary); color:var(--accent-secondary);"
data-ticket-id="${ticket.id}" data-ticket-tn="${ticket.tn}" data-ticket-title="${App.escapeHtml(ticket.title)}" data-customer-email="${App.escapeHtml(ticket.customer_email || '')}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
${emailBtnText}
</button>
<button class="btn btn-ghost btn-sm" id="qe-reset" style="height:32px;">Reset</button>
<button class="btn btn-primary btn-sm" id="qe-save" style="height:32px; display:flex; align-items:center; gap:var(--space-xs);">
💾 Salva Modifiche / Invia Nota
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -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 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 saveBtn = document.getElementById('qe-save');
const resetBtn = document.getElementById('qe-reset'); const resetBtn = document.getElementById('qe-reset');
const timeUnitInput = document.getElementById('qe-time-unit'); const queueSearchInput = document.getElementById('qe-queue-search');
const noteSubjectInput = document.getElementById('qe-note-subject'); const queueIdInput = document.getElementById('qe-queue');
const noteInput = document.getElementById('qe-note'); 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 = () => { const checkChanges = () => {
let hasChanges = false; let hasChanges = false;
@@ -2801,374 +2790,180 @@ const TicketDetailView = {
const current = el.value; const current = el.value;
const changed = current !== original; const changed = current !== original;
if (el.id === 'qe-queue') { if (el.id === 'qe-queue') {
const searchInput = document.getElementById('qe-queue-search'); if (queueSearchInput) queueSearchInput.classList.toggle('changed', changed);
if (searchInput) searchInput.classList.toggle('changed', changed);
} else { } else {
el.classList.toggle('changed', changed); el.classList.toggle('changed', changed);
} }
if (changed) hasChanges = true; if (changed) hasChanges = true;
}); });
const timeVal = parseFloat(timeUnitInput ? timeUnitInput.value : '0') || 0;
if (timeVal > 0) {
hasChanges = true;
}
const noteSubText = noteSubjectInput ? noteSubjectInput.value.trim() : ''; const noteSubText = noteSubjectInput ? noteSubjectInput.value.trim() : '';
if (noteSubjectInput) { if (noteSubText.length > 0) hasChanges = true;
noteSubjectInput.classList.toggle('changed', noteSubText.length > 0);
}
if (noteSubText.length > 0) {
hasChanges = true;
}
const noteText = noteInput ? noteInput.value.trim() : ''; const noteBodyText = (this.noteQuill ? this.noteQuill.root.innerText : '').trim();
if (noteInput) { if (noteBodyText.length > 0) hasChanges = true;
noteInput.classList.toggle('changed', noteText.length > 0);
}
if (noteText.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 => { fields.forEach(el => {
el.addEventListener('change', checkChanges); el.addEventListener('change', checkChanges);
}); });
if (timeUnitInput) {
timeUnitInput.addEventListener('input', checkChanges);
timeUnitInput.addEventListener('change', checkChanges);
}
if (noteSubjectInput) { if (noteSubjectInput) {
noteSubjectInput.addEventListener('input', checkChanges); noteSubjectInput.addEventListener('input', checkChanges);
noteSubjectInput.addEventListener('change', checkChanges);
} }
if (noteInput) { if (noteTimeUnitsInput) {
noteInput.addEventListener('input', checkChanges); noteTimeUnitsInput.addEventListener('input', checkChanges);
noteInput.addEventListener('change', checkChanges); }
if (noteCreateTimeInput) {
noteCreateTimeInput.addEventListener('change', checkChanges);
}
if (this.noteQuill) {
this.noteQuill.on('text-change', checkChanges);
} }
// Queue Autocomplete inside Quick Edit // Reset quick-edit & note form
const queueSearchInput = document.getElementById('qe-queue-search'); if (resetBtn) {
const queueSuggestionsDiv = document.getElementById('qe-queue-suggestions'); resetBtn.addEventListener('click', () => {
const queueIdInput = document.getElementById('qe-queue'); fields.forEach(el => {
el.value = this.originalValues[el.dataset.field] || '';
let queueDebounce; el.classList.remove('changed');
if (queueSearchInput) { });
queueSearchInput.addEventListener('input', () => { if (queueSearchInput) {
clearTimeout(queueDebounce); queueSearchInput.classList.remove('changed');
const q = queueSearchInput.value.trim(); queueSearchInput.value = this.originalValues['queue_name'] || '';
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 = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessuna coda trovata</div>';
queueSuggestionsDiv.style.display = 'block';
return;
}
queueSuggestionsDiv.innerHTML = queues.map(q => {
const displayName = q.name.replace(/::/g, ' › ');
return `
<div class="autocomplete-suggestion-item" data-id="${App.escapeHtml(q.id)}" data-name="${App.escapeHtml(q.name)}" style="padding: 6px 12px; font-size: 0.78rem; line-height: 1.25;">
${App.escapeHtml(displayName)}
</div>
`;
}).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'));
} }
}); if (customerSearchInput) {
const first = this.originalValues['customer_first'] || '';
queueSearchInput.addEventListener('focus', () => { const last = this.originalValues['customer_last'] || '';
queueSearchInput.value = ''; customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || '');
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 = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun utente trovato</div>';
customerSuggestionsDiv.style.display = 'block';
return;
}
customerSuggestionsDiv.innerHTML = users.map(u => `
<div class="autocomplete-suggestion-item" data-login="${App.escapeHtml(u.login || '')}" data-customer-id="${App.escapeHtml(u.customer_id || '')}" data-name="${App.escapeHtml((u.first_name + ' ' + u.last_name).trim() || u.login || '')}">
<strong>${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)}</strong>
<span style="font-size:0.75rem; color:var(--text-muted); margin-left:var(--space-xs); font-weight:normal;">(Login: ${App.escapeHtml(u.login || '')} | Azienda: ${App.escapeHtml(u.customer_id || '—')})</span>
</div>
`).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 (noteSubjectInput) noteSubjectInput.value = '';
customerSearchInput.addEventListener('blur', () => { if (this.noteQuill) this.noteQuill.root.innerHTML = '';
setTimeout(() => { customerSuggestionsDiv.style.display = 'none'; }, 150); 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 // Unified Save Quick Edit & Send Note action
resetBtn.addEventListener('click', () => { if (saveBtn) {
fields.forEach(el => { saveBtn.addEventListener('click', async () => {
el.value = this.originalValues[el.dataset.field] || ''; const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : '';
el.classList.remove('changed'); if (queueSearchInput && !queueIdInput.value) {
}); Toast.warning('Seleziona una coda valida dall\'elenco.');
if (queueSearchInput) { return;
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;
});
// Save quick-edit const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : '';
saveBtn.addEventListener('click', async () => { if (customerSearch && customerUserIdInput && !customerUserIdInput.value) {
const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : ''; customerUserIdInput.value = customerSearch;
if (queueSearchInput && !queueIdInput.value) { if (customerIdInput) customerIdInput.value = customerSearch;
Toast.warning('Seleziona una coda valida dall\'elenco.'); }
// 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 === '<p><br></p>') 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; 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 { try {
saveBtn.disabled = true; saveBtn.disabled = true;
saveBtn.textContent = 'Salvando...'; saveBtn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px;"></div> Salvataggio...';
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';
}
});
// Shortcut for Quick Edit save // 1. Perform metadata updates if changed
const quickEditContainer = container.querySelector('.quick-edit'); if (hasMetadataChanges) {
if (quickEditContainer) { await App.api(`/api/tickets/${this.ticketId}`, {
quickEditContainer.addEventListener('keydown', (e) => { method: 'PATCH',
if (App.isShortcutTriggered(e, 'quick_edit_save')) { body: JSON.stringify(updates),
e.preventDefault(); });
if (saveBtn && !saveBtn.disabled) { }
saveBtn.click();
// 2. Perform article note creation if note fields are populated
if (hasNoteContent) {
if (!noteBody && noteSubject) {
noteBody = `<p>${App.escapeHtml(noteSubject)}</p>`;
}
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 === '<p><br></p>') {
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 = `<p>${App.escapeHtml(subject)}</p>`;
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 = '<div class="spinner" style="width:14px;height:14px;border-width:2px;"></div> 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.clearDraft(this.ticketId, 'note');
App.updateDailyTimer(); App.updateDailyTimer();
TicketListView.invalidateCache();
this.render(this.ticketId); this.render(this.ticketId);
} catch (err) { } catch (err) {
Toast.error('Errore: ' + err.message); Toast.error('Errore durante il salvataggio: ' + err.message);
noteSendBtn.disabled = false; saveBtn.disabled = false;
noteSendBtn.innerHTML = 'Invia Nota';
} }
}); });
}
// Note Attachments Upload Handlers // Note Attachments Upload Handlers
const noteAttachBtn = document.getElementById('btn-note-add-attachments'); 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);
}, },
}; };