Add new dashboard frontend

This commit is contained in:
2025-01-12 19:29:16 -05:00
parent 35105f9474
commit 024155d054
9 changed files with 1170 additions and 213 deletions

View File

@@ -26,6 +26,8 @@
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-virtual": "^3.11.2",
"@tanstack/virtual-core": "^3.11.2",
"chart.js": "^4.4.7",
"chartjs-adapter-date-fns": "^3.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -33,6 +35,7 @@
"lucide-react": "^0.469.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.1",
@@ -1117,6 +1120,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3210,6 +3219,28 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chart.js": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chartjs-adapter-date-fns": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
"license": "MIT",
"peerDependencies": {
"chart.js": ">=2.8.0",
"date-fns": ">=2.0.0"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -5770,6 +5801,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-day-picker": {
"version": "8.10.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",

View File

@@ -28,6 +28,8 @@
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-virtual": "^3.11.2",
"@tanstack/virtual-core": "^3.11.2",
"chart.js": "^4.4.7",
"chartjs-adapter-date-fns": "^3.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -35,6 +37,7 @@
"lucide-react": "^0.469.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.1",

View File

@@ -0,0 +1,117 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Pie } from "react-chartjs-2"
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
import { useQuery } from "@tanstack/react-query"
ChartJS.register(ArcElement, Tooltip, Legend)
interface InventoryHealthData {
critical: number
reorder: number
healthy: number
overstock: number
}
export function InventoryHealthSummary() {
const { data, isLoading } = useQuery<InventoryHealthData>({
queryKey: ['inventoryHealth'],
queryFn: async () => {
const response = await fetch('/api/inventory/health/summary')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
})
const chartData = {
labels: ['Critical', 'Reorder', 'Healthy', 'Overstock'],
datasets: [
{
data: [
data?.critical || 0,
data?.reorder || 0,
data?.healthy || 0,
data?.overstock || 0
],
backgroundColor: [
'rgb(239, 68, 68)', // red-500
'rgb(234, 179, 8)', // yellow-500
'rgb(34, 197, 94)', // green-500
'rgb(59, 130, 246)', // blue-500
],
borderColor: [
'rgb(239, 68, 68, 0.2)',
'rgb(234, 179, 8, 0.2)',
'rgb(34, 197, 94, 0.2)',
'rgb(59, 130, 246, 0.2)',
],
borderWidth: 1,
},
],
}
const options = {
responsive: true,
plugins: {
legend: {
position: 'right' as const,
},
},
}
const total = data ? data.critical + data.reorder + data.healthy + data.overstock : 0
return (
<Card className="col-span-4">
<CardHeader>
<CardTitle>Inventory Health</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="h-[200px] flex items-center justify-center">
{!isLoading && <Pie data={chartData} options={options} />}
</div>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-sm font-medium">Critical</span>
<span className="text-2xl font-bold text-red-500">
{data?.critical || 0}
</span>
<span className="text-xs text-muted-foreground">
{total ? Math.round((data?.critical / total) * 100) : 0}% of total
</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-medium">Reorder</span>
<span className="text-2xl font-bold text-yellow-500">
{data?.reorder || 0}
</span>
<span className="text-xs text-muted-foreground">
{total ? Math.round((data?.reorder / total) * 100) : 0}% of total
</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-medium">Healthy</span>
<span className="text-2xl font-bold text-green-500">
{data?.healthy || 0}
</span>
<span className="text-xs text-muted-foreground">
{total ? Math.round((data?.healthy / total) * 100) : 0}% of total
</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-medium">Overstock</span>
<span className="text-2xl font-bold text-blue-500">
{data?.overstock || 0}
</span>
<span className="text-xs text-muted-foreground">
{total ? Math.round((data?.overstock / total) * 100) : 0}% of total
</span>
</div>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,135 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useQuery } from "@tanstack/react-query"
import { Line } from "react-chartjs-2"
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
} from 'chart.js'
import 'chartjs-adapter-date-fns'
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
)
interface TimeSeriesData {
date: string
revenue: number
cost: number
inventory_value: number
}
export function KeyMetricsCharts() {
const { data, isLoading } = useQuery<TimeSeriesData[]>({
queryKey: ['keyMetrics'],
queryFn: async () => {
const response = await fetch('/api/metrics/timeseries')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
})
const revenueVsCostData = {
labels: data?.map(d => d.date),
datasets: [
{
label: 'Revenue',
data: data?.map(d => d.revenue),
borderColor: 'rgb(34, 197, 94)', // green-500
backgroundColor: 'rgba(34, 197, 94, 0.5)',
tension: 0.3,
},
{
label: 'Cost',
data: data?.map(d => d.cost),
borderColor: 'rgb(239, 68, 68)', // red-500
backgroundColor: 'rgba(239, 68, 68, 0.5)',
tension: 0.3,
}
]
}
const inventoryValueData = {
labels: data?.map(d => d.date),
datasets: [
{
label: 'Inventory Value',
data: data?.map(d => d.inventory_value),
borderColor: 'rgb(59, 130, 246)', // blue-500
backgroundColor: 'rgba(59, 130, 246, 0.5)',
tension: 0.3,
fill: true,
}
]
}
const options = {
responsive: true,
interaction: {
mode: 'index' as const,
intersect: false,
},
scales: {
x: {
type: 'time' as const,
time: {
unit: 'month' as const,
},
title: {
display: true,
text: 'Date'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Amount ($)'
}
}
}
}
return (
<Card className="col-span-8">
<CardHeader>
<CardTitle>Key Financial Metrics</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="h-[200px]">
{!isLoading && (
<Line
data={revenueVsCostData}
options={options}
/>
)}
</div>
<div className="h-[200px]">
{!isLoading && (
<Line
data={inventoryValueData}
options={options}
/>
)}
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,93 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useQuery } from "@tanstack/react-query"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Link } from "react-router-dom"
interface StockAlert {
product_id: number
sku: string
title: string
stock_quantity: number
daily_sales_avg: number
days_of_inventory: number
reorder_point: number
stock_status: 'Critical' | 'Reorder'
}
export function StockAlerts() {
const { data, isLoading } = useQuery<StockAlert[]>({
queryKey: ['stockAlerts'],
queryFn: async () => {
const response = await fetch('/api/inventory/alerts')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
})
return (
<Card className="col-span-8">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Low Stock Alerts</CardTitle>
<Button asChild>
<Link to="/inventory/replenishment">View All</Link>
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>SKU</TableHead>
<TableHead>Title</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Stock</TableHead>
<TableHead className="text-right">Daily Sales</TableHead>
<TableHead className="text-right">Days Left</TableHead>
<TableHead className="text-right">Reorder Point</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{!isLoading && data?.map((alert) => (
<TableRow key={alert.product_id}>
<TableCell className="font-medium">
<Link
to={`/products/${alert.product_id}`}
className="text-blue-500 hover:underline"
>
{alert.sku}
</Link>
</TableCell>
<TableCell>{alert.title}</TableCell>
<TableCell>
<Badge
variant={alert.stock_status === 'Critical' ? 'destructive' : 'warning'}
>
{alert.stock_status}
</Badge>
</TableCell>
<TableCell className="text-right">{alert.stock_quantity}</TableCell>
<TableCell className="text-right">
{alert.daily_sales_avg.toFixed(1)}
</TableCell>
<TableCell className="text-right">
{alert.days_of_inventory.toFixed(1)}
</TableCell>
<TableCell className="text-right">{alert.reorder_point}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}

View File

@@ -1,71 +1,76 @@
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import config from '../../config';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useQuery } from "@tanstack/react-query"
import { Link } from "react-router-dom"
import { ArrowUpIcon, ArrowDownIcon } from "lucide-react"
interface TrendingProduct {
product_id: string;
title: string;
sku: string;
total_sales: number;
sales_growth: number;
stock_quantity: number;
image_url: string;
product_id: number
sku: string
title: string
daily_sales_avg: number
weekly_sales_avg: number
growth_rate: number // Percentage growth week over week
total_revenue: number
}
export function TrendingProducts() {
const { data, isLoading, error } = useQuery<TrendingProduct[]>({
queryKey: ['trending-products'],
const { data, isLoading } = useQuery<TrendingProduct[]>({
queryKey: ['trendingProducts'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/trending-products`);
const response = await fetch('/api/products/trending')
if (!response.ok) {
throw new Error('Failed to fetch trending products');
throw new Error('Network response was not ok')
}
return response.json();
},
});
if (isLoading) {
return <div>Loading trending products...</div>;
}
if (error) {
return <div className="text-red-500">Error loading trending products</div>;
return response.json()
}
})
return (
<div className="space-y-4">
{data?.map((product) => (
<Card key={product.product_id}>
<CardContent className="p-4">
<div className="flex items-center">
{product.image_url && (
<img
src={product.image_url}
alt={product.title}
className="h-12 w-12 rounded-md object-cover mr-4"
/>
<Card className="col-span-4">
<CardHeader>
<CardTitle>Trending Products</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-8">
{!isLoading && data?.map((product) => (
<div key={product.product_id} className="flex items-center">
<div className="space-y-1 flex-1">
<Link
to={`/products/${product.product_id}`}
className="text-sm font-medium leading-none hover:underline"
>
{product.sku}
</Link>
<p className="text-sm text-muted-foreground line-clamp-1">
{product.title}
</p>
</div>
<div className="ml-auto font-medium text-right space-y-1">
<div className="flex items-center justify-end gap-2">
<span className="text-sm">
{product.daily_sales_avg.toFixed(1)}/day
</span>
<div className={`flex items-center ${
product.growth_rate >= 0 ? 'text-green-500' : 'text-red-500'
}`}>
{product.growth_rate >= 0 ? (
<ArrowUpIcon className="h-4 w-4" />
) : (
<ArrowDownIcon className="h-4 w-4" />
)}
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">{product.title}</p>
<p className="text-sm text-muted-foreground">SKU: {product.sku}</p>
<div className="flex items-center pt-2">
<Progress value={Math.min(100, product.sales_growth)} className="h-2" />
<span className="ml-2 text-sm text-muted-foreground">
{product.sales_growth > 0 ? '+' : ''}{product.sales_growth}% growth
<span className="text-xs">
{Math.abs(product.growth_rate).toFixed(1)}%
</span>
</div>
</div>
<div className="ml-4 text-right">
<p className="text-sm font-medium">${product.total_sales.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">
{product.stock_quantity} in stock
<p className="text-xs text-muted-foreground">
${product.total_revenue.toLocaleString()}
</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
);
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,77 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useQuery } from "@tanstack/react-query"
import { Progress } from "@/components/ui/progress"
interface VendorMetrics {
vendor: string
avg_lead_time_days: number
on_time_delivery_rate: number
order_fill_rate: number
total_orders: number
total_late_orders: number
}
export function VendorPerformance() {
const { data, isLoading } = useQuery<VendorMetrics[]>({
queryKey: ['vendorMetrics'],
queryFn: async () => {
const response = await fetch('/api/vendors/metrics')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
})
// Sort vendors by on-time delivery rate
const sortedVendors = data?.sort((a, b) =>
b.on_time_delivery_rate - a.on_time_delivery_rate
).slice(0, 5)
return (
<Card className="col-span-4">
<CardHeader>
<CardTitle>Top Vendor Performance</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-8">
{!isLoading && sortedVendors?.map((vendor) => (
<div key={vendor.vendor} className="space-y-2">
<div className="flex items-center">
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
{vendor.vendor}
</p>
<p className="text-sm text-muted-foreground">
{vendor.total_orders} orders, avg {vendor.avg_lead_time_days.toFixed(1)} days
</p>
</div>
<div className="ml-auto font-medium">
{vendor.on_time_delivery_rate.toFixed(1)}%
</div>
</div>
<div className="space-y-1">
<div className="flex items-center text-xs text-muted-foreground justify-between">
<span>On-time Delivery</span>
<span>{vendor.on_time_delivery_rate.toFixed(1)}%</span>
</div>
<Progress
value={vendor.on_time_delivery_rate}
className="h-1"
/>
<div className="flex items-center text-xs text-muted-foreground justify-between">
<span>Order Fill Rate</span>
<span>{vendor.order_fill_rate.toFixed(1)}%</span>
</div>
<Progress
value={vendor.order_fill_rate}
className="h-1"
/>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,630 @@
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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;
low_stock_threshold: number;
min_reorder_quantity: number;
category_name?: string;
threshold_scope?: string;
}
interface LeadTimeThreshold {
id: number;
category_id: number | null;
vendor: string | null;
target_days: number;
warning_days: number;
critical_days: number;
}
interface SalesVelocityConfig {
id: number;
category_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;
category_id: number | null;
vendor: string | null;
coverage_days: number;
service_level: number;
}
interface TurnoverConfig {
id: number;
category_id: number | null;
vendor: string | null;
calculation_period_days: number;
target_rate: number;
}
export function Configuration() {
const [stockThresholds, setStockThresholds] = useState<StockThreshold>({
id: 1,
category_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,
category_id: null,
vendor: null,
target_days: 14,
warning_days: 21,
critical_days: 30
});
const [salesVelocityConfig, setSalesVelocityConfig] = useState<SalesVelocityConfig>({
id: 1,
category_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,
category_id: null,
vendor: null,
coverage_days: 14,
service_level: 95.0
});
const [turnoverConfig, setTurnoverConfig] = useState<TurnoverConfig>({
id: 1,
category_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: ${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 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 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'}`);
}
};
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) {
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) {
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) {
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

@@ -1,166 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Overview } from '@/components/dashboard/Overview';
import { RecentSales } from '@/components/dashboard/RecentSales';
import { InventoryStats } from '@/components/dashboard/InventoryStats';
import { SalesByCategory } from '@/components/dashboard/SalesByCategory';
import { TrendingProducts } from '@/components/dashboard/TrendingProducts';
import config from '../config';
interface DashboardStats {
totalProducts: number;
lowStockProducts: number;
totalOrders: number;
averageOrderValue: number;
totalRevenue: number;
profitMargin: number;
}
import { InventoryHealthSummary } from "@/components/dashboard/InventoryHealthSummary"
import { StockAlerts } from "@/components/dashboard/StockAlerts"
import { TrendingProducts } from "@/components/dashboard/TrendingProducts"
import { VendorPerformance } from "@/components/dashboard/VendorPerformance"
import { KeyMetricsCharts } from "@/components/dashboard/KeyMetricsCharts"
export function Dashboard() {
const { data: stats, isLoading: statsLoading } = useQuery<DashboardStats>({
queryKey: ['dashboard-stats'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/stats`);
if (!response.ok) {
throw new Error('Failed to fetch dashboard stats');
}
const data = await response.json();
return {
...data,
averageOrderValue: parseFloat(data.averageOrderValue) || 0,
totalRevenue: parseFloat(data.totalRevenue) || 0,
profitMargin: parseFloat(data.profitMargin) || 0
};
},
});
if (statsLoading) {
return <div className="p-8">Loading dashboard...</div>;
}
return (
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList className="grid w-full grid-cols-2 lg:w-[400px]">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="inventory">Inventory</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Revenue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
${(stats?.totalRevenue || 0).toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Orders
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.totalOrders || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Average Order Value
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
${(stats?.averageOrderValue || 0).toFixed(2)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Profit Margin
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{(stats?.profitMargin || 0).toFixed(1)}%
</div>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Sales Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<Overview />
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
</CardHeader>
<CardContent>
<RecentSales />
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Sales by Category</CardTitle>
</CardHeader>
<CardContent>
<SalesByCategory />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Trending Products</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-12">
<InventoryHealthSummary />
<VendorPerformance />
<KeyMetricsCharts />
<StockAlerts />
<TrendingProducts />
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="inventory" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Products
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.totalProducts || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Low Stock Products
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.lowStockProducts || 0}</div>
</CardContent>
</Card>
</div>
<InventoryStats />
</TabsContent>
</Tabs>
</div>
);
)
}