Split off config schema, update settings page

This commit is contained in:
2025-01-12 15:00:54 -05:00
parent 271a40f2c5
commit b815062525
5 changed files with 935 additions and 986 deletions

View File

@@ -0,0 +1,49 @@
-- Configuration tables schema
-- Stock threshold configurations
CREATE TABLE IF NOT EXISTS stock_thresholds (
id INT NOT NULL,
category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors
critical_days INT NOT NULL DEFAULT 7,
reorder_days INT NOT NULL DEFAULT 14,
overstock_days INT NOT NULL DEFAULT 90,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor)
);
-- Insert default thresholds with ID=1 if not exists
INSERT INTO stock_thresholds (id, category_id, vendor, critical_days, reorder_days, overstock_days)
VALUES (1, NULL, NULL, 7, 14, 90)
ON DUPLICATE KEY UPDATE
critical_days = VALUES(critical_days),
reorder_days = VALUES(reorder_days),
overstock_days = VALUES(overstock_days);
-- View to show thresholds with category names
CREATE OR REPLACE VIEW stock_thresholds_view AS
SELECT
st.*,
c.name as category_name,
CASE
WHEN st.category_id IS NULL AND st.vendor IS NULL THEN 'Global Default'
WHEN st.category_id IS NULL THEN CONCAT('Vendor: ', st.vendor)
WHEN st.vendor IS NULL THEN CONCAT('Category: ', c.name)
ELSE CONCAT('Category: ', c.name, ' / Vendor: ', st.vendor)
END as threshold_scope
FROM
stock_thresholds st
LEFT JOIN
categories c ON st.category_id = c.id
ORDER BY
CASE
WHEN st.category_id IS NULL AND st.vendor IS NULL THEN 1
WHEN st.category_id IS NULL THEN 2
WHEN st.vendor IS NULL THEN 3
ELSE 4
END,
c.name,
st.vendor;

View File

