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:
65
server.js
65
server.js
@@ -11,7 +11,7 @@ const express = require('express');
|
|||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth');
|
const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth');
|
||||||
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData } = require('./src/queries');
|
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData, fetchMerchantProfile, fetchMerchantData } = require('./src/queries');
|
||||||
const { buildHTML } = require('./src/dashboard');
|
const { buildHTML } = require('./src/dashboard');
|
||||||
const { buildAdminHTML } = require('./src/admin-panel');
|
const { buildAdminHTML } = require('./src/admin-panel');
|
||||||
const { buildAdminHomeHTML } = require('./src/admin-home');
|
const { buildAdminHomeHTML } = require('./src/admin-home');
|
||||||
@@ -421,8 +421,32 @@ app.get('/admin/api/cliente/:id/profile', requireRole('admin'), async (req, res)
|
|||||||
try {
|
try {
|
||||||
const clienteId = parseInt(req.params.id);
|
const clienteId = parseInt(req.params.id);
|
||||||
if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' });
|
if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' });
|
||||||
const data = await fetchClientProfile(clienteId);
|
const [profile, merchant] = await Promise.all([
|
||||||
res.json(data);
|
fetchClientProfile(clienteId),
|
||||||
|
fetchMerchantProfile(clienteId)
|
||||||
|
]);
|
||||||
|
if (merchant.is_merchant) {
|
||||||
|
const ck = merchant.checkout;
|
||||||
|
profile.merchant = { empresa_id: merchant.empresa_id, nome_empresa: merchant.nome_empresa };
|
||||||
|
profile.total_ops += ck.tx_count;
|
||||||
|
profile.total_vol_usd += ck.vol_usd;
|
||||||
|
profile.total_spread_revenue += ck.revenue;
|
||||||
|
profile.ltv = profile.total_spread_revenue;
|
||||||
|
// Extend date ranges
|
||||||
|
const dates = [profile.first_op, ck.first_op].filter(Boolean);
|
||||||
|
const lastDates = [profile.last_op, ck.last_op].filter(Boolean);
|
||||||
|
if (dates.length) profile.first_op = dates.sort()[0];
|
||||||
|
if (lastDates.length) {
|
||||||
|
profile.last_op = lastDates.sort().pop();
|
||||||
|
profile.days_inactive = Math.round((Date.now() - new Date(profile.last_op).getTime()) / 86400000);
|
||||||
|
}
|
||||||
|
profile.months_active = Math.max(profile.months_active, ck.months_active);
|
||||||
|
profile.avg_monthly_vol = profile.months_active > 0 ? Math.round(profile.total_vol_usd / profile.months_active) : 0;
|
||||||
|
profile.avg_monthly_ops = profile.months_active > 0 ? Math.round(profile.total_ops / profile.months_active * 10) / 10 : 0;
|
||||||
|
profile.avg_monthly_revenue = profile.months_active > 0 ? Math.round(profile.total_spread_revenue / profile.months_active * 100) / 100 : 0;
|
||||||
|
profile.checkout = ck;
|
||||||
|
}
|
||||||
|
res.json(profile);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Client profile API error:', err);
|
console.error('Client profile API error:', err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -434,8 +458,39 @@ app.get('/admin/api/cliente/:id/data', requireRole('admin'), async (req, res) =>
|
|||||||
const clienteId = parseInt(req.params.id);
|
const clienteId = parseInt(req.params.id);
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!clienteId || !start || !end) return res.status(400).json({ error: 'client ID, start and end required' });
|
if (!clienteId || !start || !end) return res.status(400).json({ error: 'client ID, start and end required' });
|
||||||
const data = await fetchClientData(clienteId, start, end);
|
|
||||||
res.json(data);
|
const merchant = await fetchMerchantProfile(clienteId);
|
||||||
|
if (merchant.is_merchant) {
|
||||||
|
const [data, mData] = await Promise.all([
|
||||||
|
fetchClientData(clienteId, start, end),
|
||||||
|
fetchMerchantData(merchant.empresa_id, start, end)
|
||||||
|
]);
|
||||||
|
// Add checkout KPIs
|
||||||
|
data.kpis.checkout = mData.kpis;
|
||||||
|
// Merge totals
|
||||||
|
data.kpis.total.qtd += mData.kpis.qtd;
|
||||||
|
data.kpis.total.vol_usd += mData.kpis.vol_usd;
|
||||||
|
data.kpis.total.spread_revenue += mData.kpis.revenue;
|
||||||
|
const totalQtd = data.kpis.total.qtd;
|
||||||
|
data.kpis.total.ticket_medio = totalQtd > 0 ? Math.round(data.kpis.total.vol_usd / totalQtd) : 0;
|
||||||
|
// Merge comparison
|
||||||
|
data.comparison.prev_qtd += mData.comparison.prev_qtd;
|
||||||
|
data.comparison.prev_vol_usd += mData.comparison.prev_vol_usd;
|
||||||
|
data.comparison.prev_spread += mData.comparison.prev_revenue;
|
||||||
|
// Merchant-specific data
|
||||||
|
data.merchant = {
|
||||||
|
monthly: mData.monthly,
|
||||||
|
topPayers: mData.topPayers,
|
||||||
|
comparison: mData.comparison
|
||||||
|
};
|
||||||
|
// Merge transactions (checkout txs get flow="Checkout")
|
||||||
|
data.transactions = data.transactions.concat(mData.transactions)
|
||||||
|
.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
res.json(data);
|
||||||
|
} else {
|
||||||
|
const data = await fetchClientData(clienteId, start, end);
|
||||||
|
res.json(data);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Client data API error:', err);
|
console.error('Client data API error:', err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
|
|||||||
@@ -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"] .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); }
|
[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 === */
|
/* === Responsive === */
|
||||||
@media (max-width: 1200px) { .hero-grid { grid-template-columns: repeat(3, 1fr); } .intel-grid { grid-template-columns: 1fr 1fr; } }
|
@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; } }
|
@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>
|
||||||
<div class="profile-name" id="profileName">--</div>
|
<div class="profile-name" id="profileName">--</div>
|
||||||
<div class="profile-id" id="profileId">--</div>
|
<div class="profile-id" id="profileId">--</div>
|
||||||
|
<div class="merchant-badge" id="merchantBadge">MERCHANT</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-stats">
|
<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 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>
|
</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 -->
|
<!-- Section: Saude & Risco -->
|
||||||
<div class="section-title" id="sectionHealth">
|
<div class="section-title" id="sectionHealth">
|
||||||
<span class="icon">🎯</span>
|
<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;">
|
<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="#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="#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="#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="#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>
|
<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 ===
|
// === Charts ===
|
||||||
var chartTimeline, chartFlowDonut, chartSpreadTrend, chartDow, chartAvgSize, chartProvider, chartMonthlyRev, chartMoM;
|
var chartTimeline, chartFlowDonut, chartSpreadTrend, chartDow, chartAvgSize, chartProvider, chartMonthlyRev, chartMoM, chartCheckoutMonthly;
|
||||||
function destroyAllCharts() {
|
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, chartCheckoutMonthly].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 = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Health Score ===
|
// === Health Score ===
|
||||||
@@ -839,6 +894,8 @@ function clearClient() {
|
|||||||
document.getElementById('emptyState').style.display = '';
|
document.getElementById('emptyState').style.display = '';
|
||||||
document.getElementById('contentArea').classList.remove('visible');
|
document.getElementById('contentArea').classList.remove('visible');
|
||||||
document.getElementById('consoleNav').style.display = 'none';
|
document.getElementById('consoleNav').style.display = 'none';
|
||||||
|
document.getElementById('merchantBadge').classList.remove('visible');
|
||||||
|
document.querySelectorAll('.checkout-section').forEach(function(el){ el.classList.remove('visible'); });
|
||||||
destroyAllCharts();
|
destroyAllCharts();
|
||||||
var url = new URL(window.location); url.searchParams.delete('id'); history.replaceState(null, '', url);
|
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('profileLTV').textContent = fmtUSD(p.ltv || p.total_spread_revenue);
|
||||||
document.getElementById('profileVolume').textContent = fmtUSD(p.total_vol_usd);
|
document.getElementById('profileVolume').textContent = fmtUSD(p.total_vol_usd);
|
||||||
document.getElementById('profileOps').textContent = p.total_ops;
|
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 ===
|
// === Data Loading ===
|
||||||
@@ -873,6 +943,11 @@ function loadData() {
|
|||||||
function renderAll(d) {
|
function renderAll(d) {
|
||||||
var t = getChartTheme(); applyChartDefaults(t);
|
var t = getChartTheme(); applyChartDefaults(t);
|
||||||
renderKPIs(d); renderIntelligence(d, t); renderCharts(d, t); renderTable(d); updatePeriodInfo();
|
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() {
|
function updatePeriodInfo() {
|
||||||
var s = new Date(currentStart), e = new Date(currentEnd);
|
var s = new Date(currentStart), e = new Date(currentEnd);
|
||||||
@@ -1077,6 +1152,55 @@ function renderProviderChart(d, t) {
|
|||||||
}); } catch(e) {}
|
}); } 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 ===
|
// === Transaction Table ===
|
||||||
function renderTable(d) { if (!d.transactions) return; currentPage = 1; _renderTablePage(); }
|
function renderTable(d) { if (!d.transactions) return; currentPage = 1; _renderTablePage(); }
|
||||||
function _getSortedTx() {
|
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>'; }
|
if (!page.length) { tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;color:var(--text-muted)">Nenhuma transacao</td></tr>'; }
|
||||||
else {
|
else {
|
||||||
tbody.innerHTML = page.map(function(r) {
|
tbody.innerHTML = page.map(function(r) {
|
||||||
var fc = r.flow === 'BRL\\u2192USD' ? 'brl-usd' : 'usd-brl';
|
var fc = r.flow === 'Checkout' ? 'checkout' : (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 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('');
|
}).join('');
|
||||||
}
|
}
|
||||||
var foot = document.getElementById('txTableFoot');
|
var foot = document.getElementById('txTableFoot');
|
||||||
@@ -1163,7 +1288,7 @@ document.querySelectorAll('[data-tl-gran]').forEach(function(btn) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Console Nav
|
// 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() {
|
function updateConsoleNav() {
|
||||||
var sy = window.scrollY + 120, active = _navSections[0];
|
var sy = window.scrollY + 120, active = _navSections[0];
|
||||||
_navSections.forEach(function(id){var el=document.getElementById(id); if(el&&el.offsetTop<=sy) active=id;});
|
_navSections.forEach(function(id){var el=document.getElementById(id); if(el&&el.offsetTop<=sy) active=id;});
|
||||||
|
|||||||
253
src/queries.js
253
src/queries.js
@@ -955,16 +955,16 @@ async function fetchRevenueAnalytics(dataInicio, dataFim, granularity = 'dia') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top 20 clients by total USD volume
|
// Top 20 clients by total USD volume (including checkout volume for merchants)
|
||||||
async function fetchTopClients() {
|
async function fetchTopClients() {
|
||||||
const conn = await pool.getConnection();
|
const conn = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
const [rows] = await conn.execute(`
|
const [rows] = await conn.execute(`
|
||||||
SELECT c.id_conta, c.nome,
|
SELECT c.id_conta, c.nome,
|
||||||
COALESCE(t1.vol_usd, 0) + COALESCE(t2.vol_usd, 0) AS total_vol_usd,
|
COALESCE(t1.vol_usd, 0) + COALESCE(t2.vol_usd, 0) + COALESCE(t3.vol_usd, 0) AS total_vol_usd,
|
||||||
COALESCE(t1.cnt, 0) + COALESCE(t2.cnt, 0) AS total_ops,
|
COALESCE(t1.cnt, 0) + COALESCE(t2.cnt, 0) + COALESCE(t3.cnt, 0) AS total_ops,
|
||||||
GREATEST(COALESCE(t1.months_active, 0), COALESCE(t2.months_active, 0)) AS months_active,
|
GREATEST(COALESCE(t1.months_active, 0), COALESCE(t2.months_active, 0), COALESCE(t3.months_active, 0)) AS months_active,
|
||||||
COALESCE(t1.last_op, t2.last_op) AS last_op
|
GREATEST(COALESCE(t1.last_op, '1970-01-01'), COALESCE(t2.last_op, '1970-01-01'), COALESCE(t3.last_op, '1970-01-01')) AS last_op
|
||||||
FROM conta c
|
FROM conta c
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT id_conta,
|
SELECT id_conta,
|
||||||
@@ -982,7 +982,19 @@ async function fetchTopClients() {
|
|||||||
MAX(created_at) AS last_op
|
MAX(created_at) AS last_op
|
||||||
FROM pagamento_br GROUP BY id_conta
|
FROM pagamento_br GROUP BY id_conta
|
||||||
) t2 ON t2.id_conta = c.id_conta
|
) t2 ON t2.id_conta = c.id_conta
|
||||||
WHERE COALESCE(t1.cnt, 0) + COALESCE(t2.cnt, 0) > 0
|
LEFT JOIN (
|
||||||
|
SELECT e.id_conta,
|
||||||
|
SUM(t.amount_usd) AS vol_usd,
|
||||||
|
COUNT(*) AS cnt,
|
||||||
|
COUNT(DISTINCT DATE_FORMAT(t.created_at, '%Y-%m')) AS months_active,
|
||||||
|
MAX(t.created_at) AS last_op
|
||||||
|
FROM br_cb_empresas e
|
||||||
|
INNER JOIN br_cb_cobranca cb ON cb.empresa_id = e.id
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
WHERE e.active = 1
|
||||||
|
GROUP BY e.id_conta
|
||||||
|
) t3 ON t3.id_conta = c.id_conta
|
||||||
|
WHERE COALESCE(t1.cnt, 0) + COALESCE(t2.cnt, 0) + COALESCE(t3.cnt, 0) > 0
|
||||||
ORDER BY total_vol_usd DESC
|
ORDER BY total_vol_usd DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`);
|
`);
|
||||||
@@ -992,14 +1004,14 @@ async function fetchTopClients() {
|
|||||||
vol: Math.round(r.total_vol_usd || 0),
|
vol: Math.round(r.total_vol_usd || 0),
|
||||||
ops: r.total_ops || 0,
|
ops: r.total_ops || 0,
|
||||||
months: r.months_active || 0,
|
months: r.months_active || 0,
|
||||||
lastOp: r.last_op ? r.last_op.toISOString().slice(0, 10) : null
|
lastOp: r.last_op && String(r.last_op) !== '1970-01-01' ? (r.last_op instanceof Date ? r.last_op.toISOString().slice(0, 10) : String(r.last_op).slice(0, 10)) : null
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
conn.release();
|
conn.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search clients by name (server-side, max 15 results)
|
// Search clients by name (server-side, max 15 results) — includes merchants
|
||||||
async function fetchClientSearch(query) {
|
async function fetchClientSearch(query) {
|
||||||
const conn = await pool.getConnection();
|
const conn = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
@@ -1008,6 +1020,7 @@ async function fetchClientSearch(query) {
|
|||||||
WHERE c.id_conta IN (
|
WHERE c.id_conta IN (
|
||||||
SELECT DISTINCT id_conta FROM br_transaction_to_usa
|
SELECT DISTINCT id_conta FROM br_transaction_to_usa
|
||||||
UNION SELECT DISTINCT id_conta FROM pagamento_br
|
UNION SELECT DISTINCT id_conta FROM pagamento_br
|
||||||
|
UNION SELECT DISTINCT id_conta FROM br_cb_empresas WHERE active = 1
|
||||||
) AND c.nome LIKE CONCAT('%', ?, '%')
|
) AND c.nome LIKE CONCAT('%', ?, '%')
|
||||||
ORDER BY c.nome LIMIT 15
|
ORDER BY c.nome LIMIT 15
|
||||||
`, [query]);
|
`, [query]);
|
||||||
@@ -1531,6 +1544,226 @@ async function fetchBIStrategic(dataInicio, dataFim) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect if client is a merchant (has active empresa in br_cb_empresas)
|
||||||
|
async function fetchMerchantProfile(clienteId) {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
// Check if this id_conta is a merchant
|
||||||
|
const [empresa] = await conn.execute(
|
||||||
|
'SELECT id, nome_empresa FROM br_cb_empresas WHERE id_conta = ? AND active = 1 LIMIT 1',
|
||||||
|
[clienteId]
|
||||||
|
);
|
||||||
|
if (!empresa.length) return { is_merchant: false };
|
||||||
|
|
||||||
|
const empresaId = empresa[0].id;
|
||||||
|
const nomeEmpresa = empresa[0].nome_empresa;
|
||||||
|
|
||||||
|
// Lifetime checkout stats via cobranca -> transactions
|
||||||
|
const [stats] = await conn.execute(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as tx_count,
|
||||||
|
ROUND(COALESCE(SUM(t.amount_usd), 0), 2) as vol_usd,
|
||||||
|
COUNT(DISTINCT t.id_conta) as unique_payers,
|
||||||
|
MIN(t.created_at) as first_op,
|
||||||
|
MAX(t.created_at) as last_op,
|
||||||
|
COUNT(DISTINCT DATE_FORMAT(t.created_at, '%Y-%m')) as months_active
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
WHERE cb.empresa_id = ?
|
||||||
|
`, [empresaId]);
|
||||||
|
|
||||||
|
// Revenue: same formula as fetchRevenueAnalytics for BR→US Checkout
|
||||||
|
const [rev] = await conn.execute(`
|
||||||
|
SELECT ROUND(COALESCE(SUM(
|
||||||
|
(
|
||||||
|
ROUND((t.amount_brl - IF(pm.provider IN ('ouribank','bs2'), 0, t.fee)) / t.ptax, 2)
|
||||||
|
- COALESCE(t.pfee, 0)
|
||||||
|
) - (
|
||||||
|
t.amount_usd + COALESCE(t.bonus_valor, 0) - COALESCE(t.taxa_cr, 0)
|
||||||
|
)
|
||||||
|
), 0), 2) as revenue
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
INNER JOIN br_payment_methods pm ON t.payment_method_id = pm.id
|
||||||
|
WHERE cb.empresa_id = ?
|
||||||
|
AND pm.provider IN ('dlocal','bexs','braza','bs2','ouribank','msb')
|
||||||
|
AND t.ptax IS NOT NULL AND t.ptax > 0
|
||||||
|
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
|
||||||
|
`, [empresaId]);
|
||||||
|
|
||||||
|
const s = stats[0] || {};
|
||||||
|
return {
|
||||||
|
is_merchant: true,
|
||||||
|
empresa_id: empresaId,
|
||||||
|
nome_empresa: nomeEmpresa,
|
||||||
|
checkout: {
|
||||||
|
tx_count: Number(s.tx_count) || 0,
|
||||||
|
vol_usd: Number(s.vol_usd) || 0,
|
||||||
|
unique_payers: Number(s.unique_payers) || 0,
|
||||||
|
revenue: Number(rev[0]?.revenue) || 0,
|
||||||
|
first_op: s.first_op ? (s.first_op instanceof Date ? s.first_op.toISOString().slice(0, 10) : String(s.first_op).slice(0, 10)) : null,
|
||||||
|
last_op: s.last_op ? (s.last_op instanceof Date ? s.last_op.toISOString().slice(0, 10) : String(s.last_op).slice(0, 10)) : null,
|
||||||
|
months_active: Number(s.months_active) || 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merchant checkout data for a period
|
||||||
|
async function fetchMerchantData(empresaId, dataInicio, dataFim) {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
// Previous period for comparison
|
||||||
|
const start = new Date(dataInicio);
|
||||||
|
const end = new Date(dataFim);
|
||||||
|
const periodDays = Math.round((end - start) / 86400000) + 1;
|
||||||
|
const prevEnd = new Date(start); prevEnd.setDate(prevEnd.getDate() - 1);
|
||||||
|
const prevStart = new Date(prevEnd); prevStart.setDate(prevStart.getDate() - periodDays + 1);
|
||||||
|
const prevStartStr = prevStart.toISOString().slice(0, 10);
|
||||||
|
const prevEndStr = prevEnd.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
// KPIs current period
|
||||||
|
const [kpi] = await conn.execute(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as qtd,
|
||||||
|
ROUND(COALESCE(SUM(t.amount_usd), 0), 2) as vol_usd,
|
||||||
|
COUNT(DISTINCT t.id_conta) as unique_payers,
|
||||||
|
ROUND(COALESCE(AVG((t.exchange_rate - t.ptax) / t.exchange_rate * 100), 0), 2) as avg_spread_pct
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
`, [empresaId, dataInicio, dataFim]);
|
||||||
|
|
||||||
|
// Revenue current period
|
||||||
|
const [rev] = await conn.execute(`
|
||||||
|
SELECT ROUND(COALESCE(SUM(
|
||||||
|
(
|
||||||
|
ROUND((t.amount_brl - IF(pm.provider IN ('ouribank','bs2'), 0, t.fee)) / t.ptax, 2)
|
||||||
|
- COALESCE(t.pfee, 0)
|
||||||
|
) - (
|
||||||
|
t.amount_usd + COALESCE(t.bonus_valor, 0) - COALESCE(t.taxa_cr, 0)
|
||||||
|
)
|
||||||
|
), 0), 2) as revenue
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
INNER JOIN br_payment_methods pm ON t.payment_method_id = pm.id
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
AND pm.provider IN ('dlocal','bexs','braza','bs2','ouribank','msb')
|
||||||
|
AND t.ptax IS NOT NULL AND t.ptax > 0
|
||||||
|
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
|
||||||
|
`, [empresaId, dataInicio, dataFim]);
|
||||||
|
|
||||||
|
// KPIs previous period
|
||||||
|
const [prevKpi] = await conn.execute(`
|
||||||
|
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(t.amount_usd), 0), 2) as vol_usd
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
`, [empresaId, prevStartStr, prevEndStr]);
|
||||||
|
|
||||||
|
const [prevRev] = await conn.execute(`
|
||||||
|
SELECT ROUND(COALESCE(SUM(
|
||||||
|
(
|
||||||
|
ROUND((t.amount_brl - IF(pm.provider IN ('ouribank','bs2'), 0, t.fee)) / t.ptax, 2)
|
||||||
|
- COALESCE(t.pfee, 0)
|
||||||
|
) - (
|
||||||
|
t.amount_usd + COALESCE(t.bonus_valor, 0) - COALESCE(t.taxa_cr, 0)
|
||||||
|
)
|
||||||
|
), 0), 2) as revenue
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
INNER JOIN br_payment_methods pm ON t.payment_method_id = pm.id
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
AND pm.provider IN ('dlocal','bexs','braza','bs2','ouribank','msb')
|
||||||
|
AND t.ptax IS NOT NULL AND t.ptax > 0
|
||||||
|
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
|
||||||
|
`, [empresaId, prevStartStr, prevEndStr]);
|
||||||
|
|
||||||
|
// Monthly breakdown
|
||||||
|
const [monthly] = await conn.execute(`
|
||||||
|
SELECT DATE_FORMAT(t.created_at, '%Y-%m') as mes,
|
||||||
|
COUNT(*) as qtd,
|
||||||
|
ROUND(SUM(t.amount_usd), 2) as vol_usd,
|
||||||
|
COUNT(DISTINCT t.id_conta) as unique_payers
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
GROUP BY DATE_FORMAT(t.created_at, '%Y-%m') ORDER BY mes
|
||||||
|
`, [empresaId, dataInicio, dataFim]);
|
||||||
|
|
||||||
|
// Top 10 payers
|
||||||
|
const [topPayers] = await conn.execute(`
|
||||||
|
SELECT c.nome, t.id_conta, COUNT(*) as tx_count, ROUND(SUM(t.amount_usd), 2) as vol_usd
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
INNER JOIN conta c ON c.id_conta = t.id_conta
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
GROUP BY t.id_conta, c.nome
|
||||||
|
ORDER BY vol_usd DESC LIMIT 10
|
||||||
|
`, [empresaId, dataInicio, dataFim]);
|
||||||
|
|
||||||
|
// Transactions (latest 500)
|
||||||
|
const [txRows] = await conn.execute(`
|
||||||
|
SELECT t.created_at as date, t.amount_usd as usd, t.amount_brl as brl,
|
||||||
|
ROUND(t.exchange_rate, 4) as rate, ROUND(t.ptax, 4) as ptax,
|
||||||
|
ROUND((t.exchange_rate - t.ptax) / t.exchange_rate * 100, 2) as spread_pct,
|
||||||
|
t.iof, t.status, COALESCE(pm.provider, '') as provider,
|
||||||
|
c.nome as payer_name, t.id_conta as payer_id
|
||||||
|
FROM br_cb_cobranca cb
|
||||||
|
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
|
||||||
|
LEFT JOIN br_payment_methods pm ON t.payment_method_id = pm.id
|
||||||
|
LEFT JOIN conta c ON c.id_conta = t.id_conta
|
||||||
|
WHERE cb.empresa_id = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
|
||||||
|
ORDER BY t.created_at DESC LIMIT 500
|
||||||
|
`, [empresaId, dataInicio, dataFim]);
|
||||||
|
|
||||||
|
const fmtDT = (d) => { try { const dt = d instanceof Date ? d : new Date(d); return dt.toISOString().slice(0, 16).replace('T', ' '); } catch(e) { return String(d); } };
|
||||||
|
|
||||||
|
const k = kpi[0] || {};
|
||||||
|
const qtd = Number(k.qtd) || 0;
|
||||||
|
const volUsd = Number(k.vol_usd) || 0;
|
||||||
|
const revenue = Number(rev[0]?.revenue) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
kpis: {
|
||||||
|
qtd,
|
||||||
|
vol_usd: volUsd,
|
||||||
|
unique_payers: Number(k.unique_payers) || 0,
|
||||||
|
avg_spread_pct: Number(k.avg_spread_pct) || 0,
|
||||||
|
revenue,
|
||||||
|
ticket_medio: qtd > 0 ? Math.round(volUsd / qtd) : 0
|
||||||
|
},
|
||||||
|
comparison: {
|
||||||
|
prev_qtd: Number(prevKpi[0]?.qtd) || 0,
|
||||||
|
prev_vol_usd: Number(prevKpi[0]?.vol_usd) || 0,
|
||||||
|
prev_revenue: Number(prevRev[0]?.revenue) || 0
|
||||||
|
},
|
||||||
|
monthly: monthly.map(r => ({
|
||||||
|
mes: r.mes,
|
||||||
|
qtd: Number(r.qtd),
|
||||||
|
vol_usd: Number(r.vol_usd),
|
||||||
|
unique_payers: Number(r.unique_payers)
|
||||||
|
})),
|
||||||
|
topPayers: topPayers.map(r => ({
|
||||||
|
nome: r.nome,
|
||||||
|
id_conta: r.id_conta,
|
||||||
|
tx_count: Number(r.tx_count),
|
||||||
|
vol_usd: Number(r.vol_usd)
|
||||||
|
})),
|
||||||
|
transactions: txRows.map(r => ({
|
||||||
|
date: fmtDT(r.date), flow: 'Checkout', usd: Number(r.usd), brl: Number(r.brl),
|
||||||
|
rate: Number(r.rate), ptax: Number(r.ptax), spread_pct: Number(r.spread_pct),
|
||||||
|
iof: Number(r.iof), status: r.status, provider: r.provider,
|
||||||
|
payer_name: r.payer_name || '', payer_id: r.payer_id
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchTransacoes,
|
fetchTransacoes,
|
||||||
fetchAllTransacoes,
|
fetchAllTransacoes,
|
||||||
@@ -1547,5 +1780,7 @@ module.exports = {
|
|||||||
fetchTopClients,
|
fetchTopClients,
|
||||||
fetchClientSearch,
|
fetchClientSearch,
|
||||||
fetchClientProfile,
|
fetchClientProfile,
|
||||||
fetchClientData
|
fetchClientData,
|
||||||
|
fetchMerchantProfile,
|
||||||
|
fetchMerchantData
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user