feat: unificata interfaccia note rapide con l'inserimento delle note.
This commit is contained in:
1 parent
a5d3431008
commit
b957272eb8
2 files changed
+136
-324
No files matched your search
@@ -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 }
|
||||
},
|
||||
{
|
||||
|
||||
+133
-321
@@ -2322,67 +2322,51 @@ 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 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>
|
||||
|
||||
<!-- Add Note Form -->
|
||||
<div class="add-note-form" style="margin-bottom: var(--space-lg);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: var(--space-md); flex-wrap:wrap; gap:var(--space-sm);">
|
||||
<div class="card-title" style="margin-bottom:0;">Aggiungi Nota</div>
|
||||
<div style="display:flex; align-items:center; gap:var(--space-xs);">
|
||||
<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>
|
||||
<!-- Integrated Note & Article Editor inside Modifica Rapida -->
|
||||
<div style="border-top: 1px solid var(--border-subtle); margin-top: var(--space-lg); padding-top: var(--space-md);">
|
||||
<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;">
|
||||
<span>📝 Aggiungi Nota / Messaggio Interno</span>
|
||||
<span style="font-size: 0.75rem; font-weight: normal; color: var(--text-muted);">Opzionale se aggiorni solo i campi sopra</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: var(--space-sm);">
|
||||
<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;" />
|
||||
</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 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);">
|
||||
<div id="note-body-editor" style="min-height: 160px; 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 class="upload-file-list" id="note-file-list" style="margin-bottom: var(--space-sm);"></div>
|
||||
|
||||
<div style="display:flex; gap:var(--space-md); justify-content:space-between; align-items:center; margin-top:var(--space-md);">
|
||||
<div style="display:flex; gap:var(--space-md); justify-content:space-between; align-items:center; flex-wrap:wrap;">
|
||||
<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;">
|
||||
<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;" />
|
||||
<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;" />
|
||||
|
||||
<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-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 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>
|
||||
|
||||
<!-- Articles Timeline -->
|
||||
<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 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,190 +2790,48 @@ 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 = '<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'));
|
||||
}
|
||||
});
|
||||
|
||||
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 = '<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'));
|
||||
}
|
||||
});
|
||||
customerSearchInput.addEventListener('blur', () => {
|
||||
setTimeout(() => { customerSuggestionsDiv.style.display = 'none'; }, 150);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset quick-edit
|
||||
// Reset quick-edit & note form
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
fields.forEach(el => {
|
||||
el.value = this.originalValues[el.dataset.field] || '';
|
||||
@@ -2999,21 +2846,19 @@ const TicketDetailView = {
|
||||
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;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Save quick-edit
|
||||
// Unified Save Quick Edit & Send Note action
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : '';
|
||||
if (queueSearchInput && !queueIdInput.value) {
|
||||
@@ -3022,11 +2867,12 @@ const TicketDetailView = {
|
||||
}
|
||||
|
||||
const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : '';
|
||||
if (customerSearch && !customerUserIdInput.value) {
|
||||
if (customerSearch && customerUserIdInput && !customerUserIdInput.value) {
|
||||
customerUserIdInput.value = customerSearch;
|
||||
customerIdInput.value = customerSearch;
|
||||
if (customerIdInput) customerIdInput.value = customerSearch;
|
||||
}
|
||||
|
||||
// Check metadata updates
|
||||
const updates = {};
|
||||
fields.forEach(el => {
|
||||
const field = el.dataset.field;
|
||||
@@ -3043,132 +2889,81 @@ const TicketDetailView = {
|
||||
}
|
||||
});
|
||||
|
||||
if (timeUnitInput && timeUnitInput.value.trim()) {
|
||||
const timeVal = parseFloat(timeUnitInput.value.trim());
|
||||
if (!isNaN(timeVal) && timeVal > 0) {
|
||||
updates.time_unit = timeVal;
|
||||
}
|
||||
}
|
||||
// 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 : '';
|
||||
|
||||
if (noteSubjectInput && noteSubjectInput.value.trim()) {
|
||||
updates.note_subject = noteSubjectInput.value.trim();
|
||||
}
|
||||
const hasMetadataChanges = Object.keys(updates).length > 0;
|
||||
const hasNoteContent = !!(noteBody || noteSubject || timeUnitVal || this.noteAttachments.length > 0 || noteCreateTimeVal);
|
||||
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
if (!hasMetadataChanges && !hasNoteContent) {
|
||||
Toast.warning('Nessuna modifica o nota da salvare');
|
||||
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...';
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px;"></div> Salvataggio...';
|
||||
|
||||
const res = await App.api(`/api/tickets/${this.ticketId}/articles`, {
|
||||
// 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 = `<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,
|
||||
body,
|
||||
time_unit,
|
||||
ticket_state_id: ticket_state_id ? parseInt(ticket_state_id) : undefined,
|
||||
subject: noteSubject,
|
||||
body: noteBody,
|
||||
time_unit: timeUnitVal,
|
||||
attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined
|
||||
}),
|
||||
});
|
||||
|
||||
this.noteAttachments = []; // Clear attachments
|
||||
this.noteAttachments = []; // Clear attachments after successful send
|
||||
|
||||
if (note_create_time && res.article_id) {
|
||||
if (noteCreateTimeVal && resNote.article_id) {
|
||||
try {
|
||||
const formattedDate = note_create_time.replace('T', ' ');
|
||||
await App.api(`/api/tickets/articles/${res.article_id}/retrodata-article`, {
|
||||
const formattedDate = noteCreateTimeVal.replace('T', ' ');
|
||||
await App.api(`/api/tickets/articles/${resNote.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);
|
||||
console.warn('Note retrodate error:', retErr);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user