All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m44s
179 lines
5.7 KiB
JavaScript
179 lines
5.7 KiB
JavaScript
const { pool } = require('../db');
|
|
|
|
const SIZE_SUFFIX_SQL_PATTERN = '\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$';
|
|
const PRODUCT_NAME_SQL = `NULLIF(TRIM(regexp_replace(split_part(COALESCE(produto_descricao, 'Unknown'), ' TAMANHO', 1), '${SIZE_SUFFIX_SQL_PATTERN}', '', 'i')), '')`;
|
|
|
|
const normalizeDateParam = (value) => {
|
|
if (!value) return null;
|
|
|
|
const match = String(value).trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
if (!match) return null;
|
|
|
|
const [, yearValue, monthValue, dayValue] = match;
|
|
const year = Number(yearValue);
|
|
const month = Number(monthValue);
|
|
const day = Number(dayValue);
|
|
const date = new Date(Date.UTC(year, month - 1, day));
|
|
|
|
if (
|
|
date.getUTCFullYear() !== year ||
|
|
date.getUTCMonth() !== month - 1 ||
|
|
date.getUTCDate() !== day
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return `${yearValue}-${monthValue}-${dayValue}`;
|
|
};
|
|
|
|
const buildDateFilter = ({ start, end } = {}) => {
|
|
const params = [];
|
|
const filters = ['data_pedido_date IS NOT NULL'];
|
|
const normalizedStart = normalizeDateParam(start);
|
|
const normalizedEnd = normalizeDateParam(end);
|
|
|
|
if (normalizedStart) {
|
|
params.push(normalizedStart);
|
|
filters.push(`data_pedido_date >= $${params.length}::date`);
|
|
}
|
|
|
|
if (normalizedEnd) {
|
|
params.push(normalizedEnd);
|
|
filters.push(`data_pedido_date <= $${params.length}::date`);
|
|
}
|
|
|
|
return {
|
|
params,
|
|
whereClause: `WHERE ${filters.join(' AND ')}`
|
|
};
|
|
};
|
|
|
|
const toNumber = (value) => Number(value || 0);
|
|
|
|
const getDashboardAnalytics = async (range = {}) => {
|
|
const { params, whereClause } = buildDateFilter(range);
|
|
const [totalsResult, salesResult, revenueResult] = await Promise.all([
|
|
pool.query(`
|
|
SELECT
|
|
COALESCE(SUM(quantidade * valor_unitario), 0) as total_revenue,
|
|
COALESCE(SUM(quantidade), 0) as total_items,
|
|
COUNT(*)::int as order_line_count
|
|
FROM orders
|
|
${whereClause};
|
|
`, params),
|
|
pool.query(`
|
|
SELECT
|
|
COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') as name,
|
|
MAX(produto_id) as id,
|
|
COALESCE(SUM(quantidade), 0) as value
|
|
FROM orders
|
|
${whereClause}
|
|
GROUP BY name
|
|
ORDER BY value DESC
|
|
LIMIT 10;
|
|
`, params),
|
|
pool.query(`
|
|
SELECT
|
|
COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') as name,
|
|
MAX(produto_id) as id,
|
|
COALESCE(SUM(quantidade * valor_unitario), 0) as value
|
|
FROM orders
|
|
${whereClause}
|
|
GROUP BY name
|
|
ORDER BY value DESC
|
|
LIMIT 10;
|
|
`, params)
|
|
]);
|
|
|
|
const totals = totalsResult.rows[0] || {};
|
|
const orderLineCount = toNumber(totals.order_line_count);
|
|
const totalRevenue = toNumber(totals.total_revenue);
|
|
|
|
return {
|
|
range: {
|
|
start: normalizeDateParam(range.start),
|
|
end: normalizeDateParam(range.end)
|
|
},
|
|
totalRevenue,
|
|
totalOrders: toNumber(totals.total_items),
|
|
orderLineCount,
|
|
averageOrderValue: orderLineCount ? totalRevenue / orderLineCount : 0,
|
|
salesByProduct: salesResult.rows.map(row => ({
|
|
name: row.name,
|
|
id: row.id,
|
|
value: toNumber(row.value)
|
|
})),
|
|
revenueByProduct: revenueResult.rows.map(row => ({
|
|
name: row.name,
|
|
id: row.id,
|
|
value: toNumber(row.value)
|
|
}))
|
|
};
|
|
};
|
|
|
|
const getProductAnalytics = async (range = {}) => {
|
|
const { params, whereClause } = buildDateFilter(range);
|
|
const result = await pool.query(`
|
|
SELECT
|
|
COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') as name,
|
|
MAX(produto_id) as id,
|
|
COALESCE(SUM(quantidade), 0) as quantity_sold,
|
|
COALESCE(SUM(quantidade * valor_unitario), 0) as revenue,
|
|
COUNT(*)::int as order_line_count,
|
|
MIN(data_pedido_date) as first_sale_date,
|
|
MAX(data_pedido_date) as last_sale_date
|
|
FROM orders
|
|
${whereClause}
|
|
GROUP BY name
|
|
ORDER BY revenue DESC, quantity_sold DESC
|
|
LIMIT 500;
|
|
`, params);
|
|
|
|
return result.rows.map(row => ({
|
|
name: row.name,
|
|
id: row.id,
|
|
quantitySold: toNumber(row.quantity_sold),
|
|
revenue: toNumber(row.revenue),
|
|
orderLineCount: toNumber(row.order_line_count),
|
|
firstSaleDate: row.first_sale_date,
|
|
lastSaleDate: row.last_sale_date
|
|
}));
|
|
};
|
|
|
|
const getClientAnalytics = async (range = {}) => {
|
|
const { params, whereClause } = buildDateFilter(range);
|
|
const result = await pool.query(`
|
|
SELECT
|
|
MAX(cliente_nome) as name,
|
|
cliente_fone as phone,
|
|
COALESCE(SUM(quantidade), 0) as quantity_purchased,
|
|
COALESCE(SUM(quantidade * valor_unitario), 0) as total_spent,
|
|
COUNT(*)::int as order_line_count,
|
|
MAX(data_pedido_date) as last_purchase_date
|
|
FROM orders
|
|
${whereClause}
|
|
AND cliente_fone IS NOT NULL
|
|
AND cliente_fone != ''
|
|
GROUP BY cliente_fone
|
|
ORDER BY total_spent DESC
|
|
LIMIT 500;
|
|
`, params);
|
|
|
|
return result.rows.map(row => ({
|
|
name: row.name,
|
|
phone: row.phone,
|
|
quantityPurchased: toNumber(row.quantity_purchased),
|
|
totalSpent: toNumber(row.total_spent),
|
|
orderLineCount: toNumber(row.order_line_count),
|
|
lastPurchaseDate: row.last_purchase_date
|
|
}));
|
|
};
|
|
|
|
module.exports = {
|
|
buildDateFilter,
|
|
getClientAnalytics,
|
|
getDashboardAnalytics,
|
|
getProductAnalytics,
|
|
normalizeDateParam
|
|
};
|