diff --git a/inventory-server/scripts/calculate-metrics.js b/inventory-server/scripts/calculate-metrics.js index c33d8b8..d4570ae 100644 --- a/inventory-server/scripts/calculate-metrics.js +++ b/inventory-server/scripts/calculate-metrics.js @@ -7,12 +7,12 @@ require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') }); // Configuration flags for controlling which metrics to calculate // Set to 1 to skip the corresponding calculation, 0 to run it -const SKIP_PRODUCT_METRICS = 1; -const SKIP_TIME_AGGREGATES = 1; -const SKIP_FINANCIAL_METRICS = 1; -const SKIP_VENDOR_METRICS = 1; -const SKIP_CATEGORY_METRICS = 1; -const SKIP_BRAND_METRICS = 1; +const SKIP_PRODUCT_METRICS = 0; +const SKIP_TIME_AGGREGATES = 0; +const SKIP_FINANCIAL_METRICS = 0; +const SKIP_VENDOR_METRICS = 0; +const SKIP_CATEGORY_METRICS = 0; +const SKIP_BRAND_METRICS = 0; const SKIP_SALES_FORECASTS = 0; // Add error handler for uncaught exceptions @@ -193,9 +193,15 @@ async function calculateMetrics() { // Update progress periodically const updateProgress = async (products = null, orders = null, purchaseOrders = null) => { - if (products !== null) processedProducts = products; - if (orders !== null) processedOrders = orders; - if (purchaseOrders !== null) processedPurchaseOrders = purchaseOrders; + // Ensure all values are valid numbers or default to previous value + if (products !== null) processedProducts = Number(products) || processedProducts || 0; + if (orders !== null) processedOrders = Number(orders) || processedOrders || 0; + if (purchaseOrders !== null) processedPurchaseOrders = Number(purchaseOrders) || processedPurchaseOrders || 0; + + // Ensure we never send NaN to the database + const safeProducts = Number(processedProducts) || 0; + const safeOrders = Number(processedOrders) || 0; + const safePurchaseOrders = Number(processedPurchaseOrders) || 0; await connection.query(` UPDATE calculate_history @@ -204,12 +210,40 @@ async function calculateMetrics() { processed_orders = ?, processed_purchase_orders = ? WHERE id = ? - `, [processedProducts, processedOrders, processedPurchaseOrders, calculateHistoryId]); + `, [safeProducts, safeOrders, safePurchaseOrders, calculateHistoryId]); }; + // Helper function to ensure valid progress numbers + const ensureValidProgress = (current, total) => ({ + current: Number(current) || 0, + total: Number(total) || 1, // Default to 1 to avoid division by zero + percentage: (((Number(current) || 0) / (Number(total) || 1)) * 100).toFixed(1) + }); + + // Initial progress + const initialProgress = ensureValidProgress(0, totalProducts); + global.outputProgress({ + status: 'running', + operation: 'Starting metrics calculation', + current: initialProgress.current, + total: initialProgress.total, + elapsed: '0s', + remaining: 'Calculating...', + rate: 0, + percentage: initialProgress.percentage, + timing: { + start_time: new Date(startTime).toISOString(), + end_time: new Date().toISOString(), + elapsed_seconds: Math.round((Date.now() - startTime) / 1000) + } + }); + if (!SKIP_PRODUCT_METRICS) { - processedProducts = await calculateProductMetrics(startTime, totalProducts); - await updateProgress(processedProducts); + const result = await calculateProductMetrics(startTime, totalProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Product metrics calculation failed'); + } } else { console.log('Skipping product metrics calculation...'); processedProducts = Math.floor(totalProducts * 0.6); @@ -233,48 +267,66 @@ async function calculateMetrics() { // Calculate time-based aggregates if (!SKIP_TIME_AGGREGATES) { - processedProducts = await calculateTimeAggregates(startTime, totalProducts, processedProducts); - await updateProgress(processedProducts); + const result = await calculateTimeAggregates(startTime, totalProducts, processedProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Time aggregates calculation failed'); + } } else { console.log('Skipping time aggregates calculation'); } // Calculate financial metrics if (!SKIP_FINANCIAL_METRICS) { - processedProducts = await calculateFinancialMetrics(startTime, totalProducts, processedProducts); - await updateProgress(processedProducts); + const result = await calculateFinancialMetrics(startTime, totalProducts, processedProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Financial metrics calculation failed'); + } } else { console.log('Skipping financial metrics calculation'); } // Calculate vendor metrics if (!SKIP_VENDOR_METRICS) { - processedProducts = await calculateVendorMetrics(startTime, totalProducts, processedProducts); - await updateProgress(processedProducts); + const result = await calculateVendorMetrics(startTime, totalProducts, processedProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Vendor metrics calculation failed'); + } } else { console.log('Skipping vendor metrics calculation'); } // Calculate category metrics if (!SKIP_CATEGORY_METRICS) { - processedProducts = await calculateCategoryMetrics(startTime, totalProducts, processedProducts); - await updateProgress(processedProducts); + const result = await calculateCategoryMetrics(startTime, totalProducts, processedProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Category metrics calculation failed'); + } } else { console.log('Skipping category metrics calculation'); } // Calculate brand metrics if (!SKIP_BRAND_METRICS) { - processedProducts = await calculateBrandMetrics(startTime, totalProducts, processedProducts); - await updateProgress(processedProducts); + const result = await calculateBrandMetrics(startTime, totalProducts, processedProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Brand metrics calculation failed'); + } } else { console.log('Skipping brand metrics calculation'); } // Calculate sales forecasts if (!SKIP_SALES_FORECASTS) { - processedProducts = await calculateSalesForecasts(startTime, totalProducts, processedProducts); - await updateProgress(processedProducts); + const result = await calculateSalesForecasts(startTime, totalProducts, processedProducts); + await updateProgress(result.processedProducts, result.processedOrders, result.processedPurchaseOrders); + if (!result.success) { + throw new Error('Sales forecasts calculation failed'); + } } else { console.log('Skipping sales forecasts calculation'); } @@ -283,12 +335,12 @@ async function calculateMetrics() { outputProgress({ status: 'running', operation: 'Starting ABC classification', - current: processedProducts, - total: totalProducts, + current: processedProducts || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedProducts, totalProducts), - rate: calculateRate(startTime, processedProducts), - percentage: ((processedProducts / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedProducts || 0, totalProducts || 0), + rate: calculateRate(startTime, processedProducts || 0), + percentage: (((processedProducts || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -296,7 +348,12 @@ async function calculateMetrics() { } }); - if (isCancelled) return processedProducts; + if (isCancelled) return { + processedProducts: processedProducts || 0, + processedOrders: processedOrders || 0, + processedPurchaseOrders: 0, + success: false + }; const [abcConfig] = await connection.query('SELECT a_threshold, b_threshold FROM abc_classification_config WHERE id = 1'); const abcThresholds = abcConfig[0] || { a_threshold: 20, b_threshold: 50 }; @@ -317,12 +374,12 @@ async function calculateMetrics() { outputProgress({ status: 'running', operation: 'Creating revenue rankings', - current: processedProducts, - total: totalProducts, + current: processedProducts || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedProducts, totalProducts), - rate: calculateRate(startTime, processedProducts), - percentage: ((processedProducts / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedProducts || 0, totalProducts || 0), + rate: calculateRate(startTime, processedProducts || 0), + percentage: (((processedProducts || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -330,7 +387,12 @@ async function calculateMetrics() { } }); - if (isCancelled) return processedProducts; + if (isCancelled) return { + processedProducts: processedProducts || 0, + processedOrders: processedOrders || 0, + processedPurchaseOrders: 0, + success: false + }; await connection.query(` INSERT INTO temp_revenue_ranks @@ -356,12 +418,12 @@ async function calculateMetrics() { outputProgress({ status: 'running', operation: 'Updating ABC classifications', - current: processedProducts, - total: totalProducts, + current: processedProducts || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedProducts, totalProducts), - rate: calculateRate(startTime, processedProducts), - percentage: ((processedProducts / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedProducts || 0, totalProducts || 0), + rate: calculateRate(startTime, processedProducts || 0), + percentage: (((processedProducts || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -369,14 +431,26 @@ async function calculateMetrics() { } }); - if (isCancelled) return processedProducts; + if (isCancelled) return { + processedProducts: processedProducts || 0, + processedOrders: processedOrders || 0, + processedPurchaseOrders: 0, + success: false + }; - // Process updates in batches - let abcProcessedProducts = 0; + // ABC classification progress tracking + let abcProcessedCount = 0; const batchSize = 5000; + let lastProgressUpdate = Date.now(); + const progressUpdateInterval = 1000; // Update every second while (true) { - if (isCancelled) return processedProducts; + if (isCancelled) return { + processedProducts: Number(processedProducts) || 0, + processedOrders: Number(processedOrders) || 0, + processedPurchaseOrders: 0, + success: false + }; // First get a batch of PIDs that need updating const [pids] = await connection.query(` @@ -417,24 +491,38 @@ async function calculateMetrics() { max_rank, abcThresholds.b_threshold, pids.map(row => row.pid)]); - abcProcessedProducts += result.affectedRows; - processedProducts = Math.floor(totalProducts * (0.99 + (abcProcessedProducts / totalCount) * 0.01)); + abcProcessedCount += result.affectedRows; + + // Calculate progress ensuring valid numbers + const currentProgress = Math.floor(totalProducts * (0.99 + (abcProcessedCount / (totalCount || 1)) * 0.01)); + processedProducts = Number(currentProgress) || processedProducts || 0; - outputProgress({ - status: 'running', - operation: 'ABC classification progress', - current: processedProducts, - total: totalProducts, - elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedProducts, totalProducts), - rate: calculateRate(startTime, processedProducts), - percentage: ((processedProducts / totalProducts) * 100).toFixed(1), - timing: { - start_time: new Date(startTime).toISOString(), - end_time: new Date().toISOString(), - elapsed_seconds: Math.round((Date.now() - startTime) / 1000) - } - }); + // Only update progress at most once per second + const now = Date.now(); + if (now - lastProgressUpdate >= progressUpdateInterval) { + const progress = ensureValidProgress(processedProducts, totalProducts); + + outputProgress({ + status: 'running', + operation: 'ABC classification progress', + current: progress.current, + total: progress.total, + elapsed: formatElapsedTime(startTime), + remaining: estimateRemaining(startTime, progress.current, progress.total), + rate: calculateRate(startTime, progress.current), + percentage: progress.percentage, + timing: { + start_time: new Date(startTime).toISOString(), + end_time: new Date().toISOString(), + elapsed_seconds: Math.round((Date.now() - startTime) / 1000) + } + }); + + lastProgressUpdate = now; + } + + // Update database progress + await updateProgress(processedProducts, processedOrders, processedPurchaseOrders); // Small delay between batches to allow other transactions await new Promise(resolve => setTimeout(resolve, 100)); @@ -446,6 +534,40 @@ async function calculateMetrics() { const endTime = Date.now(); const totalElapsedSeconds = Math.round((endTime - startTime) / 1000); + // Update calculate_status for ABC classification + await connection.query(` + INSERT INTO calculate_status (module_name, last_calculation_timestamp) + VALUES ('abc_classification', NOW()) + ON DUPLICATE KEY UPDATE last_calculation_timestamp = NOW() + `); + + // Final progress update with guaranteed valid numbers + const finalProgress = ensureValidProgress(totalProducts, totalProducts); + + // Final success message + outputProgress({ + status: 'complete', + operation: 'Metrics calculation complete', + current: finalProgress.current, + total: finalProgress.total, + elapsed: formatElapsedTime(startTime), + remaining: '0s', + rate: calculateRate(startTime, finalProgress.current), + percentage: '100', + timing: { + start_time: new Date(startTime).toISOString(), + end_time: new Date().toISOString(), + elapsed_seconds: totalElapsedSeconds + } + }); + + // Ensure all values are valid numbers before final update + const finalStats = { + processedProducts: Number(processedProducts) || 0, + processedOrders: Number(processedOrders) || 0, + processedPurchaseOrders: Number(processedPurchaseOrders) || 0 + }; + // Update history with completion await connection.query(` UPDATE calculate_history @@ -457,24 +579,11 @@ async function calculateMetrics() { processed_purchase_orders = ?, status = 'completed' WHERE id = ? - `, [totalElapsedSeconds, processedProducts, processedOrders, processedPurchaseOrders, calculateHistoryId]); - - // Final success message - outputProgress({ - status: 'complete', - operation: 'Metrics calculation complete', - current: totalProducts, - total: totalProducts, - elapsed: formatElapsedTime(startTime), - remaining: '0s', - rate: calculateRate(startTime, totalProducts), - percentage: '100', - timing: { - start_time: new Date(startTime).toISOString(), - end_time: new Date().toISOString(), - elapsed_seconds: Math.round((Date.now() - startTime) / 1000) - } - }); + `, [totalElapsedSeconds, + finalStats.processedProducts, + finalStats.processedOrders, + finalStats.processedPurchaseOrders, + calculateHistoryId]); // Clear progress file on successful completion global.clearProgress(); diff --git a/inventory-server/scripts/metrics/product-metrics.js b/inventory-server/scripts/metrics/product-metrics.js index 777c546..d1a7cd4 100644 --- a/inventory-server/scripts/metrics/product-metrics.js +++ b/inventory-server/scripts/metrics/product-metrics.js @@ -19,6 +19,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount const SKIP_PRODUCT_BASE_METRICS = 0; const SKIP_PRODUCT_TIME_AGGREGATES = 0; + // Get total product count if not provided + if (!totalProducts) { + const [productCount] = await connection.query('SELECT COUNT(*) as count FROM products'); + totalProducts = productCount[0].count; + } + if (isCancelled) { outputProgress({ status: 'cancelled', @@ -166,12 +172,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount outputProgress({ status: 'running', operation: 'Base product metrics calculated', - current: processedCount, - total: totalProducts, + current: processedCount || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedCount, totalProducts), - rate: calculateRate(startTime, processedCount), - percentage: ((processedCount / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedCount || 0, totalProducts || 0), + rate: calculateRate(startTime, processedCount || 0), + percentage: (((processedCount || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -183,12 +189,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount outputProgress({ status: 'running', operation: 'Skipping base product metrics calculation', - current: processedCount, - total: totalProducts, + current: processedCount || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedCount, totalProducts), - rate: calculateRate(startTime, processedCount), - percentage: ((processedCount / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedCount || 0, totalProducts || 0), + rate: calculateRate(startTime, processedCount || 0), + percentage: (((processedCount || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -198,8 +204,8 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount } if (isCancelled) return { - processedProducts: processedCount, - processedOrders, + processedProducts: processedCount || 0, + processedOrders: processedOrders || 0, processedPurchaseOrders: 0, // This module doesn't process POs success }; @@ -209,12 +215,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount outputProgress({ status: 'running', operation: 'Starting product time aggregates calculation', - current: processedCount, - total: totalProducts, + current: processedCount || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedCount, totalProducts), - rate: calculateRate(startTime, processedCount), - percentage: ((processedCount / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedCount || 0, totalProducts || 0), + rate: calculateRate(startTime, processedCount || 0), + percentage: (((processedCount || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -276,12 +282,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount outputProgress({ status: 'running', operation: 'Product time aggregates calculated', - current: processedCount, - total: totalProducts, + current: processedCount || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedCount, totalProducts), - rate: calculateRate(startTime, processedCount), - percentage: ((processedCount / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedCount || 0, totalProducts || 0), + rate: calculateRate(startTime, processedCount || 0), + percentage: (((processedCount || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -293,12 +299,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount outputProgress({ status: 'running', operation: 'Skipping product time aggregates calculation', - current: processedCount, - total: totalProducts, + current: processedCount || 0, + total: totalProducts || 0, elapsed: formatElapsedTime(startTime), - remaining: estimateRemaining(startTime, processedCount, totalProducts), - rate: calculateRate(startTime, processedCount), - percentage: ((processedCount / totalProducts) * 100).toFixed(1), + remaining: estimateRemaining(startTime, processedCount || 0, totalProducts || 0), + rate: calculateRate(startTime, processedCount || 0), + percentage: (((processedCount || 0) / (totalProducts || 1)) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), @@ -468,8 +474,8 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount `); return { - processedProducts: processedCount, - processedOrders, + processedProducts: processedCount || 0, + processedOrders: processedOrders || 0, processedPurchaseOrders: 0, // This module doesn't process POs success };