Files
bi-agents/server.js
root 022993b201 feat: novo dashboard admin com KPIs, tendencias e ranking
- Adiciona src/admin-dashboard.js com lazy loading
- KPIs: hoje vs media 30 dias por fluxo
- Graficos de tendencia 30 dias (consolidado e por fluxo)
- Ranking top 5 agentes com filtro de periodo
- Adiciona sistema de cache (src/cache.js)
- Cache com TTL e auto-refresh periodico (5-10min)
- APIs: /admin/api/kpis, /admin/api/trend, /admin/api/top-agentes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 13:53:55 -05:00

333 lines
11 KiB
JavaScript

/**
* BI - CCC (Central Command Center) — CambioReal
* Login Unificado: todos os usuarios acessam via /login
*
* Uso: node server.js
* Abre: http://localhost:3080
*/
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const path = require('path');
const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth');
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes } = require('./src/queries');
const { buildHTML } = require('./src/dashboard');
const { buildAdminHTML } = require('./src/admin-panel');
const { buildAdminHomeHTML } = require('./src/admin-home');
const { buildAdminDashboardHTML } = require('./src/admin-dashboard');
const bcrypt = require('bcrypt');
const db = require('./src/db-local');
const cache = require('./src/cache');
const app = express();
const PORT = process.env.PORT || 3080;
// Middleware
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(session({
secret: process.env.SESSION_SECRET || 'bi-agentes-default-secret',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 8 * 60 * 60 * 1000 }, // 8 horas
}));
// Static files
app.use('/public', express.static(path.join(__dirname, 'public')));
// --- Unified Login Routes ---
// Root -> login page (or redirect if logged in)
app.get('/', (req, res) => {
if (req.session?.user) {
return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard');
}
res.redirect('/login');
});
// Login page
app.get('/login', (req, res) => {
if (req.session?.user) {
return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard');
}
res.sendFile(path.join(__dirname, 'public', 'login.html'));
});
// Unified Login POST - detects role and redirects accordingly
app.post('/login', async (req, res) => {
const { email, senha } = req.body;
const emailParam = encodeURIComponent(email || '');
try {
const user = await authenticate(email, senha);
if (!user) return res.redirect(`/login?error=1&email=${emailParam}`);
// Unified session
req.session.user = {
id: user.id,
email: user.email,
nome: user.nome,
role: user.role || 'agente',
agente_id: user.agente_id
};
// Redirect based on role
if (user.role === 'admin') {
res.redirect('/admin');
} else {
res.redirect('/dashboard');
}
} catch (err) {
console.error('Login error:', err);
res.redirect(`/login?error=1&email=${emailParam}`);
}
});
// Unified Logout
app.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/login'));
});
// Legacy admin login - redirect to unified login
app.get('/admin/login', (req, res) => {
res.redirect('/login');
});
// Legacy admin logout - redirect to unified logout
app.get('/admin/logout', (req, res) => {
res.redirect('/logout');
});
// --- Agent Routes ---
// Dashboard (agente only)
app.get('/dashboard', requireRole('agente'), async (req, res) => {
try {
const user = req.session.user;
const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(user.agente_id);
const data = serialize(rowsBrlUsd, rowsUsdBrl);
const html = buildHTML(data, user);
res.send(html);
} catch (err) {
console.error('Dashboard error:', err);
res.status(500).send('Erro ao carregar dashboard: ' + err.message);
}
});
// --- Admin Routes ---
// Admin home (admin only) - Fast daily overview
app.get('/admin', requireRole('admin'), async (req, res) => {
try {
const stats = await fetchDailyStats();
const html = buildAdminHomeHTML(stats, req.session.user);
res.send(html);
} catch (err) {
console.error('Admin home error:', err);
res.status(500).send('Erro ao carregar home admin: ' + err.message);
}
});
// Admin agents management (admin only)
app.get('/admin/agentes', requireRole('admin'), (req, res) => {
try {
const agentes = db.prepare('SELECT * FROM agentes ORDER BY id DESC').all();
const html = buildAdminHTML(agentes, req.session.user);
res.send(html);
} catch (err) {
console.error('Admin panel error:', err);
res.status(500).send('Erro ao carregar painel admin: ' + err.message);
}
});
// Admin Dashboard - KPIs, Tendências e Ranking (com lazy load)
app.get('/admin/dashboard', requireRole('admin'), async (req, res) => {
try {
const user = req.session.user;
const html = buildAdminDashboardHTML({ nome: user.nome, email: user.email });
res.send(html);
} catch (err) {
console.error('Admin dashboard error:', err);
res.status(500).send('Erro ao carregar dashboard admin: ' + err.message);
}
});
// API endpoint for admin dashboard data (admin only)
app.get('/admin/api/data', requireRole('admin'), async (req, res) => {
try {
const dias = parseInt(req.query.dias) || 90;
const { rowsBrlUsd, rowsUsdBrl } = await fetchAllTransacoes(dias);
const data = serialize(rowsBrlUsd, rowsUsdBrl);
res.json({ success: true, data, count: data.length });
} catch (err) {
console.error('Admin API error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// API: KPIs (hoje vs média 30 dias) - com cache
app.get('/admin/api/kpis', requireRole('admin'), async (req, res) => {
try {
const data = await cache.getOrFetch('kpis', fetchKPIs, 5 * 60 * 1000);
res.json({ success: true, data });
} catch (err) {
console.error('KPIs API error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// API: Tendência 30 dias - com cache
app.get('/admin/api/trend', requireRole('admin'), async (req, res) => {
try {
const data = await cache.getOrFetch('trend30', fetchTrend30Days, 10 * 60 * 1000);
res.json({ success: true, data });
} catch (err) {
console.error('Trend API error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// API: Top 5 agentes - com cache por período
app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => {
try {
const dias = parseInt(req.query.dias) || 30;
const cacheKey = `top-agentes-${dias}`;
// Busca dados do RDS (com cache)
const rawData = await cache.getOrFetch(cacheKey, () => fetchTopAgentes(dias), 10 * 60 * 1000);
// Adiciona nomes dos agentes do SQLite local
const data = rawData.map(r => {
const agente = db.prepare('SELECT nome FROM agentes WHERE agente_id = ?').get(r.agente_id);
return {
...r,
agente: agente?.nome || `Agente #${r.agente_id}`
};
});
res.json({ success: true, data });
} catch (err) {
console.error('Top Agentes API error:', err);
res.status(500).json({ success: false, error: err.message });
}
});
// Admin emulate agent - view dashboard as specific agent (admin only)
app.get('/admin/emular/:agente_id', requireRole('admin'), async (req, res) => {
try {
const agenteId = parseInt(req.params.agente_id);
const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId);
if (!agente) {
return res.status(404).send('Agente nao encontrado');
}
const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(agenteId);
const data = serialize(rowsBrlUsd, rowsUsdBrl);
const html = buildHTML(data, {
nome: agente.nome + ' (Emulando)',
agente_id: agenteId,
email: agente.email
}, true, null, false, true); // isEmulating = true
res.send(html);
} catch (err) {
console.error('Admin emulate error:', err);
res.status(500).send('Erro ao emular agente: ' + err.message);
}
});
// Create user (admin only)
app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
const { nome, email, agente_id, senha, role } = req.body;
try {
if (!nome || !email || !senha) {
return res.status(400).json({ error: 'Nome, email e senha sao obrigatorios' });
}
const userRole = role || 'agente';
const agenteId = userRole === 'admin' ? 0 : (agente_id || 0);
if (userRole === 'agente' && !agente_id) {
return res.status(400).json({ error: 'Agente ID e obrigatorio para agentes' });
}
const result = await createUser(email, senha, nome, userRole, agenteId);
res.json({ success: true, id: result.lastInsertRowid });
} catch (err) {
console.error('Create user error:', err);
if (err.message && err.message.includes('UNIQUE')) {
return res.status(400).json({ error: 'E-mail ja cadastrado' });
}
res.status(500).json({ error: 'Erro ao criar usuario' });
}
});
// Update user (admin only)
app.put('/admin/agentes/:id', requireRole('admin'), async (req, res) => {
const { id } = req.params;
const { nome, email, agente_id, ativo, senha, role } = req.body;
try {
const agent = db.prepare('SELECT * FROM agentes WHERE id = ?').get(id);
if (!agent) {
return res.status(404).json({ error: 'Usuario nao encontrado' });
}
if (senha) {
const hash = await bcrypt.hash(senha, 10);
db.prepare('UPDATE agentes SET senha_hash = ? WHERE id = ?').run(hash, id);
}
if (nome !== undefined) {
db.prepare('UPDATE agentes SET nome = ? WHERE id = ?').run(nome, id);
}
if (email !== undefined) {
db.prepare('UPDATE agentes SET email = ? WHERE id = ?').run(email, id);
}
if (agente_id !== undefined) {
db.prepare('UPDATE agentes SET agente_id = ? WHERE id = ?').run(agente_id, id);
}
if (ativo !== undefined) {
db.prepare('UPDATE agentes SET ativo = ? WHERE id = ?').run(ativo, id);
}
if (role !== undefined) {
db.prepare('UPDATE agentes SET role = ? WHERE id = ?').run(role, id);
}
res.json({ success: true });
} catch (err) {
console.error('Update user error:', err);
if (err.message && err.message.includes('UNIQUE')) {
return res.status(400).json({ error: 'E-mail ja cadastrado' });
}
res.status(500).json({ error: 'Erro ao atualizar usuario' });
}
});
// Delete/deactivate user (admin only)
app.delete('/admin/agentes/:id', requireRole('admin'), (req, res) => {
const { id } = req.params;
try {
const result = db.prepare('UPDATE agentes SET ativo = 0 WHERE id = ?').run(id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Usuario nao encontrado' });
}
res.json({ success: true });
} catch (err) {
console.error('Delete user error:', err);
res.status(500).json({ error: 'Erro ao desativar usuario' });
}
});
// Start
app.listen(PORT, () => {
console.log(`BI - CCC rodando: http://localhost:${PORT}`);
// Inicializa cache com auto-refresh (atualiza a cada 5 minutos)
console.log('[Cache] Inicializando cache com auto-refresh...');
cache.registerAutoRefresh('kpis', fetchKPIs, 5 * 60 * 1000);
cache.registerAutoRefresh('trend30', fetchTrend30Days, 10 * 60 * 1000);
cache.registerAutoRefresh('top-agentes-30', () => fetchTopAgentes(30), 10 * 60 * 1000);
cache.registerAutoRefresh('top-agentes-7', () => fetchTopAgentes(7), 10 * 60 * 1000);
cache.registerAutoRefresh('top-agentes-90', () => fetchTopAgentes(90), 10 * 60 * 1000);
});