@@ -88,30 +88,6 @@ CREATE TABLE IF NOT EXISTS vendor_metrics (
PRIMARY KEY (vendor)
);
-- New table for stock threshold configurations
CREATE TABLE IF NOT EXISTS stock_thresholds (
id INT NOT NULL, -- Changed from AUTO_INCREMENT to explicitly set ID
category_id BIGINT, -- NULL means default/global threshold
vendor VARCHAR(100), -- NULL means applies to all vendors
critical_days INT NOT NULL DEFAULT 7,
reorder_days INT NOT NULL DEFAULT 14,
overstock_days INT NOT NULL DEFAULT 90,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE KEY unique_category_vendor (category_id, vendor),
INDEX idx_thresholds_category_vendor (category_id, vendor)
);
-- Insert default thresholds with ID=1
INSERT INTO stock_thresholds (id, category_id, vendor, critical_days, reorder_days, overstock_days)
VALUES (1, NULL, NULL, 7, 14, 90)
ON DUPLICATE KEY UPDATE
critical_days = VALUES(critical_days),
reorder_days = VALUES(reorder_days),
overstock_days = VALUES(overstock_days);
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,126 @@
import { useState } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import config from '../../config';
interface StockThreshold {
id: number;
category_id: number | null;
vendor: string | null;
critical_days: number;
reorder_days: number;
overstock_days: number;
category_name?: string;
threshold_scope?: string;
}
export function Configuration() {
const [globalThresholds, setGlobalThresholds] = useState<StockThreshold>({
id: 1,
category_id: null,
vendor: null,
critical_days: 7,
reorder_days: 14,
overstock_days: 90
});
const handleUpdateGlobalThresholds = async () => {
try {
const response = await fetch(`${config.apiUrl}/config/stock-thresholds/1`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(globalThresholds)
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update global thresholds');
}
toast.success('Global thresholds updated successfully');
} catch (error) {
toast.error(`Failed to update thresholds: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Stock Thresholds Card */}
<Card>
<CardHeader>
<CardTitle>Stock Thresholds</CardTitle>
<CardDescription>Configure stock level thresholds for inventory management</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Global Defaults Section */}
<div>
<h3 className="text-sm font-medium mb-2">Global Defaults</h3>
<div className="grid grid-cols-3 gap-4">
<div>
<Label htmlFor="critical-days">Critical Days</Label>
<Input
id="critical-days"
type="number"
min="1"
value={globalThresholds.critical_days}
onChange={(e) => setGlobalThresholds(prev => ({
...prev,
critical_days: parseInt(e.target.value) || 1
}))}
/>
</div>
<div>
<Label htmlFor="reorder-days">Reorder Days</Label>
<Input
id="reorder-days"
type="number"
min="1"
value={globalThresholds.reorder_days}
onChange={(e) => setGlobalThresholds(prev => ({
...prev,
reorder_days: parseInt(e.target.value) || 1
}))}
/>
</div>
<div>
<Label htmlFor="overstock-days">Overstock Days</Label>
<Input
id="overstock-days"
type="number"
min="1"
value={globalThresholds.overstock_days}
onChange={(e) => setGlobalThresholds(prev => ({
...prev,
overstock_days: parseInt(e.target.value) || 1
}))}
/>
</div>
</div>
<Button
className="mt-2"
onClick={handleUpdateGlobalThresholds}
>
Update Global Defaults
</Button>
</div>
{/* Category/Vendor Specific Section */}
<div>
<h3 className="text-sm font-medium mb-2">Category & Vendor Specific</h3>
<Button variant="outline" className="w-full">Add New Threshold Rule</Button>
</div>
</div>
</CardContent>
</Card>
{/* Future Config Cards can go here */}
</div>
);
}

View File

@@ -0,0 +1,745 @@
import { useState } 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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Loader2, RefreshCw, Upload, X } from "lucide-react";
import config from '../../config';
import { toast } from "sonner";
interface ImportProgress {
status: 'running' | 'error' | 'complete';
operation?: string;
current?: number;
total?: number;
rate?: number;
elapsed?: string;
remaining?: string;
progress?: string;
error?: string;
percentage?: string;
message?: string;
testLimit?: number;
added?: number;
updated?: number;
skipped?: number;
duration?: string;
}
interface ImportLimits {
products: number;
orders: number;
purchaseOrders: number;
}
export function DataManagement() {
const [isUpdating, setIsUpdating] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [isResetting, setIsResetting] = useState(false);
const [updateProgress, setUpdateProgress] = useState<ImportProgress | null>(null);
const [importProgress, setImportProgress] = useState<ImportProgress | null>(null);
const [purchaseOrdersProgress, setPurchaseOrdersProgress] = useState<ImportProgress | null>(null);
const [resetProgress, setResetProgress] = useState<ImportProgress | null>(null);
const [eventSource, setEventSource] = useState<EventSource | null>(null);
const [limits, setLimits] = useState<ImportLimits>({
products: 0,
orders: 0,
purchaseOrders: 0
});
const [isResettingMetrics, setIsResettingMetrics] = useState(false);
const [resetMetricsProgress, setResetMetricsProgress] = useState<ImportProgress | null>(null);
const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false);
const [metricsProgress, setMetricsProgress] = useState<ImportProgress | null>(null);
// Helper to connect to event source
const connectToEventSource = (type: 'update' | '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);
}
let retryCount = 0;
const MAX_RETRIES = 3;
const RETRY_DELAY = 2000;
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.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) {
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);
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);
}
};
} 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);
} else {
console.log('Max retries exceeded, but operation may still be running...');
}
}
};
setupConnection();
};
const handleProgressUpdate = (
type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics',
progressData: any,
source: EventSource
) => {
const processedData = {
...progressData,
message: progressData.message && typeof progressData.message === 'object'
? JSON.stringify(progressData.message, null, 2)
: progressData.message,
status: progressData.status || 'running'
};
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() || '';
if (operation.includes('purchase orders')) {
setPurchaseOrdersProgress(null);
} else {
setImportProgress(null);
}
if (!importProgress || !purchaseOrdersProgress) {
source.close();
setEventSource(null);
setIsImporting(false);
if (!progressData.operation?.includes('cancelled')) {
toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} completed successfully`);
}
}
return;
}
source.close();
setEventSource(null);
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;
}
if (!progressData.operation?.includes('cancelled')) {
toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} completed successfully`);
}
}
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);
try {
await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, {
method: 'POST',
credentials: 'include'
});
} catch (error) {
console.log('Cancel request failed:', error);
}
};
const handleUpdateCSV = async () => {
setIsUpdating(true);
setUpdateProgress({ status: 'running', operation: 'Starting CSV update' });
try {
connectToEventSource('update');
const response = await fetch(`${config.apiUrl}/csv/update`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
if (data.error === 'Import already in progress') {
return;
}
throw new Error(data.error || `Failed to update CSV files: ${response.status} ${response.statusText}`);
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsUpdating(false);
setUpdateProgress(null);
toast.error(`CSV update failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const handleImportCSV = async () => {
setIsImporting(true);
setImportProgress({ status: 'running', operation: 'Starting import process' });
try {
connectToEventSource('import');
const response = await fetch(`${config.apiUrl}/csv/import`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
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;
});
if (!response) {
console.log('Continuing with existing progress...');
return;
}
if (!importProgress?.current && !purchaseOrdersProgress?.current) {
if (!response.ok && response.status !== 200) {
const data = await response.json().catch(() => ({}));
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);
}
};
const handleResetDB = async () => {
setIsResetting(true);
setResetProgress({
status: 'running',
operation: 'Starting database reset',
percentage: '0'
});
try {
connectToEventSource('reset');
const response = await fetch(`${config.apiUrl}/csv/reset`, {
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}`);
}
} catch (error) {
console.error('Reset error:', error);
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsResetting(false);
setResetProgress(null);
toast.error(`Database reset failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const handleResetMetrics = async () => {
setIsResettingMetrics(true);
setResetMetricsProgress({ status: 'running', operation: 'Starting metrics reset' });
try {
connectToEventSource('reset-metrics');
const response = await fetch(`${config.apiUrl}/csv/reset-metrics`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to reset metrics');
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsResettingMetrics(false);
setResetMetricsProgress(null);
toast.error(`Metrics reset failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const handleCalculateMetrics = async () => {
setIsCalculatingMetrics(true);
setMetricsProgress({ status: 'running', operation: 'Starting metrics calculation' });
try {
connectToEventSource('calculate-metrics');
const response = await fetch(`${config.apiUrl}/csv/calculate-metrics`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to calculate metrics');
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
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 (
<div className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span>{progress.operation || 'Processing...'}</span>
{percentage !== null && <span>{Math.round(percentage)}%</span>}
</div>
{percentage !== null && (
<>
<Progress value={percentage} className="h-2" />
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
{progress.current && progress.total && (
<div className="flex justify-between">
<span>Progress:</span>
<span>{progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''}</span>
</div>
)}
{(progress.elapsed || progress.remaining) && (
<div className="flex justify-between">
<span>Time:</span>
<span>
{progress.elapsed && `${progress.elapsed} elapsed`}
{progress.elapsed && progress.remaining && ' - '}
{progress.remaining && `${progress.remaining} remaining`}
</span>
</div>
)}
{(progress.added !== undefined || progress.updated !== undefined || progress.skipped !== undefined) && (
<div className="flex justify-between">
<span>Results:</span>
<span>
{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`}
</span>
</div>
)}
{message && (
<div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)}
</div>
</>
)}
{!percentage && message && (
<div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)}
</div>
);
};
return (
<div className="max-w-[400px] space-y-4">
{/* Update CSV Card */}
<Card>
<CardHeader>
<CardTitle>Update CSV Files</CardTitle>
<CardDescription>Download the latest CSV data files</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleUpdateCSV}
disabled={isUpdating || isImporting}
>
{isUpdating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating CSV Files...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Update CSV Files
</>
)}
</Button>
{isUpdating && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{isUpdating && renderProgress(updateProgress)}
</CardContent>
</Card>
{/* Import Data Card */}
<Card>
<CardHeader>
<CardTitle>Import Data</CardTitle>
<CardDescription>Import current CSV files into database</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleImportCSV}
disabled={isImporting || isUpdating || isResetting}
>
{isImporting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Importing Data...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Import Data
</>
)}
</Button>
{isImporting && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{isImporting && (
<div className="space-y-4">
{importProgress && renderProgress(importProgress)}
{purchaseOrdersProgress && renderProgress(purchaseOrdersProgress)}
</div>
)}
</CardContent>
</Card>
{/* Metrics Calculation Card */}
<Card>
<CardHeader>
<CardTitle>Calculate Metrics</CardTitle>
<CardDescription>Calculate metrics for all products based on current data</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleCalculateMetrics}
disabled={isCalculatingMetrics || isImporting || isUpdating || isResetting || isResettingMetrics}
>
{isCalculatingMetrics ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Calculating Metrics...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Calculate Metrics
</>
)}
</Button>
{isCalculatingMetrics && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{metricsProgress && renderProgress(metricsProgress)}
</CardContent>
</Card>
{/* Database Management Card */}
<Card>
<CardHeader>
<CardTitle>Database Management</CardTitle>
<CardDescription>Reset database or metrics tables</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
className="flex-1 min-w-[140px]"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
{isResetting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Resetting Database...
</>
) : (
<>Reset Database</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset Database</AlertDialogTitle>
<AlertDialogDescription>
This will delete all data in the database and recreate the tables. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetDB}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
className="flex-1 min-w-[140px]"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
Reset Metrics Only
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset metrics tables?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete all data from metrics-related tables.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetMetrics}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{resetProgress && (
<div className="mt-4">
<Progress value={Number(resetProgress.percentage)} className="mb-2" />
<p className="text-sm text-muted-foreground">
{resetProgress.message || 'Resetting database...'}
</p>
</div>
)}
{resetMetricsProgress && (
<div className="mt-4">
<Progress value={Number(resetMetricsProgress.percentage)} className="mb-2" />
<p className="text-sm text-muted-foreground">
{resetMetricsProgress.message || 'Resetting metrics...'}
</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,975 +1,28 @@
import { useState, useEffect, useCallback } 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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Loader2, RefreshCw, Upload, X } from "lucide-react";
import config from '../config';
interface ImportProgress {
status: 'running' | 'error' | 'complete';
operation?: string;
current?: number;
total?: number;
rate?: number;
elapsed?: string;
remaining?: string;
progress?: string;
error?: string;
percentage?: string;
message?: string;
testLimit?: number;
added?: number;
updated?: number;
skipped?: number;
duration?: string;
}
interface ImportLimits {
products: number;
orders: number;
purchaseOrders: number;
}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DataManagement } from "@/components/settings/DataManagement";
import { Configuration } from "@/components/settings/Configuration";
export function Settings() {
const [isUpdating, setIsUpdating] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [isResetting, setIsResetting] = useState(false);
const [updateProgress, setUpdateProgress] = useState<ImportProgress | null>(null);
const [importProgress, setImportProgress] = useState<ImportProgress | null>(null);
const [purchaseOrdersProgress, setPurchaseOrdersProgress] = useState<ImportProgress | null>(null);
const [resetProgress, setResetProgress] = useState<ImportProgress | null>(null);
const [eventSource, setEventSource] = useState<EventSource | null>(null);
const [limits, setLimits] = useState<ImportLimits>({
products: 0,
orders: 0,
purchaseOrders: 0
});
const [isResettingMetrics, setIsResettingMetrics] = useState(false);
const [resetMetricsProgress, setResetMetricsProgress] = useState<ImportProgress | null>(null);
const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false);
const [metricsProgress, setMetricsProgress] = useState<ImportProgress | null>(null);
// Update the message handlers to use toast
const handleComplete = (operation: string) => {
toast.success(`${operation} completed successfully`);
};
const handleError = (operation: string, error: string) => {
// Skip error toast if we're cancelling or if it's a cancellation error
if (error.includes('cancelled') ||
error.includes('Process exited with code 143') ||
error.includes('Operation cancelled') ||
error.includes('500 Internal Server Error') ||
// Skip "Failed to start" errors if we have active progress
(error.includes('Failed to start CSV import') && (importProgress || purchaseOrdersProgress)) ||
// Skip connection errors if we have active progress
(error.includes('Failed to fetch') && (importProgress || purchaseOrdersProgress))) {
return;
}
toast.error(`${operation} failed: ${error}`);
};
// Helper to connect to event source
const connectToEventSource = useCallback((type: 'update' | '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);
}
let retryCount = 0;
const MAX_RETRIES = 3;
const RETRY_DELAY = 2000; // 2 seconds
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; // Reset retry count on successful connection
setEventSource(source);
};
source.onerror = async (event) => {
console.error('EventSource error:', event);
source.close();
// Only retry if we haven't exceeded max retries and we're still in the active state
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) {
console.log(`Retrying connection (${retryCount + 1}/${MAX_RETRIES})...`);
retryCount++;
// Wait before retrying
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...');
// Don't reset states or show error, just log it
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);
// First parse the outer message
const data = JSON.parse(event.data);
// If we have a progress field that's a string containing multiple JSON objects
if (data.progress && typeof data.progress === 'string') {
// Split the progress string into separate JSON objects
const progressMessages = data.progress
.split('\n')
.filter(Boolean)
.map((message: string) => {
try {
return JSON.parse(message);
} catch (err) {
console.warn('Failed to parse progress message:', message, err);
return null;
}
})
.filter((msg: unknown): msg is ImportProgress => msg !== null);
// Process each progress message
progressMessages.forEach((progressData: ImportProgress) => {
handleProgressUpdate(type, progressData, source);
});
} else {
// Handle single message case
const progressData = data.progress || data;
handleProgressUpdate(type, progressData, source);
}
} catch (error) {
console.error('Error parsing event data:', error, event.data);
// Don't show error to user for parsing issues, just log them
console.warn('Failed to parse server response:', error);
}
};
} 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);
} else {
console.log('Max retries exceeded, but operation may still be running...');
}
}
};
setupConnection();
}, [eventSource, handleComplete, handleError, isImporting, isUpdating, isResetting, isResettingMetrics, isCalculatingMetrics]);
// Helper function to process a single progress update
const handleProgressUpdate = (
type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics',
progressData: any,
source: EventSource
) => {
console.log('Processing progress data:', progressData);
// If message is an object, stringify it before setting state
const processedData = {
...progressData,
message: progressData.message && typeof progressData.message === 'object'
? JSON.stringify(progressData.message, null, 2)
: progressData.message,
status: progressData.status || 'running'
};
// For import type, handle orders and purchase orders separately
if (type === 'import' && progressData.operation) {
const operation = progressData.operation.toLowerCase();
if (operation.includes('purchase orders')) {
setPurchaseOrdersProgress(prev => ({
...prev,
...processedData
}));
return; // Don't update main import progress for PO updates
}
}
// Update progress state based on type
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
}));
}
// Handle completion
if (progressData.status === 'complete' || progressData.status === 'cancelled') {
console.log(`Operation ${type} completed or cancelled`);
// For import, only close connection when both operations are complete
if (type === 'import') {
const operation = progressData.operation?.toLowerCase() || '';
if (operation.includes('purchase orders')) {
setPurchaseOrdersProgress(null);
} else {
setImportProgress(null);
}
// Only fully complete if both are done
if (!importProgress || !purchaseOrdersProgress) {
source.close();
setEventSource(null);
setIsImporting(false);
if (!progressData.operation?.includes('cancelled')) {
handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`);
}
}
return;
}
// For other operations, close immediately
source.close();
setEventSource(null);
// Reset operation state
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;
}
if (!progressData.operation?.includes('cancelled')) {
handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`);
}
}
// Handle errors
else if (progressData.status === 'error') {
console.error(`Operation ${type} failed:`, progressData.error);
source.close();
setEventSource(null);
// Reset operation state
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;
}
handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error');
}
};
// Check for active operations on mount
useEffect(() => {
console.log('Checking status on mount'); // Debug log
const checkStatus = async () => {
try {
const response = await fetch(`${config.apiUrl}/csv/status`, {
credentials: 'include'
});
const data = await response.json();
console.log('Status check response:', data); // Debug log
if (data.active) {
// Try to determine the operation type from progress if available
let operationType: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics' | null = null;
if (data.progress?.operation) {
const operation = data.progress.operation.toLowerCase();
if (operation.includes('update')) operationType = 'update';
else if (operation.includes('import')) operationType = 'import';
else if (operation.includes('reset')) operationType = 'reset';
else if (operation.includes('reset-metrics')) operationType = 'reset-metrics';
else if (operation.includes('calculate-metrics')) operationType = 'calculate-metrics';
} else {
// If no progress data, try to connect to import stream by default
// since that's the most common long-running operation
operationType = 'import';
}
if (operationType) {
// Set initial state
if (operationType === 'update') {
setIsUpdating(true);
if (data.progress) {
setUpdateProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'import') {
setIsImporting(true);
if (data.progress) {
setImportProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'reset') {
setIsResetting(true);
if (data.progress) {
setResetProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'reset-metrics') {
setIsResettingMetrics(true);
if (data.progress) {
setResetMetricsProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'calculate-metrics') {
setIsCalculatingMetrics(true);
if (data.progress) {
setMetricsProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
}
// Connect to the appropriate event source
console.log('Connecting to event source for active operation:', operationType);
connectToEventSource(operationType);
}
}
} catch (error) {
console.error('Failed to check operation status:', error);
}
};
checkStatus();
}, []); // Remove connectToEventSource dependency to ensure it runs on mount
// Clean up function to reset state
useEffect(() => {
return () => {
if (eventSource) {
console.log('Cleaning up event source'); // Debug log
eventSource.close();
}
};
}, [eventSource]);
const handleCancel = async () => {
// Determine which operation is running
const operation = isImporting ? 'import' :
isUpdating ? 'update' :
isResetting ? 'reset' :
isCalculatingMetrics ? 'calculate-metrics' : 'reset';
// Show cancellation toast immediately
toast.warning(`${operation.charAt(0).toUpperCase() + operation.slice(1)} cancelled`);
// Clean up everything immediately
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsUpdating(false);
setIsImporting(false);
setIsResetting(false);
setIsCalculatingMetrics(false);
setUpdateProgress(null);
setImportProgress(null);
setResetProgress(null);
setMetricsProgress(null);
// Fire and forget the cancel request with the operation type
try {
await fetch(`${config.apiUrl}/csv/cancel?operation=${operation}`, {
method: 'POST',
credentials: 'include'
});
} catch (error) {
// Silently ignore any errors from the cancel request
console.log('Cancel request failed:', error);
}
};
const handleUpdateCSV = async () => {
setIsUpdating(true);
setUpdateProgress({ status: 'running', operation: 'Starting CSV update' });
try {
// Connect to SSE for progress updates
connectToEventSource('update');
// Make the update request
const response = await fetch(`${config.apiUrl}/csv/update`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
if (data.error === 'Import already in progress') {
// If there's already an import, just let the SSE connection handle showing progress
return;
}
throw new Error(data.error || `Failed to update CSV files: ${response.status} ${response.statusText}`);
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsUpdating(false);
setUpdateProgress(null);
handleError('CSV update', error instanceof Error ? error.message : 'Unknown error');
}
};
const handleImportCSV = async () => {
setIsImporting(true);
setImportProgress({ status: 'running', operation: 'Starting import process' });
try {
// Connect to SSE for progress updates first
connectToEventSource('import');
// Make the import request - no timeout needed since this just starts the process
const response = await fetch(`${config.apiUrl}/csv/import`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(limits)
}).catch(error => {
// If we already have progress, ignore network errors
if ((importProgress?.current || purchaseOrdersProgress?.current) &&
(error.name === 'TypeError')) {
console.log('Request error but import is in progress:', error);
return null;
}
throw error;
});
// If response is null, it means we caught an error but have progress
if (!response) {
console.log('Continuing with existing progress...');
return;
}
// Only check response status if we don't have any progress
if (!importProgress?.current && !purchaseOrdersProgress?.current) {
if (!response.ok && response.status !== 200) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to start CSV import');
}
} else {
console.log('Response not ok but import is in progress, continuing...');
}
} catch (error) {
handleError('CSV import', error instanceof Error ? error.message : 'Unknown error');
setIsImporting(false);
}
};
const handleResetDB = async () => {
// Set initial state
setIsResetting(true);
setResetProgress({
status: 'running',
operation: 'Starting database reset',
percentage: '0'
});
try {
// Connect to SSE for progress updates
connectToEventSource('reset');
// Make the reset request
const response = await fetch(`${config.apiUrl}/csv/reset`, {
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}`);
}
} catch (error) {
console.error('Reset error:', error);
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsResetting(false);
setResetProgress(null);
handleError('Database reset', error instanceof Error ? error.message : 'Unknown error');
}
};
const handleResetMetrics = async () => {
setIsResettingMetrics(true);
setResetMetricsProgress({ status: 'running', operation: 'Starting metrics reset' });
try {
// Connect to SSE for progress updates
connectToEventSource('reset-metrics');
// Make the reset request
const response = await fetch(`${config.apiUrl}/csv/reset-metrics`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to reset metrics');
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsResettingMetrics(false);
setResetMetricsProgress(null);
handleError('Metrics reset', error instanceof Error ? error.message : 'Unknown error');
}
};
const handleCalculateMetrics = async () => {
setIsCalculatingMetrics(true);
setMetricsProgress({ status: 'running', operation: 'Starting metrics calculation' });
try {
// Connect to SSE for progress updates
connectToEventSource('calculate-metrics');
// Make the calculation request
const response = await fetch(`${config.apiUrl}/csv/calculate-metrics`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to calculate metrics');
}
} catch (error) {
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsCalculatingMetrics(false);
setMetricsProgress(null);
handleError('Metrics calculation', 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;
// Handle object messages by converting them to strings
const message = progress.message ?
(typeof progress.message === 'object' ?
JSON.stringify(progress.message, null, 2) :
progress.message
) : null;
return (
<div className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span>{progress.operation || 'Processing...'}</span>
{percentage !== null && <span>{Math.round(percentage)}%</span>}
</div>
{percentage !== null && (
<>
<Progress value={percentage} className="h-2" />
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
{progress.current && progress.total && (
<div className="flex justify-between">
<span>Progress:</span>
<span>{progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''}</span>
</div>
)}
{(progress.elapsed || progress.remaining) && (
<div className="flex justify-between">
<span>Time:</span>
<span>
{progress.elapsed && `${progress.elapsed} elapsed`}
{progress.elapsed && progress.remaining && ' - '}
{progress.remaining && `${progress.remaining} remaining`}
</span>
</div>
)}
{(progress.added !== undefined || progress.updated !== undefined || progress.skipped !== undefined) && (
<div className="flex justify-between">
<span>Results:</span>
<span>
{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`}
</span>
</div>
)}
{message && (
<div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)}
</div>
</>
)}
{!percentage && message && (
<div className="whitespace-pre-wrap font-mono text-xs">
{message}
</div>
)}
</div>
);
};
return (
<div className="p-8 space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Settings</h1>
</div>
<div className="max-w-[400px] space-y-4">
{/* Update CSV Card */}
<Card>
<CardHeader>
<CardTitle>Update CSV Files</CardTitle>
<CardDescription>Download the latest CSV data files</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleUpdateCSV}
disabled={isUpdating || isImporting}
>
{isUpdating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating CSV Files...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Update CSV Files
</>
)}
</Button>
<Tabs defaultValue="data" className="w-full">
<TabsList>
<TabsTrigger value="data">Data Management</TabsTrigger>
<TabsTrigger value="config">Configuration</TabsTrigger>
</TabsList>
{isUpdating && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<TabsContent value="data" className="space-y-4">
<DataManagement />
</TabsContent>
{isUpdating && renderProgress(updateProgress)}
</CardContent>
</Card>
{/* Import Data Card */}
<Card>
<CardHeader>
<CardTitle>Import Data</CardTitle>
<CardDescription>Import current CSV files into database. Set limits to 0 to import all records.</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-3 gap-4">
<div>
<Label htmlFor="products-limit" className="text-xs">Products Limit</Label>
<Input
id="products-limit"
type="number"
min="0"
value={limits.products}
onChange={(e) => setLimits(prev => ({ ...prev, products: parseInt(e.target.value) || 0 }))}
disabled={isUpdating || isImporting}
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
placeholder="0 for no limit"
/>
</div>
<div>
<Label htmlFor="orders-limit" className="text-xs">Orders Limit</Label>
<Input
id="orders-limit"
type="number"
min="0"
value={limits.orders}
onChange={(e) => setLimits(prev => ({ ...prev, orders: parseInt(e.target.value) || 0 }))}
disabled={isUpdating || isImporting}
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
placeholder="0 for no limit"
/>
</div>
<div>
<Label htmlFor="purchase-orders-limit" className="text-xs">PO Limit</Label>
<Input
id="purchase-orders-limit"
type="number"
min="0"
value={limits.purchaseOrders}
onChange={(e) => setLimits(prev => ({ ...prev, purchaseOrders: parseInt(e.target.value) || 0 }))}
disabled={isUpdating || isImporting}
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
placeholder="0 for no limit"
/>
</div>
</div>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleImportCSV}
disabled={isImporting || isUpdating || isResetting}
>
{isImporting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Importing Data...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Import Data
</>
)}
</Button>
{isImporting && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{isImporting && (
<div className="space-y-4">
{importProgress && renderProgress(importProgress)}
{purchaseOrdersProgress && renderProgress(purchaseOrdersProgress)}
</div>
)}
</CardContent>
</Card>
{/* Add new Metrics Calculation Card */}
<Card>
<CardHeader>
<CardTitle>Metrics Calculation</CardTitle>
<CardDescription>Calculate metrics for all products based on current data</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleCalculateMetrics}
disabled={isCalculatingMetrics || isImporting || isUpdating || isResetting || isResettingMetrics}
>
{isCalculatingMetrics ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Calculating Metrics...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Calculate Metrics
</>
)}
</Button>
{isCalculatingMetrics && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{metricsProgress && renderProgress(metricsProgress)}
</CardContent>
</Card>
{/* Database Management Card */}
<Card>
<CardHeader>
<CardTitle>Database Management</CardTitle>
<CardDescription>Reset database or metrics tables</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
className="flex-1 min-w-[140px]"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
{isResetting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Resetting Database...
</>
) : (
<>Reset Database</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset Database</AlertDialogTitle>
<AlertDialogDescription>
This will delete all data in the database and recreate the tables. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetDB}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
className="flex-1 min-w-[140px]"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
Reset Metrics Only
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset metrics tables?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete all data from metrics-related tables.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetMetrics}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{resetProgress && (
<div className="mt-4">
<Progress value={Number(resetProgress.percentage)} className="mb-2" />
<p className="text-sm text-muted-foreground">
{resetProgress.message || 'Resetting database...'}
</p>
</div>
)}
{resetMetricsProgress && (
<div className="mt-4">
<Progress value={Number(resetMetricsProgress.percentage)} className="mb-2" />
<p className="text-sm text-muted-foreground">
{resetMetricsProgress.message || 'Resetting metrics...'}
</p>
</div>
)}
</CardContent>
</Card>
</div>
<TabsContent value="config" className="space-y-4">
<Configuration />
</TabsContent>
</Tabs>
</div>
);
}