diff --git a/public/css/style.css b/public/css/style.css index cb631e1..9e31f8f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1793,21 +1793,22 @@ body { top: 100%; left: 0; right: 0; - background: #1f2937; - border: 1px solid #4b5563; + background: var(--bg-card); + border: 1px solid var(--border-light); border-radius: var(--radius-md); max-height: 220px; overflow-y: auto; z-index: 1000; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + box-shadow: var(--shadow-lg); + backdrop-filter: blur(8px); } .autocomplete-suggestion-item { padding: 10px 14px; cursor: pointer; - border-bottom: 1px solid rgba(156, 163, 175, 0.15); + border-bottom: 1px solid var(--border-subtle); font-size: 0.85rem; - color: #f3f4f6; + color: var(--text-primary); transition: all var(--transition-fast); } @@ -1817,7 +1818,7 @@ body { } .autocomplete-suggestion-item:hover span { - color: #e5e7eb !important; + color: #ffffff !important; } .autocomplete-suggestion-item:last-child { @@ -1943,8 +1944,8 @@ body { } .bulk-select option { - background-color: #111827; - /* Sfondo scuro per gli stati e i tipi */ + background-color: var(--bg-secondary); + /* Sfondo per gli stati e i tipi */ color: var(--text-primary); } @@ -1955,23 +1956,24 @@ body { width: max-content; min-width: 100%; max-width: 400px; - background: #1f2937; + background: var(--bg-card); border: 1px solid var(--border-light); border-radius: var(--radius-md); max-height: 380px; /* Incrementata l'altezza per contenere più elementi */ overflow-y: auto; z-index: 999; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.6); + box-shadow: var(--shadow-lg); + backdrop-filter: blur(8px); } .bulk-suggestions-item { padding: 6px 10px; /* Compatto */ cursor: pointer; - border-bottom: 1px solid rgba(156, 163, 175, 0.1); + border-bottom: 1px solid var(--border-subtle); font-size: 0.8rem; - color: #e5e7eb; + color: var(--text-primary); } .bulk-suggestions-item:hover { @@ -1979,6 +1981,10 @@ body { color: white; } +.bulk-suggestions-item:hover span { + color: white !important; +} + .bulk-suggestions-item:last-child { border-bottom: none; } diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index d9b8302..6b4aca5 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -40,6 +40,10 @@ const TicketDetailView = { queue_id: ticket.queue_id, user_id: ticket.user_id, type_id: ticket.type_id, + customer_id: ticket.customer_id, + customer_user_id: ticket.customer_user_id, + customer_first: ticket.customer_first, + customer_last: ticket.customer_last, }; container.innerHTML = ` @@ -113,6 +117,13 @@ const TicketDetailView = { ` : ''} +
+ + + + + +
@@ -340,41 +351,117 @@ const TicketDetailView = { bindEvents() { // Quick-edit change detection - const selects = document.querySelectorAll('.quick-edit-select'); + const fields = document.querySelectorAll('.quick-edit-select, #qe-customer-user-id, #qe-customer-id'); const saveBtn = document.getElementById('qe-save'); const resetBtn = document.getElementById('qe-reset'); const checkChanges = () => { let hasChanges = false; - selects.forEach(sel => { - const field = sel.dataset.field; + fields.forEach(el => { + const field = el.dataset.field; const original = String(this.originalValues[field] || ''); - const current = sel.value; + const current = el.value; const changed = current !== original; - sel.classList.toggle('changed', changed); + el.classList.toggle('changed', changed); if (changed) hasChanges = true; }); saveBtn.disabled = !hasChanges; }; - selects.forEach(sel => sel.addEventListener('change', checkChanges)); + fields.forEach(el => el.addEventListener('change', checkChanges)); + + // 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(); + if (q.length < 2) { + customerSuggestionsDiv.style.display = 'none'; + customerUserIdInput.value = ''; + customerIdInput.value = ''; + customerUserIdInput.dispatchEvent(new Event('change')); + customerIdInput.dispatchEvent(new Event('change')); + return; + } + + customerDebounce = setTimeout(async () => { + try { + const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); + if (users.length === 0) { + customerSuggestionsDiv.innerHTML = '
Nessun utente trovato
'; + customerSuggestionsDiv.style.display = 'block'; + return; + } + + customerSuggestionsDiv.innerHTML = users.map(u => ` +
+ ${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)} + (Login: ${App.escapeHtml(u.login)} | Azienda: ${App.escapeHtml(u.customer_id || '—')}) +
+ `).join(''); + customerSuggestionsDiv.style.display = 'block'; + + // Bind click + customerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { + if (item.dataset.login) { + item.addEventListener('click', () => { + 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); + }); + } + + // Close suggestions on click outside + document.addEventListener('click', (e) => { + if (customerSearchInput && e.target !== customerSearchInput && e.target !== customerSuggestionsDiv) { + customerSuggestionsDiv.style.display = 'none'; + } + }); // Reset quick-edit resetBtn.addEventListener('click', () => { - selects.forEach(sel => { - sel.value = this.originalValues[sel.dataset.field] || ''; - sel.classList.remove('changed'); + fields.forEach(el => { + el.value = this.originalValues[el.dataset.field] || ''; + el.classList.remove('changed'); }); + if (customerSearchInput) { + const first = this.originalValues['customer_first'] || ''; + const last = this.originalValues['customer_last'] || ''; + customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || ''); + } saveBtn.disabled = true; }); // Save quick-edit saveBtn.addEventListener('click', async () => { const updates = {}; - selects.forEach(sel => { - const field = sel.dataset.field; - const val = sel.value ? parseInt(sel.value) : null; - if (val !== null && val !== this.originalValues[field]) { + 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; } }); diff --git a/routes/tickets.js b/routes/tickets.js index 4f5930d..d1198b8 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -483,6 +483,8 @@ router.patch('/:id', async (req, res) => { if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id; if (updates.title !== undefined) ticketFields.Title = updates.title; if (updates.ticket_lock_id !== undefined) ticketFields.LockID = updates.ticket_lock_id; + if (updates.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id; + if (updates.customer_user_id !== undefined) ticketFields.CustomerUser = updates.customer_user_id; // Auto sblocco check if (updates.ticket_state_id) { @@ -525,7 +527,7 @@ router.patch('/:id', async (req, res) => { // Fetch current ticket for history comparison const currentResult = await client.query( - `SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, ticket_lock_id + `SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, ticket_lock_id, customer_id, customer_user_id FROM ticket WHERE id = $1`, [id] ); @@ -557,7 +559,7 @@ router.patch('/:id', async (req, res) => { const setParams = []; let pIdx = 1; - const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'type_id', 'title', 'ticket_lock_id']; + const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'type_id', 'title', 'ticket_lock_id', 'customer_id', 'customer_user_id']; for (const field of allowedFields) { if (updates[field] !== undefined && updates[field] !== current[field]) { setClauses.push(`${field} = $${pIdx++}`); @@ -589,6 +591,8 @@ router.patch('/:id', async (req, res) => { user_id: 'OwnerUpdate', type_id: 'TypeUpdate', ticket_lock_id: 'Lock', + customer_id: 'CustomerUpdate', + customer_user_id: 'CustomerUpdate', }; for (const field of allowedFields) {