/** * 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, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData, fetchMerchantProfile, fetchMerchantData } = 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 { buildAdminBIHTML } = require('./src/admin-bi'); const { buildAdminClienteHTML } = require('./src/admin-cliente'); 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 --- // Helper function to get redirect URL based on role function getRedirectByRole(role) { if (role === 'admin') return '/corporate'; // Admin vai direto pro Corporate Dashboard if (role === 'corporate') return '/corporate'; return '/dashboard'; } // Root -> login page (or redirect if logged in) app.get('/', (req, res) => { if (req.session?.user) { return res.redirect(getRedirectByRole(req.session.user.role)); } res.redirect('/login'); }); // Login page app.get('/login', (req, res) => { if (req.session?.user) { return res.redirect(getRedirectByRole(req.session.user.role)); } 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 res.redirect(getRedirectByRole(user.role)); } 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 (User Management - admin only) --- // Admin home - User management panel (admin only) app.get('/admin', 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); } }); // Alias: /admin/usuarios -> /admin app.get('/admin/usuarios', requireRole('admin'), (req, res) => { res.redirect('/admin'); }); // Legacy route - redirect to /admin app.get('/admin/agentes', requireRole('admin'), (req, res) => { res.redirect('/admin'); }); // --- Corporate Routes (Dashboard + Emulation - corporate and admin) --- // Corporate Dashboard - Full KPIs, Trends and Ranking app.get('/corporate', requireRole('corporate', 'admin'), async (req, res) => { try { const user = req.session.user; const html = buildAdminDashboardHTML(user); res.send(html); } catch (err) { console.error('Corporate dashboard error:', err); res.status(500).send('Erro ao carregar dashboard corporate: ' + err.message); } }); // Legacy route - redirect to /corporate app.get('/corporate/dashboard', requireRole('corporate', 'admin'), (req, res) => { res.redirect('/corporate'); }); // Corporate emulate agent - view dashboard as specific agent app.get('/corporate/emular/:agente_id', requireRole('corporate', '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, emulatorRole: req.session.user.role // Pass the emulator's role }, true, null, false, true); // isEmulating = true res.send(html); } catch (err) { console.error('Corporate emulate error:', err); res.status(500).send('Erro ao emular agente: ' + err.message); } }); // Legacy route - redirect to /corporate/emular app.get('/admin/emular/:agente_id', requireRole('admin'), (req, res) => { res.redirect(`/corporate/emular/${req.params.agente_id}`); }); // --- Live Rate Proxy (caches for 3s to avoid rate limiting) --- let _rateCache = { data: null, ts: 0 }; app.get('/api/cotacao', async (req, res) => { try { const now = Date.now(); if (_rateCache.data && now - _rateCache.ts < 3000) { return res.json(_rateCache.data); } const token = process.env.AWESOME_API_TOKEN || ''; const url = 'https://economia.awesomeapi.com.br/json/last/USD-BRL,EUR-BRL' + (token ? '?token=' + token : ''); const resp = await fetch(url); const json = await resp.json(); _rateCache = { data: json, ts: now }; res.json(json); } catch (err) { res.status(500).json({ error: err.message }); } }); // --- Corporate API Routes (dashboard data - corporate and admin) --- // API endpoint for corporate dashboard data app.get('/corporate/api/data', requireRole('corporate', '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('Corporate API error:', err); res.status(500).json({ success: false, error: err.message }); } }); // API: KPIs (hoje vs média 30 dias) - com cache app.get('/corporate/api/kpis', requireRole('corporate', '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('/corporate/api/trend', requireRole('corporate', '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('/corporate/api/top-agentes', requireRole('corporate', '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 }); } }); // API: Corporate Dashboard - KPIs por período app.get('/corporate/api/kpis-period', requireRole('corporate', 'admin'), async (req, res) => { try { const { inicio, fim } = req.query; if (!inicio || !fim) { return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' }); } const data = await fetchKPIsByPeriod(inicio, fim); res.json({ success: true, data }); } catch (err) { console.error('Corporate KPIs API error:', err); res.status(500).json({ success: false, error: err.message }); } }); // API: Corporate Dashboard - Tendência por período app.get('/corporate/api/trend-period', requireRole('corporate', 'admin'), async (req, res) => { try { const { inicio, fim } = req.query; if (!inicio || !fim) { return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' }); } const data = await fetchTrendByPeriod(inicio, fim); res.json({ success: true, data }); } catch (err) { console.error('Corporate Trend API error:', err); res.status(500).json({ success: false, error: err.message }); } }); // Legacy API routes - redirect to /corporate/api/* app.get('/admin/api/data', requireRole('admin'), (req, res) => { res.redirect(`/corporate/api/data?${new URLSearchParams(req.query)}`); }); app.get('/admin/api/kpis', requireRole('admin'), (req, res) => { res.redirect('/corporate/api/kpis'); }); app.get('/admin/api/trend', requireRole('admin'), (req, res) => { res.redirect('/corporate/api/trend'); }); app.get('/admin/api/top-agentes', requireRole('admin'), (req, res) => { res.redirect(`/corporate/api/top-agentes?${new URLSearchParams(req.query)}`); }); app.get('/admin/api/corporate/kpis', requireRole('admin'), (req, res) => { res.redirect(`/corporate/api/kpis-period?${new URLSearchParams(req.query)}`); }); app.get('/admin/api/corporate/trend', requireRole('admin'), (req, res) => { res.redirect(`/corporate/api/trend-period?${new URLSearchParams(req.query)}`); }); app.get('/admin/dashboard', requireRole('admin'), (req, res) => { res.redirect('/corporate/dashboard'); }); // --- Admin BI Dashboard (admin only) --- app.get('/admin/bi', requireRole('admin'), (req, res) => { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate'); res.set('Pragma', 'no-cache'); const html = buildAdminBIHTML(req.session.user); res.send(html); } catch (err) { console.error('Admin BI error:', err); res.status(500).send('Erro ao carregar BI: ' + err.message); } }); app.get('/admin/api/bi', requireRole('admin'), async (req, res) => { try { const start = req.query.start; const end = req.query.end; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); const getAgenteName = (agenteId) => { const row = db.prepare('SELECT nome FROM agentes WHERE agente_id = ?').get(agenteId); return row ? row.nome : null; }; const data = await fetchBIData(start, end, getAgenteName); res.json(data); } catch (err) { console.error('Admin BI API error:', err); res.status(500).json({ error: err.message }); } }); app.get('/admin/api/bi/revenue', requireRole('admin'), async (req, res) => { try { const { start, end, granularity } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); const data = await fetchRevenueAnalytics(start, end, granularity || 'dia'); res.json(data); } catch (err) { console.error('Revenue API error:', err); res.status(500).json({ error: err.message }); } }); app.get('/admin/api/bi/strategic', requireRole('admin'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); const data = await fetchBIStrategic(start, end); res.json(data); } catch (err) { console.error('Strategic BI API error:', err); res.status(500).json({ error: err.message }); } }); // --- Admin Cliente Dashboard (admin only) --- app.get('/admin/cliente', requireRole('admin'), (req, res) => { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate'); res.set('Pragma', 'no-cache'); const html = buildAdminClienteHTML(req.session.user); res.send(html); } catch (err) { console.error('Admin Cliente error:', err); res.status(500).send('Erro ao carregar pagina de cliente: ' + err.message); } }); app.get('/admin/api/clientes/top', requireRole('admin'), async (req, res) => { try { const data = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000); res.json(data); } catch (err) { console.error('Top clients API error:', err); res.status(500).json({ error: err.message }); } }); app.get('/admin/api/clientes/search', requireRole('admin'), async (req, res) => { try { const q = (req.query.q || '').trim(); if (q.length < 2) return res.json([]); const data = await fetchClientSearch(q); res.json(data); } catch (err) { console.error('Client search API error:', err); res.status(500).json({ error: err.message }); } }); app.get('/admin/api/cliente/:id/profile', requireRole('admin'), async (req, res) => { try { const clienteId = parseInt(req.params.id); if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' }); const [profile, merchant] = await Promise.all([ fetchClientProfile(clienteId), fetchMerchantProfile(clienteId) ]); if (merchant.is_merchant) { const ck = merchant.checkout; profile.merchant = { empresa_id: merchant.empresa_id, nome_empresa: merchant.nome_empresa }; profile.total_ops += ck.tx_count; profile.total_vol_usd += ck.vol_usd; profile.total_spread_revenue += ck.revenue; profile.ltv = profile.total_spread_revenue; // Extend date ranges const dates = [profile.first_op, ck.first_op].filter(Boolean); const lastDates = [profile.last_op, ck.last_op].filter(Boolean); if (dates.length) profile.first_op = dates.sort()[0]; if (lastDates.length) { profile.last_op = lastDates.sort().pop(); profile.days_inactive = Math.round((Date.now() - new Date(profile.last_op).getTime()) / 86400000); } profile.months_active = Math.max(profile.months_active, ck.months_active); profile.avg_monthly_vol = profile.months_active > 0 ? Math.round(profile.total_vol_usd / profile.months_active) : 0; profile.avg_monthly_ops = profile.months_active > 0 ? Math.round(profile.total_ops / profile.months_active * 10) / 10 : 0; profile.avg_monthly_revenue = profile.months_active > 0 ? Math.round(profile.total_spread_revenue / profile.months_active * 100) / 100 : 0; profile.checkout = ck; } res.json(profile); } catch (err) { console.error('Client profile API error:', err); res.status(500).json({ error: err.message }); } }); app.get('/admin/api/cliente/:id/data', requireRole('admin'), async (req, res) => { try { const clienteId = parseInt(req.params.id); const { start, end } = req.query; if (!clienteId || !start || !end) return res.status(400).json({ error: 'client ID, start and end required' }); const merchant = await fetchMerchantProfile(clienteId); if (merchant.is_merchant) { const [data, mData] = await Promise.all([ fetchClientData(clienteId, start, end), fetchMerchantData(merchant.empresa_id, start, end) ]); // Add checkout KPIs data.kpis.checkout = mData.kpis; // Merge totals data.kpis.total.qtd += mData.kpis.qtd; data.kpis.total.vol_usd += mData.kpis.vol_usd; data.kpis.total.spread_revenue += mData.kpis.revenue; const totalQtd = data.kpis.total.qtd; data.kpis.total.ticket_medio = totalQtd > 0 ? Math.round(data.kpis.total.vol_usd / totalQtd) : 0; // Merge comparison data.comparison.prev_qtd += mData.comparison.prev_qtd; data.comparison.prev_vol_usd += mData.comparison.prev_vol_usd; data.comparison.prev_spread += mData.comparison.prev_revenue; // Merchant-specific data data.merchant = { monthly: mData.monthly, topPayers: mData.topPayers, comparison: mData.comparison }; // Merge transactions (checkout txs get flow="Checkout") data.transactions = data.transactions.concat(mData.transactions) .sort((a, b) => b.date.localeCompare(a.date)); res.json(data); } else { const data = await fetchClientData(clienteId, start, end); res.json(data); } } catch (err) { console.error('Client data API error:', err); res.status(500).json({ error: 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'; // Admin and Corporate don't need agente_id const agenteId = (userRole === 'admin' || userRole === 'corporate') ? 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); });