feat: add skeleton shimmer loading for BI Executive dashboard
Replace static '--' placeholders with animated skeleton screens so the page feels alive while 4 async data sources load. Theme-aware via CSS vars (works in dark Bloomberg mode + light mode). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
131
src/admin-bi.js
131
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 `<!DOCTYPE html>
|
||||
@@ -854,30 +873,30 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="hero-grid" id="heroGrid">
|
||||
<div class="hero-card spread">
|
||||
<div class="hero-label">Receita USD (Spread)</div>
|
||||
<div class="hero-value" id="kpiSpreadRevenue">--</div>
|
||||
<span class="hero-badge neutral" id="kpiSpreadBadge">--</span>
|
||||
<div class="hero-value" id="kpiSpreadRevenue"><div class="skel skel-value"></div></div>
|
||||
<span class="hero-badge neutral" id="kpiSpreadBadge"><div class="skel skel-badge"></div></span>
|
||||
<div class="hero-sub" id="kpiSpreadSub">vs periodo anterior</div>
|
||||
</div>
|
||||
<div class="hero-card volume">
|
||||
<div class="hero-label">Volume Processado</div>
|
||||
<div class="hero-value" id="kpiVolume">--</div>
|
||||
<span class="hero-badge neutral" id="kpiVolumeBadge">--</span>
|
||||
<div class="hero-value" id="kpiVolume"><div class="skel skel-value"></div></div>
|
||||
<span class="hero-badge neutral" id="kpiVolumeBadge"><div class="skel skel-badge"></div></span>
|
||||
<div class="hero-sub" id="kpiVolumeSub">USD total movimentado</div>
|
||||
</div>
|
||||
<div class="hero-card transactions">
|
||||
<div class="hero-label">Transacoes</div>
|
||||
<div class="hero-value" id="kpiTransactions">--</div>
|
||||
<span class="hero-badge neutral" id="kpiTransBadge">--</span>
|
||||
<div class="hero-value" id="kpiTransactions"><div class="skel skel-value"></div></div>
|
||||
<span class="hero-badge neutral" id="kpiTransBadge"><div class="skel skel-badge"></div></span>
|
||||
<div class="hero-sub" id="kpiTransSub">operacoes no periodo</div>
|
||||
</div>
|
||||
<div class="hero-card clients">
|
||||
<div class="hero-label">Clientes Ativos</div>
|
||||
<div class="hero-value" id="kpiClients">--</div>
|
||||
<div class="hero-value" id="kpiClients"><div class="skel skel-value"></div></div>
|
||||
<div class="hero-sub">clientes unicos no periodo</div>
|
||||
</div>
|
||||
<div class="hero-card ticket">
|
||||
<div class="hero-label">Ticket Medio</div>
|
||||
<div class="hero-value" id="kpiTicket">--</div>
|
||||
<div class="hero-value" id="kpiTicket"><div class="skel skel-value"></div></div>
|
||||
<div class="hero-sub">USD por operacao</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -890,11 +909,11 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<h3>Revenue por Corredor</h3>
|
||||
<div class="chart-wrap short"><canvas id="chartDonut"></canvas></div>
|
||||
<div class="chart-wrap short"><div class="skel skel-chart" id="skelDonut"></div><canvas id="chartDonut" style="display:none"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Spread Medio + Volume Diario <span class="badge" id="spreadBadge">--</span></h3>
|
||||
<div class="chart-wrap"><canvas id="chartSpreadTrend"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelSpreadTrend"></div><canvas id="chartSpreadTrend" style="display:none"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -909,14 +928,14 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<button id="btnResetClients" style="display:none;margin-left:auto;" class="preset-btn"
|
||||
onclick="_resetClientFilter()">Mostrar Todos</button>
|
||||
</h3>
|
||||
<div class="chart-wrap"><canvas id="chartTopClients"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelTopClients"></div><canvas id="chartTopClients" style="display:none"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div class="metric-card" style="box-shadow:none;border:1px solid var(--border);padding:16px;">
|
||||
<h3 style="margin-bottom:8px;">Taxa de Retencao</h3>
|
||||
<div class="gauge-value green" id="retentionValue">--%</div>
|
||||
<div class="gauge-value green" id="retentionValue"><div class="skel skel-gauge"></div></div>
|
||||
<div class="gauge-bar"><div class="gauge-bar-fill green" id="retentionBar" style="width:0%"></div></div>
|
||||
<div class="gauge-label" id="retentionLabel">-- de -- clientes retidos</div>
|
||||
<div class="gauge-label" id="retentionLabel"><div class="skel skel-text"></div></div>
|
||||
<div class="retention-breakdown" id="retentionBreakdown" style="display:flex;justify-content:center;gap:16px;margin-top:10px;">
|
||||
<span class="ret-stat" style="font-size:13px;font-weight:700;color:var(--green);" id="retNovos">+0 novos</span>
|
||||
<span class="ret-stat" style="font-size:13px;font-weight:700;color:var(--red);" id="retPerdidos">-0 perdidos</span>
|
||||
@@ -935,7 +954,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
</div>
|
||||
<table class="data-table" id="riskTable">
|
||||
<thead><tr><th>Cliente</th><th>Ultima Op</th><th>Dias</th><th>Volume USD</th></tr></thead>
|
||||
<tbody id="riskTableBody"><tr><td colspan="4" style="text-align:center;color:var(--text-muted);">Carregando...</td></tr></tbody>
|
||||
<tbody id="riskTableBody"><tr class="skel-tr"><td colspan="4"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="4"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="4"><div class="skel skel-row"></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -949,14 +968,14 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="charts-row equal">
|
||||
<div class="chart-card">
|
||||
<h3>Volume por Corredor</h3>
|
||||
<div class="chart-wrap"><canvas id="chartVolFlow"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelVolFlow"></div><canvas id="chartVolFlow" style="display:none"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Ranking Agentes</h3>
|
||||
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>#</th><th>Agente</th><th>Volume</th><th>Ops</th><th>Spread R$</th><th>Clientes</th></tr></thead>
|
||||
<tbody id="agentTableBody"><tr><td colspan="6" style="text-align:center;color:var(--text-muted);">Carregando...</td></tr></tbody>
|
||||
<tbody id="agentTableBody"><tr class="skel-tr"><td colspan="6"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="6"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="6"><div class="skel skel-row"></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -972,7 +991,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<h3 style="margin:0;">Forecast: Historical + Predicted Volume</h3>
|
||||
<span class="period-info" id="forecastInfo">Loading forecast...</span>
|
||||
</div>
|
||||
<div style="height:300px;"><canvas id="forecastChart"></canvas></div>
|
||||
<div style="height:300px;"><div class="skel skel-chart" id="skelForecast"></div><canvas id="forecastChart" style="display:none"></canvas></div>
|
||||
</div>
|
||||
|
||||
<!-- Section: Netting & Balanco -->
|
||||
@@ -990,26 +1009,26 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<button class="netting-gran-btn" data-netting-gran="M">M</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap"><canvas id="chartNetting"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelNetting"></div><canvas id="chartNetting" style="display:none"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<h3 style="font-size:14px;font-weight:700;color:var(--text);margin:0;">Resumo Netting</h3>
|
||||
<div id="nettingContent">
|
||||
<div class="netting-row">
|
||||
<span class="netting-label">Saida (BRL→USD)</span>
|
||||
<span class="netting-value red" id="nettingSaida">--</span>
|
||||
<span class="netting-value red" id="nettingSaida"><div class="skel skel-badge"></div></span>
|
||||
</div>
|
||||
<div class="netting-row">
|
||||
<span class="netting-label">Entrada (USD→BRL)</span>
|
||||
<span class="netting-value green" id="nettingEntrada">--</span>
|
||||
<span class="netting-value green" id="nettingEntrada"><div class="skel skel-badge"></div></span>
|
||||
</div>
|
||||
<div class="netting-row" style="border-top:2px solid var(--border);padding-top:12px;">
|
||||
<span class="netting-label" style="font-weight:700;">Posicao Liquida</span>
|
||||
<span class="netting-value" id="nettingPosicao">--</span>
|
||||
<span class="netting-value" id="nettingPosicao"><div class="skel skel-badge"></div></span>
|
||||
</div>
|
||||
<div style="margin-top:16px;">
|
||||
<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px;">Eficiencia Netting</div>
|
||||
<div class="gauge-value blue" id="nettingEficiencia" style="font-size:36px;margin:4px 0;">--%</div>
|
||||
<div class="gauge-value blue" id="nettingEficiencia" style="font-size:36px;margin:4px 0;"><div class="skel skel-gauge"></div></div>
|
||||
<div class="gauge-bar"><div class="gauge-bar-fill blue" id="nettingBar" style="width:0%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1035,22 +1054,22 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="hero-grid hero-grid-4" id="revenueKpis">
|
||||
<div class="hero-card" style="--top-color:linear-gradient(90deg,#F9A825,#FFD54F);">
|
||||
<div class="hero-label">Receita Total (P&L)</div>
|
||||
<div class="hero-value" id="revTotal">--</div>
|
||||
<div class="hero-sub" id="revTotalQtd">-- operacoes</div>
|
||||
<div class="hero-value" id="revTotal"><div class="skel skel-value"></div></div>
|
||||
<div class="hero-sub" id="revTotalQtd"><div class="skel skel-text"></div></div>
|
||||
</div>
|
||||
<div class="hero-card" style="--top-color:linear-gradient(90deg,var(--blue),#42A5F5);">
|
||||
<div class="hero-label">BR → US</div>
|
||||
<div class="hero-value" id="revBrUs">--</div>
|
||||
<div class="hero-value" id="revBrUs"><div class="skel skel-value"></div></div>
|
||||
<div class="hero-sub">Checkout + CambioTransfer</div>
|
||||
</div>
|
||||
<div class="hero-card" style="--top-color:linear-gradient(90deg,var(--green),#4CAF50);">
|
||||
<div class="hero-label">US → BR</div>
|
||||
<div class="hero-value" id="revUsBr">--</div>
|
||||
<div class="hero-value" id="revUsBr"><div class="skel skel-value"></div></div>
|
||||
<div class="hero-sub">Spread + Fees</div>
|
||||
</div>
|
||||
<div class="hero-card" style="--top-color:linear-gradient(90deg,var(--purple),#AB47BC);">
|
||||
<div class="hero-label">Receita / Operacao</div>
|
||||
<div class="hero-value" id="revTicket">--</div>
|
||||
<div class="hero-value" id="revTicket"><div class="skel skel-value"></div></div>
|
||||
<div class="hero-sub">ticket medio de receita</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1058,11 +1077,11 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="charts-row equal">
|
||||
<div class="chart-card">
|
||||
<h3>Receita por Produto ao Longo do Tempo</h3>
|
||||
<div class="chart-wrap"><canvas id="chartRevTimeline"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelRevTimeline"></div><canvas id="chartRevTimeline" style="display:none"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Composicao de Receita por Produto</h3>
|
||||
<div class="chart-wrap short"><canvas id="chartRevDonut"></canvas></div>
|
||||
<div class="chart-wrap short"><div class="skel skel-chart" id="skelRevDonut"></div><canvas id="chartRevDonut" style="display:none"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1071,7 +1090,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="data-table" id="revTable">
|
||||
<thead><tr><th>Periodo</th><th>Produto</th><th>Receita</th><th>Operacoes</th><th>Receita/Op</th></tr></thead>
|
||||
<tbody id="revTableBody"><tr><td colspan="5" style="text-align:center;color:var(--text-muted);">Carregando...</td></tr></tbody>
|
||||
<tbody id="revTableBody"><tr class="skel-tr"><td colspan="5"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="5"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="5"><div class="skel skel-row"></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1084,39 +1103,39 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="waterfall-grid" id="waterfallGrid" style="margin-bottom:28px;">
|
||||
<div class="waterfall-item wf-new">
|
||||
<div class="wf-label">Novos</div>
|
||||
<div class="wf-count" id="wfNewCount">--</div>
|
||||
<div class="wf-revenue" id="wfNewRev" style="color:var(--blue);">--</div>
|
||||
<div class="wf-count" id="wfNewCount"><div class="skel skel-badge"></div></div>
|
||||
<div class="wf-revenue" id="wfNewRev" style="color:var(--blue);"><div class="skel skel-text"></div></div>
|
||||
</div>
|
||||
<div class="waterfall-item wf-expansion">
|
||||
<div class="wf-label">Expansao</div>
|
||||
<div class="wf-count" id="wfExpCount">--</div>
|
||||
<div class="wf-revenue" id="wfExpRev" style="color:var(--green);">--</div>
|
||||
<div class="wf-count" id="wfExpCount"><div class="skel skel-badge"></div></div>
|
||||
<div class="wf-revenue" id="wfExpRev" style="color:var(--green);"><div class="skel skel-text"></div></div>
|
||||
</div>
|
||||
<div class="waterfall-item wf-stable">
|
||||
<div class="wf-label">Estavel</div>
|
||||
<div class="wf-count" id="wfStaCount">--</div>
|
||||
<div class="wf-revenue" id="wfStaRev" style="color:var(--text-secondary);">--</div>
|
||||
<div class="wf-count" id="wfStaCount"><div class="skel skel-badge"></div></div>
|
||||
<div class="wf-revenue" id="wfStaRev" style="color:var(--text-secondary);"><div class="skel skel-text"></div></div>
|
||||
</div>
|
||||
<div class="waterfall-item wf-contraction">
|
||||
<div class="wf-label">Contracao</div>
|
||||
<div class="wf-count" id="wfConCount">--</div>
|
||||
<div class="wf-revenue" id="wfConRev" style="color:var(--orange);">--</div>
|
||||
<div class="wf-count" id="wfConCount"><div class="skel skel-badge"></div></div>
|
||||
<div class="wf-revenue" id="wfConRev" style="color:var(--orange);"><div class="skel skel-text"></div></div>
|
||||
</div>
|
||||
<div class="waterfall-item wf-churned">
|
||||
<div class="wf-label">Churned</div>
|
||||
<div class="wf-count" id="wfChurnCount">--</div>
|
||||
<div class="wf-revenue" id="wfChurnRev" style="color:var(--red);">--</div>
|
||||
<div class="wf-count" id="wfChurnCount"><div class="skel skel-badge"></div></div>
|
||||
<div class="wf-revenue" id="wfChurnRev" style="color:var(--red);"><div class="skel skel-text"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row equal" style="margin-bottom:28px;">
|
||||
<div class="chart-card">
|
||||
<h3>Waterfall de Receita</h3>
|
||||
<div class="chart-wrap"><canvas id="chartWaterfall"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelWaterfall"></div><canvas id="chartWaterfall" style="display:none"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Distribuicao por Segmento</h3>
|
||||
<div class="chart-wrap"><canvas id="chartExpDonut"></canvas></div>
|
||||
<div class="chart-wrap"><div class="skel skel-chart" id="skelExpDonut"></div><canvas id="chartExpDonut" style="display:none"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1128,12 +1147,12 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div class="segment-grid">
|
||||
<div class="segment-card">
|
||||
<h3>Cross-sell <span class="badge">CambioPay vs Checkout</span></h3>
|
||||
<div class="chart-wrap short"><canvas id="chartCrossSell"></canvas></div>
|
||||
<div class="chart-wrap short"><div class="skel skel-chart" id="skelCrossSell"></div><canvas id="chartCrossSell" style="display:none"></canvas></div>
|
||||
<div class="segment-bars" id="crossSellBars" style="margin-top:16px;"></div>
|
||||
</div>
|
||||
<div class="segment-card">
|
||||
<h3>Maturidade de Clientes</h3>
|
||||
<div class="chart-wrap short"><canvas id="chartMaturity"></canvas></div>
|
||||
<div class="chart-wrap short"><div class="skel skel-chart" id="skelMaturity"></div><canvas id="chartMaturity" style="display:none"></canvas></div>
|
||||
<div class="segment-bars" id="maturityBars" style="margin-top:16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1148,7 +1167,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
||||
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch;">
|
||||
<table class="cohort-table" id="cohortTable">
|
||||
<thead id="cohortHead"><tr><th>Cohort</th><th>Clientes</th></tr></thead>
|
||||
<tbody id="cohortBody"><tr><td colspan="14" style="text-align:center;color:var(--text-muted);">Carregando...</td></tr></tbody>
|
||||
<tbody id="cohortBody"><tr class="skel-tr"><td colspan="14"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="14"><div class="skel skel-row"></div></td></tr><tr class="skel-tr"><td colspan="14"><div class="skel skel-row"></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user