feat: trading terminal live rates + fix spread negativo + fix USD→BRL
- Adiciona widget de cotações ao vivo (USD/BRL e EUR/BRL) com design estilo terminal de trading (dark theme, tipografia mono, glow effects) - Proxy server-side /api/cotacao com cache 3s e token AwesomeAPI - Auto-refresh a cada 3 segundos apenas quando a página está aberta - Corrige cálculo de spread negativo: remove Math.abs() em USD→BRL e Math.max(0,...) no spread líquido - Corrige seção USD→BRL que não aparecia (filtro status !== 'finalizado') - Corrige valor_reais no fluxo USD→BRL: agora calcula valor * cotação - Adiciona classe CSS spread-negative para destacar spreads negativos - Bandeiras de fluxo (BR/US/EU) nos botões de compra e venda Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
195
server.js
195
server.js
@@ -11,7 +11,7 @@ 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 { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod } = require('./src/queries');
|
||||
const { buildHTML } = require('./src/dashboard');
|
||||
const { buildAdminHTML } = require('./src/admin-panel');
|
||||
const { buildAdminHomeHTML } = require('./src/admin-home');
|
||||
@@ -38,10 +38,17 @@ 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(req.session.user.role === 'admin' ? '/admin' : '/dashboard');
|
||||
return res.redirect(getRedirectByRole(req.session.user.role));
|
||||
}
|
||||
res.redirect('/login');
|
||||
});
|
||||
@@ -49,7 +56,7 @@ app.get('/', (req, res) => {
|
||||
// Login page
|
||||
app.get('/login', (req, res) => {
|
||||
if (req.session?.user) {
|
||||
return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard');
|
||||
return res.redirect(getRedirectByRole(req.session.user.role));
|
||||
}
|
||||
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
||||
});
|
||||
@@ -72,11 +79,7 @@ app.post('/login', async (req, res) => {
|
||||
};
|
||||
|
||||
// Redirect based on role
|
||||
if (user.role === 'admin') {
|
||||
res.redirect('/admin');
|
||||
} else {
|
||||
res.redirect('/dashboard');
|
||||
}
|
||||
res.redirect(getRedirectByRole(user.role));
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
res.redirect(`/login?error=1&email=${emailParam}`);
|
||||
@@ -114,22 +117,10 @@ app.get('/dashboard', requireRole('agente'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Admin Routes ---
|
||||
// --- Admin Routes (User Management - admin only) ---
|
||||
|
||||
// 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) => {
|
||||
// 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);
|
||||
@@ -140,33 +131,101 @@ app.get('/admin/agentes', requireRole('admin'), (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Dashboard - KPIs, Tendências e Ranking (com lazy load)
|
||||
app.get('/admin/dashboard', requireRole('admin'), async (req, res) => {
|
||||
// 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({ nome: user.nome, email: user.email });
|
||||
const html = buildAdminDashboardHTML(user);
|
||||
res.send(html);
|
||||
} catch (err) {
|
||||
console.error('Admin dashboard error:', err);
|
||||
res.status(500).send('Erro ao carregar dashboard admin: ' + err.message);
|
||||
console.error('Corporate dashboard error:', err);
|
||||
res.status(500).send('Erro ao carregar dashboard corporate: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint for admin dashboard data (admin only)
|
||||
app.get('/admin/api/data', requireRole('admin'), async (req, res) => {
|
||||
// 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('Admin API error:', 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('/admin/api/kpis', requireRole('admin'), async (req, res) => {
|
||||
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 });
|
||||
@@ -177,7 +236,7 @@ app.get('/admin/api/kpis', requireRole('admin'), async (req, res) => {
|
||||
});
|
||||
|
||||
// API: Tendência 30 dias - com cache
|
||||
app.get('/admin/api/trend', requireRole('admin'), async (req, res) => {
|
||||
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 });
|
||||
@@ -188,7 +247,7 @@ app.get('/admin/api/trend', requireRole('admin'), async (req, res) => {
|
||||
});
|
||||
|
||||
// API: Top 5 agentes - com cache por período
|
||||
app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => {
|
||||
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}`;
|
||||
@@ -212,30 +271,59 @@ app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Admin emulate agent - view dashboard as specific agent (admin only)
|
||||
app.get('/admin/emular/:agente_id', requireRole('admin'), async (req, res) => {
|
||||
// API: Corporate Dashboard - KPIs por período
|
||||
app.get('/corporate/api/kpis-period', 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 { inicio, fim } = req.query;
|
||||
if (!inicio || !fim) {
|
||||
return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' });
|
||||
}
|
||||
|
||||
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);
|
||||
const data = await fetchKPIsByPeriod(inicio, fim);
|
||||
res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
console.error('Admin emulate error:', err);
|
||||
res.status(500).send('Erro ao emular agente: ' + err.message);
|
||||
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');
|
||||
});
|
||||
|
||||
// Create user (admin only)
|
||||
app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
|
||||
const { nome, email, agente_id, senha, role } = req.body;
|
||||
@@ -245,7 +333,8 @@ app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
|
||||
}
|
||||
|
||||
const userRole = role || 'agente';
|
||||
const agenteId = userRole === 'admin' ? 0 : (agente_id || 0);
|
||||
// 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' });
|
||||
|
||||
Reference in New Issue
Block a user