feat: dashboard cliente 360 — visao completa por cliente com KPIs, timeline, fluxos e transacoes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
server.js
51
server.js
@@ -11,12 +11,13 @@ 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 } = require('./src/queries');
|
||||
const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchClientList, fetchClientProfile, fetchClientData } = require('./src/queries');
|
||||
const { buildHTML } = require('./src/dashboard');
|
||||
const { buildAdminHTML } = require('./src/admin-panel');
|
||||
const { buildAdminHomeHTML } = require('./src/admin-home');
|
||||
const { buildAdminDashboardHTML } = require('./src/admin-dashboard');
|
||||
const { buildAdminBIHTML } = require('./src/admin-bi');
|
||||
const { buildAdminClienteHTML } = require('./src/admin-cliente');
|
||||
const bcrypt = require('bcrypt');
|
||||
const db = require('./src/db-local');
|
||||
const cache = require('./src/cache');
|
||||
@@ -369,6 +370,54 @@ app.get('/admin/api/bi/revenue', requireRole('admin'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Admin Cliente Dashboard (admin only) ---
|
||||
app.get('/admin/cliente', requireRole('admin'), (req, res) => {
|
||||
try {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
res.set('Pragma', 'no-cache');
|
||||
const html = buildAdminClienteHTML(req.session.user);
|
||||
res.send(html);
|
||||
} catch (err) {
|
||||
console.error('Admin Cliente error:', err);
|
||||
res.status(500).send('Erro ao carregar pagina de cliente: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/api/clientes', requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const data = await cache.getOrFetch('client-list', fetchClientList, 15 * 60 * 1000);
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error('Client list API error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error('Client profile API error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/api/cliente/:id/data', requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error('Client data API error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create user (admin only)
|
||||
app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
|
||||
const { nome, email, agente_id, senha, role } = req.body;
|
||||
|
||||
1276
src/admin-cliente.js
Normal file
1276
src/admin-cliente.js
Normal file
File diff suppressed because it is too large
Load Diff
304
src/queries.js
304
src/queries.js
@@ -653,9 +653,11 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
) curr ON prev.id_conta = curr.id_conta
|
||||
`, [prevStartStr, prevEndStr, prevStartStr, prevEndStr, dataInicio, dataFim, dataInicio, dataFim]);
|
||||
|
||||
// 11. Clients at risk (last transaction > 30 days ago, had meaningful volume)
|
||||
// 11. Clients at risk (inactive clients with meaningful volume)
|
||||
const [clientsAtRisk] = await conn.execute(`
|
||||
SELECT nome, MAX(last_op) as last_op, SUM(vol) as total_usd, SUM(qtd) as total_qtd FROM (
|
||||
SELECT nome, MAX(last_op) as last_op, SUM(vol) as total_usd, SUM(qtd) as total_qtd,
|
||||
DATEDIFF(CURDATE(), MAX(last_op)) as days_inactive
|
||||
FROM (
|
||||
SELECT c.nome, MAX(t.created_at) as last_op, SUM(t.amount_usd) as vol, COUNT(*) as qtd
|
||||
FROM br_transaction_to_usa t
|
||||
INNER JOIN conta c ON c.id_conta = t.id_conta
|
||||
@@ -667,8 +669,8 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
GROUP BY c.nome
|
||||
) combined
|
||||
GROUP BY nome
|
||||
HAVING MAX(last_op) < DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
ORDER BY total_usd DESC LIMIT 10
|
||||
HAVING MAX(last_op) < CURDATE()
|
||||
ORDER BY total_usd DESC LIMIT 20
|
||||
`);
|
||||
|
||||
// 12. Agent ranking with spread revenue
|
||||
@@ -747,7 +749,8 @@ async function fetchBIData(dataInicio, dataFim, getAgenteName = null) {
|
||||
retention: { prev_clients: retPrev, retained: retCurr, rate: retPrev > 0 ? Math.round(retCurr / retPrev * 100) : 0 },
|
||||
clientsAtRisk: clientsAtRisk.map(r => ({
|
||||
nome: r.nome, vol_usd: Number(r.total_usd), qtd: Number(r.total_qtd),
|
||||
last_op: r.last_op instanceof Date ? r.last_op.toISOString().slice(0, 10) : String(r.last_op).slice(0, 16)
|
||||
last_op: r.last_op instanceof Date ? r.last_op.toISOString().slice(0, 10) : String(r.last_op).slice(0, 16),
|
||||
days_inactive: Number(r.days_inactive) || 0
|
||||
})),
|
||||
agentRanking: agents,
|
||||
netting: {
|
||||
@@ -952,6 +955,292 @@ async function fetchRevenueAnalytics(dataInicio, dataFim, granularity = 'dia') {
|
||||
}
|
||||
}
|
||||
|
||||
// List clients with at least 1 transaction
|
||||
async function fetchClientList() {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
const [rows] = await conn.execute(`
|
||||
SELECT c.id_conta, c.nome FROM conta c
|
||||
WHERE c.id_conta IN (
|
||||
SELECT DISTINCT id_conta FROM br_transaction_to_usa
|
||||
UNION SELECT DISTINCT id_conta FROM pagamento_br
|
||||
) ORDER BY c.nome
|
||||
`);
|
||||
return rows.map(r => ({ id: r.id_conta, nome: r.nome }));
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Client lifetime profile (no date filter)
|
||||
async function fetchClientProfile(clienteId) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
// Name
|
||||
const [conta] = await conn.execute('SELECT nome FROM conta WHERE id_conta = ?', [clienteId]);
|
||||
const nome = conta[0]?.nome || 'Cliente #' + clienteId;
|
||||
|
||||
// BRL->USD lifetime
|
||||
const [brl] = await conn.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) * 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 = ?
|
||||
`, [clienteId]);
|
||||
|
||||
// USD->BRL lifetime
|
||||
const [usd] = await conn.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) * valor),0),2) as spread_revenue,
|
||||
MIN(created_at) as first_op, MAX(created_at) as last_op
|
||||
FROM pagamento_br WHERE id_conta = ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0
|
||||
AND (pgto IS NULL OR pgto != 'balance')
|
||||
`, [clienteId]);
|
||||
|
||||
const brlData = brl[0] || {};
|
||||
const usdData = usd[0] || {};
|
||||
|
||||
const dates = [brlData.first_op, usdData.first_op].filter(Boolean);
|
||||
const lastDates = [brlData.last_op, usdData.last_op].filter(Boolean);
|
||||
const firstOp = dates.length > 0 ? new Date(Math.min(...dates.map(d => new Date(d).getTime()))) : null;
|
||||
const lastOp = lastDates.length > 0 ? new Date(Math.max(...lastDates.map(d => new Date(d).getTime()))) : null;
|
||||
const daysInactive = lastOp ? Math.round((Date.now() - lastOp.getTime()) / 86400000) : null;
|
||||
|
||||
const brlQtd = Number(brlData.qtd) || 0;
|
||||
const usdQtd = Number(usdData.qtd) || 0;
|
||||
|
||||
return {
|
||||
id: clienteId,
|
||||
nome,
|
||||
first_op: firstOp ? firstOp.toISOString().slice(0, 10) : null,
|
||||
last_op: lastOp ? lastOp.toISOString().slice(0, 10) : null,
|
||||
days_inactive: daysInactive,
|
||||
total_ops: brlQtd + usdQtd,
|
||||
total_vol_usd: (Number(brlData.vol_usd) || 0) + (Number(usdData.vol_usd) || 0),
|
||||
total_vol_brl: (Number(brlData.vol_brl) || 0) + (Number(usdData.vol_brl) || 0),
|
||||
total_spread_revenue: (Number(brlData.spread_revenue) || 0) + (Number(usdData.spread_revenue) || 0),
|
||||
brlUsd: { qtd: brlQtd, vol_usd: Number(brlData.vol_usd) || 0 },
|
||||
usdBrl: { qtd: usdQtd, vol_usd: Number(usdData.vol_usd) || 0 }
|
||||
};
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Client data for a period — full analytics
|
||||
async function fetchClientData(clienteId, 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 BRL->USD
|
||||
const [kpiBrl] = await conn.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) * 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) <= ?
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// KPIs USD->BRL
|
||||
const [kpiUsd] = await conn.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) * 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
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Previous period
|
||||
const [prevBrl] = await conn.execute(`
|
||||
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(amount_usd),0),2) as vol_usd,
|
||||
ROUND(COALESCE(SUM((exchange_rate - ptax) * amount_usd),0),2) as spread_revenue
|
||||
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
`, [clienteId, prevStartStr, prevEndStr]);
|
||||
const [prevUsd] = await conn.execute(`
|
||||
SELECT COUNT(*) as qtd, ROUND(COALESCE(SUM(valor),0),2) as vol_usd,
|
||||
ROUND(COALESCE(SUM((ptax - cotacao) * valor),0),2) as spread_revenue
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
`, [clienteId, prevStartStr, prevEndStr]);
|
||||
|
||||
// Trend BRL->USD
|
||||
const [trendBrl] = await conn.execute(`
|
||||
SELECT DATE(created_at) as dia, COUNT(*) as qtd,
|
||||
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) <= ?
|
||||
GROUP BY DATE(created_at) ORDER BY dia
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Trend USD->BRL
|
||||
const [trendUsd] = await conn.execute(`
|
||||
SELECT DATE(created_at) as dia, COUNT(*) as qtd,
|
||||
ROUND(SUM(valor),2) as vol_usd,
|
||||
ROUND(AVG(CASE WHEN cotacao > 0 THEN (ptax - cotacao) / ptax * 100 ELSE 0 END),2) as avg_spread
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
GROUP BY DATE(created_at) ORDER BY dia
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Individual transactions BRL->USD
|
||||
const [txBrl] = await conn.execute(`
|
||||
SELECT t.created_at as date, 'BRL→USD' as flow,
|
||||
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
|
||||
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) <= ?
|
||||
ORDER BY t.created_at DESC
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Individual transactions USD->BRL
|
||||
const [txUsd] = await conn.execute(`
|
||||
SELECT p.created_at as date, 'USD→BRL' as flow,
|
||||
p.valor as usd, ROUND(p.valor * p.cotacao, 2) as brl,
|
||||
ROUND(p.cotacao,4) as rate, ROUND(p.ptax,4) as ptax,
|
||||
CASE WHEN p.cotacao > 0 THEN ROUND((p.ptax - p.cotacao) / p.ptax * 100, 2) ELSE 0 END as spread_pct,
|
||||
0 as iof, COALESCE(p.pgto, '') as status,
|
||||
COALESCE(p.tipo_envio, '') as provider
|
||||
FROM pagamento_br p
|
||||
WHERE p.id_conta = ? AND 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')
|
||||
ORDER BY p.created_at DESC
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Day of week
|
||||
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) <= ?
|
||||
GROUP BY DAYOFWEEK(created_at)
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
const [dowUsd] = await conn.execute(`
|
||||
SELECT DAYOFWEEK(created_at) as dow, COUNT(*) as qtd, ROUND(SUM(valor),2) as vol_usd
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
GROUP BY DAYOFWEEK(created_at)
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Providers
|
||||
const [provBrl] = await conn.execute(`
|
||||
SELECT COALESCE(pm.provider, 'N/A') as name, COUNT(*) as qtd, ROUND(SUM(t.amount_usd),2) as vol_usd
|
||||
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) <= ?
|
||||
GROUP BY pm.provider
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
const [provUsd] = await conn.execute(`
|
||||
SELECT COALESCE(p.tipo_envio, 'N/A') as name, COUNT(*) as qtd, ROUND(SUM(p.valor),2) as vol_usd
|
||||
FROM pagamento_br p
|
||||
WHERE p.id_conta = ? AND 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')
|
||||
GROUP BY p.tipo_envio
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Monthly average
|
||||
const [monthlyBrl] = await conn.execute(`
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, ROUND(AVG(amount_usd),2) as avg_usd, COUNT(*) as qtd
|
||||
FROM br_transaction_to_usa WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY mes
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
const [monthlyUsd] = await conn.execute(`
|
||||
SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, ROUND(AVG(valor),2) as avg_usd, COUNT(*) as qtd
|
||||
FROM pagamento_br WHERE id_conta = ? AND DATE(created_at) >= ? AND DATE(created_at) <= ?
|
||||
AND cotacao IS NOT NULL AND cotacao > 0 AND (pgto IS NULL OR pgto != 'balance')
|
||||
GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY mes
|
||||
`, [clienteId, dataInicio, dataFim]);
|
||||
|
||||
// Format helpers
|
||||
const fmtD = (d) => d instanceof Date ? d.toISOString().slice(0, 10) : String(d).slice(0, 10);
|
||||
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 fmtTrend = (rows) => rows.map(r => ({
|
||||
dia: fmtD(r.dia), qtd: Number(r.qtd), vol_usd: Number(r.vol_usd), avg_spread: Number(r.avg_spread) || 0
|
||||
}));
|
||||
|
||||
const b = kpiBrl[0] || {};
|
||||
const u = kpiUsd[0] || {};
|
||||
const bQtd = Number(b.qtd) || 0;
|
||||
const uQtd = Number(u.qtd) || 0;
|
||||
const totalQtd = bQtd + uQtd;
|
||||
const totalVolUsd = (Number(b.vol_usd) || 0) + (Number(u.vol_usd) || 0);
|
||||
|
||||
// Merge day of week
|
||||
const dowMap = {};
|
||||
for (let i = 1; i <= 7; i++) dowMap[i] = { qtd: 0, vol_usd: 0 };
|
||||
dowBrl.forEach(r => { dowMap[r.dow].qtd += Number(r.qtd); dowMap[r.dow].vol_usd += Number(r.vol_usd); });
|
||||
dowUsd.forEach(r => { dowMap[r.dow].qtd += Number(r.qtd); dowMap[r.dow].vol_usd += Number(r.vol_usd); });
|
||||
|
||||
// Merge providers
|
||||
const provMap = {};
|
||||
[...provBrl, ...provUsd].forEach(r => {
|
||||
const n = r.name || 'N/A';
|
||||
if (!provMap[n]) provMap[n] = { name: n, qtd: 0, vol_usd: 0 };
|
||||
provMap[n].qtd += Number(r.qtd);
|
||||
provMap[n].vol_usd += Number(r.vol_usd);
|
||||
});
|
||||
|
||||
// Merge monthly avg
|
||||
const monthMap = {};
|
||||
[...monthlyBrl, ...monthlyUsd].forEach(r => {
|
||||
if (!monthMap[r.mes]) monthMap[r.mes] = { mes: r.mes, total_usd: 0, total_qtd: 0 };
|
||||
monthMap[r.mes].total_usd += Number(r.avg_usd) * Number(r.qtd);
|
||||
monthMap[r.mes].total_qtd += Number(r.qtd);
|
||||
});
|
||||
|
||||
const transactions = [
|
||||
...txBrl.map(r => ({
|
||||
date: fmtDT(r.date), flow: r.flow, 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
|
||||
})),
|
||||
...txUsd.map(r => ({
|
||||
date: fmtDT(r.date), flow: r.flow, 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
|
||||
}))
|
||||
].sort((a, b) => b.date.localeCompare(a.date));
|
||||
|
||||
return {
|
||||
kpis: {
|
||||
brlUsd: { qtd: bQtd, vol_usd: Number(b.vol_usd)||0, vol_brl: Number(b.vol_brl)||0, spread_revenue: Number(b.spread_revenue)||0, avg_spread_pct: Number(b.avg_spread_pct)||0, ticket_medio: bQtd > 0 ? Math.round((Number(b.vol_usd)||0) / bQtd) : 0 },
|
||||
usdBrl: { qtd: uQtd, vol_usd: Number(u.vol_usd)||0, vol_brl: Number(u.vol_brl)||0, spread_revenue: Number(u.spread_revenue)||0, avg_spread_pct: Number(u.avg_spread_pct)||0, ticket_medio: uQtd > 0 ? Math.round((Number(u.vol_usd)||0) / uQtd) : 0 },
|
||||
total: {
|
||||
qtd: totalQtd, vol_usd: totalVolUsd, vol_brl: (Number(b.vol_brl)||0) + (Number(u.vol_brl)||0),
|
||||
spread_revenue: (Number(b.spread_revenue)||0) + (Number(u.spread_revenue)||0),
|
||||
avg_spread_pct: totalQtd > 0 ? ((Number(b.avg_spread_pct)||0) * bQtd + (Number(u.avg_spread_pct)||0) * uQtd) / totalQtd : 0,
|
||||
ticket_medio: totalQtd > 0 ? Math.round(totalVolUsd / totalQtd) : 0
|
||||
}
|
||||
},
|
||||
comparison: {
|
||||
prev_qtd: (Number(prevBrl[0]?.qtd)||0) + (Number(prevUsd[0]?.qtd)||0),
|
||||
prev_vol_usd: (Number(prevBrl[0]?.vol_usd)||0) + (Number(prevUsd[0]?.vol_usd)||0),
|
||||
prev_spread: (Number(prevBrl[0]?.spread_revenue)||0) + (Number(prevUsd[0]?.spread_revenue)||0)
|
||||
},
|
||||
trend: { brlUsd: fmtTrend(trendBrl), usdBrl: fmtTrend(trendUsd) },
|
||||
transactions,
|
||||
dayOfWeek: dowMap,
|
||||
providers: Object.values(provMap).sort((a, b) => b.vol_usd - a.vol_usd),
|
||||
monthlyAvg: Object.values(monthMap).map(m => ({ mes: m.mes, avg_usd: m.total_qtd > 0 ? Math.round(m.total_usd / m.total_qtd) : 0, qtd: m.total_qtd })).sort((a, b) => a.mes.localeCompare(b.mes))
|
||||
};
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchTransacoes,
|
||||
fetchAllTransacoes,
|
||||
@@ -963,5 +1252,8 @@ module.exports = {
|
||||
fetchTrendByPeriod,
|
||||
fetchKPIsByPeriod,
|
||||
fetchBIData,
|
||||
fetchRevenueAnalytics
|
||||
fetchRevenueAnalytics,
|
||||
fetchClientList,
|
||||
fetchClientProfile,
|
||||
fetchClientData
|
||||
};
|
||||
|
||||
@@ -366,6 +366,7 @@ function buildHeader(options = {}) {
|
||||
<nav class="header-nav">
|
||||
<a href="/corporate" class="${activePage === 'dashboard' ? 'active' : ''}">Corporate</a>
|
||||
<a href="/admin/bi" class="${activePage === 'bi' ? 'active' : ''}">BI Executive</a>
|
||||
<a href="/admin/cliente" class="${activePage === 'cliente' ? 'active' : ''}">Clientes</a>
|
||||
<a href="/admin" class="${activePage === 'users' ? 'active' : ''}">Usuarios</a>
|
||||
</nav>
|
||||
`;
|
||||
@@ -459,6 +460,7 @@ const themeScript = `
|
||||
localStorage.setItem('bi-theme', next);
|
||||
var icon = document.getElementById('themeIcon');
|
||||
if (icon) icon.textContent = next === 'dark' ? '\\u2600' : '\\u263E';
|
||||
window.dispatchEvent(new CustomEvent('themechange', {detail:{theme:next}}));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var icon = document.getElementById('themeIcon');
|
||||
|
||||
Reference in New Issue
Block a user