Improve product analytics UI
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m48s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m48s
This commit is contained in:
@@ -759,18 +759,24 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
'produto_id = $1',
|
||||
'data_pedido_date IS NOT NULL'
|
||||
];
|
||||
const variantParams = [normalizedProductId];
|
||||
const variantFilters = ['data_pedido_date IS NOT NULL'];
|
||||
|
||||
if (normalizedStart) {
|
||||
periodParams.push(normalizedStart);
|
||||
periodFilters.push(`data_pedido_date >= $${periodParams.length}::date`);
|
||||
variantParams.push(normalizedStart);
|
||||
variantFilters.push(`data_pedido_date >= $${variantParams.length}::date`);
|
||||
}
|
||||
|
||||
if (normalizedEnd) {
|
||||
periodParams.push(normalizedEnd);
|
||||
periodFilters.push(`data_pedido_date <= $${periodParams.length}::date`);
|
||||
variantParams.push(normalizedEnd);
|
||||
variantFilters.push(`data_pedido_date <= $${variantParams.length}::date`);
|
||||
}
|
||||
|
||||
const [summaryResult, periodResult] = await Promise.all([
|
||||
const [summaryResult, periodResult, variantResult] = await Promise.all([
|
||||
pool.query(`
|
||||
WITH selected_product AS (
|
||||
SELECT $1::text as id
|
||||
@@ -778,7 +784,8 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
stock_info AS (
|
||||
SELECT
|
||||
produto_id as id,
|
||||
MAX(NULLIF(nome, '')) as name
|
||||
MAX(NULLIF(nome, '')) as name,
|
||||
COALESCE(MAX(saldo), 0) as stock
|
||||
FROM stock
|
||||
WHERE produto_id = $1
|
||||
GROUP BY produto_id
|
||||
@@ -795,7 +802,8 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
SELECT
|
||||
selected_product.id,
|
||||
COALESCE(stock_info.name, order_info.name, 'Unknown') as name,
|
||||
COALESCE(order_info.price, 0) as price
|
||||
COALESCE(order_info.price, 0) as price,
|
||||
COALESCE(stock_info.stock, 0) as stock
|
||||
FROM selected_product
|
||||
LEFT JOIN stock_info ON stock_info.id = selected_product.id
|
||||
LEFT JOIN order_info ON order_info.id = selected_product.id
|
||||
@@ -805,7 +813,8 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
SELECT
|
||||
EXTRACT(HOUR FROM created_at AT TIME ZONE 'America/Sao_Paulo')::int as hour,
|
||||
COALESCE(SUM(quantidade), 0) as quantity_sold,
|
||||
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue
|
||||
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
|
||||
WHERE ${periodFilters.join(' AND ')}
|
||||
GROUP BY hour
|
||||
@@ -815,12 +824,35 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
data_pedido_date,
|
||||
MAX(data_pedido) as date_label,
|
||||
COALESCE(SUM(quantidade), 0) as quantity_sold,
|
||||
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue
|
||||
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
|
||||
WHERE ${periodFilters.join(' AND ')}
|
||||
GROUP BY data_pedido_date
|
||||
ORDER BY data_pedido_date ASC;
|
||||
`, periodParams)
|
||||
`, periodParams),
|
||||
pool.query(`
|
||||
WITH selected_base AS (
|
||||
SELECT COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') as base_name
|
||||
FROM orders
|
||||
WHERE produto_id = $1
|
||||
AND data_pedido_date IS NOT NULL
|
||||
ORDER BY data_pedido_date DESC NULLS LAST, data_pedido DESC NULLS LAST
|
||||
LIMIT 1
|
||||
)
|
||||
SELECT
|
||||
produto_id as id,
|
||||
MAX(COALESCE(NULLIF(produto_descricao, ''), 'Unknown')) as name,
|
||||
COALESCE(SUM(quantidade), 0) as quantity_sold,
|
||||
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
|
||||
WHERE ${variantFilters.join(' AND ')}
|
||||
AND COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') = (SELECT base_name FROM selected_base)
|
||||
GROUP BY produto_id
|
||||
ORDER BY quantity_sold DESC, revenue DESC, name ASC
|
||||
LIMIT 8;
|
||||
`, variantParams)
|
||||
]);
|
||||
|
||||
const summary = summaryResult.rows[0];
|
||||
@@ -829,17 +861,43 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
const chartData = useHourlyChart
|
||||
? Array.from({ length: 24 }, (_, hour) => {
|
||||
const row = periodResult.rows.find(item => toNumber(item.hour) === hour);
|
||||
const quantitySold = row ? toNumber(row.quantity_sold) : 0;
|
||||
const revenue = row ? toNumber(row.revenue) : 0;
|
||||
const orderCount = row ? toNumber(row.order_count) : 0;
|
||||
|
||||
return {
|
||||
date: `${String(hour).padStart(2, '0')}h`,
|
||||
value: row ? toNumber(row.quantity_sold) : 0
|
||||
value: quantitySold,
|
||||
quantitySold,
|
||||
revenue,
|
||||
orderCount,
|
||||
averageTicket: orderCount ? revenue / orderCount : 0
|
||||
};
|
||||
})
|
||||
: periodResult.rows.map(row => ({
|
||||
date: row.date_label || getDateOnly(row.data_pedido_date) || '',
|
||||
value: toNumber(row.quantity_sold)
|
||||
}));
|
||||
: periodResult.rows.map(row => {
|
||||
const quantitySold = toNumber(row.quantity_sold);
|
||||
const revenue = toNumber(row.revenue);
|
||||
const orderCount = toNumber(row.order_count);
|
||||
|
||||
return {
|
||||
date: row.date_label || getDateOnly(row.data_pedido_date) || '',
|
||||
value: quantitySold,
|
||||
quantitySold,
|
||||
revenue,
|
||||
orderCount,
|
||||
averageTicket: orderCount ? revenue / orderCount : 0
|
||||
};
|
||||
});
|
||||
const totalSold = periodResult.rows.reduce((sum, row) => sum + toNumber(row.quantity_sold), 0);
|
||||
const totalRevenue = periodResult.rows.reduce((sum, row) => sum + toNumber(row.revenue), 0);
|
||||
const totalOrders = periodResult.rows.reduce((sum, row) => sum + toNumber(row.order_count), 0);
|
||||
const variantBreakdown = variantResult.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
quantitySold: toNumber(row.quantity_sold),
|
||||
revenue: toNumber(row.revenue),
|
||||
orderCount: toNumber(row.order_count)
|
||||
}));
|
||||
|
||||
return {
|
||||
range: {
|
||||
@@ -849,11 +907,15 @@ const getProductDetailsAnalytics = async (productId, range = {}) => {
|
||||
productInfo: {
|
||||
id: summary.id,
|
||||
name: summary.name,
|
||||
price: toNumber(summary.price)
|
||||
price: toNumber(summary.price),
|
||||
stock: toNumber(summary.stock)
|
||||
},
|
||||
chartData,
|
||||
totalSold,
|
||||
totalRevenue
|
||||
totalRevenue,
|
||||
totalOrders,
|
||||
averageTicket: totalOrders ? totalRevenue / totalOrders : 0,
|
||||
variantBreakdown
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -526,24 +526,44 @@ test('getProductDetailsAnalytics returns product identity and period chart witho
|
||||
rows: [{
|
||||
id: '919483307',
|
||||
name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G',
|
||||
price: 11.9
|
||||
price: 11.9,
|
||||
stock: 11731
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
if (sql.includes('selected_base AS')) {
|
||||
assert.match(sql, /COALESCE\(\s+CASE/);
|
||||
assert.match(sql, /GROUP BY produto_id/);
|
||||
assert.deepEqual(params, ['919483307', '2026-06-01', '2026-06-22']);
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
id: '919483307',
|
||||
name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G',
|
||||
quantity_sold: 3,
|
||||
revenue: 35.7,
|
||||
order_count: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
data_pedido_date: '2026-06-01',
|
||||
date_label: '01-06-2026',
|
||||
quantity_sold: 3,
|
||||
revenue: 35.7
|
||||
revenue: 35.7,
|
||||
order_count: 1
|
||||
},
|
||||
{
|
||||
data_pedido_date: '2026-06-02',
|
||||
date_label: '02-06-2026',
|
||||
quantity_sold: 2,
|
||||
revenue: 23.8
|
||||
revenue: 23.8,
|
||||
order_count: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -552,7 +572,7 @@ test('getProductDetailsAnalytics returns product identity and period chart witho
|
||||
try {
|
||||
const details = await getProductDetailsAnalytics('919483307', { start: '2026-06-01', end: '2026-06-22' });
|
||||
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls.length, 3);
|
||||
assert.match(calls[0].sql, /WITH selected_product AS/);
|
||||
assert.match(calls[0].sql, /WHERE produto_id = \$1/);
|
||||
assert.deepEqual(calls[0].params, ['919483307']);
|
||||
@@ -563,14 +583,26 @@ test('getProductDetailsAnalytics returns product identity and period chart witho
|
||||
assert.deepEqual(details.productInfo, {
|
||||
id: '919483307',
|
||||
name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G',
|
||||
price: 11.9
|
||||
price: 11.9,
|
||||
stock: 11731
|
||||
});
|
||||
assert.deepEqual(details.chartData, [
|
||||
{ date: '01-06-2026', value: 3 },
|
||||
{ date: '02-06-2026', value: 2 }
|
||||
{ date: '01-06-2026', value: 3, quantitySold: 3, revenue: 35.7, orderCount: 1, averageTicket: 35.7 },
|
||||
{ date: '02-06-2026', value: 2, quantitySold: 2, revenue: 23.8, orderCount: 1, averageTicket: 23.8 }
|
||||
]);
|
||||
assert.equal(details.totalSold, 5);
|
||||
assert.equal(details.totalRevenue, 59.5);
|
||||
assert.equal(details.totalOrders, 2);
|
||||
assert.equal(details.averageTicket, 29.75);
|
||||
assert.deepEqual(details.variantBreakdown, [
|
||||
{
|
||||
id: '919483307',
|
||||
name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G',
|
||||
quantitySold: 3,
|
||||
revenue: 35.7,
|
||||
orderCount: 1
|
||||
}
|
||||
]);
|
||||
} finally {
|
||||
pool.query = originalQuery;
|
||||
}
|
||||
@@ -585,7 +617,8 @@ test('getProductDetailsAnalytics keeps known products visible with zero period s
|
||||
rows: [{
|
||||
id: 'stock-only',
|
||||
name: 'Produto sem venda no período',
|
||||
price: 0
|
||||
price: 0,
|
||||
stock: 12
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -600,6 +633,7 @@ test('getProductDetailsAnalytics keeps known products visible with zero period s
|
||||
assert.equal(details.totalSold, 0);
|
||||
assert.equal(details.totalRevenue, 0);
|
||||
assert.deepEqual(details.chartData, []);
|
||||
assert.deepEqual(details.variantBreakdown, []);
|
||||
} finally {
|
||||
pool.query = originalQuery;
|
||||
}
|
||||
@@ -614,16 +648,21 @@ test('getProductDetailsAnalytics groups single-day chart by hour', async () => {
|
||||
rows: [{
|
||||
id: '919483307',
|
||||
name: 'Produto com venda por hora',
|
||||
price: 11.9
|
||||
price: 11.9,
|
||||
stock: 4
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
if (sql.includes('selected_base AS')) {
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
assert.match(sql, /EXTRACT\(HOUR FROM created_at AT TIME ZONE 'America\/Sao_Paulo'\)::int as hour/);
|
||||
return {
|
||||
rows: [
|
||||
{ hour: 9, quantity_sold: 2, revenue: 23.8 },
|
||||
{ hour: 18, quantity_sold: 3, revenue: 35.7 }
|
||||
{ hour: 9, quantity_sold: 2, revenue: 23.8, order_count: 1 },
|
||||
{ hour: 18, quantity_sold: 3, revenue: 35.7, order_count: 1 }
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -632,8 +671,8 @@ test('getProductDetailsAnalytics groups single-day chart by hour', async () => {
|
||||
const details = await getProductDetailsAnalytics('919483307', { start: '2026-06-22', end: '2026-06-22' });
|
||||
|
||||
assert.equal(details.chartData.length, 24);
|
||||
assert.deepEqual(details.chartData[9], { date: '09h', value: 2 });
|
||||
assert.deepEqual(details.chartData[18], { date: '18h', value: 3 });
|
||||
assert.deepEqual(details.chartData[9], { date: '09h', value: 2, quantitySold: 2, revenue: 23.8, orderCount: 1, averageTicket: 23.8 });
|
||||
assert.deepEqual(details.chartData[18], { date: '18h', value: 3, quantitySold: 3, revenue: 35.7, orderCount: 1, averageTicket: 35.7 });
|
||||
assert.equal(details.totalSold, 5);
|
||||
assert.equal(details.totalRevenue, 59.5);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user