fix: alinha filtros com corpo e remove alertas

- Header e filtros alinhados com container (1600px)
- Adiciona filters-inner wrapper
- Remove secao de Alertas Inteligentes (CSS, HTML e JS)
- Responsivo atualizado

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-08 14:09:23 -05:00
parent 9bf17d290a
commit 9648095048

View File

@@ -87,9 +87,17 @@ function buildHTML(data, agente, isAgentDashboard = true, diasPeriodo = null, as
.filters {
background: var(--card); border-bottom: 1px solid var(--border);
padding: 14px 40px; display: flex; gap: 24px; align-items: center; flex-wrap: wrap;
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.filters-inner {
max-width: 1600px;
margin: 0 auto;
padding: 14px 40px;
display: flex;
gap: 24px;
align-items: center;
flex-wrap: wrap;
}
.filter-group { display: flex; align-items: center; gap: 8px; }
.filter-group label {
font-size: 12px; font-weight: 600; color: var(--text-secondary);
@@ -233,149 +241,6 @@ function buildHTML(data, agente, isAgentDashboard = true, diasPeriodo = null, as
.netting-kpi .value.neutral { color: var(--blue); }
.netting-kpi .sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
/* Alerts Section */
.alerts-section {
background: var(--card);
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
margin-bottom: 28px;
overflow: hidden;
}
.alerts-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 22px;
border-bottom: 1px solid var(--border);
background: #FAFBFC;
}
.alerts-header h3 {
font-size: 15px;
font-weight: 700;
color: var(--text);
display: flex;
align-items: center;
gap: 10px;
}
.alerts-header .alert-count {
background: var(--primary);
color: white;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
min-width: 22px;
text-align: center;
}
.alerts-header .alert-count.zero {
background: var(--green);
}
.alerts-toggle {
background: none;
border: 1px solid var(--border);
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 6px;
}
.alerts-toggle:hover {
background: var(--bg);
border-color: var(--primary);
color: var(--primary);
}
.alerts-body {
padding: 20px 22px;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
overflow: hidden;
}
.alerts-body.collapsed {
max-height: 0 !important;
padding: 0 22px;
opacity: 0;
}
.alerts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 14px;
}
.alert-card {
border-radius: 10px;
padding: 14px 16px;
display: flex;
gap: 12px;
align-items: flex-start;
}
.alert-card.critical {
background: #FEE2E2;
border: 1px solid #FECACA;
}
.alert-card.warning {
background: #FEF3C7;
border: 1px solid #FDE68A;
}
.alert-card.info {
background: var(--blue-bg);
border: 1px solid #BFDBFE;
}
.alert-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.alert-card.critical .alert-icon {
background: #DC2626;
color: white;
}
.alert-card.warning .alert-icon {
background: #F59E0B;
color: white;
}
.alert-card.info .alert-icon {
background: var(--blue);
color: white;
}
.alert-content {
flex: 1;
min-width: 0;
}
.alert-title {
font-size: 13px;
font-weight: 700;
margin-bottom: 4px;
}
.alert-card.critical .alert-title { color: #DC2626; }
.alert-card.warning .alert-title { color: #B45309; }
.alert-card.info .alert-title { color: var(--blue); }
.alert-desc {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.no-alerts {
text-align: center;
padding: 30px;
color: var(--text-muted);
font-size: 13px;
}
.no-alerts .check-icon {
font-size: 32px;
margin-bottom: 8px;
color: var(--green);
}
/* Additional Charts Grid */
.charts-grid-3 {
display: grid;
@@ -430,7 +295,7 @@ function buildHTML(data, agente, isAgentDashboard = true, diasPeriodo = null, as
.chart-card.full-width { grid-column: span 1; }
.chart-card { min-height: 320px; }
.container { padding: 20px; }
.filters { padding: 12px 20px; flex-wrap: wrap; }
.filters-inner { padding: 12px 20px; }
.header-inner { padding: 16px 20px; flex-direction: column; gap: 12px; }
}
@media (max-width: 600px) {
@@ -478,49 +343,37 @@ ${isEmulating ? `
</div>
<div class="filters">
<div class="filter-group"><label>De:</label><input type="date" id="filterStart"></div>
<div class="filter-group"><label>Ate:</label><input type="date" id="filterEnd"></div>
<div class="filter-group">
<label>Granulacao:</label>
<select id="filterGran">
<option value="dia">Dia</option>
<option value="mes" selected>Mes</option>
<option value="ano">Ano</option>
</select>
<div class="filters-inner">
<div class="filter-group"><label>De:</label><input type="date" id="filterStart"></div>
<div class="filter-group"><label>Ate:</label><input type="date" id="filterEnd"></div>
<div class="filter-group">
<label>Granulacao:</label>
<select id="filterGran">
<option value="dia">Dia</option>
<option value="mes" selected>Mes</option>
<option value="ano">Ano</option>
</select>
</div>
<div class="filter-group">
<label>Fluxo:</label>
<select id="filterFluxo">
<option value="">Todos</option>
<option value="BRL \\u2192 USD">BRL &rarr; USD</option>
<option value="USD \\u2192 BRL">USD &rarr; BRL</option>
</select>
</div>
<div class="filter-group">
<label>Cliente:</label>
<select id="filterCliente"><option value="">Todos</option></select>
</div>
<button class="btn-apply" onclick="applyFilters()">Aplicar</button>
<button class="btn-export" onclick="exportCSV()">&#x2B07; Exportar CSV</button>
</div>
<div class="filter-group">
<label>Fluxo:</label>
<select id="filterFluxo">
<option value="">Todos</option>
<option value="BRL \\u2192 USD">BRL &rarr; USD</option>
<option value="USD \\u2192 BRL">USD &rarr; BRL</option>
</select>
</div>
<div class="filter-group">
<label>Cliente:</label>
<select id="filterCliente"><option value="">Todos</option></select>
</div>
<button class="btn-apply" onclick="applyFilters()">Aplicar</button>
<button class="btn-export" onclick="exportCSV()">&#x2B07; Exportar CSV</button>
</div>
<div class="container">
<div class="kpi-grid" id="kpiGrid"></div>
<!-- Alerts Section -->
<div class="alerts-section" id="alertsSection">
<div class="alerts-header">
<h3>Alertas Inteligentes <span class="alert-count" id="alertCount">0</span></h3>
<button class="alerts-toggle" id="alertsToggle" onclick="toggleAlerts()">
<span id="alertsToggleIcon">&#x25BC;</span>
<span id="alertsToggleText">Recolher</span>
</button>
</div>
<div class="alerts-body" id="alertsBody">
<div class="alerts-grid" id="alertsGrid"></div>
</div>
</div>
<!-- Volume Charts Section -->
<div class="section-header">
<h2>Volume e Taxas</h2>
@@ -705,7 +558,6 @@ function applyFilters() {
renderKPIs();
try { renderCharts(gran); } catch(e) { console.error('Chart error:', e); }
renderTable();
renderAlerts();
renderPortfolioAnalysis();
try { renderPortfolioCharts(gran); } catch(e) { console.error('Portfolio chart error:', e); }
try { renderNettingAnalysis(gran); } catch(e) { console.error('Netting chart error:', e); }
@@ -1174,159 +1026,6 @@ function renderPortfolioAnalysis() {
}
// ============================================
// ALERTS SECTION
// ============================================
let alertsCollapsed = false;
function toggleAlerts() {
alertsCollapsed = !alertsCollapsed;
const body = document.getElementById('alertsBody');
const icon = document.getElementById('alertsToggleIcon');
const text = document.getElementById('alertsToggleText');
if (alertsCollapsed) {
body.classList.add('collapsed');
icon.innerHTML = '&#x25B6;';
text.textContent = 'Expandir';
} else {
body.classList.remove('collapsed');
icon.innerHTML = '&#x25BC;';
text.textContent = 'Recolher';
}
}
function detectAlerts() {
const alerts = [];
const today = new Date();
const endDate = document.getElementById('filterEnd').value;
const referenceDate = endDate ? new Date(endDate) : today;
// Calculate client metrics for alerts (using all data)
const clientData = {};
RAW_DATA.forEach(r => {
if (!clientData[r.cliente]) {
clientData[r.cliente] = {
cliente: r.cliente,
operations: [],
volumeTotal: 0
};
}
clientData[r.cliente].operations.push(r);
clientData[r.cliente].volumeTotal += r.valor_dolar;
});
// Calculate last operation date for each client
Object.values(clientData).forEach(c => {
const dates = c.operations
.map(op => op.data_operacao ? new Date(op.data_operacao.replace(' ', 'T')) : null)
.filter(d => d && !isNaN(d.getTime()))
.sort((a, b) => b - a);
c.lastDate = dates[0];
c.daysSinceLast = c.lastDate ? Math.floor((referenceDate - c.lastDate) / (1000 * 60 * 60 * 24)) : 999;
c.operationCount = c.operations.length;
});
/// 1. Cliente Esfriando: Clients who haven't operated in >30 days but had >2 operations
const clientesEsfriando = Object.values(clientData).filter(c =>
c.daysSinceLast > 30 && c.operationCount > 2
);
clientesEsfriando.forEach(c => {
alerts.push({
type: 'warning',
icon: '&#x2744;',
title: 'Cliente Esfriando',
desc: c.cliente + ' - ' + c.daysSinceLast + ' dias sem operar (' + c.operationCount + ' operacoes historicas)'
});
});
/// 2. Queda de Volume: If total volume dropped >20% vs previous period
const start = document.getElementById('filterStart').value;
const end = document.getElementById('filterEnd').value;
const prev = calcPreviousPeriod(start, end);
if (prev.start && prev.end) {
const currVolume = filtered.reduce((sum, r) => sum + r.valor_dolar, 0);
const prevData = RAW_DATA.filter(r => {
const dateOnly = r.data_operacao ? r.data_operacao.slice(0, 10) : null;
return dateOnly && dateOnly >= prev.start && dateOnly <= prev.end;
});
const prevVolume = prevData.reduce((sum, r) => sum + r.valor_dolar, 0);
if (prevVolume > 0) {
const dropPct = ((prevVolume - currVolume) / prevVolume) * 100;
if (dropPct > 20) {
alerts.push({
type: 'critical',
icon: '&#x2193;',
title: 'Queda de Volume',
desc: 'Volume caiu ' + fmtNum(dropPct, 1) + '% vs periodo anterior (' + fmtUSD(prevVolume) + ' -> ' + fmtUSD(currVolume) + ')'
});
}
}
}
// 3. VIP Inativo: Top 5 clients by LTV who haven't operated in >15 days
const top5ByLTV = Object.values(clientData)
.sort((a, b) => b.volumeTotal - a.volumeTotal)
.slice(0, 5);
top5ByLTV.filter(c => c.daysSinceLast > 15).forEach(c => {
alerts.push({
type: 'critical',
icon: '&#x2605;',
title: 'VIP Inativo',
desc: c.cliente + ' (LTV: ' + fmtUSD(c.volumeTotal) + ') - ' + c.daysSinceLast + ' dias sem operar'
});
});
// 4. Spread Baixo: Operations with spread % below (average - 1 std deviation)
if (filtered.length > 0) {
const spreads = filtered.map(r => r.spread_pct);
const avgSpread = spreads.reduce((a, b) => a + b, 0) / spreads.length;
const variance = spreads.reduce((sum, s) => sum + Math.pow(s - avgSpread, 2), 0) / spreads.length;
const stdDev = Math.sqrt(variance);
const threshold = avgSpread - stdDev;
const lowSpreadOps = filtered.filter(r => r.spread_pct < threshold);
if (lowSpreadOps.length > 0) {
const clientsLowSpread = [...new Set(lowSpreadOps.map(r => r.cliente))];
alerts.push({
type: 'info',
icon: '&#x25BC;',
title: 'Spread Baixo Detectado',
desc: lowSpreadOps.length + ' operacoes abaixo de ' + fmtPct(threshold) + ' (media: ' + fmtPct(avgSpread) + '). Clientes: ' + clientsLowSpread.slice(0, 3).join(', ') + (clientsLowSpread.length > 3 ? '...' : '')
});
}
}
return alerts;
}
function renderAlerts() {
const alerts = detectAlerts();
const grid = document.getElementById('alertsGrid');
const countBadge = document.getElementById('alertCount');
countBadge.textContent = alerts.length;
countBadge.className = 'alert-count' + (alerts.length === 0 ? ' zero' : '');
if (alerts.length === 0) {
grid.innerHTML = '<div class="no-alerts"><div class="check-icon">&#x2713;</div>Nenhum alerta no momento</div>';
return;
}
grid.innerHTML = alerts.map(a => \`
<div class="alert-card \${a.type}">
<div class="alert-icon">\${a.icon}</div>
<div class="alert-content">
<div class="alert-title">\${a.title}</div>
<div class="alert-desc">\${a.desc}</div>
</div>
</div>
\`).join('');
}
// ============================================
// PORTFOLIO ANALYSIS CHARTS
// ============================================