diff --git a/routes/lookups.js b/routes/lookups.js index 6e44421..694ed74 100644 --- a/routes/lookups.js +++ b/routes/lookups.js @@ -1,9 +1,23 @@ const express = require('express'); const router = express.Router(); const pool = require('../db'); +const { db } = require('../activityDb'); -// In-memory cache for customer users fetched from LDAP -let ldapCustomerUsersCache = []; +// Ensure SQLite tables for customer user cache exist in internal.db +db.exec(` + CREATE TABLE IF NOT EXISTS customer_user_cache ( + login TEXT PRIMARY KEY, + email TEXT, + first_name TEXT, + last_name TEXT, + customer_id TEXT, + phone TEXT + ); + CREATE TABLE IF NOT EXISTS sync_status ( + key TEXT PRIMARY KEY, + val TEXT + ); +`); // Helper for OTRS CE GenericInterface REST API calls async function otrsRequest(method, path, bodyData = {}) { @@ -37,7 +51,11 @@ async function otrsRequest(method, path, bodyData = {}) { const errorText = await response.text(); throw new Error(`OTRS REST API error (${response.status}): ${errorText}`); } - return await response.json(); + const result = await response.json(); + if (result && result.Error) { + throw new Error(`OTRS API Error: ${result.Error.ErrorMessage} (${result.Error.ErrorCode})`); + } + return result; } catch (err) { clearTimeout(timeoutId); if (err.name === 'AbortError') { @@ -208,34 +226,41 @@ router.get('/customer-users/search', async (req, res) => { // 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) - ); - }); - } - + try { + // Query the local SQLite cache table first + const searchTerm = q ? `%${q}%` : '%'; + let rows; if (customer_company_id) { - matchedUsers = matchedUsers.filter(u => u.customer_id === customer_company_id); + rows = db.prepare(` + SELECT login, email, first_name, last_name, customer_id + FROM customer_user_cache + WHERE customer_id = ? AND ( + login LIKE ? OR + email LIKE ? OR + first_name LIKE ? OR + last_name LIKE ? + ) + ORDER BY last_name, first_name + LIMIT 20 + `).all(customer_company_id, searchTerm, searchTerm, searchTerm, searchTerm); + } else { + rows = db.prepare(` + SELECT login, email, first_name, last_name, customer_id + FROM customer_user_cache + WHERE login LIKE ? OR + email LIKE ? OR + first_name LIKE ? OR + last_name LIKE ? + ORDER BY last_name, first_name + LIMIT 20 + `).all(searchTerm, searchTerm, searchTerm, searchTerm); } - return res.json(matchedUsers.slice(0, 20)); + if (rows.length > 0) { + return res.json(rows); + } + } catch (dbErr) { + console.warn('[LDAP Cache Search] SQLite query failed, falling back to live API:', dbErr.message); } // Fallback to live API search if cache is not yet populated @@ -396,7 +421,7 @@ router.get('/states/search', async (req, res) => { } }); -// Helper function to sync customer users from LDAP (OTRS API) into in-memory cache +// Helper function to sync customer users from LDAP (OTRS API) into SQLite 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.'); @@ -420,7 +445,20 @@ async function syncCustomerUsers() { console.log(`[LDAP Sync] Found ${logins.length} customer users. Fetching details...`); const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); - const newCache = []; + + // Prepare SQLite insert statement with upsert logic + const upsertStmt = 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 + `); + + let syncCount = 0; // 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) { @@ -433,13 +471,17 @@ async function syncCustomerUsers() { 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 || '' - }); + + // Run insertion in SQLite cache table + upsertStmt.run( + u.UserLogin, + u.UserEmail || '', + u.UserFirstname || '', + u.UserLastname || '', + u.UserCustomerID || '', + u.UserPhone || '' + ); + syncCount++; } break; // Success, break retry loop } catch (getErr) { @@ -456,9 +498,15 @@ async function syncCustomerUsers() { await delay(50); } - ldapCustomerUsersCache = newCache; - console.log(`[LDAP Sync] Completed. Synced ${ldapCustomerUsersCache.length} users to in-memory cache.`); - return ldapCustomerUsersCache.length; + // Update last sync timestamp in SQLite sync_status table + db.prepare(` + INSERT INTO sync_status (key, val) + VALUES ('last_ldap_sync_time', ?) + ON CONFLICT(key) DO UPDATE SET val=excluded.val + `).run(new Date().toISOString()); + + console.log(`[LDAP Sync] Completed. Synced ${syncCount} users to SQLite customer_user_cache.`); + return syncCount; } catch (err) { console.error('[LDAP Sync] Error during customer user sync:', err); throw err; @@ -471,13 +519,32 @@ router.post('/customer-users/sync', async (req, res) => { 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 }); + console.warn('[LDAP Sync] Sincronizzazione non riuscita:', err.message); + res.json({ message: `Sincronizzazione LDAP ignorata o non disponibile: ${err.message}`, count: 0 }); } }); -// Run LDAP synchronization in the background on startup (after 5 seconds) +// Run LDAP synchronization on startup if last sync was > LDAP_SYNC_INTERVAL_HOURS ago setTimeout(() => { - syncCustomerUsers().catch(err => console.error('Startup LDAP sync failed:', err)); + try { + const row = db.prepare("SELECT val FROM sync_status WHERE key = 'last_ldap_sync_time'").get(); + let shouldSync = true; + if (row && row.val) { + const lastSync = new Date(row.val); + const intervalHours = parseFloat(process.env.LDAP_SYNC_INTERVAL_HOURS) || 24; + const thresholdTime = new Date(Date.now() - intervalHours * 60 * 60 * 1000); + if (lastSync > thresholdTime) { + shouldSync = false; + console.log(`[LDAP Sync] Last sync was on ${lastSync.toLocaleString()} (Threshold: ${intervalHours}h). Skipping auto-sync at startup.`); + } + } + + if (shouldSync) { + syncCustomerUsers().catch(err => console.error('Startup LDAP sync failed:', err)); + } + } catch (err) { + console.error('[LDAP Sync] Failed to check LDAP sync status:', err.message); + } }, 5000); module.exports = router; diff --git a/routes/tickets.js b/routes/tickets.js index 32c2f8b..3afa5b0 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); const pool = require('../db'); -const { logAttivita } = require('../activityDb'); +const { db, logAttivita } = require('../activityDb'); // Helper: resolve agent name from DB (best-effort, non-blocking) async function resolveAgentName(agentId) { @@ -36,7 +36,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, { @@ -51,11 +51,15 @@ async function otrsRequest(method, path, bodyData = {}) { const errorText = await response.text(); throw new Error(`OTRS REST API error (${response.status}): ${errorText}`); } - return await response.json(); + const result = await response.json(); + if (result && result.Error) { + throw new Error(`OTRS API Error: ${result.Error.ErrorMessage} (${result.Error.ErrorCode})`); + } + return result; } 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; } @@ -258,6 +262,62 @@ router.get('/:id', async (req, res) => { return res.status(404).json({ error: 'Ticket not found' }); } + const ticket = ticketResult.rows[0]; + if (ticket.customer_user_id && !ticket.customer_first && process.env.OTRS_API_URL && process.env.OTRS_API_USER) { + try { + // 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); + } catch (dbErr) { + console.warn('[LDAP Cache Detail] SQLite cache read failed:', dbErr.message); + } + + if (cachedUser) { + ticket.customer_first = cachedUser.first_name || ''; + ticket.customer_last = cachedUser.last_name || ''; + 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 + 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); + } + } + } + } catch (apiErr) { + console.warn(`Failed to fetch LDAP customer details for detail view:`, apiErr.message); + } + } + // Fetch articles const articlesResult = await pool.query( `SELECT @@ -613,7 +673,7 @@ router.patch('/:id', async (req, res) => { } // 1. Try to update via REST API if configured - if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) { + if (process.env.OTRS_API_URL && process.env.OTRS_API_USER && process.env.FORCE_DB_UPDATE !== 'true') { try { const ticketFields = {}; if (updates.ticket_state_id !== undefined) ticketFields.StateID = updates.ticket_state_id; @@ -664,6 +724,23 @@ router.patch('/:id', async (req, res) => { }; } const result = await otrsRequest('PATCH', `/Ticket/${id}`, reqBody); + + // Option 1: Log the time directly to the DB if the API succeeded but OTRS didn't save it + if (!isNaN(timeUnit) && timeUnit > 0 && result && result.ArticleID) { + try { + await pool.query( + `INSERT INTO time_accounting ( + ticket_id, article_id, time_unit, + create_time, create_by, change_time, change_by + ) VALUES ($1, $2, $3, NOW(), $4, NOW(), $4)`, + [id, result.ArticleID, timeUnit, operatorId] + ); + console.log(`[Time Accounting] Successfully logged ${timeUnit} minutes for ticket ${id} via DB insert.`); + } catch (timeDbErr) { + console.error('[Time Accounting] Failed to log time unit in database:', timeDbErr.message); + } + } + const isClosing = updates.ticket_state_id && current.ticket_state_id !== updates.ticket_state_id; resolveAgentName(operatorId).then(agente_nome => { logAttivita({ @@ -942,7 +1019,7 @@ router.post('/:id/articles', async (req, res) => { const contentType = isHtml ? 'text/html; charset=utf-8' : 'text/plain; charset=utf-8'; // 1. Try to add note via REST API if configured - if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) { + if (process.env.OTRS_API_URL && process.env.OTRS_API_USER && process.env.FORCE_DB_UPDATE !== 'true') { try { const payload = { Article: { @@ -969,6 +1046,23 @@ router.post('/:id/articles', async (req, res) => { } const result = await otrsRequest('PATCH', `/Ticket/${id}`, payload); + + // Option 1: Log the time directly to the DB if the API succeeded but OTRS didn't save it + if (time_unit && result && result.ArticleID) { + try { + await pool.query( + `INSERT INTO time_accounting ( + ticket_id, article_id, time_unit, + create_time, create_by, change_time, change_by + ) VALUES ($1, $2, $3, NOW(), $4, NOW(), $4)`, + [id, result.ArticleID, parseFloat(time_unit), operatorId] + ); + console.log(`[Time Accounting] Successfully logged ${time_unit} minutes for ticket ${id} via DB insert.`); + } catch (timeDbErr) { + console.error('[Time Accounting] Failed to log time unit in database:', timeDbErr.message); + } + } + resolveAgentName(operatorId).then(agente_nome => { logAttivita({ agente_id: operatorId, @@ -1179,7 +1273,7 @@ router.patch('/batch/update', async (req, res) => { } // 1. Try to update via REST API if configured - if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) { + if (process.env.OTRS_API_URL && process.env.OTRS_API_USER && process.env.FORCE_DB_UPDATE !== 'true') { try { const ticketFields = {}; if (updates.ticket_state_id !== undefined) ticketFields.StateID = updates.ticket_state_id;