239 lines
9.7 KiB
JavaScript
239 lines
9.7 KiB
JavaScript
const { outputProgress, formatElapsedTime, estimateRemaining, calculateRate, logError } = require('./utils/progress');
|
|
const { getConnection } = require('./utils/db');
|
|
|
|
async function calculateCategoryMetrics(startTime, totalProducts, processedCount, isCancelled = false) {
|
|
const connection = await getConnection();
|
|
try {
|
|
if (isCancelled) {
|
|
outputProgress({
|
|
status: 'cancelled',
|
|
operation: 'Category metrics calculation cancelled',
|
|
current: processedCount,
|
|
total: totalProducts,
|
|
elapsed: formatElapsedTime(startTime),
|
|
remaining: null,
|
|
rate: calculateRate(startTime, processedCount),
|
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
|
});
|
|
return processedCount;
|
|
}
|
|
|
|
outputProgress({
|
|
status: 'running',
|
|
operation: 'Starting category metrics calculation',
|
|
current: processedCount,
|
|
total: totalProducts,
|
|
elapsed: formatElapsedTime(startTime),
|
|
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
|
rate: calculateRate(startTime, processedCount),
|
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
|
});
|
|
|
|
// First, calculate base category metrics
|
|
await connection.query(`
|
|
INSERT INTO category_metrics (
|
|
category_id,
|
|
product_count,
|
|
active_products,
|
|
total_value,
|
|
status,
|
|
last_calculated_at
|
|
)
|
|
SELECT
|
|
c.cat_id,
|
|
COUNT(DISTINCT p.pid) as product_count,
|
|
COUNT(DISTINCT CASE WHEN p.visible = true THEN p.pid END) as active_products,
|
|
COALESCE(SUM(p.stock_quantity * p.cost_price), 0) as total_value,
|
|
c.status,
|
|
NOW() as last_calculated_at
|
|
FROM categories c
|
|
LEFT JOIN product_categories pc ON c.cat_id = pc.cat_id
|
|
LEFT JOIN products p ON pc.pid = p.pid
|
|
GROUP BY c.cat_id, c.status
|
|
ON DUPLICATE KEY UPDATE
|
|
product_count = VALUES(product_count),
|
|
active_products = VALUES(active_products),
|
|
total_value = VALUES(total_value),
|
|
status = VALUES(status),
|
|
last_calculated_at = VALUES(last_calculated_at)
|
|
`);
|
|
|
|
processedCount = Math.floor(totalProducts * 0.90);
|
|
outputProgress({
|
|
status: 'running',
|
|
operation: 'Base category metrics calculated, updating with margin data',
|
|
current: processedCount,
|
|
total: totalProducts,
|
|
elapsed: formatElapsedTime(startTime),
|
|
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
|
rate: calculateRate(startTime, processedCount),
|
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
|
});
|
|
|
|
if (isCancelled) return processedCount;
|
|
|
|
// Then update with margin and turnover data
|
|
await connection.query(`
|
|
WITH category_sales AS (
|
|
SELECT
|
|
pc.cat_id,
|
|
SUM(o.quantity * o.price) as total_sales,
|
|
SUM(o.quantity * (o.price - p.cost_price)) as total_margin,
|
|
SUM(o.quantity) as units_sold,
|
|
AVG(GREATEST(p.stock_quantity, 0)) as avg_stock
|
|
FROM product_categories pc
|
|
JOIN products p ON pc.pid = p.pid
|
|
JOIN orders o ON p.pid = o.pid
|
|
WHERE o.canceled = false
|
|
AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 1 YEAR)
|
|
GROUP BY pc.cat_id
|
|
)
|
|
UPDATE category_metrics cm
|
|
JOIN category_sales cs ON cm.category_id = cs.cat_id
|
|
SET
|
|
cm.avg_margin = COALESCE(cs.total_margin * 100.0 / NULLIF(cs.total_sales, 0), 0),
|
|
cm.turnover_rate = LEAST(COALESCE(cs.units_sold / NULLIF(cs.avg_stock, 0), 0), 999.99),
|
|
cm.last_calculated_at = NOW()
|
|
`);
|
|
|
|
processedCount = Math.floor(totalProducts * 0.95);
|
|
outputProgress({
|
|
status: 'running',
|
|
operation: 'Margin data updated, calculating growth rates',
|
|
current: processedCount,
|
|
total: totalProducts,
|
|
elapsed: formatElapsedTime(startTime),
|
|
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
|
rate: calculateRate(startTime, processedCount),
|
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
|
});
|
|
|
|
if (isCancelled) return processedCount;
|
|
|
|
// Finally update growth rates
|
|
await connection.query(`
|
|
WITH current_period AS (
|
|
SELECT
|
|
pc.cat_id,
|
|
SUM(o.quantity * o.price) as revenue
|
|
FROM product_categories pc
|
|
JOIN products p ON pc.pid = p.pid
|
|
JOIN orders o ON p.pid = o.pid
|
|
WHERE o.canceled = false
|
|
AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 3 MONTH)
|
|
GROUP BY pc.cat_id
|
|
),
|
|
previous_period AS (
|
|
SELECT
|
|
pc.cat_id,
|
|
SUM(o.quantity * o.price) as revenue
|
|
FROM product_categories pc
|
|
JOIN products p ON pc.pid = p.pid
|
|
JOIN orders o ON p.pid = o.pid
|
|
WHERE o.canceled = false
|
|
AND o.date BETWEEN DATE_SUB(CURRENT_DATE, INTERVAL 15 MONTH)
|
|
AND DATE_SUB(CURRENT_DATE, INTERVAL 12 MONTH)
|
|
GROUP BY pc.cat_id
|
|
)
|
|
UPDATE category_metrics cm
|
|
LEFT JOIN current_period cp ON cm.category_id = cp.cat_id
|
|
LEFT JOIN previous_period pp ON cm.category_id = pp.cat_id
|
|
SET
|
|
cm.growth_rate = CASE
|
|
WHEN pp.revenue = 0 AND COALESCE(cp.revenue, 0) > 0 THEN 100.0
|
|
WHEN pp.revenue = 0 THEN 0.0
|
|
ELSE LEAST(
|
|
GREATEST(
|
|
((COALESCE(cp.revenue, 0) - pp.revenue) / pp.revenue) * 100.0,
|
|
-100.0
|
|
),
|
|
999.99
|
|
)
|
|
END,
|
|
cm.last_calculated_at = NOW()
|
|
WHERE cp.cat_id IS NOT NULL OR pp.cat_id IS NOT NULL
|
|
`);
|
|
|
|
processedCount = Math.floor(totalProducts * 0.97);
|
|
outputProgress({
|
|
status: 'running',
|
|
operation: 'Growth rates calculated, updating time-based metrics',
|
|
current: processedCount,
|
|
total: totalProducts,
|
|
elapsed: formatElapsedTime(startTime),
|
|
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
|
rate: calculateRate(startTime, processedCount),
|
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
|
});
|
|
|
|
if (isCancelled) return processedCount;
|
|
|
|
// Calculate time-based metrics
|
|
await connection.query(`
|
|
INSERT INTO category_time_metrics (
|
|
category_id,
|
|
year,
|
|
month,
|
|
product_count,
|
|
active_products,
|
|
total_value,
|
|
total_revenue,
|
|
avg_margin,
|
|
turnover_rate
|
|
)
|
|
SELECT
|
|
pc.cat_id,
|
|
YEAR(o.date) as year,
|
|
MONTH(o.date) as month,
|
|
COUNT(DISTINCT p.pid) as product_count,
|
|
COUNT(DISTINCT CASE WHEN p.visible = true THEN p.pid END) as active_products,
|
|
SUM(p.stock_quantity * p.cost_price) as total_value,
|
|
SUM(o.quantity * o.price) as total_revenue,
|
|
COALESCE(
|
|
SUM(o.quantity * (o.price - p.cost_price)) * 100.0 /
|
|
NULLIF(SUM(o.quantity * o.price), 0),
|
|
0
|
|
) as avg_margin,
|
|
COALESCE(
|
|
SUM(o.quantity) / NULLIF(AVG(GREATEST(p.stock_quantity, 0)), 0),
|
|
0
|
|
) as turnover_rate
|
|
FROM product_categories pc
|
|
JOIN products p ON pc.pid = p.pid
|
|
JOIN orders o ON p.pid = o.pid
|
|
WHERE o.canceled = false
|
|
AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 12 MONTH)
|
|
GROUP BY pc.cat_id, YEAR(o.date), MONTH(o.date)
|
|
ON DUPLICATE KEY UPDATE
|
|
product_count = VALUES(product_count),
|
|
active_products = VALUES(active_products),
|
|
total_value = VALUES(total_value),
|
|
total_revenue = VALUES(total_revenue),
|
|
avg_margin = VALUES(avg_margin),
|
|
turnover_rate = VALUES(turnover_rate)
|
|
`);
|
|
|
|
processedCount = Math.floor(totalProducts * 0.99);
|
|
outputProgress({
|
|
status: 'running',
|
|
operation: 'Time-based metrics calculated',
|
|
current: processedCount,
|
|
total: totalProducts,
|
|
elapsed: formatElapsedTime(startTime),
|
|
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
|
rate: calculateRate(startTime, processedCount),
|
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
|
});
|
|
|
|
return processedCount;
|
|
} catch (error) {
|
|
logError(error, 'Error calculating category metrics');
|
|
throw error;
|
|
} finally {
|
|
if (connection) {
|
|
connection.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = calculateCategoryMetrics;
|