Add new dashboard frontend
This commit is contained in:
41
inventory/package-lock.json
generated
41
inventory/package-lock.json
generated
@@ -26,6 +26,8 @@
|
|||||||
"@tanstack/react-query": "^5.63.0",
|
"@tanstack/react-query": "^5.63.0",
|
||||||
"@tanstack/react-virtual": "^3.11.2",
|
"@tanstack/react-virtual": "^3.11.2",
|
||||||
"@tanstack/virtual-core": "^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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
@@ -1117,6 +1120,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"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"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -5770,6 +5801,16 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-day-picker": {
|
||||||
"version": "8.10.1",
|
"version": "8.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"@tanstack/react-query": "^5.63.0",
|
"@tanstack/react-query": "^5.63.0",
|
||||||
"@tanstack/react-virtual": "^3.11.2",
|
"@tanstack/react-virtual": "^3.11.2",
|
||||||
"@tanstack/virtual-core": "^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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
|
|||||||
117
inventory/src/components/dashboard/InventoryHealthSummary.tsx
Normal file
117
inventory/src/components/dashboard/InventoryHealthSummary.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
135
inventory/src/components/dashboard/KeyMetricsCharts.tsx
Normal file
135
inventory/src/components/dashboard/KeyMetricsCharts.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
93
inventory/src/components/dashboard/StockAlerts.tsx
Normal file
93
inventory/src/components/dashboard/StockAlerts.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,71 +1,76 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Link } from "react-router-dom"
|
||||||
import config from '../../config';
|
import { ArrowUpIcon, ArrowDownIcon } from "lucide-react"
|
||||||
|
|
||||||
interface TrendingProduct {
|
interface TrendingProduct {
|
||||||
product_id: string;
|
product_id: number
|
||||||
title: string;
|
sku: string
|
||||||
sku: string;
|
title: string
|
||||||
total_sales: number;
|
daily_sales_avg: number
|
||||||
sales_growth: number;
|
weekly_sales_avg: number
|
||||||
stock_quantity: number;
|
growth_rate: number // Percentage growth week over week
|
||||||
image_url: string;
|
total_revenue: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TrendingProducts() {
|
export function TrendingProducts() {
|
||||||
const { data, isLoading, error } = useQuery<TrendingProduct[]>({
|
const { data, isLoading } = useQuery<TrendingProduct[]>({
|
||||||
queryKey: ['trending-products'],
|
queryKey: ['trendingProducts'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await fetch(`${config.apiUrl}/dashboard/trending-products`);
|
const response = await fetch('/api/products/trending')
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch trending products');
|
throw new Error('Network response was not ok')
|
||||||
}
|
}
|
||||||
return response.json();
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<Card className="col-span-4">
|
||||||
{data?.map((product) => (
|
<CardHeader>
|
||||||
<Card key={product.product_id}>
|
<CardTitle>Trending Products</CardTitle>
|
||||||
<CardContent className="p-4">
|
</CardHeader>
|
||||||
<div className="flex items-center">
|
<CardContent>
|
||||||
{product.image_url && (
|
<div className="space-y-8">
|
||||||
<img
|
{!isLoading && data?.map((product) => (
|
||||||
src={product.image_url}
|
<div key={product.product_id} className="flex items-center">
|
||||||
alt={product.title}
|
<div className="space-y-1 flex-1">
|
||||||
className="h-12 w-12 rounded-md object-cover mr-4"
|
<Link
|
||||||
/>
|
to={`/products/${product.product_id}`}
|
||||||
)}
|
className="text-sm font-medium leading-none hover:underline"
|
||||||
<div className="flex-1 space-y-1">
|
>
|
||||||
<p className="text-sm font-medium leading-none">{product.title}</p>
|
{product.sku}
|
||||||
<p className="text-sm text-muted-foreground">SKU: {product.sku}</p>
|
</Link>
|
||||||
<div className="flex items-center pt-2">
|
<p className="text-sm text-muted-foreground line-clamp-1">
|
||||||
<Progress value={Math.min(100, product.sales_growth)} className="h-2" />
|
{product.title}
|
||||||
<span className="ml-2 text-sm text-muted-foreground">
|
</p>
|
||||||
{product.sales_growth > 0 ? '+' : ''}{product.sales_growth}% growth
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 text-right">
|
<div className="ml-auto font-medium text-right space-y-1">
|
||||||
<p className="text-sm font-medium">${product.total_sales.toLocaleString()}</p>
|
<div className="flex items-center justify-end gap-2">
|
||||||
<p className="text-sm text-muted-foreground">
|
<span className="text-sm">
|
||||||
{product.stock_quantity} in stock
|
{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" />
|
||||||
|
)}
|
||||||
|
<span className="text-xs">
|
||||||
|
{Math.abs(product.growth_rate).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
${product.total_revenue.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
))}
|
||||||
</Card>
|
</div>
|
||||||
))}
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
77
inventory/src/components/dashboard/VendorPerformance.tsx
Normal file
77
inventory/src/components/dashboard/VendorPerformance.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
630
inventory/src/components/settings/Configuration.tsx
Normal file
630
inventory/src/components/settings/Configuration.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,166 +1,22 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { InventoryHealthSummary } from "@/components/dashboard/InventoryHealthSummary"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { StockAlerts } from "@/components/dashboard/StockAlerts"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { TrendingProducts } from "@/components/dashboard/TrendingProducts"
|
||||||
import { Overview } from '@/components/dashboard/Overview';
|
import { VendorPerformance } from "@/components/dashboard/VendorPerformance"
|
||||||
import { RecentSales } from '@/components/dashboard/RecentSales';
|
import { KeyMetricsCharts } from "@/components/dashboard/KeyMetricsCharts"
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Dashboard() {
|
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 (
|
return (
|
||||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||||
<div className="flex items-center justify-between space-y-2">
|
<div className="flex items-center justify-between space-y-2">
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
|
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="overview" className="space-y-4">
|
<div className="grid gap-4 md:grid-cols-12">
|
||||||
<TabsList className="grid w-full grid-cols-2 lg:w-[400px]">
|
<InventoryHealthSummary />
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<VendorPerformance />
|
||||||
<TabsTrigger value="inventory">Inventory</TabsTrigger>
|
<KeyMetricsCharts />
|
||||||
</TabsList>
|
<StockAlerts />
|
||||||
<TabsContent value="overview" className="space-y-4">
|
<TrendingProducts />
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
</div>
|
||||||
<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>
|
|
||||||
<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>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user