Fix initial errors in calculate metrics and get progress working in frontend
This commit is contained in:
@@ -43,15 +43,21 @@ function calculateRate(startTime, current) {
|
|||||||
|
|
||||||
// Helper function to output progress
|
// Helper function to output progress
|
||||||
function outputProgress(data) {
|
function outputProgress(data) {
|
||||||
process.stdout.write(JSON.stringify(data) + '\n');
|
// Format as SSE event
|
||||||
|
const event = {
|
||||||
|
progress: data
|
||||||
|
};
|
||||||
|
process.stdout.write(JSON.stringify(event) + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to log errors
|
// Helper function to log errors
|
||||||
function logError(error, context) {
|
function logError(error, context) {
|
||||||
console.error(JSON.stringify({
|
console.error(JSON.stringify({
|
||||||
status: 'error',
|
progress: {
|
||||||
error: error.message || error,
|
status: 'error',
|
||||||
context
|
error: error.message || error,
|
||||||
|
context
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,15 +77,19 @@ let isCancelled = false;
|
|||||||
|
|
||||||
function cancelCalculation() {
|
function cancelCalculation() {
|
||||||
isCancelled = true;
|
isCancelled = true;
|
||||||
process.stdout.write(JSON.stringify({
|
// Format as SSE event
|
||||||
status: 'cancelled',
|
const event = {
|
||||||
operation: 'Calculation cancelled',
|
progress: {
|
||||||
current: 0,
|
status: 'cancelled',
|
||||||
total: 0,
|
operation: 'Calculation cancelled',
|
||||||
elapsed: null,
|
current: 0,
|
||||||
remaining: null,
|
total: 0,
|
||||||
rate: 0
|
elapsed: null,
|
||||||
}) + '\n');
|
remaining: null,
|
||||||
|
rate: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.stdout.write(JSON.stringify(event) + '\n');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +109,7 @@ async function calculateMetrics() {
|
|||||||
const [countResult] = await connection.query('SELECT COUNT(*) as total FROM products');
|
const [countResult] = await connection.query('SELECT COUNT(*) as total FROM products');
|
||||||
totalProducts = countResult[0].total;
|
totalProducts = countResult[0].total;
|
||||||
|
|
||||||
// Initial progress
|
// Initial progress with percentage
|
||||||
outputProgress({
|
outputProgress({
|
||||||
status: 'running',
|
status: 'running',
|
||||||
operation: 'Processing products',
|
operation: 'Processing products',
|
||||||
@@ -107,7 +117,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts,
|
total: totalProducts,
|
||||||
elapsed: '0s',
|
elapsed: '0s',
|
||||||
remaining: 'Calculating...',
|
remaining: 'Calculating...',
|
||||||
rate: 0
|
rate: 0,
|
||||||
|
percentage: '0'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process in batches of 100
|
// Process in batches of 100
|
||||||
@@ -128,7 +139,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts,
|
total: totalProducts,
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
remaining: estimateRemaining(startTime, processedCount, totalProducts),
|
||||||
rate: calculateRate(startTime, processedCount)
|
rate: calculateRate(startTime, processedCount),
|
||||||
|
percentage: ((processedCount / totalProducts) * 100).toFixed(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process the batch
|
// Process the batch
|
||||||
@@ -175,6 +187,14 @@ async function calculateMetrics() {
|
|||||||
const weekly_sales_avg = metrics.total_quantity_sold ? metrics.total_quantity_sold / 4 : 0;
|
const weekly_sales_avg = metrics.total_quantity_sold ? metrics.total_quantity_sold / 4 : 0;
|
||||||
const monthly_sales_avg = metrics.total_quantity_sold || 0;
|
const monthly_sales_avg = metrics.total_quantity_sold || 0;
|
||||||
|
|
||||||
|
// Calculate margin percent with proper handling of edge cases
|
||||||
|
let margin_percent = 0;
|
||||||
|
if (metrics.total_revenue && metrics.total_revenue > 0) {
|
||||||
|
margin_percent = ((metrics.total_revenue - metrics.total_cost) / metrics.total_revenue) * 100;
|
||||||
|
// Handle -Infinity or Infinity cases
|
||||||
|
margin_percent = isFinite(margin_percent) ? margin_percent : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Update product metrics
|
// Update product metrics
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
INSERT INTO product_metrics (
|
INSERT INTO product_metrics (
|
||||||
@@ -236,7 +256,7 @@ async function calculateMetrics() {
|
|||||||
weekly_sales_avg ? stock.stock_quantity / weekly_sales_avg : null,
|
weekly_sales_avg ? stock.stock_quantity / weekly_sales_avg : null,
|
||||||
Math.ceil(daily_sales_avg * 14), // 14 days reorder point
|
Math.ceil(daily_sales_avg * 14), // 14 days reorder point
|
||||||
Math.ceil(daily_sales_avg * 7), // 7 days safety stock
|
Math.ceil(daily_sales_avg * 7), // 7 days safety stock
|
||||||
metrics.total_revenue ? ((metrics.total_revenue - metrics.total_cost) / metrics.total_revenue) * 100 : 0,
|
margin_percent, // Use the properly handled margin percent
|
||||||
metrics.total_revenue || 0,
|
metrics.total_revenue || 0,
|
||||||
purchases.avg_lead_time_days || 0,
|
purchases.avg_lead_time_days || 0,
|
||||||
purchases.last_purchase_date,
|
purchases.last_purchase_date,
|
||||||
@@ -257,7 +277,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts,
|
total: totalProducts,
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: estimateRemaining(startTime, totalProducts, totalProducts),
|
remaining: estimateRemaining(startTime, totalProducts, totalProducts),
|
||||||
rate: calculateRate(startTime, totalProducts)
|
rate: calculateRate(startTime, totalProducts),
|
||||||
|
percentage: '100'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate ABC classification
|
// Calculate ABC classification
|
||||||
@@ -288,7 +309,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts,
|
total: totalProducts,
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: estimateRemaining(startTime, totalProducts, totalProducts),
|
remaining: estimateRemaining(startTime, totalProducts, totalProducts),
|
||||||
rate: calculateRate(startTime, totalProducts)
|
rate: calculateRate(startTime, totalProducts),
|
||||||
|
percentage: '100'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate time-based aggregates
|
// Calculate time-based aggregates
|
||||||
@@ -386,7 +408,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts,
|
total: totalProducts,
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: estimateRemaining(startTime, totalProducts, totalProducts),
|
remaining: estimateRemaining(startTime, totalProducts, totalProducts),
|
||||||
rate: calculateRate(startTime, totalProducts)
|
rate: calculateRate(startTime, totalProducts),
|
||||||
|
percentage: '100'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate vendor metrics
|
// Calculate vendor metrics
|
||||||
@@ -428,7 +451,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts,
|
total: totalProducts,
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: '0s',
|
remaining: '0s',
|
||||||
rate: calculateRate(startTime, totalProducts)
|
rate: calculateRate(startTime, totalProducts),
|
||||||
|
percentage: '100'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -440,7 +464,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts || 0, // Use 0 if not yet defined
|
total: totalProducts || 0, // Use 0 if not yet defined
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: null,
|
remaining: null,
|
||||||
rate: calculateRate(startTime, processedCount)
|
rate: calculateRate(startTime, processedCount),
|
||||||
|
percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
outputProgress({
|
outputProgress({
|
||||||
@@ -450,7 +475,8 @@ async function calculateMetrics() {
|
|||||||
total: totalProducts || 0, // Use 0 if not yet defined
|
total: totalProducts || 0, // Use 0 if not yet defined
|
||||||
elapsed: formatElapsedTime(startTime),
|
elapsed: formatElapsedTime(startTime),
|
||||||
remaining: null,
|
remaining: null,
|
||||||
rate: calculateRate(startTime, processedCount)
|
rate: calculateRate(startTime, processedCount),
|
||||||
|
percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -357,8 +357,8 @@ router.post('/cancel', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Kill the process
|
// Kill the process with SIGTERM signal
|
||||||
activeImport.kill();
|
activeImport.kill('SIGTERM');
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
activeImport = null;
|
activeImport = null;
|
||||||
@@ -383,6 +383,9 @@ router.post('/cancel', (req, res) => {
|
|||||||
case 'reset':
|
case 'reset':
|
||||||
sendProgressToClients(resetClients, cancelMessage);
|
sendProgressToClients(resetClients, cancelMessage);
|
||||||
break;
|
break;
|
||||||
|
case 'calculate-metrics':
|
||||||
|
sendProgressToClients(calculateMetricsClients, cancelMessage);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
@@ -532,51 +535,98 @@ router.post('/reset-metrics', async (req, res) => {
|
|||||||
// Add calculate-metrics endpoint
|
// Add calculate-metrics endpoint
|
||||||
router.post('/calculate-metrics', async (req, res) => {
|
router.post('/calculate-metrics', async (req, res) => {
|
||||||
if (activeImport) {
|
if (activeImport) {
|
||||||
res.status(400).json({ error: 'Operation already in progress' });
|
return res.status(409).json({ error: 'Import already in progress' });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set active import to prevent concurrent operations
|
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'calculate-metrics.js');
|
||||||
activeImport = {
|
|
||||||
type: 'calculate-metrics',
|
|
||||||
status: 'running',
|
|
||||||
operation: 'Starting metrics calculation'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send initial response
|
if (!require('fs').existsSync(scriptPath)) {
|
||||||
res.status(200).json({ message: 'Metrics calculation started' });
|
return res.status(500).json({ error: 'Calculate metrics script not found' });
|
||||||
|
}
|
||||||
|
|
||||||
// Send initial progress through SSE
|
activeImport = spawn('node', [scriptPath]);
|
||||||
sendProgressToClients(calculateMetricsClients, {
|
let wasCancelled = false;
|
||||||
status: 'running',
|
|
||||||
operation: 'Starting metrics calculation',
|
activeImport.stdout.on('data', (data) => {
|
||||||
percentage: '0'
|
const output = data.toString().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse as JSON
|
||||||
|
const jsonData = JSON.parse(output);
|
||||||
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'running',
|
||||||
|
...jsonData
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// If not JSON, send as plain progress
|
||||||
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'running',
|
||||||
|
progress: output
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run the metrics calculation script
|
activeImport.stderr.on('data', (data) => {
|
||||||
const calculateMetrics = require('../../scripts/calculate-metrics');
|
if (wasCancelled) return; // Don't send errors if cancelled
|
||||||
await calculateMetrics();
|
|
||||||
|
|
||||||
// Send completion through SSE
|
const error = data.toString().trim();
|
||||||
sendProgressToClients(calculateMetricsClients, {
|
try {
|
||||||
status: 'complete',
|
// Try to parse as JSON
|
||||||
operation: 'Metrics calculation completed',
|
const jsonData = JSON.parse(error);
|
||||||
percentage: '100'
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'error',
|
||||||
|
...jsonData
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'error',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
activeImport = null;
|
await new Promise((resolve, reject) => {
|
||||||
|
activeImport.on('close', (code, signal) => {
|
||||||
|
wasCancelled = signal === 'SIGTERM' || code === 143;
|
||||||
|
activeImport = null;
|
||||||
|
importProgress = null;
|
||||||
|
|
||||||
|
if (code === 0 || wasCancelled) {
|
||||||
|
if (wasCancelled) {
|
||||||
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'cancelled',
|
||||||
|
operation: 'Operation cancelled'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'complete',
|
||||||
|
operation: 'Metrics calculation complete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Metrics calculation process exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during metrics calculation:', error);
|
console.error('Error calculating metrics:', error);
|
||||||
|
|
||||||
// Send error through SSE
|
|
||||||
sendProgressToClients(calculateMetricsClients, {
|
|
||||||
status: 'error',
|
|
||||||
error: error.message || 'Failed to calculate metrics'
|
|
||||||
});
|
|
||||||
|
|
||||||
activeImport = null;
|
activeImport = null;
|
||||||
res.status(500).json({ error: error.message || 'Failed to calculate metrics' });
|
importProgress = null;
|
||||||
|
|
||||||
|
// Only send error if it wasn't a cancellation
|
||||||
|
if (!error.message?.includes('code 143') && !error.message?.includes('SIGTERM')) {
|
||||||
|
sendProgressToClients(calculateMetricsClients, {
|
||||||
|
status: 'error',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'Failed to calculate metrics', details: error.message });
|
||||||
|
} else {
|
||||||
|
res.json({ success: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -254,8 +254,8 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle completion
|
// Handle completion
|
||||||
if (progressData.status === 'complete') {
|
if (progressData.status === 'complete' || progressData.status === 'cancelled') {
|
||||||
console.log(`Operation ${type} completed`);
|
console.log(`Operation ${type} completed or cancelled`);
|
||||||
|
|
||||||
// For import, only close connection when both operations are complete
|
// For import, only close connection when both operations are complete
|
||||||
if (type === 'import') {
|
if (type === 'import') {
|
||||||
@@ -433,11 +433,14 @@ export function Settings() {
|
|||||||
}, [eventSource]);
|
}, [eventSource]);
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
// Determine which operation is running first
|
// Determine which operation is running
|
||||||
const operation = isImporting ? 'Import' : isUpdating ? 'Update' : 'Reset';
|
const operation = isImporting ? 'import' :
|
||||||
|
isUpdating ? 'update' :
|
||||||
|
isResetting ? 'reset' :
|
||||||
|
isCalculatingMetrics ? 'calculate-metrics' : 'reset';
|
||||||
|
|
||||||
// Show cancellation toast immediately
|
// Show cancellation toast immediately
|
||||||
toast.warning(`${operation} cancelled`);
|
toast.warning(`${operation.charAt(0).toUpperCase() + operation.slice(1)} cancelled`);
|
||||||
|
|
||||||
// Clean up everything immediately
|
// Clean up everything immediately
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
@@ -447,13 +450,15 @@ export function Settings() {
|
|||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
setIsResetting(false);
|
setIsResetting(false);
|
||||||
|
setIsCalculatingMetrics(false);
|
||||||
setUpdateProgress(null);
|
setUpdateProgress(null);
|
||||||
setImportProgress(null);
|
setImportProgress(null);
|
||||||
setResetProgress(null);
|
setResetProgress(null);
|
||||||
|
setMetricsProgress(null);
|
||||||
|
|
||||||
// Fire and forget the cancel request with the operation type
|
// Fire and forget the cancel request with the operation type
|
||||||
try {
|
try {
|
||||||
await fetch(`${config.apiUrl}/csv/cancel?operation=${operation.toLowerCase()}`, {
|
await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user