Add new dashboard backend

This commit is contained in:
2025-01-13 00:14:15 -05:00
parent 024155d054
commit 88c51059bb
14 changed files with 1085 additions and 727 deletions

View File

@@ -1,117 +1,77 @@
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"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertCircle, AlertTriangle, CheckCircle2, PackageSearch } from "lucide-react"
import config from "@/config"
ChartJS.register(ArcElement, Tooltip, Legend)
interface InventoryHealthData {
interface InventoryHealth {
critical: number
reorder: number
healthy: number
overstock: number
total: number
}
export function InventoryHealthSummary() {
const { data, isLoading } = useQuery<InventoryHealthData>({
queryKey: ['inventoryHealth'],
const { data: summary } = useQuery<InventoryHealth>({
queryKey: ["inventory-health"],
queryFn: async () => {
const response = await fetch('/api/inventory/health/summary')
const response = await fetch(`${config.apiUrl}/dashboard/inventory/health/summary`)
if (!response.ok) {
throw new Error('Network response was not ok')
throw new Error("Failed to fetch inventory health")
}
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 stats = [
{
title: "Critical Stock",
value: summary?.critical || 0,
description: "Products needing immediate attention",
icon: AlertCircle,
className: "bg-destructive/10",
iconClassName: "text-destructive",
},
}
const total = data ? data.critical + data.reorder + data.healthy + data.overstock : 0
{
title: "Reorder Soon",
value: summary?.reorder || 0,
description: "Products approaching reorder point",
icon: AlertTriangle,
className: "bg-warning/10",
iconClassName: "text-warning",
},
{
title: "Healthy Stock",
value: summary?.healthy || 0,
description: "Products at optimal levels",
icon: CheckCircle2,
className: "bg-success/10",
iconClassName: "text-success",
},
{
title: "Overstock",
value: summary?.overstock || 0,
description: "Products exceeding optimal levels",
icon: PackageSearch,
className: "bg-muted",
iconClassName: "text-muted-foreground",
},
]
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>
<>
{stats.map((stat) => (
<Card key={stat.title} className={stat.className}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
<stat.icon className={`h-4 w-4 ${stat.iconClassName}`} />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">{stat.description}</p>
</CardContent>
</Card>
))}
</>
)
}

View File

@@ -1,135 +1,232 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useQuery } from "@tanstack/react-query"
import { Line } from "react-chartjs-2"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
Legend,
TimeScale
} from 'chart.js'
import 'chartjs-adapter-date-fns'
XAxis,
YAxis,
} from "recharts"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import config from "@/config"
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
)
interface TimeSeriesData {
interface MetricDataPoint {
date: string
revenue: number
cost: number
inventory_value: number
value: number
}
interface KeyMetrics {
revenue: MetricDataPoint[]
inventory_value: MetricDataPoint[]
gmroi: MetricDataPoint[]
}
export function KeyMetricsCharts() {
const { data, isLoading } = useQuery<TimeSeriesData[]>({
queryKey: ['keyMetrics'],
const { data: metrics } = useQuery<KeyMetrics>({
queryKey: ["key-metrics"],
queryFn: async () => {
const response = await fetch('/api/metrics/timeseries')
const response = await fetch(`${config.apiUrl}/metrics/trends`)
if (!response.ok) {
throw new Error('Network response was not ok')
throw new Error("Failed to fetch metrics trends")
}
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 ($)'
}
}
}
}
const formatCurrency = (value: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value)
return (
<Card className="col-span-8">
<>
<CardHeader>
<CardTitle>Key Financial Metrics</CardTitle>
<CardTitle className="text-lg font-medium">Key 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>
<Tabs defaultValue="revenue" className="space-y-4">
<TabsList>
<TabsTrigger value="revenue">Revenue</TabsTrigger>
<TabsTrigger value="inventory">Inventory Value</TabsTrigger>
<TabsTrigger value="gmroi">GMROI</TabsTrigger>
</TabsList>
<TabsContent value="revenue" className="space-y-4">
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={metrics?.revenue}>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickFormatter={(value) => value}
/>
<YAxis
tickLine={false}
axisLine={false}
tickFormatter={formatCurrency}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Date
</span>
<span className="font-bold">
{payload[0].payload.date}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Revenue
</span>
<span className="font-bold">
{formatCurrency(payload[0].value as number)}
</span>
</div>
</div>
</div>
)
}
return null
}}
/>
<Area
type="monotone"
dataKey="value"
stroke="#0ea5e9"
fill="#0ea5e9"
fillOpacity={0.2}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</TabsContent>
<TabsContent value="inventory" className="space-y-4">
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={metrics?.inventory_value}>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickFormatter={(value) => value}
/>
<YAxis
tickLine={false}
axisLine={false}
tickFormatter={formatCurrency}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Date
</span>
<span className="font-bold">
{payload[0].payload.date}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Value
</span>
<span className="font-bold">
{formatCurrency(payload[0].value as number)}
</span>
</div>
</div>
</div>
)
}
return null
}}
/>
<Area
type="monotone"
dataKey="value"
stroke="#84cc16"
fill="#84cc16"
fillOpacity={0.2}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</TabsContent>
<TabsContent value="gmroi" className="space-y-4">
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={metrics?.gmroi}>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickFormatter={(value) => value}
/>
<YAxis
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value.toFixed(1)}%`}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Date
</span>
<span className="font-bold">
{payload[0].payload.date}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
GMROI
</span>
<span className="font-bold">
{`${typeof payload[0].value === 'number' ? payload[0].value.toFixed(1) : payload[0].value}%`}
</span>
</div>
</div>
</div>
)
}
return null
}}
/>
<Area
type="monotone"
dataKey="value"
stroke="#f59e0b"
fill="#f59e0b"
fillOpacity={0.2}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</>
)
}

View File

@@ -0,0 +1,87 @@
import { useQuery } from "@tanstack/react-query"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { AlertCircle, AlertTriangle } from "lucide-react"
import config from "@/config"
interface LowStockProduct {
product_id: number
sku: string
title: string
stock_quantity: number
reorder_point: number
days_of_inventory: number
stock_status: "Critical" | "Reorder"
daily_sales_avg: number
}
export function LowStockAlerts() {
const { data: products } = useQuery<LowStockProduct[]>({
queryKey: ["low-stock"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/inventory/low-stock`)
if (!response.ok) {
throw new Error("Failed to fetch low stock products")
}
return response.json()
},
})
return (
<>
<CardHeader>
<CardTitle className="text-lg font-medium">Low Stock Alerts</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-[350px] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>SKU</TableHead>
<TableHead>Product</TableHead>
<TableHead className="text-right">Stock</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products?.map((product) => (
<TableRow key={product.product_id}>
<TableCell className="font-medium">{product.sku}</TableCell>
<TableCell>{product.title}</TableCell>
<TableCell className="text-right">
{product.stock_quantity} / {product.reorder_point}
</TableCell>
<TableCell className="text-right">
<Badge
variant="outline"
className={
product.stock_status === "Critical"
? "border-destructive text-destructive"
: "border-warning text-warning"
}
>
{product.stock_status === "Critical" ? (
<AlertCircle className="mr-1 h-3 w-3" />
) : (
<AlertTriangle className="mr-1 h-3 w-3" />
)}
{product.stock_status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</>
)
}

View File

@@ -1,93 +0,0 @@
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,76 +1,96 @@
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"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { TrendingUp, TrendingDown } from "lucide-react"
import config from "@/config"
interface TrendingProduct {
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
daily_sales_avg: string
weekly_sales_avg: string
growth_rate: string
total_revenue: string
}
export function TrendingProducts() {
const { data, isLoading } = useQuery<TrendingProduct[]>({
queryKey: ['trendingProducts'],
const { data: products } = useQuery<TrendingProduct[]>({
queryKey: ["trending-products"],
queryFn: async () => {
const response = await fetch('/api/products/trending')
const response = await fetch(`${config.apiUrl}/dashboard/products/trending`)
if (!response.ok) {
throw new Error('Network response was not ok')
throw new Error("Failed to fetch trending products")
}
return response.json()
}
},
})
const formatPercent = (value: number) =>
new Intl.NumberFormat("en-US", {
style: "percent",
minimumFractionDigits: 1,
maximumFractionDigits: 1,
signDisplay: "exceptZero",
}).format(value / 100)
return (
<Card className="col-span-4">
<>
<CardHeader>
<CardTitle>Trending Products</CardTitle>
<CardTitle className="text-lg font-medium">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" />
)}
<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>
</div>
</div>
))}
<div className="max-h-[400px] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Daily Sales</TableHead>
<TableHead className="text-right">Growth</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products?.map((product) => (
<TableRow key={product.product_id}>
<TableCell className="font-medium">
<div className="flex flex-col">
<span className="font-medium">{product.title}</span>
<span className="text-sm text-muted-foreground">
{product.sku}
</span>
</div>
</TableCell>
<TableCell>{parseFloat(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{parseFloat(product.growth_rate) > 0 ? (
<TrendingUp className="h-4 w-4 text-success" />
) : (
<TrendingDown className="h-4 w-4 text-destructive" />
)}
<span
className={
parseFloat(product.growth_rate) > 0 ? "text-success" : "text-destructive"
}
>
{formatPercent(parseFloat(product.growth_rate))}
</span>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</>
)
}

View File

@@ -1,6 +1,15 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useQuery } from "@tanstack/react-query"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Progress } from "@/components/ui/progress"
import config from "@/config"
interface VendorMetrics {
vendor: string
@@ -12,66 +21,58 @@ interface VendorMetrics {
}
export function VendorPerformance() {
const { data, isLoading } = useQuery<VendorMetrics[]>({
queryKey: ['vendorMetrics'],
const { data: vendors } = useQuery<VendorMetrics[]>({
queryKey: ["vendor-metrics"],
queryFn: async () => {
const response = await fetch('/api/vendors/metrics')
const response = await fetch(`${config.apiUrl}/dashboard/vendors/metrics`)
if (!response.ok) {
throw new Error('Network response was not ok')
throw new Error("Failed to fetch vendor metrics")
}
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)
const sortedVendors = vendors
?.sort((a, b) => b.on_time_delivery_rate - a.on_time_delivery_rate)
return (
<Card className="col-span-4">
<>
<CardHeader>
<CardTitle>Top Vendor Performance</CardTitle>
<CardTitle className="text-lg font-medium">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 className="max-h-[400px] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Vendor</TableHead>
<TableHead>On-Time</TableHead>
<TableHead className="text-right">Fill Rate</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedVendors?.map((vendor) => (
<TableRow key={vendor.vendor}>
<TableCell className="font-medium">{vendor.vendor}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress
value={vendor.on_time_delivery_rate}
className="h-2"
/>
<span className="w-10 text-sm">
{vendor.on_time_delivery_rate.toFixed(0)}%
</span>
</div>
</TableCell>
<TableCell className="text-right">
{vendor.order_fill_rate.toFixed(0)}%
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</>
)
}

View File

@@ -1,4 +1,4 @@
const isDev = process.env.NODE_ENV === 'development';
const isDev = import.meta.env.DEV;
const config = {
apiUrl: isDev ? '/api' : 'https://inventory.kent.pw/api',

View File

@@ -1,22 +1,37 @@
import { Card } from "@/components/ui/card"
import { InventoryHealthSummary } from "@/components/dashboard/InventoryHealthSummary"
import { StockAlerts } from "@/components/dashboard/StockAlerts"
import { LowStockAlerts } from "@/components/dashboard/LowStockAlerts"
import { TrendingProducts } from "@/components/dashboard/TrendingProducts"
import { VendorPerformance } from "@/components/dashboard/VendorPerformance"
import { KeyMetricsCharts } from "@/components/dashboard/KeyMetricsCharts"
export function Dashboard() {
return (
<div className="flex-1 space-y-4 p-8 pt-6">
<div className="flex-1 space-y-4 p-4 md: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>
<div className="grid gap-4 md:grid-cols-12">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<InventoryHealthSummary />
<VendorPerformance />
<KeyMetricsCharts />
<StockAlerts />
<TrendingProducts />
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<KeyMetricsCharts />
</Card>
<Card className="col-span-3">
<LowStockAlerts />
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<TrendingProducts />
</Card>
<Card className="col-span-3">
<VendorPerformance />
</Card>
</div>
</div>
)
}
}
export default Dashboard