Create pages and routes for new settings tables, start improving product details

This commit is contained in:
2025-04-03 22:12:53 -04:00
parent 4552fa4862
commit 4021fe487d
12 changed files with 1528 additions and 1336 deletions

View File

@@ -6,7 +6,7 @@ import {
ClipboardList,
LogOut,
Tags,
FileSpreadsheet,
Plus,
ShoppingBag,
Truck,
} from "lucide-react";
@@ -40,18 +40,6 @@ const items = [
url: "/products",
permission: "access:products"
},
{
title: "Import",
icon: FileSpreadsheet,
url: "/import",
permission: "access:import"
},
{
title: "Forecasting",
icon: IconCrystalBall,
url: "/forecasting",
permission: "access:forecasting"
},
{
title: "Categories",
icon: Tags,
@@ -82,6 +70,18 @@ const items = [
url: "/analytics",
permission: "access:analytics"
},
{
title: "Forecasting",
icon: IconCrystalBall,
url: "/forecasting",
permission: "access:forecasting"
},
{
title: "Create Products",
icon: Plus,
url: "/import",
permission: "access:import"
}
];
export function AppSidebar() {

View File

@@ -3,9 +3,9 @@ import { useQuery } from "@tanstack/react-query";
import { Drawer as VaulDrawer } from "vaul";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { X, Calendar, Users, DollarSign, Tag, Package, Clock, AlertTriangle } from "lucide-react";
import { ProductMetric, ProductStatus } from "@/types/products";
import {
getStatusBadge,
@@ -14,11 +14,38 @@ import {
formatPercentage,
formatDays,
formatDate,
formatBoolean,
getProductStatus
formatBoolean
} from "@/utils/productUtils";
import { cn } from "@/lib/utils";
import config from "@/config";
import { ResponsiveContainer, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid, Legend } from "recharts";
import { Badge } from "@/components/ui/badge";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
// Interfaces for POs and time series data
interface ProductPurchaseOrder {
poId: string;
date: string;
expectedDate: string;
receivedDate: string | null;
ordered: number;
received: number;
status: number;
receivingStatus: number;
costPrice: number;
notes: string | null;
leadTimeDays: number | null;
}
interface ProductTimeSeries {
monthlySales: {
month: string;
sales: number;
revenue: number;
profit: number;
}[];
recentPurchases: ProductPurchaseOrder[];
}
interface ProductDetailProps {
productId: number | null;
@@ -63,17 +90,77 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
transformed.pid = typeof transformed.pid === 'string' ?
parseInt(transformed.pid, 10) : transformed.pid;
// Make sure we have a status
if (!transformed.status) {
transformed.status = getProductStatus(transformed);
}
console.log("Transformed product data:", transformed);
return transformed;
},
enabled: !!productId, // Only run query when productId is truthy
});
const isLoading = isLoadingProduct;
// Fetch time series data and purchase orders history
const { data: timeSeriesData, isLoading: isLoadingTimeSeries } = useQuery<ProductTimeSeries, Error>({
queryKey: ["productTimeSeries", productId],
queryFn: async () => {
if (!productId) throw new Error("Product ID is required");
const response = await fetch(`${config.apiUrl}/products/${productId}/time-series`, {credentials: 'include'});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Failed to fetch time series data (${response.status}): ${errorData.error || 'Server error'}`);
}
const data = await response.json();
// Ensure the monthly_sales data is properly formatted for charts
const formattedMonthlySales = data.monthly_sales.map((item: any) => ({
month: item.month,
sales: Number(item.sales),
revenue: Number(item.revenue),
profit: Number(item.profit || 0)
}));
return {
monthlySales: formattedMonthlySales,
recentPurchases: data.recent_purchases || []
};
},
enabled: !!productId, // Only run query when productId is truthy
});
// Get PO status names
const getPOStatusName = (status: number): string => {
const statusMap: {[key: number]: string} = {
0: 'Canceled',
1: 'Created',
10: 'Ready to Send',
11: 'Ordered',
12: 'Preordered',
13: 'Electronically Sent',
15: 'Receiving Started',
50: 'Completed'
};
return statusMap[status] || 'Unknown';
};
// Get receiving status names
const getReceivingStatusName = (status: number): string => {
const statusMap: {[key: number]: string} = {
0: 'Canceled',
1: 'Created',
30: 'Partial Received',
40: 'Fully Received',
50: 'Paid'
};
return statusMap[status] || 'Unknown';
};
// Get status badge color class
const getStatusBadgeClass = (status: number): string => {
if (status === 0) return "bg-destructive text-destructive-foreground"; // Canceled
if (status === 50) return "bg-green-600 text-white"; // Completed
if (status >= 15) return "bg-amber-500 text-black"; // In progress
return "bg-blue-600 text-white"; // Other statuses
};
const isLoading = isLoadingProduct || isLoadingTimeSeries;
if (!productId) return null; // Don't render anything if no ID
@@ -130,10 +217,11 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
</div>
) : product ? (
<Tabs defaultValue="overview" className="p-4">
<TabsList className="mb-4 grid w-full grid-cols-4 h-auto">
<TabsList className="mb-4 grid w-full grid-cols-5 h-auto">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="inventory">Inventory</TabsTrigger>
<TabsTrigger value="performance">Performance</TabsTrigger>
<TabsTrigger value="orders">Orders</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
</TabsList>
@@ -191,6 +279,68 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
</TabsContent>
<TabsContent value="performance" className="space-y-4">
{/* Sales Performance Chart */}
<Card>
<CardHeader>
<CardTitle className="text-base">Sales & Revenue Trend</CardTitle>
<CardDescription>Monthly performance over time</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
{isLoadingTimeSeries ? (
<div className="w-full h-full flex items-center justify-center">
<Skeleton className="h-[250px] w-full" />
</div>
) : timeSeriesData && timeSeriesData.monthlySales && timeSeriesData.monthlySales.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={timeSeriesData.monthlySales}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Tooltip
formatter={(value: number, name: string) => {
if (name === 'revenue' || name === 'profit') {
return [formatCurrency(value), name];
}
return [value, name];
}}
/>
<Legend />
<Line
yAxisId="left"
type="monotone"
dataKey="sales"
name="Units Sold"
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="revenue"
name="Revenue"
stroke="#82ca9d"
/>
<Line
yAxisId="right"
type="monotone"
dataKey="profit"
name="Profit"
stroke="#ffc658"
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="w-full h-full flex flex-col items-center justify-center text-muted-foreground">
<p>No sales data available for this product.</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="text-base">Sales Performance (30 Days)</CardTitle></CardHeader>
<CardContent className="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-sm">
@@ -203,6 +353,60 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<InfoItem label="Daily Velocity" value={formatNumber(product.salesVelocityDaily, 2)} />
</CardContent>
</Card>
{/* Inventory KPIs Chart */}
<Card>
<CardHeader>
<CardTitle className="text-base">Key Inventory Metrics</CardTitle>
</CardHeader>
<CardContent className="h-[250px]">
{isLoading ? (
<Skeleton className="h-[200px] w-full" />
) : (
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={[
{
name: 'Stock Turn',
value: product.stockturn30d || 0,
fill: '#8884d8'
},
{
name: 'GMROI',
value: product.gmroi30d || 0,
fill: '#82ca9d'
},
{
name: 'Sell Through %',
value: product.sellThrough30d ? product.sellThrough30d * 100 : 0,
fill: '#ffc658'
},
{
name: 'Margin %',
value: product.margin30d ? product.margin30d * 100 : 0,
fill: '#ff8042'
}
]}
margin={{ top: 20, right: 30, left: 20, bottom: 50 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" angle={-45} textAnchor="end" height={60} />
<YAxis />
<Tooltip
formatter={(value: number, name: string) => {
if (name === 'Sell Through %' || name === 'Margin %') {
return [`${value.toFixed(1)}%`, name];
}
return [value.toFixed(2), name];
}}
/>
<Bar dataKey="value" />
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="text-base">Inventory Performance (30 Days)</CardTitle></CardHeader>
<CardContent className="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-sm">
@@ -222,6 +426,143 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<InfoItem label="Avg Lead Time" value={formatDays(product.avgLeadTimeDays, 1)} />
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="text-base">Additional Metrics</CardTitle></CardHeader>
<CardContent className="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-sm">
<InfoItem label="Returns (Units)" value={formatNumber(product.returnsUnits30d)} />
<InfoItem label="Returns (Revenue)" value={formatCurrency(product.returnsRevenue30d)} />
<InfoItem label="Return Rate" value={formatPercentage(product.returnRate30d)} />
<InfoItem label="Discounts" value={formatCurrency(product.discounts30d)} />
<InfoItem label="Discount Rate" value={formatPercentage(product.discountRate30d)} />
<InfoItem label="Markdown" value={formatCurrency(product.markdown30d)} />
<InfoItem label="Markdown Rate" value={formatPercentage(product.markdownRate30d)} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="orders" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base">Purchase Orders & Receivings</CardTitle>
<CardDescription>Recent purchase orders for this product</CardDescription>
</CardHeader>
<CardContent>
{isLoadingTimeSeries ? (
<div className="space-y-2">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
</div>
) : timeSeriesData?.recentPurchases && timeSeriesData.recentPurchases.length > 0 ? (
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>PO #</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Qty</TableHead>
<TableHead className="text-right">Received</TableHead>
<TableHead className="text-right">Cost</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{timeSeriesData.recentPurchases.map((po) => (
<TableRow key={po.poId}>
<TableCell className="font-medium">{po.poId}</TableCell>
<TableCell>{po.date}</TableCell>
<TableCell>
<Badge className={getStatusBadgeClass(po.status)}>
{getPOStatusName(po.status)}
</Badge>
</TableCell>
<TableCell className="text-right">{po.ordered}</TableCell>
<TableCell className="text-right">{po.received}</TableCell>
<TableCell className="text-right">{formatCurrency(po.costPrice)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-4 text-muted-foreground">
<p>No purchase orders found for this product.</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Order Summary</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-sm">
<InfoItem
label="Total Ordered"
value={
timeSeriesData?.recentPurchases ?
formatNumber(
timeSeriesData.recentPurchases.reduce((acc, po) => acc + po.ordered, 0)
) : 'N/A'
}
/>
<InfoItem
label="Total Received"
value={
timeSeriesData?.recentPurchases ?
formatNumber(
timeSeriesData.recentPurchases.reduce((acc, po) => acc + po.received, 0)
) : 'N/A'
}
/>
<InfoItem
label="Fulfillment Rate"
value={
timeSeriesData?.recentPurchases ?
(() => {
const totalOrdered = timeSeriesData.recentPurchases.reduce((acc, po) => acc + po.ordered, 0);
const totalReceived = timeSeriesData.recentPurchases.reduce((acc, po) => acc + po.received, 0);
return totalOrdered > 0 ? formatPercentage(totalReceived / totalOrdered) : 'N/A';
})() : 'N/A'
}
/>
<InfoItem
label="Total PO Cost"
value={
timeSeriesData?.recentPurchases ?
formatCurrency(
timeSeriesData.recentPurchases.reduce((acc, po) => acc + (po.ordered * po.costPrice), 0)
) : 'N/A'
}
/>
<InfoItem
label="Avg Lead Time"
value={
timeSeriesData?.recentPurchases ?
(() => {
const leadTimes = timeSeriesData.recentPurchases
.filter(po => po.leadTimeDays != null)
.map(po => po.leadTimeDays as number);
const avgLeadTime = leadTimes.length > 0
? leadTimes.reduce((acc, lt) => acc + lt, 0) / leadTimes.length
: null;
return formatDays(avgLeadTime);
})() : 'N/A'
}
/>
<InfoItem
label="Active Orders"
value={
timeSeriesData?.recentPurchases ?
formatNumber(
timeSeriesData.recentPurchases.filter(
po => po.status < 50 && po.receivingStatus < 40
).length
) : 'N/A'
}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="details" className="space-y-4">
@@ -242,6 +583,17 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<CardContent className="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-sm">
<InfoItem label="Lead Time" value={formatDays(product.configLeadTime)} />
<InfoItem label="Days of Stock" value={formatDays(product.configDaysOfStock)} />
<InfoItem label="Safety Stock" value={formatNumber(product.configSafetyStock)} />
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="text-base">Forecasting</CardTitle></CardHeader>
<CardContent className="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-sm">
<InfoItem label="Replenishment Units" value={formatNumber(product.replenishmentUnits)} />
<InfoItem label="Replenishment Cost" value={formatCurrency(product.replenishmentCost)} />
<InfoItem label="To Order Units" value={formatNumber(product.toOrderUnits)} />
<InfoItem label="Forecast Lost Sales" value={formatNumber(product.forecastLostSalesUnits)} />
<InfoItem label="Forecast Lost Revenue" value={formatCurrency(product.forecastLostRevenue)} />
</CardContent>
</Card>
</TabsContent>

View File

@@ -1,130 +0,0 @@
import { useState, useEffect } 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 SalesVelocityConfig {
id: number;
cat_id: number | null;
vendor: string | null;
daily_window_days: number;
weekly_window_days: number;
monthly_window_days: number;
}
export function CalculationSettings() {
const [salesVelocityConfig, setSalesVelocityConfig] = useState<SalesVelocityConfig>({
id: 1,
cat_id: null,
vendor: null,
daily_window_days: 30,
weekly_window_days: 7,
monthly_window_days: 90
});
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();
setSalesVelocityConfig(data.salesVelocityConfig);
} catch (error) {
toast.error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
loadConfig();
}, []);
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) {
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
return (
<div className="max-w-[700px] 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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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>
</div>
);
}

View File

@@ -1,626 +0,0 @@
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>
);
}

View File

@@ -0,0 +1,188 @@
import { useState, useEffect } 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';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface GlobalSetting {
setting_key: string;
setting_value: string;
description: string;
updated_at: string;
}
export function GlobalSettings() {
const [settings, setSettings] = useState<GlobalSetting[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const response = await fetch(`${config.apiUrl}/config/global`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to load global settings');
}
const data = await response.json();
setSettings(data);
} catch (error) {
toast.error(`Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setLoading(false);
}
};
const updateSetting = async (key: string, value: string) => {
const updatedSettings = settings.map(s =>
s.setting_key === key ? { ...s, setting_value: value } : s
);
setSettings(updatedSettings);
};
const handleSaveSettings = async () => {
try {
const response = await fetch(`${config.apiUrl}/config/global`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(settings)
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update global settings');
}
toast.success('Global settings updated successfully');
await loadSettings(); // Reload to get fresh data
} catch (error) {
toast.error(`Failed to update settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const renderSettingInput = (setting: GlobalSetting) => {
// Handle different input types based on setting key or value
if (setting.setting_key === 'abc_calculation_basis') {
return (
<Select
value={setting.setting_value}
onValueChange={(value) => updateSetting(setting.setting_key, value)}
>
<SelectTrigger>
<SelectValue placeholder="Select calculation basis" />
</SelectTrigger>
<SelectContent>
<SelectItem value="revenue_30d">Revenue (30 days)</SelectItem>
<SelectItem value="sales_30d">Sales Quantity (30 days)</SelectItem>
<SelectItem value="lifetime_revenue">Lifetime Revenue</SelectItem>
</SelectContent>
</Select>
);
} else if (setting.setting_key === 'default_forecast_method') {
return (
<Select
value={setting.setting_value}
onValueChange={(value) => updateSetting(setting.setting_key, value)}
>
<SelectTrigger>
<SelectValue placeholder="Select forecast method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="standard">Standard</SelectItem>
<SelectItem value="seasonal">Seasonal</SelectItem>
</SelectContent>
</Select>
);
} else if (setting.setting_key.includes('threshold')) {
// Percentage inputs
return (
<Input
type="number"
min="0"
max="1"
step="0.01"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.setting_value}
onChange={(e) => updateSetting(setting.setting_key, e.target.value)}
/>
);
} else {
// Default to number input for other settings
return (
<Input
type="number"
min="0"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.setting_value}
onChange={(e) => updateSetting(setting.setting_key, e.target.value)}
/>
);
}
};
// Group settings by their purpose
const abcSettings = settings.filter(s => s.setting_key.startsWith('abc_'));
const defaultSettings = settings.filter(s => s.setting_key.startsWith('default_'));
if (loading) {
return <div className="py-4">Loading settings...</div>;
}
return (
<div className="max-w-[700px] space-y-6">
<Card>
<CardHeader>
<CardTitle>ABC Classification Settings</CardTitle>
<CardDescription>Configure how products are classified into A, B, and C categories</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{abcSettings.map(setting => (
<div key={setting.setting_key} className="grid gap-2">
<Label htmlFor={setting.setting_key}>
{setting.description}
</Label>
{renderSettingInput(setting)}
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Default Settings</CardTitle>
<CardDescription>Configure system-wide default values</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{defaultSettings.map(setting => (
<div key={setting.setting_key} className="grid gap-2">
<Label htmlFor={setting.setting_key}>
{setting.description}
</Label>
{renderSettingInput(setting)}
</div>
))}
</div>
</CardContent>
</Card>
<div className="flex justify-end">
<Button onClick={handleSaveSettings}>
Save All Settings
</Button>
</div>
</div>
);
}

View File

@@ -1,291 +0,0 @@
import { useState, useEffect } 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';
import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table";
interface LeadTimeThreshold {
id: number;
cat_id: number | null;
vendor: string | null;
target_days: number;
warning_days: number;
critical_days: number;
}
interface ABCClassificationConfig {
id: number;
cat_id: number | null;
vendor: string | null;
a_threshold: number;
b_threshold: number;
classification_period_days: number;
}
interface TurnoverConfig {
id: number;
cat_id: number | null;
vendor: string | null;
calculation_period_days: number;
target_rate: number;
}
export function PerformanceMetrics() {
const [leadTimeThresholds, setLeadTimeThresholds] = useState<LeadTimeThreshold>({
id: 1,
cat_id: null,
vendor: null,
target_days: 14,
warning_days: 21,
critical_days: 30
});
const [abcConfigs, setAbcConfigs] = useState<ABCClassificationConfig[]>([]);
const [turnoverConfigs, setTurnoverConfigs] = useState<TurnoverConfig[]>([]);
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();
setLeadTimeThresholds(data.leadTimeThresholds);
setAbcConfigs(data.abcConfigs);
setTurnoverConfigs(data.turnoverConfigs);
} catch (error) {
toast.error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
loadConfig();
}, []);
const handleUpdateLeadTimeThresholds = async () => {
try {
const response = await fetch(`${config.apiUrl}/config/lead-time-thresholds/1`, {
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) {
toast.error(`Failed to update thresholds: ${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(abcConfigs)
});
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) {
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(turnoverConfigs)
});
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) {
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
function getCategoryName(cat_id: number): import("react").ReactNode {
// Simple implementation that just returns the ID as a string
return `Category ${cat_id}`;
}
return (
<div className="max-w-[700px] 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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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">
<Table>
<TableHeader>
<TableRow>
<TableCell>Category</TableCell>
<TableCell>Vendor</TableCell>
<TableCell className="text-right">A Threshold</TableCell>
<TableCell className="text-right">B Threshold</TableCell>
<TableCell className="text-right">Period Days</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{abcConfigs && abcConfigs.length > 0 ? abcConfigs.map((config) => (
<TableRow key={`${config.cat_id}-${config.vendor}`}>
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
<TableCell className="text-right">{config.a_threshold !== undefined ? `${config.a_threshold}%` : '0%'}</TableCell>
<TableCell className="text-right">{config.b_threshold !== undefined ? `${config.b_threshold}%` : '0%'}</TableCell>
<TableCell className="text-right">{config.classification_period_days || 0}</TableCell>
</TableRow>
)) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-4">No ABC configurations available</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<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">
<Table>
<TableHeader>
<TableRow>
<TableCell>Category</TableCell>
<TableCell>Vendor</TableCell>
<TableCell className="text-right">Period Days</TableCell>
<TableCell className="text-right">Target Rate</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{turnoverConfigs && turnoverConfigs.length > 0 ? turnoverConfigs.map((config) => (
<TableRow key={`${config.cat_id}-${config.vendor}`}>
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
<TableCell className="text-right">{config.calculation_period_days}</TableCell>
<TableCell className="text-right">
{config.target_rate !== undefined && config.target_rate !== null
? (typeof config.target_rate === 'number'
? config.target_rate.toFixed(2)
: (isNaN(parseFloat(String(config.target_rate)))
? '0.00'
: parseFloat(String(config.target_rate)).toFixed(2)))
: '0.00'}
</TableCell>
</TableRow>
)) : (
<TableRow>
<TableCell colSpan={4} className="text-center py-4">No turnover configurations available</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<Button onClick={handleUpdateTurnoverConfig}>
Update Turnover Configuration
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,322 @@
import { useState, useEffect, useCallback, useMemo } 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 { toast } from "sonner";
import config from '../../config';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Search } from 'lucide-react';
import { Switch } from "@/components/ui/switch";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
interface ProductSetting {
pid: string;
lead_time_days: number | null;
days_of_stock: number | null;
safety_stock: number;
forecast_method: string | null;
exclude_from_forecast: boolean;
updated_at: string;
product_name?: string; // Added for display purposes
}
export function ProductSettings() {
const [settings, setSettings] = useState<ProductSetting[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pageSize] = useState(50);
const [totalCount, setTotalCount] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const [pendingChanges, setPendingChanges] = useState<Record<string, boolean>>({});
// Use useCallback to avoid unnecessary re-renders
const loadSettings = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(`${config.apiUrl}/config/products?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchQuery)}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to load product settings');
}
const data = await response.json();
setSettings(data.items);
setTotalCount(data.total);
} catch (error) {
toast.error(`Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setLoading(false);
}
}, [page, searchQuery, pageSize]);
useEffect(() => {
loadSettings();
}, [loadSettings]);
const updateSetting = useCallback((pid: string, field: keyof ProductSetting, value: any) => {
setSettings(prev => prev.map(setting =>
setting.pid === pid ? { ...setting, [field]: value } : setting
));
setPendingChanges(prev => ({ ...prev, [pid]: true }));
}, []);
const handleSaveSetting = useCallback(async (pid: string) => {
try {
const setting = settings.find(s => s.pid === pid);
if (!setting) return;
const response = await fetch(`${config.apiUrl}/config/products/${pid}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
lead_time_days: setting.lead_time_days,
days_of_stock: setting.days_of_stock,
safety_stock: setting.safety_stock,
forecast_method: setting.forecast_method,
exclude_from_forecast: setting.exclude_from_forecast
})
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update product setting');
}
toast.success(`Settings updated for product ${pid}`);
setPendingChanges(prev => ({ ...prev, [pid]: false }));
} catch (error) {
toast.error(`Failed to update setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [settings]);
const handleResetToDefault = useCallback(async (pid: string) => {
try {
const response = await fetch(`${config.apiUrl}/config/products/${pid}/reset`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to reset product setting');
}
toast.success(`Settings reset for product ${pid}`);
loadSettings(); // Reload settings to get defaults
} catch (error) {
toast.error(`Failed to reset setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [loadSettings]);
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
// Generate page numbers for pagination
const paginationItems = useMemo(() => {
const pages = [];
const maxVisiblePages = 5;
// Always include first page
pages.push(1);
// Calculate range of visible pages
let startPage = Math.max(2, page - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 3);
// Adjust if we're near the end
if (endPage <= startPage) {
endPage = Math.min(totalPages - 1, startPage + 1);
}
// Add ellipsis after first page if needed
if (startPage > 2) {
pages.push('ellipsis1');
}
// Add visible pages
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
// Add ellipsis before last page if needed
if (endPage < totalPages - 1) {
pages.push('ellipsis2');
}
// Always include last page if it exists and is not already included
if (totalPages > 1) {
pages.push(totalPages);
}
return pages;
}, [page, totalPages]);
if (loading && settings.length === 0) {
return <div className="py-4">Loading settings...</div>;
}
return (
<div className="max-w-[900px] space-y-6">
<Card>
<CardHeader>
<CardTitle>Product-Specific Settings</CardTitle>
<CardDescription>Configure settings for individual products that override global defaults</CardDescription>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search products by ID or name..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px] rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Product ID</TableHead>
<TableHead>Lead Time (days)</TableHead>
<TableHead>Days of Stock</TableHead>
<TableHead>Safety Stock</TableHead>
<TableHead>Forecast Method</TableHead>
<TableHead>Exclude</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{settings.map(setting => (
<TableRow key={setting.pid}>
<TableCell>{setting.pid} {setting.product_name && <span className="text-muted-foreground text-xs block">{setting.product_name}</span>}</TableCell>
<TableCell>
<Input
type="number"
min="1"
className="w-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.lead_time_days ?? ''}
onChange={(e) => updateSetting(setting.pid, 'lead_time_days', e.target.value ? parseInt(e.target.value) : null)}
/>
</TableCell>
<TableCell>
<Input
type="number"
min="1"
className="w-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.days_of_stock ?? ''}
onChange={(e) => updateSetting(setting.pid, 'days_of_stock', e.target.value ? parseInt(e.target.value) : null)}
/>
</TableCell>
<TableCell>
<Input
type="number"
min="0"
className="w-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.safety_stock}
onChange={(e) => updateSetting(setting.pid, 'safety_stock', parseInt(e.target.value) || 0)}
/>
</TableCell>
<TableCell>
<Select
value={setting.forecast_method || 'default'}
onValueChange={(value) => updateSetting(setting.pid, 'forecast_method', value === 'default' ? null : value)}
>
<SelectTrigger className="w-28">
<SelectValue placeholder="Default" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="standard">Standard</SelectItem>
<SelectItem value="seasonal">Seasonal</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Switch
checked={setting.exclude_from_forecast}
onCheckedChange={(checked) => updateSetting(setting.pid, 'exclude_from_forecast', checked)}
/>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleSaveSetting(setting.pid)}
disabled={!pendingChanges[setting.pid]}
>
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleResetToDefault(setting.pid)}
>
Reset
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
{/* shadcn/ui Pagination */}
{totalPages > 1 && (
<div className="mt-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage(p => Math.max(1, p - 1))}
className={page === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{paginationItems.map((item, i) => (
typeof item === 'number' ? (
<PaginationItem key={i}>
<PaginationLink
onClick={() => setPage(item)}
isActive={page === item}
>
{item}
</PaginationLink>
</PaginationItem>
) : (
<PaginationItem key={i}>
<PaginationEllipsis />
</PaginationItem>
)
))}
<PaginationItem>
<PaginationNext
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className={page === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,248 +0,0 @@
import { useState, useEffect } 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;
cat_id: number | null;
vendor: string | null;
critical_days: number;
reorder_days: number;
overstock_days: number;
low_stock_threshold: number;
min_reorder_quantity: number;
}
interface SafetyStockConfig {
id: number;
cat_id: number | null;
vendor: string | null;
coverage_days: number;
service_level: number;
}
export function StockManagement() {
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 [safetyStockConfig, setSafetyStockConfig] = useState<SafetyStockConfig>({
id: 1,
cat_id: null,
vendor: null,
coverage_days: 14,
service_level: 95.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);
setSafetyStockConfig(data.safetyStockConfig);
} catch (error) {
toast.error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
loadConfig();
}, []);
const handleUpdateStockThresholds = async () => {
try {
const response = await fetch(`${config.apiUrl}/config/stock-thresholds/1`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(stockThresholds)
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update stock thresholds');
}
toast.success('Stock thresholds updated successfully');
} catch (error) {
toast.error(`Failed to update thresholds: ${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) {
toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
return (
<div className="max-w-[700px] 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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
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>
</div>
);
}

View File

@@ -0,0 +1,283 @@
import { useState, useEffect, useCallback, useMemo } 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';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Search } from 'lucide-react';
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { useDebounce } from '@/hooks/useDebounce';
interface VendorSetting {
vendor: string;
default_lead_time_days: number | null;
default_days_of_stock: number | null;
updated_at: string;
}
export function VendorSettings() {
const [settings, setSettings] = useState<VendorSetting[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pageSize] = useState(50);
const [totalCount, setTotalCount] = useState(0);
const [searchInputValue, setSearchInputValue] = useState('');
const searchQuery = useDebounce(searchInputValue, 300); // 300ms debounce
const [pendingChanges, setPendingChanges] = useState<Record<string, boolean>>({});
// Use useCallback to avoid unnecessary re-renders
const loadSettings = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(`${config.apiUrl}/config/vendors?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchQuery)}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to load vendor settings');
}
const data = await response.json();
setSettings(data.items);
setTotalCount(data.total);
} catch (error) {
toast.error(`Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setLoading(false);
}
}, [page, searchQuery, pageSize]);
useEffect(() => {
loadSettings();
}, [loadSettings]);
const updateSetting = useCallback((vendor: string, field: keyof VendorSetting, value: any) => {
setSettings(prev => prev.map(setting =>
setting.vendor === vendor ? { ...setting, [field]: value } : setting
));
setPendingChanges(prev => ({ ...prev, [vendor]: true }));
}, []);
const handleSaveSetting = useCallback(async (vendor: string) => {
try {
const setting = settings.find(s => s.vendor === vendor);
if (!setting) return;
const response = await fetch(`${config.apiUrl}/config/vendors/${encodeURIComponent(vendor)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
default_lead_time_days: setting.default_lead_time_days,
default_days_of_stock: setting.default_days_of_stock
})
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update vendor setting');
}
toast.success(`Settings updated for vendor ${vendor}`);
setPendingChanges(prev => ({ ...prev, [vendor]: false }));
} catch (error) {
toast.error(`Failed to update setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [settings]);
const handleResetToDefault = useCallback(async (vendor: string) => {
try {
const response = await fetch(`${config.apiUrl}/config/vendors/${encodeURIComponent(vendor)}/reset`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to reset vendor setting');
}
toast.success(`Settings reset for vendor ${vendor}`);
loadSettings(); // Reload settings to get defaults
} catch (error) {
toast.error(`Failed to reset setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [loadSettings]);
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
// Generate page numbers for pagination
const paginationItems = useMemo(() => {
const pages = [];
const maxVisiblePages = 5;
// Always include first page
pages.push(1);
// Calculate range of visible pages
let startPage = Math.max(2, page - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 3);
// Adjust if we're near the end
if (endPage <= startPage) {
endPage = Math.min(totalPages - 1, startPage + 1);
}
// Add ellipsis after first page if needed
if (startPage > 2) {
pages.push('ellipsis1');
}
// Add visible pages
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
// Add ellipsis before last page if needed
if (endPage < totalPages - 1) {
pages.push('ellipsis2');
}
// Always include last page if it exists and is not already included
if (totalPages > 1) {
pages.push(totalPages);
}
return pages;
}, [page, totalPages]);
if (loading && settings.length === 0) {
return <div className="py-4">Loading settings...</div>;
}
return (
<div className="max-w-[900px] space-y-6">
<Card>
<CardHeader>
<CardTitle>Vendor-Specific Settings</CardTitle>
<CardDescription>Configure default settings for products from specific vendors</CardDescription>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search vendors..."
className="pl-8"
value={searchInputValue}
onChange={(e) => setSearchInputValue(e.target.value)}
/>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px] rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Vendor</TableHead>
<TableHead>Default Lead Time (days)</TableHead>
<TableHead>Default Days of Stock</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{settings.map(setting => (
<TableRow key={setting.vendor}>
<TableCell>{setting.vendor}</TableCell>
<TableCell>
<Input
type="number"
min="1"
className="w-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.default_lead_time_days ?? ''}
onChange={(e) => updateSetting(setting.vendor, 'default_lead_time_days', e.target.value ? parseInt(e.target.value) : null)}
/>
</TableCell>
<TableCell>
<Input
type="number"
min="1"
className="w-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={setting.default_days_of_stock ?? ''}
onChange={(e) => updateSetting(setting.vendor, 'default_days_of_stock', e.target.value ? parseInt(e.target.value) : null)}
/>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleSaveSetting(setting.vendor)}
disabled={!pendingChanges[setting.vendor]}
>
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleResetToDefault(setting.vendor)}
>
Reset
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
{/* shadcn/ui Pagination */}
{totalPages > 1 && (
<div className="mt-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage(p => Math.max(1, p - 1))}
className={page === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{paginationItems.map((item, i) => (
typeof item === 'number' ? (
<PaginationItem key={i}>
<PaginationLink
onClick={() => setPage(item)}
isActive={page === item}
>
{item}
</PaginationLink>
</PaginationItem>
) : (
<PaginationItem key={i}>
<PaginationEllipsis />
</PaginationItem>
)
))}
<PaginationItem>
<PaginationNext
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className={page === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { useState, useEffect } from 'react';
/**
* A hook that returns a debounced value after the specified delay
* @param value The value to debounce
* @param delay The delay in milliseconds
* @returns The debounced value
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
// Update the debounced value after the specified delay
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Clean up the timeout on unmount or when value/delay changes
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}

View File

@@ -1,8 +1,8 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DataManagement } from "@/components/settings/DataManagement";
import { StockManagement } from "@/components/settings/StockManagement";
import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
import { CalculationSettings } from "@/components/settings/CalculationSettings";
import { GlobalSettings } from "@/components/settings/GlobalSettings";
import { ProductSettings } from "@/components/settings/ProductSettings";
import { VendorSettings } from "@/components/settings/VendorSettings";
import { TemplateManagement } from "@/components/settings/TemplateManagement";
import { UserManagement } from "@/components/settings/UserManagement";
import { PromptManagement } from "@/components/settings/PromptManagement";
@@ -33,9 +33,9 @@ const SETTINGS_GROUPS: SettingsGroup[] = [
id: "inventory",
label: "Inventory Settings",
tabs: [
{ id: "stock-management", permission: "settings:stock_management", label: "Stock Management" },
{ id: "performance-metrics", permission: "settings:performance_metrics", label: "Performance Metrics" },
{ id: "calculation-settings", permission: "settings:calculation_settings", label: "Calculation Settings" },
{ id: "global-settings", permission: "settings:global", label: "Global Settings" },
{ id: "product-settings", permission: "settings:products", label: "Product Settings" },
{ id: "vendor-settings", permission: "settings:vendors", label: "Vendor Settings" },
]
},
{
@@ -160,48 +160,48 @@ export function Settings() {
</Protected>
</TabsContent>
<TabsContent value="stock-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<TabsContent value="global-settings" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected
permission="settings:stock_management"
permission="settings:global"
fallback={
<Alert>
<AlertDescription>
You don't have permission to access Stock Management.
You don't have permission to access Global Settings.
</AlertDescription>
</Alert>
}
>
<StockManagement />
<GlobalSettings />
</Protected>
</TabsContent>
<TabsContent value="performance-metrics" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<TabsContent value="product-settings" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected
permission="settings:performance_metrics"
permission="settings:products"
fallback={
<Alert>
<AlertDescription>
You don't have permission to access Performance Metrics.
You don't have permission to access Product Settings.
</AlertDescription>
</Alert>
}
>
<PerformanceMetrics />
<ProductSettings />
</Protected>
</TabsContent>
<TabsContent value="calculation-settings" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<TabsContent value="vendor-settings" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Protected
permission="settings:calculation_settings"
permission="settings:vendors"
fallback={
<Alert>
<AlertDescription>
You don't have permission to access Calculation Settings.
You don't have permission to access Vendor Settings.
</AlertDescription>
</Alert>
}
>
<CalculationSettings />
<VendorSettings />
</Protected>
</TabsContent>