diff --git a/src/dashboard.js b/src/dashboard.js index 1681def..d8954ff 100644 --- a/src/dashboard.js +++ b/src/dashboard.js @@ -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 ? `
-
-
-
- - +
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
-
- - -
-
- - -
- -
- -
-
-

Alertas Inteligentes 0

- -
-
-
-
-
-

Volume e Taxas

@@ -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 = '▶'; - text.textContent = 'Expandir'; - } else { - body.classList.remove('collapsed'); - icon.innerHTML = '▼'; - 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: '❄', - 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: '↓', - 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: '★', - 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: '▼', - 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 = '
Nenhum alerta no momento
'; - return; - } - - grid.innerHTML = alerts.map(a => \` -
-
\${a.icon}
-
-
\${a.title}
-
\${a.desc}
-
-
- \`).join(''); -} - // ============================================ // PORTFOLIO ANALYSIS CHARTS // ============================================