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
+213
-67
No files matched your search
+1
-1
@@ -88,7 +88,7 @@
|
|||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
<line x1="16" y1="13" x2="8" y2="13" />
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
<line x1="16" y1="17" x2="8" y2="17" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Note Temporanee</span>
|
<span>Note</span>
|
||||||
<span class="nav-badge" id="temp-notes-count"></span>
|
<span class="nav-badge" id="temp-notes-count"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
+12
-3
@@ -313,7 +313,7 @@ const App = {
|
|||||||
|
|
||||||
} else if (hash === '#/note-temporanee') {
|
} else if (hash === '#/note-temporanee') {
|
||||||
document.getElementById('nav-note-temporanee')?.classList.add('active');
|
document.getElementById('nav-note-temporanee')?.classList.add('active');
|
||||||
titleEl.textContent = 'Note Temporanee';
|
titleEl.textContent = 'Note';
|
||||||
NoteTemporaneeView.render();
|
NoteTemporaneeView.render();
|
||||||
|
|
||||||
} else if (hash === '#/tickets/new') {
|
} else if (hash === '#/tickets/new') {
|
||||||
@@ -897,7 +897,7 @@ const App = {
|
|||||||
<p style="margin-bottom: var(--space-lg, 24px); color: var(--text-muted, #a6adc8); font-size: 0.95rem; line-height: 1.5;">${message}</p>
|
<p style="margin-bottom: var(--space-lg, 24px); color: var(--text-muted, #a6adc8); font-size: 0.95rem; line-height: 1.5;">${message}</p>
|
||||||
<div style="display: flex; gap: var(--space-sm, 12px); justify-content: flex-end;">
|
<div style="display: flex; gap: var(--space-sm, 12px); justify-content: flex-end;">
|
||||||
<button id="confirm-btn-cancel" class="btn btn-ghost" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px);">${options.cancelText || 'Annulla'}</button>
|
<button id="confirm-btn-cancel" class="btn btn-ghost" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px);">${options.cancelText || 'Annulla'}</button>
|
||||||
<button id="confirm-btn-ok" class="btn btn-danger" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px); font-weight: 500; cursor: pointer;">${options.confirmText || 'Conferma'}</button>
|
<button id="confirm-btn-ok" class="btn ${options.confirmClass || 'btn-danger'}" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px); font-weight: 500; cursor: pointer;">${options.confirmText || 'Conferma'}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1136,10 +1136,19 @@ const App = {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelect(items[currentIndex]);
|
onSelect(items[currentIndex]);
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Tab' || e.key === 'Escape') {
|
||||||
|
if (currentIndex >= 0 && items[currentIndex] && e.key === 'Tab') {
|
||||||
|
onSelect(items[currentIndex]);
|
||||||
|
}
|
||||||
suggestionsEl.style.display = 'none';
|
suggestionsEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputEl.addEventListener('blur', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (suggestionsEl) suggestionsEl.style.display = 'none';
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const NoteTemporaneeView = {
|
|||||||
<div style="display:flex; align-items:center; gap: 10px;">
|
<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 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>
|
<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);">
|
<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 🚀.
|
Le note vengono mantenute in locale finché non le elimini con il cestino 🗑️ o le invii a OTRS 🚀.
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
<div style="display:flex; align-items:center; gap:4px;">
|
<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-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -375,10 +382,10 @@ const NoteTemporaneeView = {
|
|||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Bind item actions
|
// Bind item actions
|
||||||
listContainer.querySelectorAll('.btn-edit-note').forEach(btn => {
|
listContainer.querySelectorAll('.btn-delete-note').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
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 => {
|
listContainer.querySelectorAll('.saved-note-card').forEach(card => {
|
||||||
card.addEventListener('click', () => {
|
card.addEventListener('click', () => {
|
||||||
this.loadNoteIntoForm(parseInt(card.dataset.noteId, 10));
|
this.loadNoteIntoForm(parseInt(card.dataset.noteId, 10));
|
||||||
@@ -447,7 +447,7 @@ const NoteTemporaneeView = {
|
|||||||
const deleteBtn = document.getElementById('note-delete-current-btn');
|
const deleteBtn = document.getElementById('note-delete-current-btn');
|
||||||
const ticketClearBtn = document.getElementById('note-ticket-clear-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 (ticketInput) ticketInput.value = '';
|
||||||
if (ticketClearBtn) ticketClearBtn.style.display = 'none';
|
if (ticketClearBtn) ticketClearBtn.style.display = 'none';
|
||||||
if (subjectInput) subjectInput.value = '';
|
if (subjectInput) subjectInput.value = '';
|
||||||
@@ -487,7 +487,7 @@ const NoteTemporaneeView = {
|
|||||||
try {
|
try {
|
||||||
const res = await App.api('/api/notes', {
|
const res = await App.api('/api/notes', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: payload
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
Toast.success('Nota temporanea salvata nel database locale!');
|
Toast.success('Nota temporanea salvata nel database locale!');
|
||||||
@@ -518,12 +518,19 @@ const NoteTemporaneeView = {
|
|||||||
const note = this.savedNotes.find(n => n.id === noteId);
|
const note = this.savedNotes.find(n => n.id === noteId);
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
|
|
||||||
if (!note.ticket_tn && !note.ticket_id) {
|
const hasTicket = Boolean(note.ticket_tn || note.ticket_id);
|
||||||
Toast.error('Inserisci un ticket di riferimento (TN o ID) prima di inviare a OTRS');
|
const modalTitle = hasTicket ? 'Invia Nota a OTRS' : 'Crea Nuovo Ticket da Nota';
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +556,12 @@ const NoteTemporaneeView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async deleteNote(noteId) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const RicercaSpudorataView = {
|
|||||||
<div style="display:flex; align-items:center; gap: 10px;">
|
<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 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>
|
<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);">
|
<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.
|
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>
|
</p>
|
||||||
|
|||||||
@@ -462,10 +462,11 @@ const TicketCreateView = {
|
|||||||
`).join('');
|
`).join('');
|
||||||
ownerSuggestionsDiv.style.display = 'block';
|
ownerSuggestionsDiv.style.display = 'block';
|
||||||
|
|
||||||
// Bind click
|
// Bind click/mousedown
|
||||||
ownerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
ownerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||||
if (item.dataset.id) {
|
if (item.dataset.id) {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
ownerSearchInput.value = item.dataset.name;
|
ownerSearchInput.value = item.dataset.name;
|
||||||
ownerIdInput.value = item.dataset.id;
|
ownerIdInput.value = item.dataset.id;
|
||||||
ownerSuggestionsDiv.style.display = 'none';
|
ownerSuggestionsDiv.style.display = 'none';
|
||||||
@@ -517,10 +518,11 @@ const TicketCreateView = {
|
|||||||
`).join('');
|
`).join('');
|
||||||
responsibleSuggestionsDiv.style.display = 'block';
|
responsibleSuggestionsDiv.style.display = 'block';
|
||||||
|
|
||||||
// Bind click
|
// Bind click/mousedown
|
||||||
responsibleSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
responsibleSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||||
if (item.dataset.id) {
|
if (item.dataset.id) {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
responsibleSearchInput.value = item.dataset.name;
|
responsibleSearchInput.value = item.dataset.name;
|
||||||
responsibleIdInput.value = item.dataset.id;
|
responsibleIdInput.value = item.dataset.id;
|
||||||
responsibleSuggestionsDiv.style.display = 'none';
|
responsibleSuggestionsDiv.style.display = 'none';
|
||||||
@@ -571,10 +573,11 @@ const TicketCreateView = {
|
|||||||
}).join('');
|
}).join('');
|
||||||
queueSuggestionsDiv.style.display = 'block';
|
queueSuggestionsDiv.style.display = 'block';
|
||||||
|
|
||||||
// Bind click
|
// Bind click/mousedown
|
||||||
queueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
queueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||||
if (item.dataset.id) {
|
if (item.dataset.id) {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
queueSearchInput.value = item.dataset.name;
|
queueSearchInput.value = item.dataset.name;
|
||||||
queueIdInput.value = item.dataset.id;
|
queueIdInput.value = item.dataset.id;
|
||||||
queueSuggestionsDiv.style.display = 'none';
|
queueSuggestionsDiv.style.display = 'none';
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ const TicketDetailView = {
|
|||||||
<label class="quick-edit-label">Tempo (minuti)</label>
|
<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;" />
|
<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>
|
||||||
|
<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">
|
<div class="quick-edit-field full-width">
|
||||||
<label class="quick-edit-label">Nota / Commento</label>
|
<label class="quick-edit-label">Nota / Commento</label>
|
||||||
<textarea class="quick-edit-textarea" id="qe-note" placeholder="Inserisci una nota..."></textarea>
|
<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 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 timeUnitInput = document.getElementById('qe-time-unit');
|
||||||
|
const noteSubjectInput = document.getElementById('qe-note-subject');
|
||||||
const noteInput = document.getElementById('qe-note');
|
const noteInput = document.getElementById('qe-note');
|
||||||
|
|
||||||
const checkChanges = () => {
|
const checkChanges = () => {
|
||||||
@@ -622,6 +627,14 @@ const TicketDetailView = {
|
|||||||
hasChanges = true;
|
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() : '';
|
const noteText = noteInput ? noteInput.value.trim() : '';
|
||||||
if (noteInput) {
|
if (noteInput) {
|
||||||
noteInput.classList.toggle('changed', noteText.length > 0);
|
noteInput.classList.toggle('changed', noteText.length > 0);
|
||||||
@@ -633,11 +646,17 @@ const TicketDetailView = {
|
|||||||
saveBtn.disabled = !hasChanges;
|
saveBtn.disabled = !hasChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
fields.forEach(el => el.addEventListener('change', checkChanges));
|
fields.forEach(el => {
|
||||||
|
el.addEventListener('change', checkChanges);
|
||||||
|
});
|
||||||
if (timeUnitInput) {
|
if (timeUnitInput) {
|
||||||
timeUnitInput.addEventListener('input', checkChanges);
|
timeUnitInput.addEventListener('input', checkChanges);
|
||||||
timeUnitInput.addEventListener('change', checkChanges);
|
timeUnitInput.addEventListener('change', checkChanges);
|
||||||
}
|
}
|
||||||
|
if (noteSubjectInput) {
|
||||||
|
noteSubjectInput.addEventListener('input', checkChanges);
|
||||||
|
noteSubjectInput.addEventListener('change', checkChanges);
|
||||||
|
}
|
||||||
if (noteInput) {
|
if (noteInput) {
|
||||||
noteInput.addEventListener('input', checkChanges);
|
noteInput.addEventListener('input', checkChanges);
|
||||||
noteInput.addEventListener('change', checkChanges);
|
noteInput.addEventListener('change', checkChanges);
|
||||||
@@ -796,6 +815,10 @@ const TicketDetailView = {
|
|||||||
if (timeUnitInput) {
|
if (timeUnitInput) {
|
||||||
timeUnitInput.value = '';
|
timeUnitInput.value = '';
|
||||||
}
|
}
|
||||||
|
if (noteSubjectInput) {
|
||||||
|
noteSubjectInput.value = '';
|
||||||
|
noteSubjectInput.classList.remove('changed');
|
||||||
|
}
|
||||||
if (noteInput) {
|
if (noteInput) {
|
||||||
noteInput.value = '';
|
noteInput.value = '';
|
||||||
noteInput.classList.remove('changed');
|
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()) {
|
if (noteInput && noteInput.value.trim()) {
|
||||||
updates.note = noteInput.value.trim();
|
updates.note = noteInput.value.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,16 @@ const TicketGroupsView = {
|
|||||||
const container = document.getElementById('view-container');
|
const container = document.getElementById('view-container');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="ticket-groups-container" style="display: flex; gap: var(--space-lg); min-height: calc(100vh - 120px); padding: var(--space-lg) 0;">
|
<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 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);">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Group Modal -->
|
<!-- Create/Edit Group Modal -->
|
||||||
@@ -89,8 +89,29 @@ const TicketGroupsView = {
|
|||||||
masterInput.value = prefillMasterTn;
|
masterInput.value = prefillMasterTn;
|
||||||
}
|
}
|
||||||
} else if (this.selectedGroupId) {
|
} else if (this.selectedGroupId) {
|
||||||
|
const exists = this.groups.some(g => g.id === this.selectedGroupId);
|
||||||
|
if (exists) {
|
||||||
this.selectGroup(this.selectedGroupId);
|
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() {
|
async loadGroups() {
|
||||||
@@ -166,12 +187,11 @@ const TicketGroupsView = {
|
|||||||
this.selectedGroupData = await App.api(`/api/groups/${id}`);
|
this.selectedGroupData = await App.api(`/api/groups/${id}`);
|
||||||
this.renderGroupDetails();
|
this.renderGroupDetails();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.error('Errore nel caricamento del dettaglio gruppo: ' + err.message);
|
console.warn(`[TicketGroups] Failed to load group ${id}:`, err.message);
|
||||||
detailPane.innerHTML = `
|
this.selectedGroupId = null;
|
||||||
<div style="color: var(--danger); text-align: center; padding: var(--space-xl);">
|
localStorage.removeItem('otrs_selected_group_id');
|
||||||
Errore nel caricamento dei dati: ${err.message}
|
this.renderGroupsList();
|
||||||
</div>
|
this.renderEmptyDetailsPane();
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+86
-12
@@ -135,14 +135,13 @@ router.post('/:id/submit', async (req, res) => {
|
|||||||
return res.status(404).json({ error: 'Nota temporanea non trovata' });
|
return res.status(404).json({ error: 'Nota temporanea non trovata' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!note.ticket_id && !note.ticket_tn) {
|
let resolved = null;
|
||||||
return res.status(400).json({ error: 'Impossibile inviare a OTRS: nessun ticket (TN/ID) associato a questa nota' });
|
if (note.ticket_id || note.ticket_tn) {
|
||||||
}
|
resolved = await resolveTicketInfo(note.ticket_id, note.ticket_tn);
|
||||||
|
|
||||||
const resolved = await resolveTicketInfo(note.ticket_id, note.ticket_tn);
|
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
return res.status(400).json({ error: `Ticket non trovato nel sistema OTRS (TN: ${note.ticket_tn || ''}, ID: ${note.ticket_id || ''})` });
|
return res.status(400).json({ error: `Ticket non trovato nel sistema OTRS (TN: ${note.ticket_tn || ''}, ID: ${note.ticket_id || ''})` });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get agent login user
|
// Get agent login user
|
||||||
let userLogin = process.env.OTRS_API_USER || 'root@localhost';
|
let userLogin = process.env.OTRS_API_USER || 'root@localhost';
|
||||||
@@ -156,6 +155,81 @@ router.post('/:id/submit', async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
// If no ticket is associated, create a new ticket from this note
|
||||||
|
if (!resolved) {
|
||||||
|
// Default queue (first queue or 1)
|
||||||
|
let queueId = 1;
|
||||||
|
const qRes = await pool.query('SELECT id FROM queue ORDER BY id ASC LIMIT 1');
|
||||||
|
if (qRes.rows.length > 0) queueId = qRes.rows[0].id;
|
||||||
|
|
||||||
|
// Default state ('new' / 1)
|
||||||
|
let stateId = 1;
|
||||||
|
const sRes = await pool.query(`SELECT ts.id FROM ticket_state ts JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name = 'new' LIMIT 1`);
|
||||||
|
if (sRes.rows.length > 0) stateId = sRes.rows[0].id;
|
||||||
|
|
||||||
|
const ticketTitle = note.subject || 'Nuova Nota Temporanea';
|
||||||
|
|
||||||
|
// Generate TN
|
||||||
|
const now = new Date();
|
||||||
|
const systemId = process.env.OTRS_SYSTEM_ID || String(agentId);
|
||||||
|
const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`;
|
||||||
|
|
||||||
|
const maxTicketResult = await pool.query(
|
||||||
|
`SELECT tn FROM ticket WHERE tn LIKE $1`,
|
||||||
|
[`${datePrefix}%`]
|
||||||
|
);
|
||||||
|
let maxCounter = 0;
|
||||||
|
for (const row of maxTicketResult.rows) {
|
||||||
|
const counterPart = row.tn.substring(datePrefix.length);
|
||||||
|
const parsed = parseInt(counterPart, 10);
|
||||||
|
if (!isNaN(parsed) && parsed > maxCounter) {
|
||||||
|
maxCounter = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const counterPadding = parseInt(process.env.OTRS_COUNTER_PADDING, 10) || 6;
|
||||||
|
const newTn = `${datePrefix}${String(maxCounter + 1).padStart(counterPadding, '0')}`;
|
||||||
|
|
||||||
|
// Insert new ticket into DB
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const nowTs = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||||
|
|
||||||
|
const newTicketRes = await client.query(
|
||||||
|
`INSERT INTO ticket (
|
||||||
|
tn, title, queue_id, ticket_lock_id, type_id,
|
||||||
|
user_id, responsible_user_id, ticket_priority_id, ticket_state_id,
|
||||||
|
create_time, create_by, change_time, change_by
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, 1, 1,
|
||||||
|
$4, $4, 3, $5,
|
||||||
|
$6, $4, $6, $4
|
||||||
|
) RETURNING id, tn`,
|
||||||
|
[newTn, ticketTitle, queueId, agentId, stateId, nowTs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newTicketId = newTicketRes.rows[0].id;
|
||||||
|
resolved = { id: newTicketId, tn: newTn, title: ticketTitle };
|
||||||
|
|
||||||
|
// Insert NewTicket history
|
||||||
|
const htRes = await client.query(`SELECT id FROM ticket_history_type WHERE name = 'NewTicket'`);
|
||||||
|
const htId = htRes.rows.length > 0 ? htRes.rows[0].id : 1;
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO ticket_history (
|
||||||
|
name, history_type_id, ticket_id, type_id, queue_id, owner_id, priority_id, state_id, create_time, create_by, change_time, change_by
|
||||||
|
) VALUES ('%%', $1, $2, 1, $3, $4, 3, $5, $6, $4, $6, $4)`,
|
||||||
|
[htId, newTicketId, queueId, agentId, stateId, nowTs]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} catch (createErr) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw createErr;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare Article for OTRS REST API
|
// Prepare Article for OTRS REST API
|
||||||
const isInternal = note.note_type === 'note-internal';
|
const isInternal = note.note_type === 'note-internal';
|
||||||
const isEmail = note.note_type === 'email-external';
|
const isEmail = note.note_type === 'email-external';
|
||||||
@@ -195,13 +269,13 @@ router.post('/:id/submit', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log action
|
// Log action
|
||||||
logAttivita(
|
logAttivita({
|
||||||
agentId,
|
agente_id: agentId,
|
||||||
userLogin,
|
agente_nome: userLogin,
|
||||||
`Aggiunta nota a Ticket #${resolved.tn}`,
|
titolo_azione: `Aggiunta nota a Ticket #${resolved.tn}`,
|
||||||
{ ticket_id: resolved.id, ticket_tn: resolved.tn, subject: note.subject, time_units: note.time_units },
|
azione: { ticket_id: resolved.id, ticket_tn: resolved.tn, subject: note.subject, time_units: note.time_units },
|
||||||
'successo'
|
esito: 'successo'
|
||||||
);
|
});
|
||||||
|
|
||||||
// Delete note from local SQLite DB upon successful OTRS upload
|
// Delete note from local SQLite DB upon successful OTRS upload
|
||||||
db.prepare('DELETE FROM ticket_draft_notes WHERE id = ?').run(note.id);
|
db.prepare('DELETE FROM ticket_draft_notes WHERE id = ?').run(note.id);
|
||||||
|
|||||||
+8
-7
@@ -482,7 +482,7 @@ router.post('/', async (req, res) => {
|
|||||||
|
|
||||||
// Generate ticket number: get next counter value for today (daily reset)
|
// Generate ticket number: get next counter value for today (daily reset)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const systemId = process.env.OTRS_SYSTEM_ID || '10';
|
const systemId = process.env.OTRS_SYSTEM_ID || String(operatorId);
|
||||||
const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`;
|
const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`;
|
||||||
|
|
||||||
// 1. Fetch current maximum counter in ticket table for today
|
// 1. Fetch current maximum counter in ticket table for today
|
||||||
@@ -748,8 +748,9 @@ router.patch('/:id', async (req, res) => {
|
|||||||
const updates = req.body; // { ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, note }
|
const updates = req.body; // { ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, note }
|
||||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||||
const timeUnit = parseFloat(updates.time_unit);
|
const timeUnit = parseFloat(updates.time_unit);
|
||||||
|
const noteSubjectText = typeof updates.note_subject === 'string' ? updates.note_subject.trim() : '';
|
||||||
const noteText = typeof updates.note === 'string' ? updates.note.trim() : (typeof updates.note_text === 'string' ? updates.note_text.trim() : '');
|
const noteText = typeof updates.note === 'string' ? updates.note.trim() : (typeof updates.note_text === 'string' ? updates.note_text.trim() : '');
|
||||||
const hasNote = noteText.length > 0;
|
const hasNote = noteText.length > 0 || noteSubjectText.length > 0;
|
||||||
|
|
||||||
// Fetch current ticket first (needed for both REST dummy fields and database fallback)
|
// Fetch current ticket first (needed for both REST dummy fields and database fallback)
|
||||||
let current;
|
let current;
|
||||||
@@ -810,8 +811,8 @@ router.patch('/:id', async (req, res) => {
|
|||||||
Ticket: ticketFields
|
Ticket: ticketFields
|
||||||
};
|
};
|
||||||
if (hasNote || (!isNaN(timeUnit) && timeUnit > 0)) {
|
if (hasNote || (!isNaN(timeUnit) && timeUnit > 0)) {
|
||||||
const noteSubject = hasNote ? noteText : 'Consuntivazione';
|
const noteSubject = noteSubjectText || (hasNote ? noteText : 'Consuntivazione');
|
||||||
const noteBody = hasNote ? noteText : 'Consuntivazione';
|
const noteBody = noteText || noteSubjectText || 'Consuntivazione';
|
||||||
const articleObj = {
|
const articleObj = {
|
||||||
CommunicationChannel: 'Internal',
|
CommunicationChannel: 'Internal',
|
||||||
SenderType: 'agent',
|
SenderType: 'agent',
|
||||||
@@ -993,8 +994,8 @@ router.patch('/:id', async (req, res) => {
|
|||||||
const articleId = articleResult.rows[0].id;
|
const articleId = articleResult.rows[0].id;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const contentPath = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
|
const contentPath = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
|
||||||
const noteSubject = hasNote ? noteText : 'Consuntivazione';
|
const noteSubject = noteSubjectText || (hasNote ? noteText : 'Consuntivazione');
|
||||||
const noteBodyRaw = hasNote ? noteText : 'Consuntivazione';
|
const noteBodyRaw = noteText || noteSubjectText || 'Consuntivazione';
|
||||||
const safeBody = noteBodyRaw.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br/>');
|
const safeBody = noteBodyRaw.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br/>');
|
||||||
const htmlBody = `<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">${safeBody}</body></html>`;
|
const htmlBody = `<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">${safeBody}</body></html>`;
|
||||||
const binaryBody = Buffer.from(htmlBody, 'utf-8');
|
const binaryBody = Buffer.from(htmlBody, 'utf-8');
|
||||||
@@ -2210,7 +2211,7 @@ router.post('/auto-time', async (req, res) => {
|
|||||||
|
|
||||||
// Generate ticket number (daily reset)
|
// Generate ticket number (daily reset)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const systemId = process.env.OTRS_SYSTEM_ID || '10';
|
const systemId = process.env.OTRS_SYSTEM_ID || String(operatorId);
|
||||||
const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`;
|
const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`;
|
||||||
|
|
||||||
const maxTicketResult = await client.query(
|
const maxTicketResult = await client.query(
|
||||||
|
|||||||
Reference in new issue
Block a user