626 lines
21 KiB
TypeScript
626 lines
21 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import axios from "axios";
|
|
import config from "@/config";
|
|
import { toast } from "sonner";
|
|
|
|
interface StockThreshold {
|
|
id: number;
|
|
cat_id: number | null;
|
|
vendor: string | null;
|
|
critical_days: number;
|
|
reorder_days: number;
|
|
overstock_days: number;
|
|
low_stock_threshold: number;
|
|
min_reorder_quantity: number;
|
|
category_name?: string;
|
|
threshold_scope?: string;
|
|
}
|
|
|
|
interface LeadTimeThreshold {
|
|
id: number;
|
|
cat_id: number | null;
|
|
vendor: string | null;
|
|
target_days: number;
|
|
warning_days: number;
|
|
critical_days: number;
|
|
}
|
|
|
|
interface SalesVelocityConfig {
|
|
id: number;
|
|
cat_id: number | null;
|
|
vendor: string | null;
|
|
daily_window_days: number;
|
|
weekly_window_days: number;
|
|
monthly_window_days: number;
|
|
}
|
|
|
|
interface ABCClassificationConfig {
|
|
id: number;
|
|
a_threshold: number;
|
|
b_threshold: number;
|
|
classification_period_days: number;
|
|
}
|
|
|
|
interface SafetyStockConfig {
|
|
id: number;
|
|
cat_id: number | null;
|
|
vendor: string | null;
|
|
coverage_days: number;
|
|
service_level: number;
|
|
}
|
|
|
|
interface TurnoverConfig {
|
|
id: number;
|
|
cat_id: number | null;
|
|
vendor: string | null;
|
|
calculation_period_days: number;
|
|
target_rate: number;
|
|
}
|
|
|
|
export function Configuration() {
|
|
const [stockThresholds, setStockThresholds] = useState<StockThreshold>({
|
|
id: 1,
|
|
cat_id: null,
|
|
vendor: null,
|
|
critical_days: 7,
|
|
reorder_days: 14,
|
|
overstock_days: 90,
|
|
low_stock_threshold: 5,
|
|
min_reorder_quantity: 1
|
|
});
|
|
|
|
const [leadTimeThresholds, setLeadTimeThresholds] = useState<LeadTimeThreshold>({
|
|
id: 1,
|
|
cat_id: null,
|
|
vendor: null,
|
|
target_days: 14,
|
|
warning_days: 21,
|
|
critical_days: 30
|
|
});
|
|
|
|
const [salesVelocityConfig, setSalesVelocityConfig] = useState<SalesVelocityConfig>({
|
|
id: 1,
|
|
cat_id: null,
|
|
vendor: null,
|
|
daily_window_days: 30,
|
|
weekly_window_days: 7,
|
|
monthly_window_days: 90
|
|
});
|
|
|
|
const [abcConfig, setAbcConfig] = useState<ABCClassificationConfig>({
|
|
id: 1,
|
|
a_threshold: 20.0,
|
|
b_threshold: 50.0,
|
|
classification_period_days: 90
|
|
});
|
|
|
|
const [safetyStockConfig, setSafetyStockConfig] = useState<SafetyStockConfig>({
|
|
id: 1,
|
|
cat_id: null,
|
|
vendor: null,
|
|
coverage_days: 14,
|
|
service_level: 95.0
|
|
});
|
|
|
|
const [turnoverConfig, setTurnoverConfig] = useState<TurnoverConfig>({
|
|
id: 1,
|
|
cat_id: null,
|
|
vendor: null,
|
|
calculation_period_days: 30,
|
|
target_rate: 1.0
|
|
});
|
|
|
|
useEffect(() => {
|
|
const loadConfig = async () => {
|
|
try {
|
|
const response = await fetch(`${config.apiUrl}/config`, {
|
|
credentials: 'include'
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load configuration');
|
|
}
|
|
const data = await response.json();
|
|
setStockThresholds(data.stockThresholds);
|
|
setLeadTimeThresholds(data.leadTimeThresholds);
|
|
setSalesVelocityConfig(data.salesVelocityConfig);
|
|
setAbcConfig(data.abcConfig);
|
|
setSafetyStockConfig(data.safetyStockConfig);
|
|
setTurnoverConfig(data.turnoverConfig);
|
|
} catch (error) {
|
|
toast.error('Failed to load configuration');
|
|
}
|
|
};
|
|
loadConfig();
|
|
}, []);
|
|
|
|
const handleUpdateStockThresholds = async () => {
|
|
try {
|
|
const response = await axios.post(`${config.apiUrl}/settings/stock-thresholds`, stockThresholds);
|
|
if (response.status === 200) {
|
|
toast.success('Stock thresholds updated successfully');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating stock thresholds:", error);
|
|
toast.error('Failed to update stock thresholds');
|
|
}
|
|
};
|
|
|
|
const handleUpdateLeadTimeThresholds = async () => {
|
|
try {
|
|
const response = await fetch(`${config.apiUrl}/config/lead-time`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(leadTimeThresholds)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Failed to update lead time thresholds');
|
|
}
|
|
|
|
toast.success('Lead time thresholds updated successfully');
|
|
} catch (error) {
|
|
console.error("Error updating lead time thresholds:", error);
|
|
toast.error(`Failed to update thresholds: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const handleUpdateSalesVelocityConfig = async () => {
|
|
try {
|
|
const response = await fetch(`${config.apiUrl}/config/sales-velocity/1`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(salesVelocityConfig)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Failed to update sales velocity configuration');
|
|
}
|
|
|
|
toast.success('Sales velocity configuration updated successfully');
|
|
} catch (error) {
|
|
console.error("Error updating sales velocity configuration:", error);
|
|
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const handleUpdateABCConfig = async () => {
|
|
try {
|
|
const response = await fetch(`${config.apiUrl}/config/abc-classification/1`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(abcConfig)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Failed to update ABC classification configuration');
|
|
}
|
|
|
|
toast.success('ABC classification configuration updated successfully');
|
|
} catch (error) {
|
|
console.error("Error updating ABC classification configuration:", error);
|
|
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const handleUpdateSafetyStockConfig = async () => {
|
|
try {
|
|
const response = await fetch(`${config.apiUrl}/config/safety-stock/1`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(safetyStockConfig)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Failed to update safety stock configuration');
|
|
}
|
|
|
|
toast.success('Safety stock configuration updated successfully');
|
|
} catch (error) {
|
|
console.error("Error updating safety stock configuration:", error);
|
|
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const handleUpdateTurnoverConfig = async () => {
|
|
try {
|
|
const response = await fetch(`${config.apiUrl}/config/turnover/1`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(turnoverConfig)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || 'Failed to update turnover configuration');
|
|
}
|
|
|
|
toast.success('Turnover configuration updated successfully');
|
|
} catch (error) {
|
|
console.error("Error updating turnover configuration:", error);
|
|
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Tabs defaultValue="stock" className="w-full">
|
|
<TabsList>
|
|
<TabsTrigger value="stock">Stock Management</TabsTrigger>
|
|
<TabsTrigger value="performance">Performance Metrics</TabsTrigger>
|
|
<TabsTrigger value="calculation">Calculation Settings</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="stock" className="space-y-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">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="critical-days">Critical Days</Label>
|
|
<Input
|
|
id="critical-days"
|
|
type="number"
|
|
min="1"
|
|
value={stockThresholds.critical_days}
|
|
onChange={(e) => setStockThresholds(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={stockThresholds.reorder_days}
|
|
onChange={(e) => setStockThresholds(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={stockThresholds.overstock_days}
|
|
onChange={(e) => setStockThresholds(prev => ({
|
|
...prev,
|
|
overstock_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="low-stock-threshold">Low Stock Threshold</Label>
|
|
<Input
|
|
id="low-stock-threshold"
|
|
type="number"
|
|
min="0"
|
|
value={stockThresholds.low_stock_threshold}
|
|
onChange={(e) => setStockThresholds(prev => ({
|
|
...prev,
|
|
low_stock_threshold: parseInt(e.target.value) || 0
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="min-reorder-quantity">Minimum Reorder Quantity</Label>
|
|
<Input
|
|
id="min-reorder-quantity"
|
|
type="number"
|
|
min="1"
|
|
value={stockThresholds.min_reorder_quantity}
|
|
onChange={(e) => setStockThresholds(prev => ({
|
|
...prev,
|
|
min_reorder_quantity: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleUpdateStockThresholds}>
|
|
Update Stock Thresholds
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Safety Stock Configuration Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Safety Stock</CardTitle>
|
|
<CardDescription>Configure safety stock parameters</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="coverage-days">Coverage Days</Label>
|
|
<Input
|
|
id="coverage-days"
|
|
type="number"
|
|
min="1"
|
|
value={safetyStockConfig.coverage_days}
|
|
onChange={(e) => setSafetyStockConfig(prev => ({
|
|
...prev,
|
|
coverage_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="service-level">Service Level (%)</Label>
|
|
<Input
|
|
id="service-level"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
step="0.1"
|
|
value={safetyStockConfig.service_level}
|
|
onChange={(e) => setSafetyStockConfig(prev => ({
|
|
...prev,
|
|
service_level: parseFloat(e.target.value) || 0
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleUpdateSafetyStockConfig}>
|
|
Update Safety Stock Configuration
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="performance" className="space-y-4">
|
|
{/* Lead Time Thresholds Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Lead Time Thresholds</CardTitle>
|
|
<CardDescription>Configure lead time thresholds for vendor performance</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<Label htmlFor="target-days">Target Days</Label>
|
|
<Input
|
|
id="target-days"
|
|
type="number"
|
|
min="1"
|
|
value={leadTimeThresholds.target_days}
|
|
onChange={(e) => setLeadTimeThresholds(prev => ({
|
|
...prev,
|
|
target_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="warning-days">Warning Days</Label>
|
|
<Input
|
|
id="warning-days"
|
|
type="number"
|
|
min="1"
|
|
value={leadTimeThresholds.warning_days}
|
|
onChange={(e) => setLeadTimeThresholds(prev => ({
|
|
...prev,
|
|
warning_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="critical-days-lead">Critical Days</Label>
|
|
<Input
|
|
id="critical-days-lead"
|
|
type="number"
|
|
min="1"
|
|
value={leadTimeThresholds.critical_days}
|
|
onChange={(e) => setLeadTimeThresholds(prev => ({
|
|
...prev,
|
|
critical_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleUpdateLeadTimeThresholds}>
|
|
Update Lead Time Thresholds
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* ABC Classification Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>ABC Classification</CardTitle>
|
|
<CardDescription>Configure ABC classification parameters</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<Label htmlFor="a-threshold">A Threshold (%)</Label>
|
|
<Input
|
|
id="a-threshold"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
step="0.1"
|
|
value={abcConfig.a_threshold}
|
|
onChange={(e) => setAbcConfig(prev => ({
|
|
...prev,
|
|
a_threshold: parseFloat(e.target.value) || 0
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="b-threshold">B Threshold (%)</Label>
|
|
<Input
|
|
id="b-threshold"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
step="0.1"
|
|
value={abcConfig.b_threshold}
|
|
onChange={(e) => setAbcConfig(prev => ({
|
|
...prev,
|
|
b_threshold: parseFloat(e.target.value) || 0
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="classification-period">Classification Period (days)</Label>
|
|
<Input
|
|
id="classification-period"
|
|
type="number"
|
|
min="1"
|
|
value={abcConfig.classification_period_days}
|
|
onChange={(e) => setAbcConfig(prev => ({
|
|
...prev,
|
|
classification_period_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleUpdateABCConfig}>
|
|
Update ABC Classification
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Turnover Configuration Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Turnover Rate</CardTitle>
|
|
<CardDescription>Configure turnover rate calculations</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="calculation-period">Calculation Period (days)</Label>
|
|
<Input
|
|
id="calculation-period"
|
|
type="number"
|
|
min="1"
|
|
value={turnoverConfig.calculation_period_days}
|
|
onChange={(e) => setTurnoverConfig(prev => ({
|
|
...prev,
|
|
calculation_period_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="target-rate">Target Rate</Label>
|
|
<Input
|
|
id="target-rate"
|
|
type="number"
|
|
min="0"
|
|
step="0.1"
|
|
value={turnoverConfig.target_rate}
|
|
onChange={(e) => setTurnoverConfig(prev => ({
|
|
...prev,
|
|
target_rate: parseFloat(e.target.value) || 0
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleUpdateTurnoverConfig}>
|
|
Update Turnover Configuration
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="calculation" className="space-y-4">
|
|
{/* Sales Velocity Configuration Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Sales Velocity Windows</CardTitle>
|
|
<CardDescription>Configure time windows for sales velocity calculations</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<Label htmlFor="daily-window">Daily Window (days)</Label>
|
|
<Input
|
|
id="daily-window"
|
|
type="number"
|
|
min="1"
|
|
value={salesVelocityConfig.daily_window_days}
|
|
onChange={(e) => setSalesVelocityConfig(prev => ({
|
|
...prev,
|
|
daily_window_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="weekly-window">Weekly Window (days)</Label>
|
|
<Input
|
|
id="weekly-window"
|
|
type="number"
|
|
min="1"
|
|
value={salesVelocityConfig.weekly_window_days}
|
|
onChange={(e) => setSalesVelocityConfig(prev => ({
|
|
...prev,
|
|
weekly_window_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="monthly-window">Monthly Window (days)</Label>
|
|
<Input
|
|
id="monthly-window"
|
|
type="number"
|
|
min="1"
|
|
value={salesVelocityConfig.monthly_window_days}
|
|
onChange={(e) => setSalesVelocityConfig(prev => ({
|
|
...prev,
|
|
monthly_window_days: parseInt(e.target.value) || 1
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleUpdateSalesVelocityConfig}>
|
|
Update Sales Velocity Windows
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
);
|
|
}
|