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 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; 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')` ), // 10 most recent tickets 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 10` ), // 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), }); } 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 }); } }); module.exports = router;