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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user