diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index 58b4d41..c72a5d4 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -4201,36 +4201,53 @@ const TicketDetailView = { customerSearchInput.addEventListener('input', () => { clearTimeout(customerDebounce); const q = customerSearchInput.value.trim(); - if (customerUserIdInput) customerUserIdInput.value = ''; - if (customerIdInput) customerIdInput.value = ''; + const origCustFirst = (this.originalValues['customer_first'] || '').trim(); + const origCustLast = (this.originalValues['customer_last'] || '').trim(); + const origCustName = origCustFirst ? `${origCustFirst} ${origCustLast}`.trim() : (this.originalValues['customer_user_id'] || '').trim(); + + if (q !== origCustName) { + if (customerUserIdInput) customerUserIdInput.value = ''; + if (customerIdInput) customerIdInput.value = ''; + } else { + if (customerUserIdInput) customerUserIdInput.value = this.originalValues['customer_user_id'] || ''; + if (customerIdInput) customerIdInput.value = this.originalValues['customer_id'] || ''; + } checkChanges(); customerDebounce = setTimeout(async () => { try { - const customers = await App.api(`/api/customers/search?q=${encodeURIComponent(q)}`); - if (!customers || customers.length === 0) { + const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); + if (!users || users.length === 0) { customerSuggestionsDiv.innerHTML = '
Nessun cliente trovato
'; customerSuggestionsDiv.style.display = 'block'; return; } - customerSuggestionsDiv.innerHTML = customers.map(c => ` -
- ${App.escapeHtml(c.first_name ? `${c.first_name} ${c.last_name}` : c.customer_user_id)} - ${c.email ? `(${App.escapeHtml(c.email)})` : ''} -
- `).join(''); + customerSuggestionsDiv.innerHTML = users.map(u => { + const fullName = (u.first_name || u.last_name) ? `${u.first_name || ''} ${u.last_name || ''}`.trim() : (u.login || ''); + return ` +
+ ${App.escapeHtml(fullName)} + (Login: ${App.escapeHtml(u.login || '')}${u.customer_id ? ` | ${App.escapeHtml(u.customer_id)}` : ''}) + ${u.email ? `
${App.escapeHtml(u.email)}
` : ''} +
+ `; + }).join(''); customerSuggestionsDiv.style.display = 'block'; + const selectCustomerItem = (item) => { + customerSearchInput.value = item.dataset.name; + if (customerUserIdInput) customerUserIdInput.value = item.dataset.login; + if (customerIdInput) customerIdInput.value = item.dataset.customerId; + customerSuggestionsDiv.style.display = 'none'; + checkChanges(); + }; + customerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { - if (item.dataset.userId) { + if (item.dataset.login) { item.addEventListener('mousedown', (e) => { e.preventDefault(); - customerSearchInput.value = item.dataset.name; - if (customerUserIdInput) customerUserIdInput.value = item.dataset.userId; - if (customerIdInput) customerIdInput.value = item.dataset.customerId; - customerSuggestionsDiv.style.display = 'none'; - checkChanges(); + selectCustomerItem(item); }); } }); @@ -4240,6 +4257,18 @@ const TicketDetailView = { }, 150); }); + if (App.setupAutocompleteKeyboardNavigation) { + App.setupAutocompleteKeyboardNavigation(customerSearchInput, customerSuggestionsDiv, (item) => { + if (item && item.dataset.login) { + customerSearchInput.value = item.dataset.name; + if (customerUserIdInput) customerUserIdInput.value = item.dataset.login; + if (customerIdInput) customerIdInput.value = item.dataset.customerId; + customerSuggestionsDiv.style.display = 'none'; + checkChanges(); + } + }); + } + customerSearchInput.addEventListener('focus', () => { customerSearchInput.dispatchEvent(new Event('input')); }); @@ -4270,9 +4299,18 @@ const TicketDetailView = { } const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : ''; - if (customerSearch && customerUserIdInput && !customerUserIdInput.value) { - customerUserIdInput.value = customerSearch; - if (customerIdInput) customerIdInput.value = customerSearch; + const origCustFirst = (this.originalValues['customer_first'] || '').trim(); + const origCustLast = (this.originalValues['customer_last'] || '').trim(); + const origCustName = origCustFirst ? `${origCustFirst} ${origCustLast}`.trim() : (this.originalValues['customer_user_id'] || '').trim(); + + if (customerSearchInput) { + if (!customerSearch) { + if (customerUserIdInput) customerUserIdInput.value = ''; + if (customerIdInput) customerIdInput.value = ''; + } else if (customerSearch === origCustName) { + if (customerUserIdInput) customerUserIdInput.value = this.originalValues['customer_user_id'] || ''; + if (customerIdInput) customerIdInput.value = this.originalValues['customer_id'] || ''; + } } // Check metadata updates diff --git a/routes/lookups.js b/routes/lookups.js index f79b01c..74acb04 100644 --- a/routes/lookups.js +++ b/routes/lookups.js @@ -296,6 +296,12 @@ router.get('/customer-companies/search', async (req, res) => { } }); +// Alias /api/customers/search -> /api/customer-users/search for backwards compatibility +router.get('/customers/search', (req, res, next) => { + req.url = '/customer-users/search' + (req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''); + router.handle(req, res, next); +}); + // GET /api/customer-users/search — Search customer users (supports LDAP via REST, fallbacks to DB) router.get('/customer-users/search', async (req, res) => { const { q = '', customer_company_id } = req.query; diff --git a/routes/tickets.js b/routes/tickets.js index b9b0c71..da7304d 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -387,7 +387,11 @@ router.get('/:id', async (req, res, next) => { // 1. Try to read from local SQLite cache in internal.db let cachedUser = null; try { - cachedUser = db.prepare('SELECT email, first_name, last_name, phone FROM customer_user_cache WHERE login = ?').get(ticket.customer_user_id); + cachedUser = db.prepare(` + SELECT login, email, first_name, last_name, phone + FROM customer_user_cache + WHERE login = ? OR (first_name || ' ' || last_name) = ? COLLATE NOCASE + `).get(ticket.customer_user_id, ticket.customer_user_id); } catch (dbErr) { console.warn('[LDAP Cache Detail] SQLite cache read failed:', dbErr.message); } @@ -398,37 +402,50 @@ router.get('/:id', async (req, res, next) => { ticket.customer_email = cachedUser.email || ''; ticket.customer_phone = cachedUser.phone || ''; } else { - // 2. Fallback to live API fetch - const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: ticket.customer_user_id }); - const getData = (getResult && getResult.Data) || getResult; - if (getData && getData.CustomerUser) { - const u = getData.CustomerUser; - ticket.customer_first = u.UserFirstname || ''; - ticket.customer_last = u.UserLastname || ''; - ticket.customer_email = u.UserEmail || ''; - ticket.customer_phone = u.UserPhone || ''; - - // 3. Store in SQLite cache for subsequent detail queries + // 2. Fallback to live API fetch (only if login doesn't contain spaces) + let targetLogin = ticket.customer_user_id; + if (targetLogin.includes(' ')) { try { - db.prepare(` - INSERT INTO customer_user_cache (login, email, first_name, last_name, customer_id, phone) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(login) DO UPDATE SET - email=excluded.email, - first_name=excluded.first_name, - last_name=excluded.last_name, - customer_id=excluded.customer_id, - phone=excluded.phone - `).run( - u.UserLogin, - u.UserEmail || '', - u.UserFirstname || '', - u.UserLastname || '', - u.UserCustomerID || '', - u.UserPhone || '' - ); - } catch (cacheErr) { - console.warn('[LDAP Cache Detail] Failed to write fetched user to SQLite cache:', cacheErr.message); + const sRes = await otrsRequest('POST', '/CustomerUserSearch', { Search: `*${targetLogin}*`, Valid: 1 }); + const sData = (sRes && sRes.Data) || sRes; + if (sData && sData.CustomerUserID) { + targetLogin = Array.isArray(sData.CustomerUserID) ? sData.CustomerUserID[0] : sData.CustomerUserID; + } + } catch (_) {} + } + + if (targetLogin && !targetLogin.includes(' ')) { + const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: targetLogin }); + const getData = (getResult && getResult.Data) || getResult; + if (getData && getData.CustomerUser) { + const u = getData.CustomerUser; + ticket.customer_first = u.UserFirstname || ''; + ticket.customer_last = u.UserLastname || ''; + ticket.customer_email = u.UserEmail || ''; + ticket.customer_phone = u.UserPhone || ''; + + // 3. Store in SQLite cache for subsequent detail queries + try { + db.prepare(` + INSERT INTO customer_user_cache (login, email, first_name, last_name, customer_id, phone) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(login) DO UPDATE SET + email=excluded.email, + first_name=excluded.first_name, + last_name=excluded.last_name, + customer_id=excluded.customer_id, + phone=excluded.phone + `).run( + u.UserLogin, + u.UserEmail || '', + u.UserFirstname || '', + u.UserLastname || '', + u.UserCustomerID || '', + u.UserPhone || '' + ); + } catch (cacheErr) { + console.warn('[LDAP Cache Detail] Failed to write fetched user to SQLite cache:', cacheErr.message); + } } } } @@ -940,9 +957,19 @@ router.patch('/:id', async (req, res) => { if (updates.responsible_user_id !== undefined) ticketFields.ResponsibleID = updates.responsible_user_id; 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_user_id !== undefined && updates.customer_user_id !== null && updates.customer_user_id !== '') { + try { + const match = db.prepare(`SELECT login, customer_id FROM customer_user_cache WHERE login = ? OR (first_name || ' ' || last_name) = ? COLLATE NOCASE`).get(updates.customer_user_id, updates.customer_user_id); + if (match) { + updates.customer_user_id = match.login; + if (!updates.customer_id && match.customer_id) { + updates.customer_id = match.customer_id; + } + } + } catch (e) {} + ticketFields.CustomerUser = updates.customer_user_id; + } if (updates.customer_id !== undefined && updates.customer_id !== null && updates.customer_id !== '') ticketFields.CustomerID = updates.customer_id; - if (updates.customer_user_id !== undefined && updates.customer_user_id !== null && updates.customer_user_id !== '') ticketFields.CustomerUser = updates.customer_user_id; // If ticketFields is empty but we have a note or timeUnit to log, supply a dummy field (StateID) so Ticket is not empty if (Object.keys(ticketFields).length === 0 && (hasNote || (!isNaN(timeUnit) && timeUnit > 0))) { @@ -1673,9 +1700,19 @@ router.patch('/batch/update', async (req, res) => { if (updates.queue_id !== undefined) ticketFields.QueueID = updates.queue_id; if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id; if (updates.user_id !== undefined) ticketFields.OwnerID = updates.user_id; - if (updates.responsible_user_id !== undefined) ticketFields.ResponsibleID = updates.responsible_user_id; - if (updates.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id; - if (updates.customer_user_id !== undefined) ticketFields.CustomerUser = updates.customer_user_id; + if (updates.customer_user_id !== undefined && updates.customer_user_id !== null && updates.customer_user_id !== '') { + try { + const match = db.prepare(`SELECT login, customer_id FROM customer_user_cache WHERE login = ? OR (first_name || ' ' || last_name) = ? COLLATE NOCASE`).get(updates.customer_user_id, updates.customer_user_id); + if (match) { + updates.customer_user_id = match.login; + if (!updates.customer_id && match.customer_id) { + updates.customer_id = match.customer_id; + } + } + } catch (e) {} + ticketFields.CustomerUser = updates.customer_user_id; + } + if (updates.customer_id !== undefined && updates.customer_id !== null && updates.customer_id !== '') ticketFields.CustomerID = updates.customer_id; // Auto sblocco check if (updates.ticket_state_id) {