feat: update queries and client dashboard improvements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:28:53 -04:00
parent 8641100a18
commit c5b377e788
4 changed files with 109 additions and 44 deletions

View File

@@ -480,18 +480,8 @@ app.get('/admin/api/cliente/:id/data', requirePermission('cliente'), async (req,
fetchClientData(clienteId, start, end),
fetchMerchantData(merchant.empresa_id, start, end)
]);
// Add checkout KPIs
// Pass checkout KPIs separately (no merge into hero totals)
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,

View File

@@ -460,6 +460,14 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente', permissi
<button class="export-btn" onclick="window.location.href='/admin/api/export/clients-excel'" title="Export Top Clients to Excel">Export Excel</button>
</div>
<!-- CambioPay label (merchant only) -->
<div class="checkout-section" id="cambioPayLabel">
<div class="section-title">
<span class="icon">&#x1F4B1;</span>
CambioPay (Transfers)
</div>
</div>
<!-- Hero KPIs (6) -->
<div class="hero-grid" id="heroGrid">
<div class="hero-card volume"><div class="hero-label">Volume USD</div><div class="hero-value" id="kpiVolume"><div class="skel skel-value"></div></div><span class="hero-badge neutral" id="kpiVolBadge"><div class="skel skel-badge"></div></span></div>
@@ -967,6 +975,19 @@ function renderProfile(p) {
document.querySelectorAll('.checkout-section').forEach(function(el) {
el.classList.toggle('visible', isMerchant);
});
// Relabel hero cards for merchants to clarify CambioPay-only
var heroLabels = document.querySelectorAll('#heroGrid .hero-label');
heroLabels.forEach(function(lbl) {
if (isMerchant) {
if (lbl.textContent === 'Volume USD') lbl.textContent = 'CambioPay Volume';
else if (lbl.textContent === 'Transacoes') lbl.textContent = 'CambioPay Ops';
else if (lbl.textContent === 'Receita USD') lbl.textContent = 'Receita CambioPay';
} else {
if (lbl.textContent === 'CambioPay Volume') lbl.textContent = 'Volume USD';
else if (lbl.textContent === 'CambioPay Ops') lbl.textContent = 'Transacoes';
else if (lbl.textContent === 'Receita CambioPay') lbl.textContent = 'Receita USD';
}
});
}
function renderChurnRisk(churn) {

View File

@@ -15,29 +15,40 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
[trendBrlUsd], [trendUsdBrl], [trendUsdUsd],
[topClients], [retention], [clientsAtRisk], [agentRanking]
] = await Promise.all([
// 1. BRL→USD KPIs
// 1. BRL→USD KPIs (real revenue = P&L formula)
pool.execute(`
SELECT COUNT(*) as qtd,
ROUND(COALESCE(SUM(amount_usd), 0), 2) as vol_usd,
ROUND(COALESCE(SUM(amount_brl), 0), 2) as vol_brl,
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd), 0), 2) as spread_revenue,
ROUND(COALESCE(AVG((exchange_rate - ptax) / exchange_rate * 100), 0), 2) as avg_spread_pct,
COUNT(DISTINCT id_conta) as clientes
FROM br_transaction_to_usa
WHERE DATE(created_at) >= ? AND DATE(created_at) <= ?
ROUND(COALESCE(SUM(t.amount_usd), 0), 2) as vol_usd,
ROUND(COALESCE(SUM(t.amount_brl), 0), 2) as vol_brl,
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 spread_revenue,
ROUND(COALESCE(AVG((t.exchange_rate - t.ptax) / t.exchange_rate * 100), 0), 2) as avg_spread_pct,
COUNT(DISTINCT t.id_conta) as clientes
FROM br_transaction_to_usa t
JOIN br_payment_methods pm ON t.payment_method_id = pm.id
WHERE 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')
`, [dataInicio, dataFim]),
// 2. USD→BRL KPIs
// 2. USD→BRL KPIs (real revenue: spread + fee for non-balance, fee only for balance)
pool.execute(`
SELECT COUNT(*) as qtd,
ROUND(COALESCE(SUM(valor), 0), 2) as vol_usd,
ROUND(COALESCE(SUM(valor_sol), 0), 2) as vol_brl,
ROUND(COALESCE(SUM((ptax - cotacao) / ptax * valor), 0), 2) as spread_revenue,
ROUND(COALESCE(AVG(CASE WHEN cotacao > 0 THEN (ptax - cotacao) / ptax * 100 ELSE 0 END), 0), 2) as avg_spread_pct,
COUNT(DISTINCT id_conta) as clientes
FROM pagamento_br
WHERE DATE(created_at) >= ? AND DATE(created_at) <= ?
AND cotacao IS NOT NULL AND cotacao > 0
AND (pgto IS NULL OR pgto != 'balance')
ROUND(COALESCE(SUM(pb.valor), 0), 2) as vol_usd,
ROUND(COALESCE(SUM(pb.valor_sol), 0), 2) as vol_brl,
ROUND(COALESCE(SUM(
CASE WHEN pb.tipo_envio = 'balance' THEN COALESCE(pb.fee, 0)
ELSE COALESCE(CASE WHEN pb.ptax IS NOT NULL AND pb.ptax > 0 THEN ((pb.ptax - pb.cotacao) * pb.valor) / pb.ptax ELSE 0 END, 0) + COALESCE(pb.fee, 0)
END
), 0), 2) as spread_revenue,
ROUND(COALESCE(AVG(CASE WHEN pb.cotacao > 0 AND pb.tipo_envio != 'balance' THEN (pb.ptax - pb.cotacao) / pb.ptax * 100 ELSE 0 END), 0), 2) as avg_spread_pct,
COUNT(DISTINCT pb.id_conta) as clientes
FROM pagamento_br pb
WHERE pb.valor > 0 AND pb.data_cp IS NOT NULL AND pb.data_cp <> '0000-00-00'
AND DATE(CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END) >= ?
AND DATE(CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END) <= ?
`, [dataInicio, dataFim]),
// 3. USD→USD KPIs
pool.execute(`
@@ -56,17 +67,31 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
SELECT id_conta FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ?
) all_clients
`, [dataInicio, dataFim, dataInicio, dataFim]),
// 5. Previous period
// 5. Previous period (real revenue formulas)
pool.execute(`
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(amount_usd),0),2) as vol_usd,
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue
FROM br_transaction_to_usa WHERE DATE(created_at) >= ? AND DATE(created_at) <= ?
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(t.amount_usd),0),2) as vol_usd,
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 spread_revenue
FROM br_transaction_to_usa t
JOIN br_payment_methods pm ON t.payment_method_id = pm.id
WHERE 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')
`, [prevStartStr, prevEndStr]),
pool.execute(`
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd,
ROUND(COALESCE(SUM((ptax - cotacao) / ptax * valor),0),2) as spread_revenue
FROM pagamento_br WHERE DATE(created_at) >= ? AND DATE(created_at) <= ?
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(pb.valor),0),2) as vol_usd,
ROUND(COALESCE(SUM(
CASE WHEN pb.tipo_envio = 'balance' THEN COALESCE(pb.fee, 0)
ELSE COALESCE(CASE WHEN pb.ptax IS NOT NULL AND pb.ptax > 0 THEN ((pb.ptax - pb.cotacao) * pb.valor) / pb.ptax ELSE 0 END, 0) + COALESCE(pb.fee, 0)
END
),0),2) as spread_revenue
FROM pagamento_br pb
WHERE pb.valor > 0 AND pb.data_cp IS NOT NULL AND pb.data_cp <> '0000-00-00'
AND DATE(CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END) >= ?
AND DATE(CASE WHEN pb.tipo_envio = 'balance' THEN pb.data_cp ELSE pb.created_at END) <= ?
`, [prevStartStr, prevEndStr]),
pool.execute(`
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd
@@ -133,22 +158,35 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
FROM pagamento_br p INNER JOIN conta c ON c.id_conta = p.id_conta GROUP BY c.nome
) combined GROUP BY nome HAVING MAX(last_op) < CURDATE() ORDER BY total_usd DESC LIMIT 20
`),
// 12. Agent ranking
// 12. Agent ranking (real revenue formulas)
pool.execute(`
SELECT agente_id, SUM(vol) as total_usd, SUM(qtd) as total_qtd,
ROUND(SUM(spread_rev), 2) as total_spread, COUNT(DISTINCT client_id) as clientes
FROM (
SELECT ac.agente_id, t.id_conta as client_id, SUM(t.amount_usd) as vol, COUNT(*) as qtd,
SUM((t.exchange_rate - t.ptax) / t.exchange_rate * t.amount_usd) as spread_rev
FROM br_transaction_to_usa t INNER JOIN ag_contas ac ON ac.conta_id = t.id_conta
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))
) as spread_rev
FROM br_transaction_to_usa t
INNER JOIN ag_contas ac ON ac.conta_id = t.id_conta
INNER JOIN br_payment_methods pm ON t.payment_method_id = pm.id
WHERE 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')
GROUP BY ac.agente_id, t.id_conta
UNION ALL
SELECT ac.agente_id, p.id_conta as client_id, SUM(p.valor) as vol, COUNT(*) as qtd,
SUM((p.ptax - p.cotacao) / p.ptax * p.valor) as spread_rev
SUM(
CASE WHEN p.tipo_envio = 'balance' THEN COALESCE(p.fee, 0)
ELSE COALESCE(CASE WHEN p.ptax IS NOT NULL AND p.ptax > 0 THEN ((p.ptax - p.cotacao) * p.valor) / p.ptax ELSE 0 END, 0) + COALESCE(p.fee, 0)
END
) as spread_rev
FROM pagamento_br p INNER JOIN ag_contas ac ON ac.conta_id = p.id_conta
WHERE DATE(p.created_at) >= ? AND DATE(p.created_at) <= ?
AND p.cotacao IS NOT NULL AND p.cotacao > 0 AND (p.pgto IS NULL OR p.pgto != 'balance')
WHERE DATE(CASE WHEN p.tipo_envio = 'balance' THEN p.data_cp ELSE p.created_at END) >= ?
AND DATE(CASE WHEN p.tipo_envio = 'balance' THEN p.data_cp ELSE p.created_at END) <= ?
AND p.valor > 0 AND p.data_cp IS NOT NULL AND p.data_cp <> '0000-00-00'
GROUP BY ac.agente_id, p.id_conta
) combined GROUP BY agente_id ORDER BY total_usd DESC LIMIT 10
`, [dataInicio, dataFim, dataInicio, dataFim])

View File

@@ -21,7 +21,9 @@ async function fetchTopClients() {
COUNT(*) AS cnt,
COUNT(DISTINCT DATE_FORMAT(created_at, '%Y-%m')) AS months_active,
MAX(created_at) AS last_op
FROM br_transaction_to_usa GROUP BY id_conta
FROM br_transaction_to_usa
WHERE (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY id_conta
) t1 ON t1.id_conta = c.id_conta
LEFT JOIN (
SELECT id_conta,
@@ -92,6 +94,7 @@ async function fetchClientProfile(clienteId) {
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue,
MIN(created_at) as first_op, MAX(created_at) as last_op
FROM br_transaction_to_usa WHERE id_conta = ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
`, [clienteId]);
const [usd] = await conn.execute(`
@@ -122,6 +125,7 @@ async function fetchClientProfile(clienteId) {
const [monthsRows] = await conn.execute(`
SELECT COUNT(DISTINCT mes) as months_active FROM (
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes FROM br_transaction_to_usa WHERE id_conta = ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
UNION
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes FROM pagamento_br WHERE id_conta = ?
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
@@ -165,6 +169,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue,
ROUND(COALESCE(AVG((exchange_rate - ptax) / exchange_rate * 100),0),2) as avg_spread_pct
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
`, [clienteId, dataInicio, dataFim]);
// KPIs USD->BRL
@@ -182,6 +187,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(amount_usd),0),2) as vol_usd,
ROUND(COALESCE(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),0),2) as spread_revenue
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
`, [clienteId, prevStartStr, prevEndStr]);
const [prevUsd] = await conn.execute(`
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd,
@@ -196,6 +202,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
ROUND(SUM(amount_usd),2) as vol_usd,
ROUND(AVG((exchange_rate - ptax) / exchange_rate * 100),2) as avg_spread
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY DATE(created_at) ORDER BY dia
`, [clienteId, dataInicio, dataFim]);
@@ -220,6 +227,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
FROM br_transaction_to_usa t
LEFT JOIN br_payment_methods pm ON t.payment_method_id = pm.id
WHERE t.id_conta = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
ORDER BY t.created_at DESC
`, [clienteId, dataInicio, dataFim]);
@@ -241,6 +249,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
const [dowBrl] = await conn.execute(`
SELECT DAYOFWEEK(created_at) as dow, COUNT(*) as qtd, ROUND(SUM(amount_usd),2) as vol_usd
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY DAYOFWEEK(created_at)
`, [clienteId, dataInicio, dataFim]);
const [dowUsd] = await conn.execute(`
@@ -256,6 +265,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
FROM br_transaction_to_usa t
LEFT JOIN br_payment_methods pm ON t.payment_method_id = pm.id
WHERE t.id_conta = ? AND DATE(t.created_at) >= ? AND DATE(t.created_at) <= ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY pm.provider
`, [clienteId, dataInicio, dataFim]);
const [provUsd] = await conn.execute(`
@@ -272,6 +282,7 @@ async function fetchClientData(clienteId, dataInicio, dataFim) {
ROUND(SUM(amount_usd),2) as vol_usd,
ROUND(SUM((exchange_rate - ptax) / exchange_rate * amount_usd),2) as spread_revenue
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
AND (status IN ('boleto_pago','finalizado') OR date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY mes
`, [clienteId, dataInicio, dataFim]);
const [monthlyUsd] = await conn.execute(`
@@ -378,6 +389,7 @@ async function fetchMerchantProfile(clienteId) {
FROM br_cb_cobranca cb
INNER JOIN br_transaction_to_usa t ON t.cobranca_id = cb.id
WHERE cb.empresa_id = ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
`, [empresaId]);
const [rev] = await conn.execute(`
@@ -433,6 +445,7 @@ async function fetchMerchantData(empresaId, dataInicio, dataFim) {
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) <= ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
`, [empresaId, dataInicio, dataFim]);
const [rev] = await conn.execute(`
@@ -458,6 +471,7 @@ async function fetchMerchantData(empresaId, dataInicio, dataFim) {
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) <= ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
`, [empresaId, prevStartStr, prevEndStr]);
const [prevRev] = await conn.execute(`
@@ -486,6 +500,7 @@ async function fetchMerchantData(empresaId, dataInicio, dataFim) {
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) <= ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY DATE_FORMAT(t.created_at, '%Y-%m') ORDER BY mes
`, [empresaId, dataInicio, dataFim]);
@@ -495,6 +510,7 @@ async function fetchMerchantData(empresaId, dataInicio, dataFim) {
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) <= ?
AND (t.status IN ('boleto_pago','finalizado') OR t.date_sent_usa <> '0000-00-00 00:00:00')
GROUP BY t.id_conta, c.nome
ORDER BY vol_usd DESC LIMIT 10
`, [empresaId, dataInicio, dataFim]);