const { outputProgress, formatElapsedTime, estimateRemaining, calculateRate, logError } = require('./utils/progress'); const { getConnection } = require('./utils/db'); async function calculateCategoryMetrics(startTime, totalProducts, processedCount = 0, isCancelled = false) { const connection = await getConnection(); let success = false; const BATCH_SIZE = 5000; try { // Get last calculation timestamp const [lastCalc] = await connection.query(` SELECT last_calculation_timestamp FROM calculate_status WHERE module_name = 'category_metrics' `); const lastCalculationTime = lastCalc[0]?.last_calculation_timestamp || '1970-01-01'; // Get total count of categories needing updates const [categoryCount] = await connection.query(` SELECT COUNT(DISTINCT c.cat_id) as count FROM categories c JOIN product_categories pc ON c.cat_id = pc.cat_id LEFT JOIN products p ON pc.pid = p.pid AND p.updated > ? LEFT JOIN orders o ON p.pid = o.pid AND o.updated > ? WHERE c.status = 'active' AND ( p.pid IS NOT NULL OR o.id IS NOT NULL ) `, [lastCalculationTime, lastCalculationTime]); const totalCategories = categoryCount[0].count; if (totalCategories === 0) { console.log('No categories need metric updates'); return { processedProducts: 0, processedOrders: 0, processedPurchaseOrders: 0, success: true }; } if (isCancelled) { outputProgress({ status: 'cancelled', operation: 'Category metrics calculation cancelled', current: processedCount, total: totalCategories, elapsed: formatElapsedTime(startTime), remaining: null, rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalCategories) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); return { processedProducts: processedCount, processedOrders: 0, processedPurchaseOrders: 0, success }; } outputProgress({ status: 'running', operation: 'Starting category metrics calculation', current: processedCount, total: totalCategories, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, processedCount, totalCategories), rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalCategories) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); // Process in batches let lastCatId = 0; while (true) { if (isCancelled) break; const [batch] = await connection.query(` SELECT DISTINCT c.cat_id FROM categories c JOIN product_categories pc ON c.cat_id = pc.cat_id LEFT JOIN products p ON pc.pid = p.pid AND p.updated > ? LEFT JOIN orders o ON p.pid = o.pid AND o.updated > ? WHERE c.status = 'active' AND c.cat_id > ? AND ( p.pid IS NOT NULL OR o.id IS NOT NULL ) ORDER BY c.cat_id LIMIT ? `, [lastCalculationTime, lastCalculationTime, lastCatId, BATCH_SIZE]); if (batch.length === 0) break; // Update category metrics for this batch await connection.query(` INSERT INTO category_metrics ( category_id, product_count, active_products, total_value, avg_margin, turnover_rate, growth_rate, 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, SUM(p.stock_quantity * p.cost_price) as total_value, AVG(pm.avg_margin_percent) as avg_margin, AVG(pm.turnover_rate) as turnover_rate, ((SUM(CASE WHEN o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY) THEN o.quantity * o.price ELSE 0 END) / NULLIF(SUM(CASE WHEN o.date BETWEEN DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY) AND DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY) THEN o.quantity * o.price ELSE 0 END), 0) - 1) * 100) as growth_rate, c.status, NOW() as last_calculated_at FROM categories c JOIN product_categories pc ON c.cat_id = pc.cat_id LEFT JOIN products p ON pc.pid = p.pid LEFT JOIN product_metrics pm ON p.pid = pm.pid LEFT JOIN orders o ON p.pid = o.pid AND o.canceled = false AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY) WHERE c.cat_id IN (?) 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), avg_margin = VALUES(avg_margin), turnover_rate = VALUES(turnover_rate), growth_rate = VALUES(growth_rate), status = VALUES(status), last_calculated_at = NOW() `, [batch.map(row => row.cat_id)]); lastCatId = batch[batch.length - 1].cat_id; processedCount += batch.length; outputProgress({ status: 'running', operation: 'Processing category metrics batch', current: processedCount, total: totalCategories, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, processedCount, totalCategories), rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalCategories) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); } // If we get here, everything completed successfully success = true; // Update calculate_status await connection.query(` INSERT INTO calculate_status (module_name, last_calculation_timestamp) VALUES ('category_metrics', NOW()) ON DUPLICATE KEY UPDATE last_calculation_timestamp = NOW() `); return { processedProducts: processedCount, processedOrders: 0, processedPurchaseOrders: 0, success }; } catch (error) { success = false; logError(error, 'Error calculating category metrics'); throw error; } finally { if (connection) { connection.release(); } } } module.exports = calculateCategoryMetrics;