diff --git a/.gitignore b/.gitignore index da1663e..2b9c0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ dashboard-server/meta-server/._utils dashboard-server/meta-server/._routes dashboard-server/meta-server/._package-lock.json dashboard-server/meta-server/._services +*.tsbuildinfo # CSV data files *.csv @@ -54,4 +55,5 @@ csv/* csv/**/* **/csv/* **/csv/**/* -!csv/.gitkeep \ No newline at end of file +!csv/.gitkeep +inventory/tsconfig.tsbuildinfo diff --git a/inventory-server/scripts/reset-db.js b/inventory-server/scripts/reset-db.js index de044e6..abe285b 100644 --- a/inventory-server/scripts/reset-db.js +++ b/inventory-server/scripts/reset-db.js @@ -155,6 +155,7 @@ async function resetDatabase() { SELECT GROUP_CONCAT(table_name) as tables FROM information_schema.tables WHERE table_schema = DATABASE() + AND table_name != 'users' `); if (!tables[0].tables) { @@ -173,6 +174,7 @@ async function resetDatabase() { DROP TABLE IF EXISTS ${tables[0].tables .split(',') + .filter(table => table !== 'users') .map(table => '`' + table + '`') .join(', ')} `; diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index cd378a0..9ab9209 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -18,7 +18,7 @@ import config from '../../config'; import { toast } from "sonner"; interface ImportProgress { - status: 'running' | 'error' | 'complete'; + status: 'running' | 'error' | 'complete' | 'cancelled'; operation?: string; current?: number; total?: number; @@ -61,76 +61,159 @@ export function DataManagement() { const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false); const [metricsProgress, setMetricsProgress] = useState(null); + // Add states for completed operations + const [lastUpdateStatus, setLastUpdateStatus] = useState(null); + const [lastImportStatus, setLastImportStatus] = useState(null); + const [lastResetStatus, setLastResetStatus] = useState(null); + const [lastMetricsStatus, setLastMetricsStatus] = useState(null); + const [lastResetMetricsStatus, setLastResetMetricsStatus] = useState(null); + + // Track cancellation state + const [cancelledOperations, setCancelledOperations] = useState>(new Set()); + // Helper to check if any operation is running const isAnyOperationRunning = () => { return isUpdating || isImporting || isResetting || isResettingMetrics || isCalculatingMetrics; }; - // Check status on mount - useEffect(() => { - const checkStatus = async () => { - try { - // Check calculate-metrics status first - const metricsResponse = await fetch(`${config.apiUrl}/csv/calculate-metrics/status`, { - credentials: 'include' - }); - const metricsData = await metricsResponse.json(); - - if (metricsData.active && metricsData.progress) { - setIsCalculatingMetrics(true); - setMetricsProgress(metricsData.progress); - connectToEventSource('calculate-metrics'); - return; - } else { - setIsCalculatingMetrics(false); - setMetricsProgress(null); - } + // Helper function to get progress bar color based on status + const getProgressBarColor = (status?: string) => { + switch (status?.toLowerCase()) { + case 'complete': + return 'bg-green-500'; + case 'error': + return 'bg-red-500'; + case 'cancelled': + return 'bg-yellow-500'; + default: + return 'bg-gray-500'; + } + }; - // Check other operations - const response = await fetch(`${config.apiUrl}/csv/status`, { - credentials: 'include' - }); - const data = await response.json(); - - if (data.active && data.progress) { - if (data.progress?.operation?.toLowerCase().includes('import')) { - setIsImporting(true); - setImportProgress(data.progress); - connectToEventSource('import'); - } else if (data.progress?.operation?.toLowerCase().includes('update')) { - setIsUpdating(true); - setUpdateProgress(data.progress); - connectToEventSource('update'); - } else if (data.progress?.operation?.toLowerCase().includes('reset')) { - setIsResetting(true); - setResetProgress(data.progress); - connectToEventSource('reset'); - } - } else { - // Reset all states if no active process - setIsImporting(false); - setIsUpdating(false); - setIsResetting(false); - setImportProgress(null); - setUpdateProgress(null); - setResetProgress(null); - } - } catch (error) { - console.error('Error checking status:', error); - // Reset all states on error - setIsCalculatingMetrics(false); - setIsImporting(false); - setIsUpdating(false); - setIsResetting(false); - setMetricsProgress(null); - setImportProgress(null); - setUpdateProgress(null); - setResetProgress(null); - } - }; + // Helper function to format elapsed time + const formatElapsedTime = (elapsed?: string | number) => { + if (!elapsed) return ''; + + // If it's already a formatted string, return it + if (typeof elapsed === 'string') return elapsed; + + // Convert seconds to a readable format + const seconds = typeof elapsed === 'number' ? elapsed : parseInt(elapsed); + if (isNaN(seconds)) return ''; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + const parts = []; + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`); + + return parts.join(' '); + }; - checkStatus(); - }, []); + // Helper function to get status text based on status + const getStatusText = (status?: string) => { + switch (status?.toLowerCase()) { + case 'complete': + return 'Completed'; + case 'error': + return 'Failed'; + case 'cancelled': + return 'Cancelled'; + default: + return 'Running'; + } + }; + + // Helper function to render progress + const renderProgress = (progress: any) => { + if (!progress) return null; + + const status = progress.status?.toLowerCase(); + const isComplete = status === 'complete'; + const isError = status === 'error'; + const isCancelled = status === 'cancelled'; + const isFinished = isComplete || isError || isCancelled; + + // For finished states, show 100% if we were tracking progress + const current = isFinished && progress.total ? progress.total : (progress.current || 0); + const total = progress.total || 100; + const percent = isFinished && progress.total ? 100 : Math.min(100, Math.round((current / total) * 100)) || 0; + + const barColor = getProgressBarColor(status); + const statusText = getStatusText(status); + const elapsedTime = formatElapsedTime(progress.elapsed); + + return ( +
+
+ + {progress.operation || progress.message || statusText} + + + {isFinished ? statusText : `${percent}%`} + +
+
+
+
+ {/* Additional details */} +
+ {/* Progress details (only show during active progress) */} + {!isFinished && progress.current && progress.total && ( +
+ Progress: + + {progress.current.toLocaleString()} / {progress.total.toLocaleString()} + {progress.rate ? ` (${Math.round(progress.rate)}/s)` : ''} + +
+ )} + + {/* Time information (show elapsed time even after completion) */} + {elapsedTime && ( +
+ Time: + + {isFinished ? + isCancelled ? `Ran for ${elapsedTime}` : + isComplete ? `Completed in ${elapsedTime}` : + `Failed after ${elapsedTime}` : + `${elapsedTime} elapsed${progress.remaining ? ` - ${progress.remaining} remaining` : ''}` + } + +
+ )} + + {/* Results summary */} + {(progress.added !== undefined || progress.updated !== undefined || progress.skipped !== undefined) && ( +
+ Results: + + {progress.added !== undefined && `${progress.added.toLocaleString()} added`} + {progress.added !== undefined && progress.updated !== undefined && ', '} + {progress.updated !== undefined && `${progress.updated.toLocaleString()} updated`} + {((progress.added !== undefined || progress.updated !== undefined) && progress.skipped !== undefined) && ', '} + {progress.skipped !== undefined && `${progress.skipped.toLocaleString()} skipped`} + +
+ )} + + {/* Error message */} + {isError && progress.error && !progress.error.includes('cancelled') && !cancelledOperations.has(type) && ( +
+ {progress.error} +
+ )} +
+
+ ); + }; // Helper to connect to event source const connectToEventSource = (type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => { @@ -162,35 +245,37 @@ export function DataManagement() { source.onerror = async (event) => { console.error('EventSource error:', event); - source.close(); - const isActive = type === 'import' ? isImporting : - type === 'update' ? isUpdating : - type === 'reset' ? isResetting : - type === 'reset-metrics' ? isResettingMetrics : - type === 'calculate-metrics' ? isCalculatingMetrics : false; - - if (retryCount < MAX_RETRIES && isActive) { + // Don't close the source on error unless we're at max retries + if (retryCount >= MAX_RETRIES) { + source.close(); + setEventSource(null); + console.warn(`Lost connection to ${type} progress stream after ${MAX_RETRIES} retries`); + + // Try to reconnect via status check if the operation might still be running + if ( + (type === 'calculate-metrics' && isCalculatingMetrics) || + (type === 'import' && isImporting) || + (type === 'update' && isUpdating) || + (type === 'reset' && isResetting) || + (type === 'reset-metrics' && isResettingMetrics) + ) { + console.log('Operation may still be running, will attempt to reconnect via status check'); + } + } else { console.log(`Retrying connection (${retryCount + 1}/${MAX_RETRIES})...`); retryCount++; await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); - setupConnection(); - } else if (retryCount >= MAX_RETRIES) { - console.log('Max retries exceeded, but operation may still be running...'); - console.warn(`Lost connection to ${type} progress stream after ${MAX_RETRIES} retries`); } - - setEventSource(null); }; source.onmessage = (event) => { try { - console.log(`Received message for ${type}:`, event.data); const data = JSON.parse(event.data); + console.log(`Received message for ${type}:`, data); handleProgressUpdate(type, data.progress || data, source); } catch (error) { console.error('Error parsing event data:', error, event.data); - console.warn('Failed to parse server response:', error); } }; @@ -200,8 +285,6 @@ export function DataManagement() { console.log(`Retrying connection (${retryCount + 1}/${MAX_RETRIES})...`); retryCount++; setTimeout(setupConnection, RETRY_DELAY); - } else { - console.log('Max retries exceeded, but operation may still be running...'); } } }; @@ -214,6 +297,12 @@ export function DataManagement() { progressData: any, source: EventSource ) => { + // If this operation was cancelled, ignore error events and convert them to cancelled status + if (cancelledOperations.has(type) && progressData.status === 'error') { + console.log('Converting error event to cancelled for operation:', type); + progressData = { ...progressData, status: 'cancelled' }; + } + const processedData = { ...progressData, message: progressData.message && typeof progressData.message === 'object' @@ -222,157 +311,267 @@ export function DataManagement() { status: progressData.status || 'running' }; + // Special handling for import operations if (type === 'import' && progressData.operation) { const operation = progressData.operation.toLowerCase(); - if (operation.includes('purchase orders')) { - setPurchaseOrdersProgress(prev => ({ - ...prev, - ...processedData - })); - return; - } - } - - switch (type) { - case 'update': - setUpdateProgress(prev => ({ - ...prev, - ...processedData - })); - break; - case 'reset': - setResetProgress(prev => ({ - ...prev, - ...processedData - })); - break; - case 'reset-metrics': - setResetMetricsProgress(prev => ({ - ...prev, - ...processedData - })); - break; - case 'calculate-metrics': - setMetricsProgress(prev => ({ - ...prev, - ...processedData - })); - break; - default: - setImportProgress(prev => ({ - ...prev, - ...processedData - })); - } - - if (progressData.status === 'complete' || progressData.status === 'cancelled') { - console.log(`Operation ${type} completed or cancelled`); - if (type === 'import') { - const operation = progressData.operation?.toLowerCase() || ''; + // Handle purchase orders progress separately + if (operation.includes('purchase orders')) { + setPurchaseOrdersProgress(processedData); + } else { + setImportProgress(processedData); + } + + // Handle completion states + if (progressData.status === 'complete' || progressData.status === 'error' || progressData.status === 'cancelled') { if (operation.includes('purchase orders')) { - setPurchaseOrdersProgress(null); + // Save the final status for purchase orders + setLastImportStatus({ + ...processedData, + operation: 'Purchase Orders Import' + }); } else { - setImportProgress(null); + // Save the final status for main import + setLastImportStatus(processedData); } - - if (!importProgress || !purchaseOrdersProgress) { + + // Only close connection if both operations are done + const otherProgress = operation.includes('purchase orders') ? importProgress : purchaseOrdersProgress; + if (!otherProgress || otherProgress.status === 'complete' || otherProgress.status === 'error' || otherProgress.status === 'cancelled') { source.close(); setEventSource(null); setIsImporting(false); - if (!progressData.operation?.includes('cancelled')) { - toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} completed successfully`); + + // Show appropriate toast based on final status + if (progressData.status === 'complete') { + toast.success('Import completed successfully'); + } else if (progressData.status === 'cancelled') { + toast.warning('Import cancelled'); + } else if (progressData.status === 'error' && !cancelledOperations.has(type)) { + toast.error(`Import failed: ${progressData.error || 'Unknown error'}`); + } + + // Clear cancelled state after completion + if (cancelledOperations.has(type)) { + setCancelledOperations(prev => { + const next = new Set(prev); + next.delete(type); + return next; + }); } } - return; } - + return; + } + + // Handle other operation types + let setProgress; + let setLastStatus; + let setIsRunning; + let operationName; + + switch (type) { + case 'update': + setProgress = setUpdateProgress; + setLastStatus = setLastUpdateStatus; + setIsRunning = setIsUpdating; + operationName = 'Update'; + break; + case 'reset': + setProgress = setResetProgress; + setLastStatus = setLastResetStatus; + setIsRunning = setIsResetting; + operationName = 'Reset'; + break; + case 'reset-metrics': + setProgress = setResetMetricsProgress; + setLastStatus = setLastResetMetricsStatus; + setIsRunning = setIsResettingMetrics; + operationName = 'Reset metrics'; + break; + case 'calculate-metrics': + setProgress = setMetricsProgress; + setLastStatus = setLastMetricsStatus; + setIsRunning = setIsCalculatingMetrics; + operationName = 'Calculate metrics'; + break; + default: + return; + } + + // Update the progress + setProgress(processedData); + + // Handle completion + if (progressData.status === 'complete' || progressData.status === 'error' || progressData.status === 'cancelled') { source.close(); setEventSource(null); + setIsRunning(false); - switch (type) { - case 'update': - setIsUpdating(false); - setUpdateProgress(null); - break; - case 'reset': - setIsResetting(false); - setResetProgress(null); - break; - case 'reset-metrics': - setIsResettingMetrics(false); - setResetMetricsProgress(null); - break; - case 'calculate-metrics': - setIsCalculatingMetrics(false); - setMetricsProgress(null); - break; + // Save the final status with elapsed time + setLastStatus(processedData); + setProgress(null); + + // Show appropriate toast based on final status + if (progressData.status === 'complete') { + toast.success(`${operationName} completed successfully`); + } else if (progressData.status === 'cancelled') { + toast.warning(`${operationName} cancelled`); + } else if (progressData.status === 'error' && !cancelledOperations.has(type)) { + toast.error(`${operationName} failed: ${progressData.error || 'Unknown error'}`); } - if (!progressData.operation?.includes('cancelled')) { - toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} completed successfully`); + // Clear cancelled state after completion + if (cancelledOperations.has(type)) { + setCancelledOperations(prev => { + const next = new Set(prev); + next.delete(type); + return next; + }); } - } - else if (progressData.status === 'error') { - console.error(`Operation ${type} failed:`, progressData.error); - source.close(); - setEventSource(null); - - switch (type) { - case 'update': - setIsUpdating(false); - break; - case 'import': - setIsImporting(false); - setImportProgress(null); - setPurchaseOrdersProgress(null); - break; - case 'reset': - setIsResetting(false); - break; - case 'reset-metrics': - setIsResettingMetrics(false); - break; - case 'calculate-metrics': - setIsCalculatingMetrics(false); - break; - } - - toast.error(`${type.charAt(0).toUpperCase() + type.slice(1)} failed: ${progressData.error || 'Unknown error'}`); } }; - const handleCancel = async () => { - const operation = isImporting ? 'import' : - isUpdating ? 'update' : - isResetting ? 'reset' : - isCalculatingMetrics ? 'calculate-metrics' : 'reset'; - - toast.warning(`${operation.charAt(0).toUpperCase() + operation.slice(1)} cancelled`); - - if (eventSource) { - eventSource.close(); - setEventSource(null); - } - setIsUpdating(false); - setIsImporting(false); - setIsResetting(false); - setIsCalculatingMetrics(false); - setUpdateProgress(null); - setImportProgress(null); - setResetProgress(null); - setMetricsProgress(null); - + const handleCancel = async (type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => { try { - await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, { + // Mark this operation as cancelled + setCancelledOperations(prev => new Set(prev).add(type)); + + // First close any existing event source + if (eventSource) { + eventSource.close(); + setEventSource(null); + } + + // Send cancel request with the correct endpoint format + const response = await fetch(`${config.apiUrl}/csv/cancel?operation=${type}`, { method: 'POST', credentials: 'include' }); + + // Set cancelled state immediately + switch (type) { + case 'import': + setLastImportStatus({ ...importProgress, status: 'cancelled' }); + setImportProgress(null); + setIsImporting(false); + break; + case 'update': + setLastUpdateStatus({ ...updateProgress, status: 'cancelled' }); + setUpdateProgress(null); + setIsUpdating(false); + break; + case 'reset': + setLastResetStatus({ ...resetProgress, status: 'cancelled' }); + setResetProgress(null); + setIsResetting(false); + break; + case 'reset-metrics': + setLastResetMetricsStatus({ ...resetMetricsProgress, status: 'cancelled' }); + setResetMetricsProgress(null); + setIsResettingMetrics(false); + break; + case 'calculate-metrics': + setLastMetricsStatus({ ...metricsProgress, status: 'cancelled' }); + setMetricsProgress(null); + setIsCalculatingMetrics(false); + break; + } + + toast.warning(`${type.charAt(0).toUpperCase() + type.slice(1).replace('-', ' ')} cancelled`); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + console.error(`Failed to cancel ${type}:`, data.error || 'Unknown error'); + } } catch (error) { - console.log('Cancel request failed:', error); + console.error(`Error cancelling ${type}:`, error); } }; + // Check status on mount and periodically + useEffect(() => { + const checkStatus = async () => { + try { + // Check all operations in parallel + const [metricsResponse, importResponse] = await Promise.all([ + fetch(`${config.apiUrl}/csv/calculate-metrics/status`, { + credentials: 'include' + }).catch(() => null), + fetch(`${config.apiUrl}/csv/status`, { + credentials: 'include' + }).catch(() => null) + ]); + + // Handle metrics status + if (metricsResponse?.ok) { + const metricsData = await metricsResponse.json().catch(() => null); + if (metricsData?.active) { + setIsCalculatingMetrics(true); + setMetricsProgress(metricsData.progress); + connectToEventSource('calculate-metrics'); + } else if (metricsData?.lastStatus) { + setLastMetricsStatus(metricsData.lastStatus); + } + } + + // Handle import/update/reset status + if (importResponse?.ok) { + const importData = await importResponse.json().catch(() => null); + if (importData?.active) { + const operation = importData.progress?.operation?.toLowerCase() || ''; + + if (operation.includes('import')) { + setIsImporting(true); + if (operation.includes('purchase orders')) { + setPurchaseOrdersProgress(importData.progress); + } else { + setImportProgress(importData.progress); + } + connectToEventSource('import'); + } else if (operation.includes('update')) { + setIsUpdating(true); + setUpdateProgress(importData.progress); + connectToEventSource('update'); + } else if (operation.includes('reset')) { + if (operation.includes('metrics')) { + setIsResettingMetrics(true); + setResetMetricsProgress(importData.progress); + connectToEventSource('reset-metrics'); + } else { + setIsResetting(true); + setResetProgress(importData.progress); + connectToEventSource('reset'); + } + } + } else if (importData?.lastStatus) { + // Handle last status based on operation type + const operation = importData.lastStatus?.operation?.toLowerCase() || ''; + if (operation.includes('import')) { + setLastImportStatus(importData.lastStatus); + } else if (operation.includes('update')) { + setLastUpdateStatus(importData.lastStatus); + } else if (operation.includes('reset')) { + if (operation.includes('metrics')) { + setLastResetMetricsStatus(importData.lastStatus); + } else { + setLastResetStatus(importData.lastStatus); + } + } + } + } + } catch (error) { + console.error('Error checking status:', error); + } + }; + + // Check status immediately and then every 5 seconds + checkStatus(); + const interval = setInterval(checkStatus, 5000); + + return () => clearInterval(interval); + }, []); + const handleUpdateCSV = async () => { setIsUpdating(true); setUpdateProgress({ status: 'running', operation: 'Starting CSV update' }); @@ -410,6 +609,20 @@ export function DataManagement() { try { connectToEventSource('import'); + // First check if import is already running + const statusResponse = await fetch(`${config.apiUrl}/csv/status`, { + credentials: 'include' + }).catch(() => null); + + if (statusResponse) { + const statusData = await statusResponse.json().catch(() => null); + if (statusData?.active && statusData?.progress) { + console.log('Import already running, connecting to existing process'); + return; + } + } + + // Start new import const response = await fetch(`${config.apiUrl}/csv/import`, { method: 'POST', headers: { @@ -418,30 +631,34 @@ export function DataManagement() { credentials: 'include', body: JSON.stringify(limits) }).catch(error => { - if ((importProgress?.current || purchaseOrdersProgress?.current) && - (error.name === 'TypeError')) { - console.log('Request error but import is in progress:', error); - return null; - } - throw error; + // Ignore network errors as the import might still be running + console.log('Import request error (may be timeout):', error); + return null; }); - if (!response) { - console.log('Continuing with existing progress...'); + // If we got no response but have progress, assume it's still running + if (!response && (importProgress?.current || purchaseOrdersProgress?.current)) { + console.log('No response but import appears to be running, continuing...'); return; } - if (!importProgress?.current && !purchaseOrdersProgress?.current) { - if (!response.ok && response.status !== 200) { - const data = await response.json().catch(() => ({})); + // If we got a response, check if it indicates an actual error + if (response) { + const data = await response.json().catch(() => null); + if (!response.ok && data?.error && !data.error.includes('already in progress')) { throw new Error(data.error || 'Failed to start CSV import'); } - } else { - console.log('Response not ok but import is in progress, continuing...'); } } catch (error) { - toast.error(`CSV import failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - setIsImporting(false); + // Only handle actual errors, not timeouts or connection issues + if (error instanceof Error && !error.message.includes('NetworkError') && !error.message.includes('Failed to fetch')) { + toast.error(`CSV import failed: ${error.message}`); + setIsImporting(false); + setImportProgress(null); + setPurchaseOrdersProgress(null); + } else { + console.log('Ignoring network error, import may still be running:', error); + } } }; @@ -485,23 +702,52 @@ export function DataManagement() { try { connectToEventSource('reset-metrics'); + // First check if reset is already running + const statusResponse = await fetch(`${config.apiUrl}/csv/reset-metrics/status`, { + credentials: 'include' + }).catch(() => null); + + if (statusResponse) { + const statusData = await statusResponse.json().catch(() => null); + if (statusData?.active && statusData?.progress) { + console.log('Metrics reset already running, connecting to existing process'); + setResetMetricsProgress(statusData.progress); + return; + } + } + + // Start new reset const response = await fetch(`${config.apiUrl}/csv/reset-metrics`, { method: 'POST', credentials: 'include' + }).catch(error => { + // Ignore network errors as the reset might still be running + console.log('Metrics reset request error (may be timeout):', error); + return null; }); - if (!response.ok) { - const data = await response.json().catch(() => ({})); - throw new Error(data.error || 'Failed to reset metrics'); + // If we got no response but have progress, assume it's still running + if (!response && resetMetricsProgress?.current) { + console.log('No response but metrics reset appears to be running, continuing...'); + return; + } + + // If we got a response, check if it indicates an actual error + if (response) { + const data = await response.json().catch(() => null); + if (!response.ok && data?.error && !data.error.includes('already in progress')) { + throw new Error(data.error || 'Failed to reset metrics'); + } } } catch (error) { - if (eventSource) { - eventSource.close(); - setEventSource(null); + // Only handle actual errors, not timeouts or connection issues + if (error instanceof Error && !error.message.includes('NetworkError') && !error.message.includes('Failed to fetch')) { + toast.error(`Metrics reset failed: ${error.message}`); + setIsResettingMetrics(false); + setResetMetricsProgress(null); + } else { + console.log('Ignoring network error, metrics reset may still be running:', error); } - setIsResettingMetrics(false); - setResetMetricsProgress(null); - toast.error(`Metrics reset failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; @@ -512,93 +758,55 @@ export function DataManagement() { try { connectToEventSource('calculate-metrics'); + // First check if metrics calculation is already running + const statusResponse = await fetch(`${config.apiUrl}/csv/calculate-metrics/status`, { + credentials: 'include' + }).catch(() => null); + + if (statusResponse) { + const statusData = await statusResponse.json().catch(() => null); + if (statusData?.active && statusData?.progress) { + console.log('Metrics calculation already running, connecting to existing process'); + setMetricsProgress(statusData.progress); + return; + } + } + + // Start new metrics calculation const response = await fetch(`${config.apiUrl}/csv/calculate-metrics`, { method: 'POST', credentials: 'include' + }).catch(error => { + // Ignore network errors as the calculation might still be running + console.log('Metrics calculation request error (may be timeout):', error); + return null; }); - if (!response.ok) { - const data = await response.json().catch(() => ({})); - throw new Error(data.error || 'Failed to calculate metrics'); + // If we got no response but have progress, assume it's still running + if (!response && metricsProgress?.current) { + console.log('No response but metrics calculation appears to be running, continuing...'); + return; + } + + // If we got a response, check if it indicates an actual error + if (response) { + const data = await response.json().catch(() => null); + if (!response.ok && data?.error && !data.error.includes('already in progress')) { + throw new Error(data.error || 'Failed to calculate metrics'); + } } } catch (error) { - if (eventSource) { - eventSource.close(); - setEventSource(null); + // Only handle actual errors, not timeouts or connection issues + if (error instanceof Error && !error.message.includes('NetworkError') && !error.message.includes('Failed to fetch')) { + toast.error(`Metrics calculation failed: ${error.message}`); + setIsCalculatingMetrics(false); + setMetricsProgress(null); + } else { + console.log('Ignoring network error, metrics calculation may still be running:', error); } - setIsCalculatingMetrics(false); - setMetricsProgress(null); - toast.error(`Metrics calculation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; - const renderProgress = (progress: ImportProgress | null) => { - if (!progress) return null; - - let percentage = progress.percentage ? parseFloat(progress.percentage) : - (progress.current && progress.total) ? (progress.current / progress.total) * 100 : null; - - const message = progress.message ? - (typeof progress.message === 'object' ? - JSON.stringify(progress.message, null, 2) : - progress.message - ) : null; - - return ( -
-
- {progress.operation || 'Processing...'} - {percentage !== null && {Math.round(percentage)}%} -
- {percentage !== null && ( - <> - -
- {progress.current && progress.total && ( -
- Progress: - {progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''} -
- )} - {(progress.elapsed || progress.remaining) && ( -
- Time: - - {progress.elapsed && `${progress.elapsed} elapsed`} - {progress.elapsed && progress.remaining && ' - '} - {progress.remaining && `${progress.remaining} remaining`} - -
- )} - {(progress.added !== undefined || progress.updated !== undefined || progress.skipped !== undefined) && ( -
- Results: - - {progress.added !== undefined && `${progress.added.toLocaleString()} added`} - {progress.added !== undefined && progress.updated !== undefined && ', '} - {progress.updated !== undefined && `${progress.updated.toLocaleString()} updated`} - {((progress.added !== undefined || progress.updated !== undefined) && progress.skipped !== undefined) && ', '} - {progress.skipped !== undefined && `${progress.skipped.toLocaleString()} skipped`} - -
- )} - {message && ( -
- {message} -
- )} -
- - )} - {!percentage && message && ( -
- {message} -
- )} -
- ); - }; - return (
{/* Update CSV Card */} @@ -630,14 +838,14 @@ export function DataManagement() { {isUpdating && ( )}
- {isUpdating && renderProgress(updateProgress)} + {(isUpdating || lastUpdateStatus) && renderProgress(updateProgress || lastUpdateStatus)} @@ -648,7 +856,7 @@ export function DataManagement() { Import current CSV files into database -
+
)}
- {isImporting && ( + {(isImporting || lastImportStatus) && (
- {importProgress && renderProgress(importProgress)} - {purchaseOrdersProgress && renderProgress(purchaseOrdersProgress)} + {renderProgress(importProgress || lastImportStatus)} + {renderProgress(purchaseOrdersProgress)}
)} @@ -715,14 +923,14 @@ export function DataManagement() { {isCalculatingMetrics && ( )}
- {metricsProgress && renderProgress(metricsProgress)} + {(isCalculatingMetrics || lastMetricsStatus) && renderProgress(metricsProgress || lastMetricsStatus)}
@@ -790,21 +998,15 @@ export function DataManagement() {
- {resetProgress && ( + {(resetProgress || lastResetStatus) && (
- -

- {resetProgress.message || 'Resetting database...'} -

+ {renderProgress(resetProgress || lastResetStatus)}
)} - {resetMetricsProgress && ( + {(resetMetricsProgress || lastResetMetricsStatus) && (
- -

- {resetMetricsProgress.message || 'Resetting metrics...'} -

+ {renderProgress(resetMetricsProgress || lastResetMetricsStatus)}
)} diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 0c39092..02134cd 100644 --- a/inventory/tsconfig.tsbuildinfo +++ b/inventory/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/dashboard/inventoryhealthsummary.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/keymetricscharts.tsx","./src/components/dashboard/lowstockalerts.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/dashboard/vendorperformance.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/productdetail.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/dashboard/inventoryhealthsummary.tsx","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/keymetricscharts.tsx","./src/components/dashboard/lowstockalerts.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/dashboard/vendorperformance.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/stockmanagement.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/dashboard.tsx","./src/pages/login.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx"],"version":"5.6.3"} \ No newline at end of file