const express = require('express'); const router = express.Router(); const pool = require('../db'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { db } = require('../activityDb'); const XLSX = require('xlsx'); const ALGORITHM = 'aes-256-cbc'; const SECRET_KEY = crypto.createHash('sha256').update(process.env.CRYPTO_KEY || 'default_secret_key_12345').digest(); const IV_LENGTH = 16; function encrypt(text) { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, SECRET_KEY, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } function decrypt(text) { const textParts = text.split(':'); const iv = Buffer.from(textParts.shift(), 'hex'); const encryptedText = Buffer.from(textParts.join(':'), 'hex'); const decipher = crypto.createDecipheriv(ALGORITHM, SECRET_KEY, iv); let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // Ensure SQLite table exists for phrases db.exec(` CREATE TABLE IF NOT EXISTS frasi_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, tipo TEXT, testo TEXT ) `); // Seed function to import plain-text files into SQLite cache function seedPhrases() { try { const countRow = db.prepare("SELECT COUNT(*) AS count FROM frasi_cache").get(); if (countRow.count === 0) { console.log('[Phrases Seed] SQLite frasi_cache is empty. Seeding...'); const seedFile = (fileName, type) => { const txtPath = path.join(__dirname, `../public/${fileName}`); if (fs.existsSync(txtPath)) { const text = fs.readFileSync(txtPath, 'utf8'); const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0); const insertStmt = db.prepare("INSERT INTO frasi_cache (tipo, testo) VALUES (?, ?)"); db.transaction(() => { for (const line of lines) { insertStmt.run(type, encrypt(line)); } })(); console.log(`[Phrases Seed] Successfully seeded ${lines.length} encrypted ${type} phrases.`); } }; seedFile('demotivational.txt', 'demotivational'); seedFile('motivational.txt', 'motivational'); } } catch (err) { console.error('[Phrases Seed] Seeding failed:', err.message); } } seedPhrases(); // GET /api/dashboard/stats — Dashboard statistics router.get('/stats', async (req, res) => { const activeAgentId = parseInt(req.headers['x-agent-id'], 10) || 1; let previewLimit = 10; try { const row = db.prepare("SELECT preview_limit FROM agent_settings WHERE agent_id = ?").get(activeAgentId); if (row) { previewLimit = row.preview_limit; } } catch (err) { console.error('Error reading agent_settings:', err.message); } try { // All queries in parallel for speed const [ byState, byPriority, byQueue, todayCount, weekCount, totalOpen, recentTickets, escalated, myOpenCount, ] = await Promise.all([ // Tickets by state (only open-ish states) pool.query( `SELECT ts.name AS state, tst.name AS state_type, COUNT(*) AS count FROM ticket t JOIN ticket_state ts ON t.ticket_state_id = ts.id JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name IN ('new', 'open', 'pending reminder', 'pending auto') GROUP BY ts.name, tst.name ORDER BY count DESC` ), // Tickets by priority (open only) pool.query( `SELECT tp.name AS priority, tp.color, COUNT(*) AS count FROM ticket t JOIN ticket_priority tp ON t.ticket_priority_id = tp.id JOIN ticket_state ts ON t.ticket_state_id = ts.id JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name IN ('new', 'open', 'pending reminder', 'pending auto') GROUP BY tp.name, tp.color, tp.id ORDER BY tp.id` ), // Tickets by queue (open only, top 10) pool.query( `SELECT q.name AS queue, COUNT(*) AS count FROM ticket t JOIN queue q ON t.queue_id = q.id JOIN ticket_state ts ON t.ticket_state_id = ts.id JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name IN ('new', 'open', 'pending reminder', 'pending auto') GROUP BY q.name ORDER BY count DESC LIMIT 10` ), // Created today pool.query( `SELECT COUNT(*) AS count FROM ticket WHERE create_time >= CURRENT_DATE` ), // Created this week pool.query( `SELECT COUNT(*) AS count FROM ticket WHERE create_time >= date_trunc('week', CURRENT_DATE)` ), // Total open pool.query( `SELECT COUNT(*) AS count FROM ticket t JOIN ticket_state ts ON t.ticket_state_id = ts.id JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name IN ('new', 'open', 'pending reminder', 'pending auto')` ), // Custom most recent tickets based on agent settings pool.query( `SELECT t.id, t.tn, t.title, ts.name AS state_name, tp.name AS priority_name, tp.color AS priority_color, q.name AS queue_name, t.create_time FROM ticket t JOIN ticket_state ts ON t.ticket_state_id = ts.id JOIN ticket_priority tp ON t.ticket_priority_id = tp.id JOIN queue q ON t.queue_id = q.id ORDER BY t.create_time DESC LIMIT $1`, [previewLimit] ), // Escalated tickets pool.query( `SELECT COUNT(*) AS count FROM ticket WHERE escalation_time > 0 AND escalation_time < EXTRACT(EPOCH FROM NOW())` ), // My open tickets count pool.query( `SELECT COUNT(*) AS count FROM ticket t JOIN ticket_state ts ON t.ticket_state_id = ts.id JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name IN ('new', 'open', 'pending reminder', 'pending auto') AND t.user_id = $1`, [activeAgentId] ), ]); res.json({ by_state: byState.rows, by_priority: byPriority.rows, by_queue: byQueue.rows, created_today: parseInt(todayCount.rows[0].count), created_this_week: parseInt(weekCount.rows[0].count), total_open: parseInt(totalOpen.rows[0].count), recent_tickets: recentTickets.rows, escalated: parseInt(escalated.rows[0].count), total_my_open: parseInt(myOpenCount.rows[0].count), preview_limit: previewLimit, }); } catch (err) { console.error('Error fetching dashboard stats:', err); res.status(500).json({ error: err.message }); } }); // GET /api/dashboard/phrases — Get decrypted phrases from SQLite cache router.get('/phrases', (req, res) => { const { tipo = 'demotivational' } = req.query; try { const rows = db.prepare("SELECT testo FROM frasi_cache WHERE tipo = ?").all(tipo); const decryptedPhrases = rows.map(row => { try { return decrypt(row.testo); } catch (decErr) { console.warn('[Decrypt Phrase] Failed to decrypt phrase:', decErr.message); return null; } }).filter(Boolean); res.json(decryptedPhrases); } catch (err) { console.error('Error fetching decrypted phrases:', err); res.status(500).json({ error: err.message }); } }); // GET /api/dashboard/settings — Retrieve settings for the agent router.get('/settings', (req, res) => { const activeAgentId = parseInt(req.headers['x-agent-id'], 10) || 1; try { const row = db.prepare("SELECT preview_limit, tickets_per_page FROM agent_settings WHERE agent_id = ?").get(activeAgentId); if (row) { res.json(row); } else { res.json({ preview_limit: 10, tickets_per_page: 50 }); } } catch (err) { console.error('Error reading agent settings:', err); res.status(500).json({ error: err.message }); } }); // GET /api/dashboard/chart-lines router.get('/chart-lines', (req, res) => { try { const lines = db.prepare("SELECT * FROM dashboard_chart_lines ORDER BY is_default DESC, id ASC").all(); const formatted = lines.map(line => ({ ...line, statuses: JSON.parse(line.statuses || '[]'), types: JSON.parse(line.types || '[]'), queues: JSON.parse(line.queues || '[]'), owners: JSON.parse(line.owners || '[]'), responsibles: JSON.parse(line.responsibles || '[]') })); res.json(formatted); } catch (err) { console.error('Error fetching chart lines:', err); res.status(500).json({ error: err.message }); } }); // POST /api/dashboard/chart-lines router.post('/chart-lines', (req, res) => { const { name, statuses, types, queues, owners, responsibles, color, is_visible } = req.body; if (!name) return res.status(400).json({ error: 'Name is required' }); try { const info = db.prepare(` INSERT INTO dashboard_chart_lines (name, statuses, types, queues, owners, responsibles, color, is_visible, is_default) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0) `).run( name, JSON.stringify(statuses || []), JSON.stringify(types || []), JSON.stringify(queues || []), JSON.stringify(owners || []), JSON.stringify(responsibles || []), color || '#4f46e5', is_visible !== undefined ? parseInt(is_visible, 10) : 1 ); res.json({ success: true, id: info.lastInsertRowid }); } catch (err) { console.error('Error saving chart line:', err); res.status(500).json({ error: err.message }); } }); // PUT /api/dashboard/chart-lines/:id router.put('/chart-lines/:id', (req, res) => { const { id } = req.params; const { name, statuses, types, queues, owners, responsibles, color, is_visible } = req.body; if (!name) return res.status(400).json({ error: 'Name is required' }); try { const info = db.prepare(` UPDATE dashboard_chart_lines SET name = ?, statuses = ?, types = ?, queues = ?, owners = ?, responsibles = ?, color = ?, is_visible = ? WHERE id = ? `).run( name, JSON.stringify(statuses || []), JSON.stringify(types || []), JSON.stringify(queues || []), JSON.stringify(owners || []), JSON.stringify(responsibles || []), color || '#4f46e5', is_visible !== undefined ? parseInt(is_visible, 10) : 1, id ); if (info.changes === 0) return res.status(404).json({ error: 'Line not found' }); res.json({ success: true }); } catch (err) { console.error('Error updating chart line:', err); res.status(500).json({ error: err.message }); } }); // DELETE /api/dashboard/chart-lines/:id router.delete('/chart-lines/:id', (req, res) => { const { id } = req.params; try { const line = db.prepare("SELECT is_default FROM dashboard_chart_lines WHERE id = ?").get(id); if (!line) return res.status(404).json({ error: 'Line not found' }); if (line.is_default === 1) { return res.status(400).json({ error: 'Cannot delete default line' }); } db.prepare("DELETE FROM dashboard_chart_lines WHERE id = ?").run(id); res.json({ success: true }); } catch (err) { console.error('Error deleting chart line:', err); res.status(500).json({ error: err.message }); } }); // GET /api/dashboard/chart-data router.get('/chart-data', async (req, res) => { const { start_date, end_date } = req.query; if (!start_date || !end_date) { return res.status(400).json({ error: 'start_date and end_date are required' }); } try { // Only query data series that are configured as visible const lines = db.prepare("SELECT * FROM dashboard_chart_lines WHERE is_visible = 1 ORDER BY is_default DESC, id ASC").all(); const dates = []; let curr = new Date(start_date); const endLimit = new Date(end_date); while (curr <= endLimit) { const y = curr.getFullYear(); const m = String(curr.getMonth() + 1).padStart(2, '0'); const d = String(curr.getDate()).padStart(2, '0'); dates.push(`${y}-${m}-${d}`); curr.setDate(curr.getDate() + 1); } const linesData = await Promise.all(lines.map(async (line) => { const conditions = []; const params = []; let paramIdx = 1; conditions.push(`t.create_time >= $${paramIdx++}`); params.push(start_date + ' 00:00:00'); conditions.push(`t.create_time <= $${paramIdx++}`); params.push(end_date + ' 23:59:59'); const statuses = JSON.parse(line.statuses || '[]'); if (statuses.length > 0) { const placeholders = statuses.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.ticket_state_id IN (${placeholders})`); params.push(...statuses.map(id => parseInt(id))); } const types = JSON.parse(line.types || '[]'); if (types.length > 0) { const placeholders = types.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.type_id IN (${placeholders})`); params.push(...types.map(id => parseInt(id))); } const queues = JSON.parse(line.queues || '[]'); if (queues.length > 0) { const placeholders = queues.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.queue_id IN (${placeholders})`); params.push(...queues.map(id => parseInt(id))); } const owners = JSON.parse(line.owners || '[]'); if (owners.length > 0) { const placeholders = owners.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.user_id IN (${placeholders})`); params.push(...owners.map(id => parseInt(id))); } const responsibles = JSON.parse(line.responsibles || '[]'); if (responsibles.length > 0) { const placeholders = responsibles.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.responsible_user_id IN (${placeholders})`); params.push(...responsibles.map(id => parseInt(id))); } const sql = ` SELECT CAST(t.create_time AS DATE) AS date_val, COUNT(*) AS count FROM ticket t WHERE ${conditions.join(' AND ')} GROUP BY CAST(t.create_time AS DATE) `; const result = await pool.query(sql, params); const countsByDate = {}; for (const row of result.rows) { let dateStr = row.date_val; if (dateStr instanceof Date) { const y = dateStr.getFullYear(); const m = String(dateStr.getMonth() + 1).padStart(2, '0'); const d = String(dateStr.getDate()).padStart(2, '0'); dateStr = `${y}-${m}-${d}`; } else if (typeof dateStr === 'string') { dateStr = dateStr.split('T')[0]; } countsByDate[dateStr] = parseInt(row.count) || 0; } const data = dates.map(d => countsByDate[d] || 0); return { id: line.id, name: line.name, color: line.color, is_default: line.is_default, data }; })); res.json({ labels: dates, lines: linesData }); } catch (err) { console.error('Error generating chart data:', err); res.status(500).json({ error: err.message }); } }); // GET /api/dashboard/export-excel router.get('/export-excel', async (req, res) => { const { start_date, end_date, series_ids } = req.query; if (!start_date || !end_date) { return res.status(400).json({ error: 'start_date and end_date are required' }); } try { let lines = []; if (series_ids) { const ids = series_ids.split(',').map(id => parseInt(id, 10)).filter(id => !isNaN(id)); if (ids.length > 0) { const placeholders = ids.map(() => '?').join(', '); lines = db.prepare(`SELECT * FROM dashboard_chart_lines WHERE is_visible = 1 AND id IN (${placeholders}) ORDER BY is_default DESC, id ASC`).all(...ids); } } else { lines = db.prepare("SELECT * FROM dashboard_chart_lines WHERE is_visible = 1 ORDER BY is_default DESC, id ASC").all(); } const wb = XLSX.utils.book_new(); const allRows = []; for (const line of lines) { const conditions = []; const params = []; let paramIdx = 1; conditions.push(`t.create_time >= $${paramIdx++}`); params.push(start_date + ' 00:00:00'); conditions.push(`t.create_time <= $${paramIdx++}`); params.push(end_date + ' 23:59:59'); const statuses = JSON.parse(line.statuses || '[]'); if (statuses.length > 0) { const placeholders = statuses.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.ticket_state_id IN (${placeholders})`); params.push(...statuses.map(id => parseInt(id))); } const types = JSON.parse(line.types || '[]'); if (types.length > 0) { const placeholders = types.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.type_id IN (${placeholders})`); params.push(...types.map(id => parseInt(id))); } const queues = JSON.parse(line.queues || '[]'); if (queues.length > 0) { const placeholders = queues.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.queue_id IN (${placeholders})`); params.push(...queues.map(id => parseInt(id))); } const owners = JSON.parse(line.owners || '[]'); if (owners.length > 0) { const placeholders = owners.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.user_id IN (${placeholders})`); params.push(...owners.map(id => parseInt(id))); } const responsibles = JSON.parse(line.responsibles || '[]'); if (responsibles.length > 0) { const placeholders = responsibles.map(() => `$${paramIdx++}`).join(', '); conditions.push(`t.responsible_user_id IN (${placeholders})`); params.push(...responsibles.map(id => parseInt(id))); } const sql = ` SELECT t.id AS ticket_id, t.tn, t.title, tt.name AS type_name, ts.name AS state_name, tst.name AS state_type_name, q.name AS queue_name, COALESCE(u.first_name || ' ' || u.last_name, u.login) AS owner_name, COALESCE(ru.first_name || ' ' || ru.last_name, ru.login) AS responsible_name, t.create_time, t.change_time FROM ticket t LEFT JOIN ticket_type tt ON t.type_id = tt.id LEFT JOIN ticket_state ts ON t.ticket_state_id = ts.id LEFT JOIN ticket_state_type tst ON ts.type_id = tst.id LEFT JOIN queue q ON t.queue_id = q.id LEFT JOIN users u ON t.user_id = u.id LEFT JOIN users ru ON t.responsible_user_id = ru.id WHERE ${conditions.join(' AND ')} ORDER BY t.create_time DESC `; const result = await pool.query(sql, params); const rows = result.rows.map(t => { const isClosed = t.state_type_name && ( t.state_type_name.toLowerCase().includes('closed') || t.state_name.toLowerCase().includes('closed') || t.state_name.toLowerCase().includes('chiuso') || t.state_name.toLowerCase().includes('chiusa') ); const closureDate = isClosed ? t.change_time : ''; return { 'Origine della serie': line.name, 'ID Ticket': t.ticket_id, 'Numero (TN)': t.tn, 'Oggetto': t.title || '', 'Tipo': t.type_name || '', 'Stato': t.state_name || '', 'Coda': t.queue_name || '', 'Owner': t.owner_name || '', 'Responsabile': t.responsible_name || '', 'Data di Creazione': t.create_time, 'Data di Chiusura': closureDate }; }); allRows.push(...rows); const ws = XLSX.utils.json_to_sheet(rows); const cleanName = line.name.replace(/[\\\/\?\*\:\[\]]/g, '').slice(0, 30); XLSX.utils.book_append_sheet(wb, ws, cleanName || `Serie ${line.id}`); } if (allRows.length > 0) { // Sort combined rows by Data di Creazione descending allRows.sort((a, b) => new Date(b['Data di Creazione']) - new Date(a['Data di Creazione'])); const wsAll = XLSX.utils.json_to_sheet(allRows); wb.SheetNames.unshift('Tutte le serie'); wb.Sheets['Tutte le serie'] = wsAll; } const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); res.setHeader('Content-Disposition', `attachment; filename="Andamento_Ticket_${start_date}_${end_date}.xlsx"`); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.send(buf); } catch (err) { console.error('Error exporting excel:', err); res.status(500).json({ error: err.message }); } }); module.exports = router;