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:
@@ -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">🛒</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">🎯</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">🔍</span><span class="nav-label">Busca</span></a>
|
||||
<a href="#heroGrid" class="console-nav-btn" data-section="heroGrid"><span class="nav-icon">📊</span><span class="nav-label">KPIs</span></a>
|
||||
<a href="#sectionCheckout" class="console-nav-btn checkout-section" data-section="sectionCheckout"><span class="nav-icon">🛒</span><span class="nav-label">Checkout</span></a>
|
||||
<a href="#sectionHealth" class="console-nav-btn" data-section="sectionHealth"><span class="nav-icon">🎯</span><span class="nav-label">Saude</span></a>
|
||||
<a href="#sectionRevenue" class="console-nav-btn" data-section="sectionRevenue"><span class="nav-icon">💰</span><span class="nav-label">Revenue</span></a>
|
||||
<a href="#sectionTimeline" class="console-nav-btn" data-section="sectionTimeline"><span class="nav-icon">📈</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;});
|
||||
|
||||
Reference in New Issue
Block a user