diff --git a/src/admin-bi.js b/src/admin-bi.js
index a559915..0a203c2 100644
--- a/src/admin-bi.js
+++ b/src/admin-bi.js
@@ -796,6 +796,25 @@ function buildAdminBIHTML(user) {
[data-theme="dark"] .gran-btn:hover { border-color: #F9A825; color: #F9A825; }
[data-theme="dark"] .date-inputs input[type="date"] { background: var(--card); color: var(--text); border-color: var(--border); }
[data-theme="dark"] .data-table tr:hover td { background: rgba(255,255,255,0.03); }
+
+ /* === Skeleton Loading Shimmer === */
+ @keyframes shimmer {
+ 0% { background-position: -400px 0; }
+ 100% { background-position: 400px 0; }
+ }
+ .skel {
+ background: linear-gradient(90deg, var(--card-bg,var(--card)) 25%, var(--border) 50%, var(--card-bg,var(--card)) 75%);
+ background-size: 800px 100%;
+ animation: shimmer 1.5s infinite ease-in-out;
+ border-radius: 6px;
+ display: inline-block;
+ }
+ .skel-value { height: 32px; width: 55%; }
+ .skel-badge { height: 18px; width: 70px; }
+ .skel-text { height: 14px; width: 80%; margin: 4px 0; }
+ .skel-chart { height: 100%; width: 100%; min-height: 180px; border-radius: 12px; }
+ .skel-row { height: 32px; margin: 6px 0; width: 100%; }
+ .skel-gauge { height: 40px; width: 90px; }
`;
return `
@@ -854,30 +873,30 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Receita USD (Spread)
-
--
-
--
+
+
vs periodo anterior
Volume Processado
-
--
-
--
+
+
USD total movimentado
Transacoes
-
--
-
--
+
+
operacoes no periodo
Clientes Ativos
-
--
+
clientes unicos no periodo
Ticket Medio
-
--
+
USD por operacao
@@ -890,11 +909,11 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Spread Medio + Volume Diario --
-
+
@@ -909,14 +928,14 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
-
+
Taxa de Retencao
-
--%
+
-
-- de -- clientes retidos
+
+0 novos
-0 perdidos
@@ -935,7 +954,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
| Cliente | Ultima Op | Dias | Volume USD |
- | Carregando... |
+ |
|
|
@@ -949,14 +968,14 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Ranking Agentes
| # | Agente | Volume | Ops | Spread R$ | Clientes |
- | Carregando... |
+ |
|
|
@@ -972,7 +991,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Forecast: Historical + Predicted Volume
Loading forecast...
-
+
@@ -990,26 +1009,26 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
-
+
Resumo Netting
Eficiencia Netting
-
--%
+
@@ -1035,22 +1054,22 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Receita Total (P&L)
-
--
-
-- operacoes
+
+
BR → US
-
--
+
Checkout + CambioTransfer
US → BR
-
--
+
Spread + Fees
Receita / Operacao
-
--
+
ticket medio de receita
@@ -1058,11 +1077,11 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Receita por Produto ao Longo do Tempo
-
+
Composicao de Receita por Produto
-
+
@@ -1071,7 +1090,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
| Periodo | Produto | Receita | Operacoes | Receita/Op |
- | Carregando... |
+ |
|
|
@@ -1084,39 +1103,39 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Distribuicao por Segmento
-
+
@@ -1128,12 +1147,12 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Cross-sell CambioPay vs Checkout
-
+
Maturidade de Clientes
-
+
@@ -1148,7 +1167,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
| Cohort | Clientes |
- | Carregando... |
+ |
|
|
@@ -1211,6 +1230,14 @@ const fmtPct = (curr, prev) => {
return { text: (pct > 0 ? '+' : '') + pct + '%', cls: pct >= 0 ? 'up' : 'down' };
};
+// === Skeleton helpers ===
+function showChart(skelId, canvasId) {
+ var s = document.getElementById(skelId);
+ if (s) s.remove();
+ var c = document.getElementById(canvasId);
+ if (c) c.style.display = '';
+}
+
// === Date & Filter Logic ===
let currentStart = '${thirtyDaysAgo}';
let currentEnd = '${today}';
@@ -1325,6 +1352,11 @@ function _renderBICharts(d) {
if (typeof Chart === 'undefined') return;
var _ct = getChartTheme();
try { destroyCharts(); } catch(e) {}
+ showChart('skelDonut', 'chartDonut');
+ showChart('skelSpreadTrend', 'chartSpreadTrend');
+ showChart('skelTopClients', 'chartTopClients');
+ showChart('skelVolFlow', 'chartVolFlow');
+ showChart('skelNetting', 'chartNetting');
// Donut: Revenue por Corredor
try {
@@ -1759,6 +1791,8 @@ function _renderRevCharts(d) {
if (typeof Chart === 'undefined' || !d.timeline || !d.totals) return;
var _ct = getChartTheme();
try { destroyRevCharts(); } catch(e) {}
+ showChart('skelRevTimeline', 'chartRevTimeline');
+ showChart('skelRevDonut', 'chartRevDonut');
var periods = [...new Set(d.timeline.map(function(r){return r.periodo_label;}))].sort();
var products = [...new Set(d.timeline.map(function(r){return r.produto;}))].sort();
@@ -1944,6 +1978,8 @@ function renderStrategic(d) {
// --- Expansion / Contraction ---
function renderExpansion(exp, t) {
+ showChart('skelWaterfall', 'chartWaterfall');
+ showChart('skelExpDonut', 'chartExpDonut');
document.getElementById('wfNewCount').textContent = exp.new_clients.count;
document.getElementById('wfNewRev').textContent = '+' + fmtUSD(exp.new_clients.revenue);
document.getElementById('wfExpCount').textContent = exp.expansion.count;
@@ -1991,6 +2027,7 @@ function renderExpansion(exp, t) {
// --- Cross-sell ---
function renderCrossSell(cs, t) {
+ showChart('skelCrossSell', 'chartCrossSell');
if (typeof Chart === 'undefined') return;
try { if (chartCrossSell) chartCrossSell.destroy(); } catch(e){}
@@ -2024,6 +2061,7 @@ function renderCrossSell(cs, t) {
// --- Client Maturity ---
function renderMaturity(mat, t) {
+ showChart('skelMaturity', 'chartMaturity');
if (typeof Chart === 'undefined') return;
try { if (chartMaturity) chartMaturity.destroy(); } catch(e){}
@@ -2120,6 +2158,7 @@ async function loadForecast() {
var resp = await fetch('/admin/api/bi/forecast?metric=volume&days=30');
var data = await resp.json();
if (!data.historical || data.historical.length === 0) {
+ showChart('skelForecast', 'forecastChart');
document.getElementById('forecastInfo').textContent = 'No data';
return;
}
@@ -2134,6 +2173,7 @@ async function loadForecast() {
// Extend historical with nulls for prediction period
var histFull = histValues.concat(new Array(predLabels.length).fill(null));
+ showChart('skelForecast', 'forecastChart');
if (_forecastChart) _forecastChart.destroy();
var ctx = document.getElementById('forecastChart');
if (!ctx) return;
@@ -2164,6 +2204,7 @@ async function loadForecast() {
document.getElementById('forecastInfo').textContent = data.predicted.length + '-day forecast';
} catch (e) {
console.error('Forecast error:', e);
+ showChart('skelForecast', 'forecastChart');
document.getElementById('forecastInfo').textContent = 'Forecast error';
}
}