fix: note temporanee che non funzionavano. feat: le note possono essere inviate ad otrs senza numero di ticket. style: rivisti alcuni nomi e colore per invio di una nota temporanea in otrs.
This commit is contained in:
1 parent
b828d60bb2
commit
f21be0abcb
9 files changed
+216
-70
No files matched your search
@@ -23,7 +23,7 @@ const NoteTemporaneeView = {
|
||||
<div style="display:flex; align-items:center; gap: 10px;">
|
||||
<div style="display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; background: linear-gradient(135deg, rgba(79, 70, 229, 0.2), rgba(16, 185, 129, 0.2)); border-radius: 50%; color: var(--accent-primary); font-size:1.2rem; font-weight:bold;">📝</div>
|
||||
<div>
|
||||
<h2 style="margin:0; font-size:1.2rem; font-weight:700; color:var(--text-primary);" id="editor-title-text">Nuova Nota Temporanea</h2>
|
||||
<h2 style="margin:0; font-size:1.2rem; font-weight:700; color:var(--text-primary);" id="editor-title-text">Nuova Nota</h2>
|
||||
<p style="margin:2px 0 0 0; font-size:0.82rem; color:var(--text-secondary);">
|
||||
Le note vengono mantenute in locale finché non le elimini con il cestino 🗑️ o le invii a OTRS 🚀.
|
||||
</p>
|
||||
@@ -173,6 +173,14 @@ const NoteTemporaneeView = {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent tab navigation on Quill toolbar items
|
||||
const toolbar = document.querySelector('#note-body-editor-wrapper .ql-toolbar');
|
||||
if (toolbar) {
|
||||
toolbar.querySelectorAll('button, select, span[role="button"], input').forEach(el => {
|
||||
el.setAttribute('tabindex', '-1');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -364,9 +372,8 @@ const NoteTemporaneeView = {
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:4px;">
|
||||
<button class="btn btn-ghost btn-xs btn-edit-note" data-id="${n.id}" title="Carica ed modifica nota" style="padding:2px 5px; font-size:0.75rem;">✏️</button>
|
||||
<button class="btn btn-ghost btn-xs btn-submit-note" data-id="${n.id}" title="Invia direttamente a OTRS" style="padding:2px 5px; font-size:0.75rem; color:var(--accent-primary);">🚀</button>
|
||||
<button class="btn btn-ghost btn-xs btn-delete-note" data-id="${n.id}" title="Elimina definitivamente dal DB locale" style="padding:2px 5px; font-size:0.75rem; color:var(--error);">🗑️</button>
|
||||
<button class="btn btn-ghost btn-xs btn-submit-note" data-id="${n.id}" title="Invia direttamente a OTRS" style="padding:2px 5px; font-size:0.75rem; color:var(--accent-primary);">🚀</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -375,10 +382,10 @@ const NoteTemporaneeView = {
|
||||
}).join('');
|
||||
|
||||
// Bind item actions
|
||||
listContainer.querySelectorAll('.btn-edit-note').forEach(btn => {
|
||||
listContainer.querySelectorAll('.btn-delete-note').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.loadNoteIntoForm(parseInt(btn.dataset.id, 10));
|
||||
this.deleteNote(parseInt(btn.dataset.id, 10));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -389,13 +396,6 @@ const NoteTemporaneeView = {
|
||||
});
|
||||
});
|
||||
|
||||
listContainer.querySelectorAll('.btn-delete-note').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.deleteNote(parseInt(btn.dataset.id, 10));
|
||||
});
|
||||
});
|
||||
|
||||
listContainer.querySelectorAll('.saved-note-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
this.loadNoteIntoForm(parseInt(card.dataset.noteId, 10));
|
||||
@@ -447,7 +447,7 @@ const NoteTemporaneeView = {
|
||||
const deleteBtn = document.getElementById('note-delete-current-btn');
|
||||
const ticketClearBtn = document.getElementById('note-ticket-clear-btn');
|
||||
|
||||
if (editorTitle) editorTitle.textContent = 'Nuova Nota Temporanea';
|
||||
if (editorTitle) editorTitle.textContent = 'Nuova Nota';
|
||||
if (ticketInput) ticketInput.value = '';
|
||||
if (ticketClearBtn) ticketClearBtn.style.display = 'none';
|
||||
if (subjectInput) subjectInput.value = '';
|
||||
@@ -487,7 +487,7 @@ const NoteTemporaneeView = {
|
||||
try {
|
||||
const res = await App.api('/api/notes', {
|
||||
method: 'POST',
|
||||
body: payload
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
Toast.success('Nota temporanea salvata nel database locale!');
|
||||
@@ -518,12 +518,19 @@ const NoteTemporaneeView = {
|
||||
const note = this.savedNotes.find(n => n.id === noteId);
|
||||
if (!note) return;
|
||||
|
||||
if (!note.ticket_tn && !note.ticket_id) {
|
||||
Toast.error('Inserisci un ticket di riferimento (TN o ID) prima di inviare a OTRS');
|
||||
return;
|
||||
}
|
||||
const hasTicket = Boolean(note.ticket_tn || note.ticket_id);
|
||||
const modalTitle = hasTicket ? 'Invia Nota a OTRS' : 'Crea Nuovo Ticket da Nota';
|
||||
const confirmMsg = hasTicket
|
||||
? `Sei sicuro di voler inviare questa nota a OTRS per il ticket #${note.ticket_tn || note.ticket_id}? La nota verrà poi rimossa dal database locale.`
|
||||
: `Non hai specificato un ticket. Vuoi creare un NUOVO ticket a partire da questa nota ed inviarla a OTRS?`;
|
||||
|
||||
if (!confirm(`Sei sicuro di voler inviare questa nota a OTRS per il ticket #${note.ticket_tn || note.ticket_id}? La nota verrà poi rimossa dal database locale.`)) {
|
||||
const ok = await App.confirm(modalTitle, confirmMsg, {
|
||||
confirmText: hasTicket ? 'Invia a OTRS' : 'Crea Ticket e Invia',
|
||||
confirmClass: 'btn-success',
|
||||
cancelText: 'Annulla'
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -549,7 +556,12 @@ const NoteTemporaneeView = {
|
||||
},
|
||||
|
||||
async deleteNote(noteId) {
|
||||
if (!confirm('Sei sicuro di voler eliminare questa nota temporanea dal database locale?')) {
|
||||
const ok = await App.confirm(
|
||||
'Elimina Nota Temporanea',
|
||||
'Sei sicuro di voler eliminare questa nota temporanea dal database locale?',
|
||||
{ confirmText: 'Elimina', cancelText: 'Annulla' }
|
||||
);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ const RicercaSpudorataView = {
|
||||
<div style="display:flex; align-items:center; gap: 10px;">
|
||||
<div style="display:inline-flex; align-items:center; justify-content:center; width:36px; height:36px; background: linear-gradient(135deg, rgba(235, 114, 110, 0.2), rgba(160, 65, 71, 0.3)); border-radius: 50%; color: var(--accent-primary); font-size:1.2rem; font-weight:bold;">🚀</div>
|
||||
<div>
|
||||
<h2 style="margin:0; font-size:1.2rem; font-weight:700; color:var(--text-primary);">Ricerca Spudorata Testuale</h2>
|
||||
<h2 style="margin:0; font-size:1.2rem; font-weight:700; color:var(--text-primary);">Ricerca Testuale Spudorata</h2>
|
||||
<p style="margin:2px 0 0 0; font-size:0.82rem; color:var(--text-secondary);">
|
||||
Esegui la ricerca nel testo sui ticket. I filtri applicati successivamente operano <strong>istantaneamente in cache locale</strong> senza fare nuove richieste al server.
|
||||
</p>
|
||||
|
||||
@@ -462,10 +462,11 @@ const TicketCreateView = {
|
||||
`).join('');
|
||||
ownerSuggestionsDiv.style.display = 'block';
|
||||
|
||||
// Bind click
|
||||
// Bind click/mousedown
|
||||
ownerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||
if (item.dataset.id) {
|
||||
item.addEventListener('click', () => {
|
||||
item.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
ownerSearchInput.value = item.dataset.name;
|
||||
ownerIdInput.value = item.dataset.id;
|
||||
ownerSuggestionsDiv.style.display = 'none';
|
||||
@@ -517,10 +518,11 @@ const TicketCreateView = {
|
||||
`).join('');
|
||||
responsibleSuggestionsDiv.style.display = 'block';
|
||||
|
||||
// Bind click
|
||||
// Bind click/mousedown
|
||||
responsibleSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||
if (item.dataset.id) {
|
||||
item.addEventListener('click', () => {
|
||||
item.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
responsibleSearchInput.value = item.dataset.name;
|
||||
responsibleIdInput.value = item.dataset.id;
|
||||
responsibleSuggestionsDiv.style.display = 'none';
|
||||
@@ -571,10 +573,11 @@ const TicketCreateView = {
|
||||
}).join('');
|
||||
queueSuggestionsDiv.style.display = 'block';
|
||||
|
||||
// Bind click
|
||||
// Bind click/mousedown
|
||||
queueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||
if (item.dataset.id) {
|
||||
item.addEventListener('click', () => {
|
||||
item.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
queueSearchInput.value = item.dataset.name;
|
||||
queueIdInput.value = item.dataset.id;
|
||||
queueSuggestionsDiv.style.display = 'none';
|
||||
|
||||
@@ -245,6 +245,10 @@ const TicketDetailView = {
|
||||
<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 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>
|
||||
@@ -599,6 +603,7 @@ const TicketDetailView = {
|
||||
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 checkChanges = () => {
|
||||
@@ -622,6 +627,14 @@ const TicketDetailView = {
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const noteSubText = noteSubjectInput ? noteSubjectInput.value.trim() : '';
|
||||
if (noteSubjectInput) {
|
||||
noteSubjectInput.classList.toggle('changed', noteSubText.length > 0);
|
||||
}
|
||||
if (noteSubText.length > 0) {
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const noteText = noteInput ? noteInput.value.trim() : '';
|
||||
if (noteInput) {
|
||||
noteInput.classList.toggle('changed', noteText.length > 0);
|
||||
@@ -633,11 +646,17 @@ const TicketDetailView = {
|
||||
saveBtn.disabled = !hasChanges;
|
||||
};
|
||||
|
||||
fields.forEach(el => el.addEventListener('change', checkChanges));
|
||||
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);
|
||||
@@ -796,6 +815,10 @@ const TicketDetailView = {
|
||||
if (timeUnitInput) {
|
||||
timeUnitInput.value = '';
|
||||
}
|
||||
if (noteSubjectInput) {
|
||||
noteSubjectInput.value = '';
|
||||
noteSubjectInput.classList.remove('changed');
|
||||
}
|
||||
if (noteInput) {
|
||||
noteInput.value = '';
|
||||
noteInput.classList.remove('changed');
|
||||
@@ -840,6 +863,10 @@ const TicketDetailView = {
|
||||
}
|
||||
}
|
||||
|
||||
if (noteSubjectInput && noteSubjectInput.value.trim()) {
|
||||
updates.note_subject = noteSubjectInput.value.trim();
|
||||
}
|
||||
|
||||
if (noteInput && noteInput.value.trim()) {
|
||||
updates.note = noteInput.value.trim();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,16 @@ const TicketGroupsView = {
|
||||
const container = document.getElementById('view-container');
|
||||
container.innerHTML = `
|
||||
<div class="ticket-groups-container" style="display: flex; gap: var(--space-lg); min-height: calc(100vh - 120px); padding: var(--space-lg) 0;">
|
||||
<!-- Left Sidebar: Groups List -->
|
||||
<!-- Left Content: Group Details -->
|
||||
<div class="group-details-pane card" id="group-details-pane" style="flex: 1; padding: var(--space-lg); display: flex; flex-direction: column; max-height: calc(100vh - 120px); overflow-y: auto;">
|
||||
<div class="empty-state" style="margin: auto; text-align: center; color: var(--text-muted);">
|
||||
<div style="font-size: 3rem; margin-bottom: var(--space-sm);">📂</div>
|
||||
<h4>Nessun gruppo selezionato</h4>
|
||||
<p style="font-size: 0.85rem;">Seleziona un gruppo dall'elenco o creane uno nuovo per iniziare a gestire le relazioni master/slave.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar: Groups List -->
|
||||
<div class="groups-sidebar card" style="flex: 0 0 320px; display: flex; flex-direction: column; padding: var(--space-md); max-height: calc(100vh - 120px); overflow-y: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md);">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 700; color: var(--text-primary);">🗂️ Gruppi Ticket</h3>
|
||||
@@ -25,15 +34,6 @@ const TicketGroupsView = {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Content: Group Details -->
|
||||
<div class="group-details-pane card" id="group-details-pane" style="flex: 1; padding: var(--space-lg); display: flex; flex-direction: column; max-height: calc(100vh - 120px); overflow-y: auto;">
|
||||
<div class="empty-state" style="margin: auto; text-align: center; color: var(--text-muted);">
|
||||
<div style="font-size: 3rem; margin-bottom: var(--space-sm);">📂</div>
|
||||
<h4>Nessun gruppo selezionato</h4>
|
||||
<p style="font-size: 0.85rem;">Seleziona un gruppo dalla barra laterale o creane uno nuovo per iniziare a gestire le relazioni master/slave.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Group Modal -->
|
||||
@@ -89,10 +89,31 @@ const TicketGroupsView = {
|
||||
masterInput.value = prefillMasterTn;
|
||||
}
|
||||
} else if (this.selectedGroupId) {
|
||||
this.selectGroup(this.selectedGroupId);
|
||||
const exists = this.groups.some(g => g.id === this.selectedGroupId);
|
||||
if (exists) {
|
||||
this.selectGroup(this.selectedGroupId);
|
||||
} else {
|
||||
this.selectedGroupId = null;
|
||||
localStorage.removeItem('otrs_selected_group_id');
|
||||
this.renderEmptyDetailsPane();
|
||||
}
|
||||
} else {
|
||||
this.renderEmptyDetailsPane();
|
||||
}
|
||||
},
|
||||
|
||||
renderEmptyDetailsPane() {
|
||||
const detailPane = document.getElementById('group-details-pane');
|
||||
if (!detailPane) return;
|
||||
detailPane.innerHTML = `
|
||||
<div class="empty-state" style="margin: auto; text-align: center; color: var(--text-muted);">
|
||||
<div style="font-size: 3rem; margin-bottom: var(--space-sm);">📂</div>
|
||||
<h4>Nessun gruppo selezionato</h4>
|
||||
<p style="font-size: 0.85rem;">Seleziona un gruppo dalla barra laterale o creane uno nuovo per iniziare a gestire le relazioni master/slave.</p>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
async loadGroups() {
|
||||
try {
|
||||
this.groups = await App.api('/api/groups');
|
||||
@@ -166,12 +187,11 @@ const TicketGroupsView = {
|
||||
this.selectedGroupData = await App.api(`/api/groups/${id}`);
|
||||
this.renderGroupDetails();
|
||||
} catch (err) {
|
||||
Toast.error('Errore nel caricamento del dettaglio gruppo: ' + err.message);
|
||||
detailPane.innerHTML = `
|
||||
<div style="color: var(--danger); text-align: center; padding: var(--space-xl);">
|
||||
Errore nel caricamento dei dati: ${err.message}
|
||||
</div>
|
||||
`;
|
||||
console.warn(`[TicketGroups] Failed to load group ${id}:`, err.message);
|
||||
this.selectedGroupId = null;
|
||||
localStorage.removeItem('otrs_selected_group_id');
|
||||
this.renderGroupsList();
|
||||
this.renderEmptyDetailsPane();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user