Add seller analytics pages
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m47s

This commit is contained in:
Cauê Faleiros
2026-08-04 09:59:49 -03:00
parent f18e7c4845
commit ff0f73e87c
8 changed files with 369 additions and 2 deletions

View File

@@ -6,6 +6,8 @@ const {
getClientFilterOptions,
getClientPurchasePatternAnalytics,
getDashboardAnalytics,
getSellerAnalytics,
getSellerDetailsAnalytics,
getProductAnalytics,
getProductDetailsAnalytics,
getRfmAnalytics
@@ -35,6 +37,29 @@ router.get('/analytics/dashboard', verifyToken, async (req, res) => {
}
});
router.get('/analytics/sellers', verifyToken, async (req, res) => {
try {
res.json(await getSellerAnalytics(getRange(req.query)));
} catch (error) {
console.error('Error fetching seller analytics:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
router.get('/analytics/sellers/:sellerId/details', verifyToken, async (req, res) => {
try {
const details = await getSellerDetailsAnalytics(req.params.sellerId, getRange(req.query));
if (!details) {
res.status(404).json({ error: 'Seller not found' });
return;
}
res.json(details);
} catch (error) {
console.error('Error fetching seller details analytics:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
router.get('/analytics/products', verifyToken, async (req, res) => {
try {
res.json(await getProductAnalytics(getRange(req.query)));

View File

@@ -694,6 +694,119 @@ const getDashboardAnalytics = async (range = {}) => {
};
};
const getSellerAnalytics = async (range = {}) => {
const { params, whereClause } = buildDateFilter(range);
const result = await pool.query(`
WITH seller_orders AS (
SELECT
COALESCE(${SELLER_ID_SQL}, 'name:' || ${SELLER_NAME_SQL}) as seller_id,
COALESCE(${SELLER_NAME_SQL}, ${SELLER_ID_SQL}, 'Sem vendedor') as seller_name,
quantidade,
valor_unitario,
pedido_id,
data_pedido,
valor_pedido,
data_pedido_date,
COALESCE(NULLIF(cliente_fone, ''), 'name:' || COALESCE(${NORMALIZED_CUSTOMER_NAME_SQL}, 'desconhecido')) as customer_key
FROM orders
${whereClause}
AND (${SELLER_ID_SQL} IS NOT NULL OR ${SELLER_NAME_SQL} IS NOT NULL)
)
SELECT
seller_id as id,
MAX(seller_name) as name,
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue,
COALESCE(SUM(quantidade), 0) as quantity_sold,
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count,
COUNT(DISTINCT customer_key)::int as customer_count,
MAX(data_pedido_date) as last_order_date
FROM seller_orders
GROUP BY seller_id
ORDER BY revenue DESC, order_count DESC, name ASC;
`, params);
return result.rows.map(row => ({
id: row.id,
name: row.name,
revenue: toNumber(row.revenue),
quantitySold: toNumber(row.quantity_sold),
orderCount: toNumber(row.order_count),
customerCount: toNumber(row.customer_count),
lastOrderDate: row.last_order_date,
averageTicket: toNumber(row.order_count) ? toNumber(row.revenue) / toNumber(row.order_count) : 0
}));
};
const getSellerDetailsAnalytics = async (sellerId, range = {}) => {
const seller = normalizeSellerFilter(sellerId);
if (!seller) return null;
const sellerRange = { ...range, seller: sellerId };
const { params, whereClause } = buildDateFilter(sellerRange);
const [summaryResult, trendResult, marketplaceResult, products, clients] = await Promise.all([
pool.query(`
SELECT
COALESCE(MAX(${SELLER_NAME_SQL}), MAX(${SELLER_ID_SQL}), 'Sem vendedor') as name,
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue,
COALESCE(SUM(quantidade), 0) as quantity_sold,
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count,
MAX(data_pedido_date) as last_order_date
FROM orders
${whereClause};
`, params),
pool.query(`
SELECT
data_pedido_date::text as date,
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue,
COALESCE(SUM(quantidade), 0) as quantity_sold,
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count
FROM orders
${whereClause}
GROUP BY data_pedido_date
ORDER BY data_pedido_date ASC;
`, params),
pool.query(`
SELECT
COALESCE(NULLIF(TRIM(marketplace), ''), 'Sem marketplace') as name,
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue,
COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count
FROM orders
${whereClause}
GROUP BY name
ORDER BY revenue DESC
LIMIT 6;
`, params),
getProductAnalytics(sellerRange),
getClientAnalytics(sellerRange)
]);
const summary = summaryResult.rows[0];
if (!summary || (!toNumber(summary.revenue) && !toNumber(summary.order_count))) return null;
const orderCount = toNumber(summary.order_count);
return {
seller: {
id: String(sellerId),
name: summary.name,
revenue: toNumber(summary.revenue),
quantitySold: toNumber(summary.quantity_sold),
orderCount,
customerCount: clients.length,
lastOrderDate: summary.last_order_date,
averageTicket: orderCount ? toNumber(summary.revenue) / orderCount : 0
},
chartData: trendResult.rows.map(row => ({
date: row.date,
revenue: toNumber(row.revenue),
quantitySold: toNumber(row.quantity_sold),
orderCount: toNumber(row.order_count)
})),
topProducts: products.slice(0, 8),
topClients: clients.slice(0, 8),
marketplaces: marketplaceResult.rows.map(row => ({ name: row.name, revenue: toNumber(row.revenue), orderCount: toNumber(row.order_count) }))
};
};
const getProductAnalytics = async (range = {}) => {
const { params, whereClause } = buildDateFilter(range);
const result = await pool.query(`
@@ -1533,6 +1646,8 @@ module.exports = {
getRfmSegment,
getClientAnalytics,
getDashboardAnalytics,
getSellerAnalytics,
getSellerDetailsAnalytics,
getProductDetailsAnalytics,
getProductAnalytics,
normalizeDateParam,