diff --git a/server.js b/server.js
index 06dc420..80ba1b8 100644
--- a/server.js
+++ b/server.js
@@ -11,7 +11,7 @@ const express = require('express');
const session = require('express-session');
const path = require('path');
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 { buildAdminHTML } = require('./src/admin-panel');
const { buildAdminHomeHTML } = require('./src/admin-home');
@@ -421,8 +421,32 @@ app.get('/admin/api/cliente/:id/profile', requireRole('admin'), async (req, res)
try {
const clienteId = parseInt(req.params.id);
if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' });
- const data = await fetchClientProfile(clienteId);
- res.json(data);
+ const [profile, merchant] = await Promise.all([
+ 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) {
console.error('Client profile API error:', err);
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 { start, end } = req.query;
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) {
console.error('Client data API error:', err);
res.status(500).json({ error: err.message });
diff --git a/src/admin-cliente.js b/src/admin-cliente.js
index 3f6e538..dea0cf7 100644
--- a/src/admin-cliente.js
+++ b/src/admin-cliente.js
@@ -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' })}
@@ -419,6 +443,36 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
Spread Medio
--
% ponderado
+
+
+
+ 🛒
+ CambioCheckout (Merchant)
+
+
+
+
Payers Unicos
--
pagadores distintos
+
+
+
Ticket Checkout
--
USD / operacao
+
+
+
+
+
+
+
+
+
Checkout Mensal: Volume + Payers
+
+
+
+
+
+
🎯
@@ -566,6 +620,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}