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:
root
2026-02-10 22:30:43 -05:00
parent 1ad28f54dd
commit 7ee15ad5e5
12 changed files with 1285 additions and 436 deletions

View File

@@ -10,9 +10,12 @@ const cssVariables = `
--primary-light: #9B2DE5;
--primary-dark: #5A0091;
--primary-bg: #F5EAFA;
--admin-accent: #5A0091;
--admin-dark: #3D0066;
--admin-bg: #F5EAFA;
--admin-accent: #2E7D32;
--admin-dark: #1B5E20;
--admin-bg: #E8F5E9;
--corporate-accent: #7600be;
--corporate-dark: #5A0091;
--corporate-bg: #F5EAFA;
--bg: #F0F2F5;
--card: #FFFFFF;
--text: #1A1D23;
@@ -51,6 +54,10 @@ const headerCSS = `
--header-color: var(--admin-accent);
--header-dark: var(--admin-dark);
}
.app-header.corporate {
--header-color: var(--corporate-accent);
--header-dark: var(--corporate-dark);
}
.app-header.agent {
--header-color: var(--primary);
--header-dark: var(--primary-dark);
@@ -69,6 +76,11 @@ const headerCSS = `
display: flex;
align-items: center;
gap: 16px;
text-decoration: none;
color: white;
}
.header-brand:hover {
opacity: 0.95;
}
.header-brand .logo {
height: 36px;
@@ -195,32 +207,80 @@ const headerCSS = `
/* Responsive */
@media (max-width: 768px) {
.header-inner {
padding: 16px 20px;
padding: 12px 16px;
height: auto;
flex-direction: column;
gap: 12px;
gap: 10px;
}
.header-brand {
gap: 10px;
}
.header-brand .logo {
height: 28px;
padding: 4px 8px;
}
.header-brand .app-name-badge {
font-size: 12px;
padding: 5px 10px;
}
.header-brand .app-subtitle {
display: none;
}
.header-nav {
width: 100%;
justify-content: center;
gap: 6px;
}
.header-nav a {
font-size: 12px;
padding: 8px 12px;
}
.header-user {
width: 100%;
justify-content: center;
gap: 10px;
}
.header-user .user-info {
font-size: 12px;
}
.header-user .avatar {
width: 32px;
height: 32px;
font-size: 11px;
}
.header-user .btn-logout {
font-size: 11px;
padding: 6px 12px;
}
.app-container {
padding: 20px;
padding: 16px;
}
.header-brand .app-subtitle {
}
@media (max-width: 480px) {
.header-inner {
padding: 10px 12px;
}
.header-nav {
flex-wrap: wrap;
}
.header-nav a {
font-size: 11px;
padding: 6px 10px;
}
.header-user .user-info {
display: none;
}
.app-container {
padding: 12px;
}
}
`;
/**
* Gera o header HTML
* @param {Object} options
* @param {string} options.role - 'admin' ou 'agente'
* @param {string} options.role - 'admin', 'corporate' ou 'agente'
* @param {string} options.userName - Nome do usuário
* @param {string} options.activePage - Página ativa para nav
* @param {boolean} options.showNav - Mostrar navegação
@@ -228,7 +288,12 @@ const headerCSS = `
function buildHeader(options = {}) {
const { role = 'agente', userName = '', activePage = '', showNav = true } = options;
const isAdmin = role === 'admin';
const headerClass = isAdmin ? 'admin' : 'agent';
const isCorporate = role === 'corporate';
// Determine header class: admin (green), corporate (purple), agent (purple)
let headerClass = 'agent';
if (isAdmin) headerClass = 'admin';
else if (isCorporate) headerClass = 'corporate';
const initials = userName
.split(' ')
@@ -237,36 +302,59 @@ function buildHeader(options = {}) {
.join('')
.toUpperCase();
// Admin navigation: Corporate Dashboard + Users (admin on the right)
const adminNav = `
<nav class="header-nav">
<a href="/admin" class="${activePage === 'home' ? 'active' : ''}">Home</a>
<a href="/admin/agentes" class="${activePage === 'users' ? 'active' : ''}">Usuarios</a>
<a href="/admin/dashboard" class="${activePage === 'dashboard' ? 'active' : ''}">Dashboard</a>
<a href="/corporate" class="${activePage === 'dashboard' ? 'active' : ''}">Corporate</a>
<a href="/admin" class="${activePage === 'users' ? 'active' : ''}">Usuarios</a>
</nav>
`;
// Corporate navigation: Dashboard only
const corporateNav = `
<nav class="header-nav">
<a href="/corporate" class="${activePage === 'dashboard' ? 'active' : ''}">Dashboard</a>
</nav>
`;
// Agent navigation: Just their dashboard
const agentNav = `
<nav class="header-nav">
<a href="/dashboard" class="active">Meu Dashboard</a>
</nav>
`;
// Select navigation based on role
let nav = agentNav;
if (isAdmin) nav = adminNav;
else if (isCorporate) nav = corporateNav;
// Home URL based on role
let homeUrl = '/dashboard';
if (isAdmin) homeUrl = '/corporate'; // Admin home is Corporate Dashboard
else if (isCorporate) homeUrl = '/corporate';
// Role label for display
let roleLabel = 'Agente';
if (isAdmin) roleLabel = 'Admin';
else if (isCorporate) roleLabel = 'Corporate';
return `
<header class="app-header ${headerClass}">
<div class="header-inner">
<div class="header-brand">
<a href="${homeUrl}" class="header-brand">
<img src="/public/logo.png" alt="CambioReal" class="logo">
<div class="app-name">
<span class="app-name-badge">BI - CCC</span>
<span class="app-subtitle">Central Command Center</span>
</div>
</div>
${showNav ? (isAdmin ? adminNav : agentNav) : ''}
</a>
${showNav ? nav : ''}
<div class="header-user">
<div class="user-info">
<span class="user-avatar">${initials}</span>
<span>${userName}</span>
<span class="user-role">${isAdmin ? 'Admin' : 'Agente'}</span>
<span class="user-role">${roleLabel}</span>
</div>
<a href="/logout" class="btn-logout">Sair</a>
</div>