From 78a26fe970b9e3006676e85789e8d714fd8aad88 Mon Sep 17 00:00:00 2001 From: Gabriele Cimaschi Date: Wed, 8 Jul 2026 00:58:52 +0200 Subject: [PATCH] fix: verificare, ma dovrebbe essere corretta la lattura da ldap --- .agents/rules/utilizzo-di-git.md | 3 +- public/css/style.css | 2 + public/js/app.js | 3 + public/js/views/ticketCreate.js | 28 +++-- public/js/views/ticketDetail.js | 35 +++--- routes/lookups.js | 180 ++++++++++++++++++++++++++++++- routes/tickets.js | 14 ++- 7 files changed, 231 insertions(+), 34 deletions(-) diff --git a/.agents/rules/utilizzo-di-git.md b/.agents/rules/utilizzo-di-git.md index 513a1d3..fbb09cf 100644 --- a/.agents/rules/utilizzo-di-git.md +++ b/.agents/rules/utilizzo-di-git.md @@ -1,6 +1,5 @@ --- trigger: always_on -glob: -description: --- +Per affrontare una issue di git tieni in considerazione anche i commenti interni. \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index 5cb0bef..56ab5d1 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -487,6 +487,7 @@ body { display: flex; flex-direction: column; min-height: 100vh; + min-width: 0; } /* ---- Topbar ---- */ @@ -568,6 +569,7 @@ body { flex: 1; padding: var(--space-xl); animation: fadeIn var(--transition-base); + min-width: 0; } @keyframes fadeIn { diff --git a/public/js/app.js b/public/js/app.js index 5e916b9..9e0cda8 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -223,6 +223,9 @@ const App = { } try { + // Sync LDAP customer users to local DB cache + await this.api('/api/customer-users/sync', { method: 'POST' }); + await this.ensureLookups(true); await this.initAgentSelector(); Toast.success('Dati locali (code, utenti, ecc.) aggiornati con successo!'); diff --git a/public/js/views/ticketCreate.js b/public/js/views/ticketCreate.js index 9d4330a..4bbe93d 100644 --- a/public/js/views/ticketCreate.js +++ b/public/js/views/ticketCreate.js @@ -388,11 +388,16 @@ const TicketCreateView = { userSearchInput.addEventListener('input', () => { clearTimeout(userDebounce); const q = userSearchInput.value.trim(); - // Do not block empty query to allow all results on focus userDebounce = setTimeout(async () => { try { const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); + console.log('[Frontend LDAP Search] Users returned:', users); + // Prevent race conditions: discard results if the input value has changed + if (userSearchInput.value.trim() !== q) { + console.log('[Frontend LDAP Search] Discarding stale results for query:', q); + return; + } if (users.length === 0) { userSuggestionsDiv.innerHTML = '
Nessun utente trovato
'; userSuggestionsDiv.style.display = 'block'; @@ -400,17 +405,18 @@ const TicketCreateView = { } userSuggestionsDiv.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 || '—')}) + (Login: ${App.escapeHtml(u.login || '')} | Azienda: ${App.escapeHtml(u.customer_id || '—')})
`).join(''); userSuggestionsDiv.style.display = 'block'; - // Bind click + // Use mousedown instead of click to fire before blur event userSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { if (item.dataset.login) { - item.addEventListener('click', () => { + item.addEventListener('mousedown', (e) => { + e.preventDefault(); // prevent input from losing focus before value is set userSearchInput.value = item.dataset.name; customerUserIdInput.value = item.dataset.login; userSuggestionsDiv.style.display = 'none'; @@ -429,12 +435,18 @@ const TicketCreateView = { }, 300); }); userSearchInput.addEventListener('focus', () => { - userSearchInput.value = ''; - customerUserIdInput.value = ''; - userSearchInput.dispatchEvent(new Event('input')); + if (!customerUserIdInput.value) { + // Only search if no user is selected yet + userSearchInput.dispatchEvent(new Event('input')); + } + }); + userSearchInput.addEventListener('blur', () => { + // Small delay to allow mousedown on item to fire first + setTimeout(() => { userSuggestionsDiv.style.display = 'none'; }, 150); }); } + // Owner Autocomplete (dynamic backend search) let ownerDebounce; if (ownerSearchInput) { diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index beb812f..afdcdda 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -455,11 +455,16 @@ const TicketDetailView = { customerSearchInput.addEventListener('input', () => { clearTimeout(customerDebounce); const q = customerSearchInput.value.trim(); - // Do not block empty query to allow all results on focus customerDebounce = setTimeout(async () => { try { const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); + console.log('[Frontend LDAP Search Detail] Users returned:', users); + // Prevent race conditions: discard results if the input value has changed + if (customerSearchInput.value.trim() !== q) { + console.log('[Frontend LDAP Search Detail] Discarding stale results for query:', q); + return; + } if (users.length === 0) { customerSuggestionsDiv.innerHTML = '
Nessun utente trovato
'; customerSuggestionsDiv.style.display = 'block'; @@ -467,17 +472,18 @@ const TicketDetailView = { } 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 || '—')}) + (Login: ${App.escapeHtml(u.login || '')} | Azienda: ${App.escapeHtml(u.customer_id || '—')})
`).join(''); customerSuggestionsDiv.style.display = 'block'; - // Bind click + // Use mousedown instead of click to fire before blur event customerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { if (item.dataset.login) { - item.addEventListener('click', () => { + item.addEventListener('mousedown', (e) => { + e.preventDefault(); // prevent input from losing focus before value is set customerSearchInput.value = item.dataset.name; customerUserIdInput.value = item.dataset.login; customerIdInput.value = item.dataset.customerId || ''; @@ -493,22 +499,15 @@ const TicketDetailView = { }, 300); }); customerSearchInput.addEventListener('focus', () => { - customerSearchInput.value = ''; - customerUserIdInput.value = ''; - customerIdInput.value = ''; - customerUserIdInput.dispatchEvent(new Event('change')); - customerIdInput.dispatchEvent(new Event('change')); - customerSearchInput.dispatchEvent(new Event('input')); + if (!customerUserIdInput.value) { + customerSearchInput.dispatchEvent(new Event('input')); + } + }); + customerSearchInput.addEventListener('blur', () => { + setTimeout(() => { customerSuggestionsDiv.style.display = 'none'; }, 150); }); } - // 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', () => { fields.forEach(el => { diff --git a/routes/lookups.js b/routes/lookups.js index 22be36a..6e44421 100644 --- a/routes/lookups.js +++ b/routes/lookups.js @@ -2,6 +2,9 @@ const express = require('express'); const router = express.Router(); const pool = require('../db'); +// In-memory cache for customer users fetched from LDAP +let ldapCustomerUsersCache = []; + // Helper for OTRS CE GenericInterface REST API calls async function otrsRequest(method, path, bodyData = {}) { const OTRS_API_USER = process.env.OTRS_API_USER; @@ -19,7 +22,7 @@ async function otrsRequest(method, path, bodyData = {}) { }; const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 6000); + const timeoutId = setTimeout(() => controller.abort(), 15000); try { const response = await fetch(url, { @@ -38,7 +41,7 @@ async function otrsRequest(method, path, bodyData = {}) { } catch (err) { clearTimeout(timeoutId); if (err.name === 'AbortError') { - throw new Error('OTRS REST API request timed out (6s limit exceeded)'); + throw new Error('OTRS REST API request timed out (15s limit exceeded)'); } throw err; } @@ -199,11 +202,94 @@ router.get('/customer-companies/search', async (req, res) => { } }); -// GET /api/customer-users/search — Search customer users (always from local DB) +// GET /api/customer-users/search — Search customer users (supports LDAP via REST, fallbacks to DB) router.get('/customer-users/search', async (req, res) => { - try { - const { q = '', customer_company_id } = req.query; + const { q = '', customer_company_id } = req.query; + // 1. Try to search via OTRS GenericInterface REST API if configured + if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) { + // Search the in-memory cache first if it contains data + if (ldapCustomerUsersCache.length > 0) { + const searchTerms = q.toLowerCase().split(/\s+/).filter(Boolean); + let matchedUsers = ldapCustomerUsersCache; + + if (searchTerms.length > 0) { + matchedUsers = matchedUsers.filter(u => { + const login = (u.login || '').toLowerCase(); + const email = (u.email || '').toLowerCase(); + const first = (u.first_name || '').toLowerCase(); + const last = (u.last_name || '').toLowerCase(); + const company = (u.customer_id || '').toLowerCase(); + + return searchTerms.every(term => + login.includes(term) || + email.includes(term) || + first.includes(term) || + last.includes(term) || + company.includes(term) + ); + }); + } + + if (customer_company_id) { + matchedUsers = matchedUsers.filter(u => u.customer_id === customer_company_id); + } + + return res.json(matchedUsers.slice(0, 20)); + } + + // Fallback to live API search if cache is not yet populated + try { + const searchPattern = q ? `*${q}*` : '*'; + const searchPayload = { + Search: searchPattern, + Valid: 1 + }; + if (customer_company_id) { + searchPayload.CustomerID = customer_company_id; + } + + const searchResult = await otrsRequest('POST', '/CustomerUserSearch', searchPayload); + + const searchData = (searchResult && searchResult.Data) || searchResult; + if (searchData && searchData.CustomerUserID) { + let logins = searchData.CustomerUserID; + if (!Array.isArray(logins)) { + logins = [logins]; + } + logins = logins.slice(0, 20); // limit to 20 results + + // Fetch details for each login in parallel + const detailsPromises = logins.map(async (login) => { + try { + const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: login }); + const getData = (getResult && getResult.Data) || getResult; + if (getData && getData.CustomerUser) { + const u = getData.CustomerUser; + return { + login: u.UserLogin, + email: u.UserEmail || '', + first_name: u.UserFirstname || '', + last_name: u.UserLastname || '', + customer_id: u.UserCustomerID || '' + }; + } + } catch (getErr) { + console.warn(`Failed to fetch details for customer user ${login}:`, getErr.message); + } + return null; + }); + + const users = (await Promise.all(detailsPromises)).filter(u => u !== null); + return res.json(users); + } + } catch (restErr) { + console.warn('Failed to search customer users via REST API, falling back to database query:', restErr.message); + } + } + + // 2. Fallback to local DB query + try { let queryText; let queryParams; if (q) { @@ -310,4 +396,88 @@ router.get('/states/search', async (req, res) => { } }); +// Helper function to sync customer users from LDAP (OTRS API) into in-memory cache +async function syncCustomerUsers() { + if (!process.env.OTRS_API_URL || !process.env.OTRS_API_USER) { + console.log('[LDAP Sync] OTRS API not configured, skipping customer user sync.'); + return 0; + } + + console.log('[LDAP Sync] Starting customer user synchronization...'); + try { + const searchResult = await otrsRequest('POST', '/CustomerUserSearch', { Search: '*', Valid: 1 }); + const searchData = (searchResult && searchResult.Data) || searchResult; + if (!searchData || !searchData.CustomerUserID) { + console.log('[LDAP Sync] No customer users found to sync.'); + return 0; + } + + let logins = searchData.CustomerUserID; + if (!Array.isArray(logins)) { + logins = [logins]; + } + + console.log(`[LDAP Sync] Found ${logins.length} customer users. Fetching details...`); + + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + const newCache = []; + // Fetch details in batches of 3 to avoid overloading the OTRS CGI server + const batchSize = 3; + for (let i = 0; i < logins.length; i += batchSize) { + const batchLogins = logins.slice(i, i + batchSize); + await Promise.all(batchLogins.map(async (login) => { + let retries = 2; + while (retries >= 0) { + try { + const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: login }); + const getData = (getResult && getResult.Data) || getResult; + if (getData && getData.CustomerUser) { + const u = getData.CustomerUser; + newCache.push({ + login: u.UserLogin, + email: u.UserEmail || '', + first_name: u.UserFirstname || '', + last_name: u.UserLastname || '', + customer_id: u.UserCustomerID || '' + }); + } + break; // Success, break retry loop + } catch (getErr) { + if (retries === 0) { + console.warn(`[LDAP Sync] Failed to sync customer user ${login} after retries:`, getErr.message); + } else { + await delay(200); // Wait 200ms before retrying + } + retries--; + } + } + })); + // Add a small delay between batches + await delay(50); + } + + ldapCustomerUsersCache = newCache; + console.log(`[LDAP Sync] Completed. Synced ${ldapCustomerUsersCache.length} users to in-memory cache.`); + return ldapCustomerUsersCache.length; + } catch (err) { + console.error('[LDAP Sync] Error during customer user sync:', err); + throw err; + } +} + +// POST /api/customer-users/sync — Sync customer users from LDAP +router.post('/customer-users/sync', async (req, res) => { + try { + const count = await syncCustomerUsers(); + res.json({ message: `Sincronizzazione completata! ${count} utenti sincronizzati.`, count }); + } catch (err) { + res.status(500).json({ error: 'Errore durante la sincronizzazione', message: err.message }); + } +}); + +// Run LDAP synchronization in the background on startup (after 5 seconds) +setTimeout(() => { + syncCustomerUsers().catch(err => console.error('Startup LDAP sync failed:', err)); +}, 5000); + module.exports = router; diff --git a/routes/tickets.js b/routes/tickets.js index 8d044ce..32c2f8b 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -459,6 +459,18 @@ router.post('/', async (req, res) => { const c = custRes.rows[0]; customerFrom = `${c.first_name} ${c.last_name} <${c.email}>`; senderTypeName = 'customer'; + } else if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) { + try { + const getResult = await otrsRequest('POST', '/CustomerUserGet', { UserLogin: customer_user_id }); + const getData = (getResult && getResult.Data) || getResult; + if (getData && getData.CustomerUser) { + const u = getData.CustomerUser; + customerFrom = `${u.UserFirstname || ''} ${u.UserLastname || ''} <${u.UserEmail || ''}>`.trim(); + senderTypeName = 'customer'; + } + } catch (apiErr) { + console.warn(`Failed to fetch LDAP customer details for ${customer_user_id}:`, apiErr.message); + } } } @@ -1351,7 +1363,7 @@ router.get('/attachments/:id', async (req, res) => { } else if (typeof attachment.content === 'string') { contentStr = attachment.content; } - + const cleaned = contentStr.replace(/\s+/g, ''); const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; if (cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {