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' })}
--
--
+
MERCHANT
@@ -419,6 +443,36 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
Spread Medio
--
% ponderado
+ +
+
+ 🛒 + CambioCheckout (Merchant) +
+
+
Checkout Volume
--
--
+
Payers Unicos
--
pagadores distintos
+
Receita Checkout
--
--
+
Checkout Ops
--
--
+
Ticket Checkout
--
USD / operacao
+
Spread Checkout
--
% medio
+
+
+ + +
+
+
+

Checkout Mensal: Volume + Payers

+
+
+
+

Top 10 Pagadores

+
+
+
+
+
🎯 @@ -566,6 +620,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}