diff --git a/inventory-server/src/routes/csv.js b/inventory-server/src/routes/csv.js index 013bda9..7adbb87 100644 --- a/inventory-server/src/routes/csv.js +++ b/inventory-server/src/routes/csv.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const { spawn } = require('child_process'); const path = require('path'); +const db = require('../utils/db'); // Debug middleware MUST be first router.use((req, res, next) => { @@ -9,9 +10,11 @@ router.use((req, res, next) => { next(); }); -// Store active import process and its progress +// Store active processes and their progress let activeImport = null; let importProgress = null; +let activeFullUpdate = null; +let activeFullReset = null; // SSE clients for progress updates const updateClients = new Set(); @@ -19,17 +22,16 @@ const importClients = new Set(); const resetClients = new Set(); const resetMetricsClients = new Set(); const calculateMetricsClients = new Set(); +const fullUpdateClients = new Set(); +const fullResetClients = new Set(); // Helper to send progress to specific clients -function sendProgressToClients(clients, progress) { - const data = typeof progress === 'string' ? { progress } : progress; - - // Ensure we have a status field - if (!data.status) { - data.status = 'running'; - } - - const message = `data: ${JSON.stringify(data)}\n\n`; +function sendProgressToClients(clients, data) { + // If data is a string, send it directly + // If it's an object, convert it to JSON + const message = typeof data === 'string' + ? `data: ${data}\n\n` + : `data: ${JSON.stringify(data)}\n\n`; clients.forEach(client => { try { @@ -45,115 +47,118 @@ function sendProgressToClients(clients, progress) { }); } +// Helper to run a script and stream progress +function runScript(scriptPath, type, clients) { + return new Promise((resolve, reject) => { + // Kill any existing process of this type + let activeProcess; + switch (type) { + case 'update': + if (activeFullUpdate) { + try { activeFullUpdate.kill(); } catch (e) { } + } + activeProcess = activeFullUpdate; + break; + case 'reset': + if (activeFullReset) { + try { activeFullReset.kill(); } catch (e) { } + } + activeProcess = activeFullReset; + break; + } + + const child = spawn('node', [scriptPath], { + stdio: ['inherit', 'pipe', 'pipe'] + }); + + switch (type) { + case 'update': + activeFullUpdate = child; + break; + case 'reset': + activeFullReset = child; + break; + } + + let output = ''; + + child.stdout.on('data', (data) => { + const text = data.toString(); + output += text; + // Send raw output directly + sendProgressToClients(clients, text); + }); + + child.stderr.on('data', (data) => { + const text = data.toString(); + console.error(text); + // Send stderr output directly too + sendProgressToClients(clients, text); + }); + + child.on('close', (code) => { + switch (type) { + case 'update': + activeFullUpdate = null; + break; + case 'reset': + activeFullReset = null; + break; + } + + if (code !== 0) { + const error = `Script ${scriptPath} exited with code ${code}`; + sendProgressToClients(clients, error); + reject(new Error(error)); + } else { + sendProgressToClients(clients, `${type} completed successfully`); + resolve({ output }); + } + }); + + child.on('error', (err) => { + switch (type) { + case 'update': + activeFullUpdate = null; + break; + case 'reset': + activeFullReset = null; + break; + } + sendProgressToClients(clients, err.message); + reject(err); + }); + }); +} + // Progress endpoints -router.get('/update/progress', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': req.headers.origin || '*', - 'Access-Control-Allow-Credentials': 'true' - }); - - // Send an initial message to test the connection - res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n'); - - // Add this client to the update set - updateClients.add(res); - - // Remove client when connection closes - req.on('close', () => { - updateClients.delete(res); - }); -}); - -router.get('/import/progress', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': req.headers.origin || '*', - 'Access-Control-Allow-Credentials': 'true' - }); - - // Send an initial message to test the connection - res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n'); - - // Add this client to the import set - importClients.add(res); - - // Remove client when connection closes - req.on('close', () => { - importClients.delete(res); - }); -}); - -router.get('/reset/progress', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': req.headers.origin || '*', - 'Access-Control-Allow-Credentials': 'true' - }); - - // Send an initial message to test the connection - res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n'); - - // Add this client to the reset set - resetClients.add(res); - - // Remove client when connection closes - req.on('close', () => { - resetClients.delete(res); - }); -}); - -// Add reset-metrics progress endpoint -router.get('/reset-metrics/progress', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': req.headers.origin || '*', - 'Access-Control-Allow-Credentials': 'true' - }); - - // Send an initial message to test the connection - res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n'); - - // Add this client to the reset-metrics set - resetMetricsClients.add(res); - - // Remove client when connection closes - req.on('close', () => { - resetMetricsClients.delete(res); - }); -}); - -// Add calculate-metrics progress endpoint -router.get('/calculate-metrics/progress', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': req.headers.origin || '*', - 'Access-Control-Allow-Credentials': 'true' - }); - - // Send current progress if it exists - if (importProgress) { - res.write(`data: ${JSON.stringify(importProgress)}\n\n`); - } else { - res.write('data: {"status":"running","operation":"Initializing connection..."}\n\n'); +router.get('/:type/progress', (req, res) => { + const { type } = req.params; + if (!['update', 'reset'].includes(type)) { + return res.status(400).json({ error: 'Invalid operation type' }); } - // Add this client to the calculate-metrics set - calculateMetricsClients.add(res); + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': req.headers.origin || '*', + 'Access-Control-Allow-Credentials': 'true' + }); - // Remove client when connection closes + // Add this client to the correct set + const clients = type === 'update' ? fullUpdateClients : fullResetClients; + clients.add(res); + + // Send initial connection message + sendProgressToClients(new Set([res]), JSON.stringify({ + status: 'running', + operation: 'Initializing connection...' + })); + + // Handle client disconnect req.on('close', () => { - calculateMetricsClients.delete(res); + clients.delete(res); }); }); @@ -174,7 +179,6 @@ router.get('/status', (req, res) => { // Add calculate-metrics status endpoint router.get('/calculate-metrics/status', (req, res) => { - console.log('Calculate metrics status endpoint hit'); const calculateMetrics = require('../../scripts/calculate-metrics'); const progress = calculateMetrics.getProgress(); @@ -371,49 +375,35 @@ router.post('/import', async (req, res) => { // Route to cancel active process router.post('/cancel', (req, res) => { - if (!activeImport) { - return res.status(404).json({ error: 'No active process to cancel' }); + let killed = false; + + // Get the operation type from the request + const { type } = req.query; + const clients = type === 'update' ? fullUpdateClients : fullResetClients; + const activeProcess = type === 'update' ? activeFullUpdate : activeFullReset; + + if (activeProcess) { + try { + activeProcess.kill('SIGTERM'); + if (type === 'update') { + activeFullUpdate = null; + } else { + activeFullReset = null; + } + killed = true; + sendProgressToClients(clients, JSON.stringify({ + status: 'cancelled', + operation: 'Operation cancelled' + })); + } catch (err) { + console.error(`Error killing ${type} process:`, err); + } } - try { - // If it's the prod import module, call its cancel function - if (typeof activeImport.cancelImport === 'function') { - activeImport.cancelImport(); - } else { - // Otherwise it's a child process - activeImport.kill('SIGTERM'); - } - - // Get the operation type from the request - const { operation } = req.query; - - // Send cancel message only to the appropriate client set - const cancelMessage = { - status: 'cancelled', - operation: 'Operation cancelled' - }; - - switch (operation) { - case 'update': - sendProgressToClients(updateClients, cancelMessage); - break; - case 'import': - sendProgressToClients(importClients, cancelMessage); - break; - case 'reset': - sendProgressToClients(resetClients, cancelMessage); - break; - case 'calculate-metrics': - sendProgressToClients(calculateMetricsClients, cancelMessage); - break; - } - + if (killed) { res.json({ success: true }); - } catch (error) { - // Even if there's an error, try to clean up - activeImport = null; - importProgress = null; - res.status(500).json({ error: 'Failed to cancel process' }); + } else { + res.status(404).json({ error: 'No active process to cancel' }); } }); @@ -552,20 +542,6 @@ router.post('/reset-metrics', async (req, res) => { } }); -// Add calculate-metrics status endpoint -router.get('/calculate-metrics/status', (req, res) => { - const calculateMetrics = require('../../scripts/calculate-metrics'); - const progress = calculateMetrics.getProgress(); - - // Only consider it active if both the process is running and we have progress - const isActive = !!activeImport && !!progress; - - res.json({ - active: isActive, - progress: isActive ? progress : null - }); -}); - // Add calculate-metrics endpoint router.post('/calculate-metrics', async (req, res) => { if (activeImport) { @@ -711,4 +687,96 @@ router.post('/import-from-prod', async (req, res) => { } }); +// POST /csv/full-update - Run full update script +router.post('/full-update', async (req, res) => { + try { + const scriptPath = path.join(__dirname, '../../scripts/full-update.js'); + runScript(scriptPath, 'update', fullUpdateClients) + .catch(error => { + console.error('Update failed:', error); + }); + res.status(202).json({ message: 'Update started' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /csv/full-reset - Run full reset script +router.post('/full-reset', async (req, res) => { + try { + const scriptPath = path.join(__dirname, '../../scripts/full-reset.js'); + runScript(scriptPath, 'reset', fullResetClients) + .catch(error => { + console.error('Reset failed:', error); + }); + res.status(202).json({ message: 'Reset started' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /history/import - Get recent import history +router.get('/history/import', async (req, res) => { + try { + const pool = req.app.locals.pool; + const [rows] = await pool.query(` + SELECT * FROM import_history + ORDER BY start_time DESC + LIMIT 20 + `); + res.json(rows || []); + } catch (error) { + console.error('Error fetching import history:', error); + res.status(500).json({ error: error.message }); + } +}); + +// GET /history/calculate - Get recent calculation history +router.get('/history/calculate', async (req, res) => { + try { + const pool = req.app.locals.pool; + const [rows] = await pool.query(` + SELECT * FROM calculate_history + ORDER BY start_time DESC + LIMIT 20 + `); + res.json(rows || []); + } catch (error) { + console.error('Error fetching calculate history:', error); + res.status(500).json({ error: error.message }); + } +}); + +// GET /status/modules - Get module calculation status +router.get('/status/modules', async (req, res) => { + try { + const pool = req.app.locals.pool; + const [rows] = await pool.query(` + SELECT module_name, last_calculation_timestamp + FROM calculate_status + ORDER BY module_name + `); + res.json(rows || []); + } catch (error) { + console.error('Error fetching module status:', error); + res.status(500).json({ error: error.message }); + } +}); + +// GET /status/tables - Get table sync status +router.get('/status/tables', async (req, res) => { + try { + const pool = req.app.locals.pool; + const [rows] = await pool.query(` + SELECT table_name, last_sync_timestamp + FROM sync_status + ORDER BY table_name + `); + res.json(rows || []); + } catch (error) { + console.error('Error fetching table status:', error); + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index c0b4f31..f736640 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -1,7 +1,18 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Progress } from "@/components/ui/progress"; // Import Progress component from shadcn +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { AlertDialog, AlertDialogAction, @@ -13,12 +24,13 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Loader2, RefreshCw, X, Database } from "lucide-react"; -import config from '../../config'; +import { Loader2, X, RefreshCw, AlertTriangle } from "lucide-react"; +import config from "../../config"; import { toast } from "sonner"; +import { Table, TableBody, TableCell, TableRow, TableHeader, TableHead } from "@/components/ui/table"; interface ImportProgress { - status: 'running' | 'error' | 'complete' | 'cancelled'; + status: "running" | "error" | "complete" | "cancelled"; operation?: string; current?: number; total?: number; @@ -29,923 +41,684 @@ interface ImportProgress { error?: string; percentage?: string; message?: string; - testLimit?: number; - added?: number; - updated?: number; - skipped?: number; - duration?: string; } +interface HistoryRecord { + id: number; + start_time: string; + end_time: string | null; + duration_minutes: number; + status: "running" | "completed" | "failed" | "cancelled"; + error_message: string | null; + additional_info?: Record; +} + +interface ImportHistoryRecord extends HistoryRecord { + table_name: string; + records_added: number; + records_updated: number; + is_incremental: boolean; +} + +interface CalculateHistoryRecord extends HistoryRecord { + total_products: number; + total_orders: number; + total_purchase_orders: number; + processed_products: number; + processed_orders: number; + processed_purchase_orders: number; +} + +interface ModuleStatus { + module_name: string; + last_calculation_timestamp: string; +} + +interface TableStatus { + table_name: string; + last_sync_timestamp: string; +} export function DataManagement() { - const [isImportingProd, setIsImportingProd] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); const [isResetting, setIsResetting] = useState(false); - const [importProgress, setImportProgress] = useState(null); - const [purchaseOrdersProgress, setPurchaseOrdersProgress] = useState(null); - const [resetProgress, setResetProgress] = useState(null); + const [] = useState(null); const [eventSource, setEventSource] = useState(null); - const [isResettingMetrics, setIsResettingMetrics] = useState(false); - const [resetMetricsProgress, setResetMetricsProgress] = useState(null); - const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false); - const [metricsProgress, setMetricsProgress] = useState(null); + const [importHistory, setImportHistory] = useState([]); + const [calculateHistory, setCalculateHistory] = useState< + CalculateHistoryRecord[] + >([]); + const [moduleStatus, setModuleStatus] = useState([]); + const [tableStatus, setTableStatus] = useState([]); + const [scriptOutput, setScriptOutput] = useState([]); - // Add states for completed operations - const [lastImportStatus, setLastImportStatus] = useState(null); - const [lastResetStatus, setLastResetStatus] = useState(null); - const [lastMetricsStatus, setLastMetricsStatus] = useState(null); - const [lastResetMetricsStatus, setLastResetMetricsStatus] = useState(null); + // Add useRef for scroll handling + const terminalRef = useRef(null); - // Track cancellation state - const [cancelledOperations, setCancelledOperations] = useState>(new Set()); - - // Add new state for testing connection - const [isTestingConnection, setIsTestingConnection] = useState(false); - - // Helper to check if any operation is running - const isAnyOperationRunning = () => { - return isImportingProd || isTestingConnection || isResetting || isCalculatingMetrics || isResettingMetrics; + // Helper to format date + const formatDate = (date: string) => { + return new Date(date).toLocaleString(); }; - // 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'; - } - }; + // Helper to format duration with seconds + const formatDurationWithSeconds = (minutes: number) => { + if (minutes < 1 / 60) return "Less than a second"; - // 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 hours = Math.floor(minutes / 60); + const remainingMinutes = Math.floor(minutes % 60); + const seconds = Math.round((minutes % 1) * 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(' '); + if (remainingMinutes > 0) parts.push(`${remainingMinutes}m`); + if (seconds > 0) parts.push(`${seconds}s`); + + return parts.join(" "); }; - // 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 to format status time + const formatStatusTime = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMinutes = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60) + ); + + if (diffMinutes < 60) { + return `${diffMinutes}m ago`; + } else if (diffMinutes < 1440) { + const hours = Math.floor(diffMinutes / 60); + return `${hours}h ago`; + } else { + const days = Math.floor(diffMinutes / 1440); + return `${days}d ago`; } }; - // Helper function to render progress - const renderProgress = (progress: any, operationType: 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => { - if (!progress) return null; + // Helper to format JSON data + const formatJsonData = (data: Record) => { + if (!data) 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); + // Find the longest key length + const maxKeyLength = Object.keys(data).reduce( + (max, key) => Math.max(max, key.length), + 0 + ); return ( -
-
- - {progress.operation || progress.message || statusText} +
+ {Object.entries(data).map(([key, value]) => ( +
+ + {key}: - - {isFinished ? statusText : `${percent}%`} + + {typeof value === "object" + ? JSON.stringify(value) + : value?.toString()}
- - {/* 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(operationType) && ( -
- {progress.error} -
- )} -
+ ))}
); }; - // Helper to connect to event source - const connectToEventSource = (type: 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => { - console.log(`Setting up EventSource for ${type}...`); - - // Clean up existing connection first - if (eventSource) { - console.log('Closing existing event source'); - eventSource.close(); - setEventSource(null); - } + const handleFullUpdate = async () => { + setIsUpdating(true); + setScriptOutput([]); - let retryCount = 0; - const MAX_RETRIES = 3; - const RETRY_DELAY = 2000; + try { + const source = new EventSource(`${config.apiUrl}/csv/update/progress`, { + withCredentials: true, + }); - const setupConnection = () => { - try { - console.log(`Creating new EventSource for ${config.apiUrl}/csv/${type}/progress`); - const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, { - withCredentials: true - }); - - source.onopen = () => { - console.log('EventSource connected successfully'); - retryCount = 0; - setEventSource(source); - }; + source.onmessage = (event) => { + setScriptOutput((prev) => [...prev, event.data]); - source.onerror = async (event) => { - console.error('EventSource error:', event); - - // Don't close the source on error unless we're at max retries - if (retryCount >= MAX_RETRIES) { + // Try to parse for status updates, but don't affect display + try { + const data = JSON.parse(event.data); + if ( + data.status === "complete" || + data.status === "error" || + data.status === "cancelled" + ) { 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' && isImportingProd) || - (type === 'reset' && isResetting) || - (type === 'reset-metrics' && isResettingMetrics) - ) { - console.log('Operation may still be running, will attempt to reconnect via status check'); + setIsUpdating(false); + + if (data.status === "complete") { + toast.success("Update completed successfully"); + fetchHistory(); + } else if (data.status === "error") { + toast.error(`Update failed: ${data.error || "Unknown error"}`); + } else { + toast.warning("Update cancelled"); } - } else { - console.log(`Retrying connection (${retryCount + 1}/${MAX_RETRIES})...`); - retryCount++; - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); } - }; - - source.onmessage = (event) => { - try { - 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); - } - }; - - } catch (error) { - console.error('Failed to set up EventSource:', error); - if (retryCount < MAX_RETRIES) { - console.log(`Retrying connection (${retryCount + 1}/${MAX_RETRIES})...`); - retryCount++; - setTimeout(setupConnection, RETRY_DELAY); + // Not JSON, just display as is } - } - }; + }; - setupConnection(); - }; - - const handleProgressUpdate = ( - type: 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics', - 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' - ? JSON.stringify(progressData.message, null, 2) - : progressData.message, - status: progressData.status || 'running' - }; - - // Special handling for import operations - if (type === 'import' && progressData.operation) { - 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')) { - // Save the final status for purchase orders - setLastImportStatus({ - ...processedData, - operation: 'Purchase Orders Import' - }); - } else { - // Save the final status for main import - setLastImportStatus(processedData); - } - - // 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.onerror = (error) => { + setScriptOutput((prev) => [...prev, `[Error] ${error.type}`]); source.close(); setEventSource(null); - setIsImportingProd(false); - - // 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'}`); - } + setIsUpdating(false); + }; - // Clear cancelled state after completion - if (cancelledOperations.has(type)) { - setCancelledOperations(prev => { - const next = new Set(prev); - next.delete(type); - return next; - }); - } - } + setEventSource(source); + + const response = await fetch(`${config.apiUrl}/csv/full-update`, { + method: "POST", + credentials: "include", + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Failed to start update"); } - return; - } - - // Handle other operation types - let setProgress; - let setLastStatus; - let setIsRunning; - let operationName; - - switch (type) { - 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(); + } catch (error) { + if (error instanceof Error) { + toast.error(`Update failed: ${error.message}`); + } + setIsUpdating(false); setEventSource(null); - setIsRunning(false); - - // 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'}`); - } - - // Clear cancelled state after completion - if (cancelledOperations.has(type)) { - setCancelledOperations(prev => { - const next = new Set(prev); - next.delete(type); - return next; - }); - } } }; - const handleCancel = async (operation: 'import' | 'reset' | 'calculate-metrics') => { - try { - const response = await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, { - method: 'POST', - credentials: 'include' - }); - - if (!response.ok) { - throw new Error('Failed to cancel operation'); - } - - // Reset the appropriate state - if (operation === 'import') { - setIsImportingProd(false); - setImportProgress(null); - setPurchaseOrdersProgress(null); - } - // ... other operation states ... - } catch (error) { - toast.error(`Failed to cancel operation: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - }; - - // Check status on mount - useEffect(() => { - const checkStatus = async () => { - console.log('Checking status...'); - 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) - ]); - - console.log('Got responses:', { - metricsOk: metricsResponse?.ok, - importOk: importResponse?.ok - }); - - // Handle metrics status - if (metricsResponse?.ok) { - const metricsData = await metricsResponse.json().catch(() => null); - console.log('Metrics data:', metricsData); - - if (metricsData?.status === 'running' || metricsData?.active) { - console.log('Metrics calculation is running'); - setIsCalculatingMetrics(true); - setMetricsProgress(metricsData.progress || metricsData); - if (!eventSource) { - connectToEventSource('calculate-metrics'); - } - } else if (metricsData?.lastStatus) { - console.log('Setting last metrics status'); - setLastMetricsStatus(metricsData.lastStatus); - } - } - - // Handle import/update/reset status - if (importResponse?.ok) { - const importData = await importResponse.json().catch(() => null); - console.log('Import data:', importData); - - if (importData?.status === 'running' || importData?.active) { - console.log('Found running operation:', importData); - const operation = (importData.progress?.operation || importData.operation || '').toLowerCase(); - - if (operation.includes('import')) { - console.log('Import is running'); - setIsImportingProd(true); - if (operation.includes('purchase orders')) { - setPurchaseOrdersProgress(importData.progress || importData); - } else { - setImportProgress(importData.progress || importData); - } - if (!eventSource) { - connectToEventSource('import'); - } - } else if (operation.includes('reset')) { - if (operation.includes('metrics')) { - console.log('Reset metrics is running'); - setIsResettingMetrics(true); - setResetMetricsProgress(importData.progress || importData); - if (!eventSource) { - connectToEventSource('reset-metrics'); - } - } else { - console.log('Reset is running'); - setIsResetting(true); - setResetProgress(importData.progress || importData); - if (!eventSource) { - connectToEventSource('reset'); - } - } - } - } else if (importData?.lastStatus) { - console.log('Setting last status for:', importData.lastStatus?.operation); - const operation = (importData.lastStatus?.operation || '').toLowerCase(); - if (operation.includes('import')) { - setLastImportStatus(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); - } - }; - - console.log('Checking status on page load'); - checkStatus(); - }, []); - - const handleTestConnection = async () => { - setIsTestingConnection(true); - try { - const response = await fetch(`${config.apiUrl}/test-prod-connection`, { - credentials: 'include' - }); - - const data = await response.json(); - - if (response.ok) { - toast.success(`Successfully connected to production database. Found ${data.productCount.toLocaleString()} products.`); - } else { - throw new Error(data.error || 'Failed to connect to production database'); - } - } catch (error) { - toast.error(`Connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } finally { - setIsTestingConnection(false); - } - }; - - const handleImportFromProd = async () => { - setIsImportingProd(true); - setImportProgress({ status: 'running', operation: 'Starting import from production' }); - - 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-from-prod`, { - method: 'POST', - credentials: 'include' - }).catch(error => { - console.log('Import request error (may be timeout):', error); - return null; - }); - - // 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 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 production import'); - } - } - } catch (error) { - // 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(`Production import failed: ${error.message}`); - setIsImportingProd(false); - setImportProgress(null); - setPurchaseOrdersProgress(null); - } else { - console.log('Ignoring network error, import may still be running:', error); - } - } - }; - - const handleCalculateMetrics = async () => { - setIsCalculatingMetrics(true); - setMetricsProgress({ status: 'running', operation: 'Starting metrics calculation' }); - - 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 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) { - // 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); - } - } - }; - - const handleResetDB = async () => { + const handleFullReset = async () => { setIsResetting(true); - setResetProgress({ - status: 'running', - operation: 'Starting database reset', - percentage: '0' - }); - - try { - connectToEventSource('reset'); + setScriptOutput([]); - const response = await fetch(`${config.apiUrl}/csv/reset`, { - method: 'POST', - credentials: 'include' + try { + const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, { + withCredentials: true, + }); + + source.onmessage = (event) => { + setScriptOutput((prev) => [...prev, event.data]); + + // Try to parse for status updates, but don't affect display + try { + const data = JSON.parse(event.data); + if ( + data.status === "complete" || + data.status === "error" || + data.status === "cancelled" + ) { + source.close(); + setEventSource(null); + setIsResetting(false); + + if (data.status === "complete") { + toast.success("Reset completed successfully"); + fetchHistory(); + } else if (data.status === "error") { + toast.error(`Reset failed: ${data.error || "Unknown error"}`); + } else { + toast.warning("Reset cancelled"); + } + } + } catch (error) { + // Not JSON, just display as is + } + }; + + source.onerror = (error) => { + setScriptOutput((prev) => [...prev, `[Error] ${error.type}`]); + source.close(); + setEventSource(null); + setIsResetting(false); + }; + + setEventSource(source); + + const response = await fetch(`${config.apiUrl}/csv/full-reset`, { + method: "POST", + credentials: "include", + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Failed to start reset"); + } + } catch (error) { + if (error instanceof Error) { + toast.error(`Reset failed: ${error.message}`); + } + setIsResetting(false); + setEventSource(null); + } + }; + + const handleCancel = async () => { + try { + // Determine which operation is running + const type = isUpdating ? "update" : "reset"; + + const response = await fetch(`${config.apiUrl}/csv/cancel?type=${type}`, { + method: "POST", + credentials: "include", }); if (!response.ok) { - const data = await response.json().catch(() => ({})); - console.error('Reset request failed:', response.status, response.statusText, data); - throw new Error(data.error || `Failed to reset database: ${response.status} ${response.statusText}`); + throw new Error("Failed to cancel operation"); } - } catch (error) { - console.error('Reset error:', error); + + // Close the event source if (eventSource) { eventSource.close(); setEventSource(null); } + + // Reset states + setIsUpdating(false); setIsResetting(false); - setResetProgress(null); - toast.error(`Database reset failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + + toast.info("Operation cancelled"); + } catch (error) { + toast.error( + `Failed to cancel operation: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); } }; - const handleResetMetrics = async () => { - setIsResettingMetrics(true); - setResetMetricsProgress({ status: 'running', operation: 'Starting metrics reset' }); - + const fetchHistory = async () => { try { - connectToEventSource('reset-metrics'); + const [importRes, calcRes, moduleRes, tableRes] = await Promise.all([ + fetch(`${config.apiUrl}/csv/history/import`), + fetch(`${config.apiUrl}/csv/history/calculate`), + fetch(`${config.apiUrl}/csv/status/modules`), + fetch(`${config.apiUrl}/csv/status/tables`), + ]); - // First check if reset is already running - const statusResponse = await fetch(`${config.apiUrl}/csv/reset-metrics/status`, { - credentials: 'include' - }).catch(() => null); + const [importData, calcData, moduleData, tableData] = await Promise.all([ + importRes.json(), + calcRes.json(), + moduleRes.json(), + tableRes.json(), + ]); - 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 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'); - } - } + setImportHistory(importData); + setCalculateHistory(calcData); + setModuleStatus(moduleData); + setTableStatus(tableData); } catch (error) { - // 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); - } + console.error("Error fetching history:", error); } }; + useEffect(() => { + fetchHistory(); + }, []); + + // Add useEffect to handle auto-scrolling + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.scrollTop = terminalRef.current.scrollHeight; + } + }, [scriptOutput]); + + // Replace renderTerminal with new version + const renderTerminal = () => { + if (!scriptOutput.length) return null; + + return ( + + + Script Output + + {isUpdating + ? "Running full update..." + : isResetting + ? "Running full reset..." + : "Process output"} + + + +
+
+              {scriptOutput.join("\n")}
+            
+
+
+
+ ); + }; + return ( -
- {/* Test Production Connection Card */} +
+
+ {/* Full Update Card */} - Test Production Connection - Verify connection to production database - - - - - - - {/* Import Data Card */} - - - Import Data - Import data from production database - - -
- - - {isImportingProd && ( - - )} -
- - {(isImportingProd || lastImportStatus) && ( -
- {renderProgress(importProgress || lastImportStatus, 'import')} - {renderProgress(purchaseOrdersProgress, 'import')} -
- )} -
-
- - {/* Metrics Calculation Card */} - - - Calculate Metrics - Calculate metrics for all products based on current data + Full Update + + Import latest data and recalculate all metrics +
- {isCalculatingMetrics && ( - )}
- - {(isCalculatingMetrics || lastMetricsStatus) && renderProgress(metricsProgress || lastMetricsStatus, 'calculate-metrics')}
- {/* Database Management Card */} + {/* Full Reset Card */} - Database Management - Reset database or metrics tables + Full Reset + + Reset database, reimport all data, and recalculate metrics + - -
+ +
- Reset Database + + Are you absolutely sure? + - This will delete all data in the database and recreate the tables. This action cannot be undone. + This will completely reset the database, delete all data, + and reimport everything from scratch. This action cannot + be undone. Cancel - Continue + + Continue + - - - - - - - Reset metrics tables? - - This action cannot be undone. This will permanently delete all data from metrics-related tables. - - - - Cancel - Continue - - - -
+ )} +
+
+
+
- {(resetProgress || lastResetStatus) && ( -
- {renderProgress(resetProgress || lastResetStatus, 'reset')} -
- )} - - {(resetMetricsProgress || lastResetMetricsStatus) && ( -
- {renderProgress(resetMetricsProgress || lastResetMetricsStatus, 'reset-metrics')} + {/* Terminal Output */} + {(isUpdating || isResetting) && renderTerminal()} + + {/* History Section */} +
+

History & Status

+ +
+ {/* Table Status */} + + + Last Import Times + + +
+ {tableStatus.map((table) => ( +
+ {table.table_name} + + {formatStatusTime(table.last_sync_timestamp)} + +
+ ))} +
+
+
+ {/* Module Status */} + + + Last Calculation Times + + +
+ {moduleStatus.map((module) => ( +
+ {module.module_name} + + {formatStatusTime(module.last_calculation_timestamp)} + +
+ ))} +
+
+
+
+ {/* Recent Import History */} + + + Recent Imports + + + + + {importHistory.slice(0, 20).map((record) => ( + + + + + +
+ + #{record.id} + + + {formatDate(record.start_time)} + + + {formatDurationWithSeconds( + record.duration_minutes + )} + + + {record.status} + +
+
+ +
+
+ End Time: + + {record.end_time + ? formatDate(record.end_time) + : "N/A"} + +
+
+ Records: + + {record.records_added} added,{" "} + {record.records_updated} updated + +
+ {record.error_message && ( +
+ {record.error_message} +
+ )} + {record.additional_info && + formatJsonData(record.additional_info)} +
+
+
+
+
+
+ ))} +
+
+
+
+ + {/* Recent Calculate History */} + + + Recent Calculations + + + + + {calculateHistory.slice(0, 20).map((record) => ( + + + + + +
+ + #{record.id} + + + {formatDate(record.start_time)} + + + {formatDurationWithSeconds( + record.duration_minutes + )} + + + + {record.status} + +
+
+ +
+
+ End Time: + + {record.end_time + ? formatDate(record.end_time) + : "N/A"} + +
+
+ + Processed Products: + + {record.processed_products} +
+
+ + Processed Orders: + + {record.processed_orders} +
+
+ + Processed Purchase Orders: + + {record.processed_purchase_orders} +
+ {record.error_message && ( +
+ {record.error_message}
)} + {record.additional_info && + formatJsonData(record.additional_info)} +
+
+
+
+
+
+ ))} +
+
+
); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/PerformanceMetrics.tsx b/inventory/src/components/settings/PerformanceMetrics.tsx index 74bb1f0..0e3cfa9 100644 --- a/inventory/src/components/settings/PerformanceMetrics.tsx +++ b/inventory/src/components/settings/PerformanceMetrics.tsx @@ -133,6 +133,10 @@ export function PerformanceMetrics() { } }; + function getCategoryName(_cat_id: number): import("react").ReactNode { + throw new Error('Function not implemented.'); + } + return (
{/* Lead Time Thresholds Card */} @@ -205,11 +209,11 @@ export function PerformanceMetrics() { - Category - Vendor - A Threshold - B Threshold - Period Days + Category + Vendor + A Threshold + B Threshold + Period Days @@ -242,10 +246,10 @@ export function PerformanceMetrics() {
- Category - Vendor - Period Days - Target Rate + Category + Vendor + Period Days + Target Rate diff --git a/inventory/src/components/settings/StockManagement.tsx b/inventory/src/components/settings/StockManagement.tsx index 6a88ee5..a716fd9 100644 --- a/inventory/src/components/settings/StockManagement.tsx +++ b/inventory/src/components/settings/StockManagement.tsx @@ -5,7 +5,6 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; import config from '../../config'; -import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table"; interface StockThreshold { id: number; @@ -244,54 +243,6 @@ export function StockManagement() { - -
- - - Category - Vendor - Critical Days - Reorder Days - Overstock Days - Low Stock - Min Reorder - - - - {stockThresholds.map((threshold) => ( - - {threshold.cat_id ? getCategoryName(threshold.cat_id) : 'Global'} - {threshold.vendor || 'All Vendors'} - {threshold.critical_days} - {threshold.reorder_days} - {threshold.overstock_days} - {threshold.low_stock_threshold} - {threshold.min_reorder_quantity} - - ))} - -
- - - - - Category - Vendor - Coverage Days - Service Level - - - - {safetyStockConfigs.map((config) => ( - - {config.cat_id ? getCategoryName(config.cat_id) : 'Global'} - {config.vendor || 'All Vendors'} - {config.coverage_days} - {config.service_level}% - - ))} - -
); } \ No newline at end of file