feat: integra dados de merchant/checkout no Client 360

Merchants (via br_cb_empresas) agora mostram dados de CambioCheckout:
- fetchMerchantProfile detecta merchant e retorna lifetime checkout stats
- fetchMerchantData retorna KPIs, monthly, top payers e transacoes por periodo
- fetchTopClients inclui checkout volume (merchants sobem no ranking)
- fetchClientSearch inclui merchants nos resultados de busca
- Profile/data endpoints fazem merge automatico dos dados checkout
- UI: badge MERCHANT roxo, 6 hero cards checkout, chart mensal, top 10 payers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-16 16:47:11 -05:00
parent 600a8044c2
commit a76ab30730
3 changed files with 435 additions and 20 deletions

View File

@@ -327,6 +327,29 @@ function buildAdminClienteHTML(user) {
[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); }
/* === Merchant / Checkout === */
.merchant-badge {
display: none; padding: 4px 12px; border-radius: 8px; font-size: 11px; font-weight: 800;
letter-spacing: 0.5px; background: var(--purple-bg); color: var(--purple, #7B1FA2);
text-transform: uppercase; margin-top: 4px;
}
.merchant-badge.visible { display: inline-block; }
.hero-card.checkout::before { background: linear-gradient(90deg, var(--purple, #7B1FA2), #AB47BC); }
.checkout-section { display: none; }
.checkout-section.visible { display: block; }
.flow-tag.checkout { background: var(--purple-bg); color: var(--purple, #7B1FA2); }
.top-payer-row { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.1s; }
.top-payer-row:last-child { border-bottom: none; }
.top-payer-row:hover { background: var(--bg); }
[data-theme="dark"] .top-payer-row:hover { background: rgba(0,255,136,0.04); }
.top-payer-rank { width: 28px; font-size: 11px; font-weight: 800; color: var(--text-muted); text-align: center; }
.top-payer-info { flex: 1; min-width: 0; }
.top-payer-name { font-size: 13px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.top-payer-stats { font-size: 11px; color: var(--text-muted); }
.top-payer-vol { font-size: 14px; font-weight: 700; color: var(--purple, #7B1FA2); font-variant-numeric: tabular-nums; }
[data-theme="dark"] .top-payer-vol { color: #BC8CFF; }
[data-theme="dark"] .top-payer-name { font-family: 'SF Mono','Fira Code','Consolas',monospace; }
/* === Responsive === */
@media (max-width: 1200px) { .hero-grid { grid-template-columns: repeat(3, 1fr); } .intel-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 900px) { .charts-row, .charts-row.wide-left { grid-template-columns: 1fr; } .charts-row.triple { grid-template-columns: 1fr 1fr; } .charts-row.triple > :last-child { grid-column: span 2; } .intel-grid { grid-template-columns: 1fr; } .filter-divider { display: none; } .profile-card { flex-direction: column; text-align: center; } .profile-left { flex-direction: column; } .profile-stats { justify-content: center; } .health-gauge-wrap { flex-direction: column; align-items: center; } }
@@ -375,6 +398,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
<div>
<div class="profile-name" id="profileName">--</div>
<div class="profile-id" id="profileId">--</div>
<div class="merchant-badge" id="merchantBadge">MERCHANT</div>
</div>
</div>
<div class="profile-stats">
@@ -419,6 +443,36 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
<div class="hero-card avgspread"><div class="hero-label">Spread Medio</div><div class="hero-value" id="kpiAvgSpread">--</div><div class="hero-sub">% ponderado</div></div>
</div>
<!-- Checkout KPIs (merchant only) -->
<div class="checkout-section" id="checkoutKpiSection">
<div class="section-title" id="sectionCheckout">
<span class="icon">&#x1F6D2;</span>
CambioCheckout (Merchant)
</div>
<div class="hero-grid" id="checkoutHeroGrid">
<div class="hero-card checkout"><div class="hero-label">Checkout Volume</div><div class="hero-value" id="ckVolume">--</div><span class="hero-badge neutral" id="ckVolBadge">--</span></div>
<div class="hero-card checkout"><div class="hero-label">Payers Unicos</div><div class="hero-value" id="ckPayers">--</div><div class="hero-sub">pagadores distintos</div></div>
<div class="hero-card checkout"><div class="hero-label">Receita Checkout</div><div class="hero-value" id="ckRevenue">--</div><span class="hero-badge neutral" id="ckRevBadge">--</span></div>
<div class="hero-card checkout"><div class="hero-label">Checkout Ops</div><div class="hero-value" id="ckOps">--</div><span class="hero-badge neutral" id="ckOpsBadge">--</span></div>
<div class="hero-card checkout"><div class="hero-label">Ticket Checkout</div><div class="hero-value" id="ckTicket">--</div><div class="hero-sub">USD / operacao</div></div>
<div class="hero-card checkout"><div class="hero-label">Spread Checkout</div><div class="hero-value" id="ckSpread">--</div><div class="hero-sub">% medio</div></div>
</div>
</div>
<!-- Checkout Analytics (merchant only) -->
<div class="checkout-section" id="checkoutAnalyticsSection">
<div class="charts-row wide-left">
<div class="chart-card">
<h3>Checkout Mensal: Volume + Payers</h3>
<div class="chart-wrap"><canvas id="chartCheckoutMonthly"></canvas></div>
</div>
<div class="chart-card">
<h3>Top 10 Pagadores</h3>
<div id="topPayersList" style="max-height:310px;overflow-y:auto;"></div>
</div>
</div>
</div>
<!-- Section: Saude & Risco -->
<div class="section-title" id="sectionHealth">
<span class="icon">&#x1F3AF;</span>
@@ -566,6 +620,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
<nav class="console-nav" id="consoleNav" style="display:none;">
<a href="#searchWrap" class="console-nav-btn" data-section="searchWrap"><span class="nav-icon">&#x1F50D;</span><span class="nav-label">Busca</span></a>
<a href="#heroGrid" class="console-nav-btn" data-section="heroGrid"><span class="nav-icon">&#x1F4CA;</span><span class="nav-label">KPIs</span></a>
<a href="#sectionCheckout" class="console-nav-btn checkout-section" data-section="sectionCheckout"><span class="nav-icon">&#x1F6D2;</span><span class="nav-label">Checkout</span></a>
<a href="#sectionHealth" class="console-nav-btn" data-section="sectionHealth"><span class="nav-icon">&#x1F3AF;</span><span class="nav-label">Saude</span></a>
<a href="#sectionRevenue" class="console-nav-btn" data-section="sectionRevenue"><span class="nav-icon">&#x1F4B0;</span><span class="nav-label">Revenue</span></a>
<a href="#sectionTimeline" class="console-nav-btn" data-section="sectionTimeline"><span class="nav-icon">&#x1F4C8;</span><span class="nav-label">Timeline</span></a>
@@ -627,10 +682,10 @@ function applyChartDefaults(t) {
}
// === Charts ===
var chartTimeline, chartFlowDonut, chartSpreadTrend, chartDow, chartAvgSize, chartProvider, chartMonthlyRev, chartMoM;
var chartTimeline, chartFlowDonut, chartSpreadTrend, chartDow, chartAvgSize, chartProvider, chartMonthlyRev, chartMoM, chartCheckoutMonthly;
function destroyAllCharts() {
[chartTimeline, chartFlowDonut, chartSpreadTrend, chartDow, chartAvgSize, chartProvider, chartMonthlyRev, chartMoM].forEach(function(c){if(c)c.destroy();});
chartTimeline = chartFlowDonut = chartSpreadTrend = chartDow = chartAvgSize = chartProvider = chartMonthlyRev = chartMoM = null;
[chartTimeline, chartFlowDonut, chartSpreadTrend, chartDow, chartAvgSize, chartProvider, chartMonthlyRev, chartMoM, chartCheckoutMonthly].forEach(function(c){if(c)c.destroy();});
chartTimeline = chartFlowDonut = chartSpreadTrend = chartDow = chartAvgSize = chartProvider = chartMonthlyRev = chartMoM = chartCheckoutMonthly = null;
}
// === Health Score ===
@@ -839,6 +894,8 @@ function clearClient() {
document.getElementById('emptyState').style.display = '';
document.getElementById('contentArea').classList.remove('visible');
document.getElementById('consoleNav').style.display = 'none';
document.getElementById('merchantBadge').classList.remove('visible');
document.querySelectorAll('.checkout-section').forEach(function(el){ el.classList.remove('visible'); });
destroyAllCharts();
var url = new URL(window.location); url.searchParams.delete('id'); history.replaceState(null, '', url);
}
@@ -860,6 +917,19 @@ function renderProfile(p) {
document.getElementById('profileLTV').textContent = fmtUSD(p.ltv || p.total_spread_revenue);
document.getElementById('profileVolume').textContent = fmtUSD(p.total_vol_usd);
document.getElementById('profileOps').textContent = p.total_ops;
// Merchant badge
var isMerchant = !!(p.merchant && p.merchant.nome_empresa);
var badge = document.getElementById('merchantBadge');
if (isMerchant) {
badge.textContent = 'MERCHANT: ' + p.merchant.nome_empresa;
badge.classList.add('visible');
} else {
badge.classList.remove('visible');
}
// Show/hide checkout sections
document.querySelectorAll('.checkout-section').forEach(function(el) {
el.classList.toggle('visible', isMerchant);
});
}
// === Data Loading ===
@@ -873,6 +943,11 @@ function loadData() {
function renderAll(d) {
var t = getChartTheme(); applyChartDefaults(t);
renderKPIs(d); renderIntelligence(d, t); renderCharts(d, t); renderTable(d); updatePeriodInfo();
if (profileData && profileData.merchant) {
renderCheckoutKPIs(d, t);
renderCheckoutMonthly(d, t);
renderTopPayers(d);
}
}
function updatePeriodInfo() {
var s = new Date(currentStart), e = new Date(currentEnd);
@@ -1077,6 +1152,55 @@ function renderProviderChart(d, t) {
}); } catch(e) {}
}
// === Checkout Renders (Merchant) ===
function renderCheckoutKPIs(d) {
var ck = d.kpis.checkout; if (!ck) return;
var cmp = (d.merchant && d.merchant.comparison) || {};
document.getElementById('ckVolume').textContent = fmtUSD(ck.vol_usd);
document.getElementById('ckPayers').textContent = ck.unique_payers;
document.getElementById('ckRevenue').textContent = fmtUSD(ck.revenue);
document.getElementById('ckOps').textContent = ck.qtd;
document.getElementById('ckTicket').textContent = fmtUSD(ck.ticket_medio);
document.getElementById('ckSpread').textContent = fmtPct(ck.avg_spread_pct);
var bv = badge(ck.vol_usd, cmp.prev_vol_usd);
document.getElementById('ckVolBadge').className = 'hero-badge ' + bv.cls; document.getElementById('ckVolBadge').textContent = bv.text;
var br = badge(ck.revenue, cmp.prev_revenue);
document.getElementById('ckRevBadge').className = 'hero-badge ' + br.cls; document.getElementById('ckRevBadge').textContent = br.text;
var bo = badge(ck.qtd, cmp.prev_qtd);
document.getElementById('ckOpsBadge').className = 'hero-badge ' + bo.cls; document.getElementById('ckOpsBadge').textContent = bo.text;
}
function renderCheckoutMonthly(d, t) {
if (!d.merchant || !d.merchant.monthly || !d.merchant.monthly.length) return;
var m = d.merchant.monthly;
if (chartCheckoutMonthly) chartCheckoutMonthly.destroy();
try {
chartCheckoutMonthly = new Chart(document.getElementById('chartCheckoutMonthly'), {
type: 'bar', data: {
labels: m.map(function(x){return x.mes;}),
datasets: [
{ label: 'Volume USD', data: m.map(function(x){return x.vol_usd;}), backgroundColor: 'rgba(188,140,255,0.2)', borderColor: 'rgba(188,140,255,0.6)', borderWidth: 1, borderRadius: 4, yAxisID: 'y', order: 2 },
{ label: 'Payers', data: m.map(function(x){return x.unique_payers;}), type: 'line', borderColor: t.lineQtd, backgroundColor: 'transparent', borderWidth: 2, pointRadius: 3, tension: 0.3, yAxisID: 'y1', order: 1 }
]
}, options: {
responsive: true, maintainAspectRatio: false, interaction: {mode:'index', intersect:false},
plugins: { legend: {position:'top', labels:{font:{size:11,weight:600},padding:12}},
tooltip: { callbacks: { label: function(ctx) { return ctx.dataset.label === 'Volume USD' ? 'Volume: ' + fmtUSD(ctx.raw) : 'Payers: ' + ctx.raw; }}}},
scales: {
y: { position:'left', grid:{color:t.grid}, ticks:{callback:function(v){return fmtUSD(v);}, font:{size:10}} },
y1: { position:'right', grid:{display:false}, ticks:{font:{size:10}, stepSize:1}, min:0 },
x: { grid:{display:false}, ticks:{font:{size:10}} }
}
}
}); } catch(e) { console.warn('chartCheckoutMonthly:', e.message); }
}
function renderTopPayers(d) {
var el = document.getElementById('topPayersList');
if (!d.merchant || !d.merchant.topPayers || !d.merchant.topPayers.length) { el.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:20px;">Sem dados</div>'; return; }
el.innerHTML = d.merchant.topPayers.map(function(p, i) {
return '<div class="top-payer-row" onclick="selectClient('+p.id_conta+',\\''+esc(p.nome).replace(/'/g,"\\\\'")+'\\')"><span class="top-payer-rank">#'+(i+1)+'</span><div class="top-payer-info"><div class="top-payer-name" title="'+esc(p.nome)+'">'+esc(p.nome)+'</div><div class="top-payer-stats">'+p.tx_count+' ops</div></div><span class="top-payer-vol">'+fmtUSD(p.vol_usd)+'</span></div>';
}).join('');
}
// === Transaction Table ===
function renderTable(d) { if (!d.transactions) return; currentPage = 1; _renderTablePage(); }
function _getSortedTx() {
@@ -1095,8 +1219,9 @@ function _renderTablePage() {
if (!page.length) { tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;color:var(--text-muted)">Nenhuma transacao</td></tr>'; }
else {
tbody.innerHTML = page.map(function(r) {
var fc = r.flow === 'BRL\\u2192USD' ? 'brl-usd' : 'usd-brl';
return '<tr><td>'+r.date.slice(0,16)+'</td><td><span class="flow-tag '+fc+'">'+r.flow+'</span></td><td>'+fmtUSD(r.usd)+'</td><td>'+fmtBRL(r.brl)+'</td><td>'+Number(r.rate).toFixed(4)+'</td><td>'+Number(r.ptax).toFixed(4)+'</td><td>'+fmtPct(r.spread_pct)+'</td><td>'+(r.iof?fmtPct(r.iof):'--')+'</td><td>'+(r.status||'--')+'</td><td>'+(r.provider||'--')+'</td></tr>';
var fc = r.flow === 'Checkout' ? 'checkout' : (r.flow === 'BRL\\u2192USD' ? 'brl-usd' : 'usd-brl');
var provCol = r.flow === 'Checkout' && r.payer_name ? esc(r.payer_name) : (r.provider||'--');
return '<tr><td>'+r.date.slice(0,16)+'</td><td><span class="flow-tag '+fc+'">'+r.flow+'</span></td><td>'+fmtUSD(r.usd)+'</td><td>'+fmtBRL(r.brl)+'</td><td>'+Number(r.rate).toFixed(4)+'</td><td>'+Number(r.ptax).toFixed(4)+'</td><td>'+fmtPct(r.spread_pct)+'</td><td>'+(r.iof?fmtPct(r.iof):'--')+'</td><td>'+(r.status||'--')+'</td><td>'+provCol+'</td></tr>';
}).join('');
}
var foot = document.getElementById('txTableFoot');
@@ -1163,7 +1288,7 @@ document.querySelectorAll('[data-tl-gran]').forEach(function(btn) {
});
// Console Nav
var _navSections = ['searchWrap','heroGrid','sectionHealth','sectionRevenue','sectionTimeline','sectionFlows','sectionTable','sectionInsights'];
var _navSections = ['searchWrap','heroGrid','sectionCheckout','sectionHealth','sectionRevenue','sectionTimeline','sectionFlows','sectionTable','sectionInsights'];
function updateConsoleNav() {
var sy = window.scrollY + 120, active = _navSections[0];
_navSections.forEach(function(id){var el=document.getElementById(id); if(el&&el.offsetTop<=sy) active=id;});