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

@@ -48,6 +48,7 @@ function buildAdminHTML(agentes, admin) {
.status-badge.active { background: var(--green-bg); color: var(--green); }
.status-badge.inactive { background: var(--red-bg); color: var(--red); }
.status-badge.admin { background: var(--admin-bg); color: var(--admin-accent); }
.status-badge.corporate { background: var(--corporate-bg); color: var(--corporate-accent); }
.status-badge.agent { background: var(--blue-bg); color: var(--blue); }
.actions { display: flex; gap: 6px; }
@@ -130,7 +131,49 @@ function buildAdminHTML(agentes, admin) {
.alert.show { display: block; }
@media (max-width: 768px) {
.toolbar { flex-direction: column; gap: 12px; align-items: flex-start; }
.toolbar { flex-direction: column; gap: 12px; align-items: stretch; }
.toolbar h2 { font-size: 16px; }
.btn-create { text-align: center; }
/* Mobile table improvements */
table { font-size: 12px; }
thead th { padding: 10px 8px; font-size: 10px; }
tbody td { padding: 10px 8px; }
/* Stack action buttons on mobile */
.actions {
flex-direction: column;
gap: 4px;
}
.btn-action {
padding: 8px 10px;
font-size: 11px;
text-align: center;
width: 100%;
}
/* Modal improvements for mobile */
.modal {
margin: 10px;
max-height: calc(100vh - 20px);
border-radius: 12px;
}
.modal-header { padding: 16px 20px; }
.modal-body { padding: 16px 20px; }
.modal-footer {
padding: 12px 20px;
flex-direction: column;
gap: 8px;
}
.modal-footer button { width: 100%; }
}
@media (max-width: 480px) {
.table-card { border-radius: 8px; }
table { font-size: 11px; }
thead th { padding: 8px 6px; }
tbody td { padding: 8px 6px; }
.status-badge { font-size: 10px; padding: 3px 8px; }
}
</style>
`;
@@ -173,12 +216,12 @@ ${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })}
<td>${a.id}</td>
<td>${a.nome}</td>
<td>${a.email}</td>
<td><span class="status-badge ${a.role === 'admin' ? 'admin' : 'agent'}">${a.role === 'admin' ? 'Admin' : 'Agente'}</span></td>
<td>${a.role === 'admin' ? '-' : a.agente_id}</td>
<td><span class="status-badge ${a.role === 'admin' ? 'admin' : a.role === 'corporate' ? 'corporate' : 'agent'}">${a.role === 'admin' ? 'Admin' : a.role === 'corporate' ? 'Corporate' : 'Agente'}</span></td>
<td>${(a.role === 'admin' || a.role === 'corporate') ? '-' : a.agente_id}</td>
<td><span class="status-badge ${a.ativo ? 'active' : 'inactive'}">${a.ativo ? 'Ativo' : 'Inativo'}</span></td>
<td>${a.created_at ? new Date(a.created_at).toLocaleDateString('pt-BR') : '-'}</td>
<td class="actions">
${a.role === 'agente' ? `<a href="/admin/emular/${a.agente_id}" class="btn-action btn-emular" title="Ver como este agente">Emular</a>` : ''}
${a.role === 'agente' ? `<a href="/corporate/emular/${a.agente_id}" class="btn-action btn-emular" title="Ver como este agente">Emular</a>` : ''}
<button class="btn-action btn-edit" onclick="openEditModal(${a.id}, '${a.nome.replace(/'/g, "\\'")}', '${a.email.replace(/'/g, "\\'")}', ${a.agente_id}, '${a.role || 'agente'}', event)">Editar</button>
<button class="btn-action btn-toggle" onclick="toggleAgente(${a.id}, ${a.ativo})">${a.ativo ? 'Desativar' : 'Ativar'}</button>
<button class="btn-action btn-password" onclick="openPasswordModal(${a.id}, '${a.nome.replace(/'/g, "\\'")}')">Senha</button>
@@ -215,6 +258,7 @@ ${buildFooter()}
<label>Tipo de Usuario</label>
<select id="agentRole" name="role" onchange="toggleAgenteIdField()">
<option value="agente">Agente</option>
<option value="corporate">Corporate</option>
<option value="admin">Administrador</option>
</select>
</div>
@@ -276,7 +320,8 @@ function toggleAgenteIdField() {
const role = document.getElementById('agentRole').value;
const agenteIdGroup = document.getElementById('agenteIdGroup');
const agenteIdInput = document.getElementById('agentAgenteId');
if (role === 'admin') {
// Admin and Corporate don't need agente_id
if (role === 'admin' || role === 'corporate') {
agenteIdGroup.style.display = 'none';
agenteIdInput.required = false;
agenteIdInput.value = '';
@@ -344,6 +389,7 @@ async function submitAgentForm(e) {
role: role,
};
// Only agents need agente_id
if (role === 'agente') {
const agenteId = document.getElementById('agentAgenteId').value;
if (!isEditing && !agenteId) {
@@ -351,6 +397,9 @@ async function submitAgentForm(e) {
return;
}
data.agente_id = parseInt(agenteId) || 0;
} else {
// Admin and Corporate don't have agente_id
data.agente_id = 0;
}
if (!isEditing) {