Files
otrs-turbo/routes/dashboard.js
T

266 lines
9.1 KiB
JavaScript

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;
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 });
}
});
// POST /api/dashboard/settings — Save settings for the agent
router.post('/settings', (req, res) => {
const activeAgentId = parseInt(req.headers['x-agent-id'], 10) || 1;
const { preview_limit, tickets_per_page } = req.body;
try {
let currentSettings = { preview_limit: 10, tickets_per_page: 50 };
const row = db.prepare("SELECT preview_limit, tickets_per_page FROM agent_settings WHERE agent_id = ?").get(activeAgentId);
if (row) {
currentSettings = row;
}
const newPreviewLimit = preview_limit !== undefined ? parseInt(preview_limit, 10) : currentSettings.preview_limit;
const newTicketsPerPage = tickets_per_page !== undefined ? parseInt(tickets_per_page, 10) : currentSettings.tickets_per_page;
db.prepare(`
INSERT OR REPLACE INTO agent_settings (agent_id, preview_limit, tickets_per_page)
VALUES (?, ?, ?)
`).run(activeAgentId, newPreviewLimit, newTicketsPerPage);
res.json({ success: true, preview_limit: newPreviewLimit, tickets_per_page: newTicketsPerPage });
} catch (err) {
console.error('Error saving agent settings:', err);
res.status(500).json({ error: err.message });
}
});
module.exports = router;