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) {