Add new frontend dashboard components and update scripts/schema
This commit is contained in:
171
inventory/src/components/dashboard/BestSellers.tsx
Normal file
171
inventory/src/components/dashboard/BestSellers.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface BestSellerProduct {
|
||||
product_id: number
|
||||
sku: string
|
||||
title: string
|
||||
units_sold: number
|
||||
revenue: number
|
||||
profit: number
|
||||
}
|
||||
|
||||
interface BestSellerVendor {
|
||||
vendor: string
|
||||
products_sold: number
|
||||
revenue: number
|
||||
profit: number
|
||||
order_fill_rate: number
|
||||
}
|
||||
|
||||
interface BestSellerCategory {
|
||||
category_id: number
|
||||
name: string
|
||||
products_sold: number
|
||||
revenue: number
|
||||
profit: number
|
||||
growth_rate: number
|
||||
}
|
||||
|
||||
interface BestSellersData {
|
||||
products: BestSellerProduct[]
|
||||
vendors: BestSellerVendor[]
|
||||
categories: BestSellerCategory[]
|
||||
}
|
||||
|
||||
export function BestSellers() {
|
||||
const { data } = useQuery<BestSellersData>({
|
||||
queryKey: ["best-sellers"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/best-sellers`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch best sellers")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Best Sellers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="products">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="products">Products</TabsTrigger>
|
||||
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
||||
<TabsTrigger value="categories">Categories</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="products">
|
||||
<ScrollArea className="h-[400px] w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Units</TableHead>
|
||||
<TableHead className="text-right">Revenue</TableHead>
|
||||
<TableHead className="text-right">Profit</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.products.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{product.title}</p>
|
||||
<p className="text-sm text-muted-foreground">{product.sku}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.units_sold.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.revenue)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.profit)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vendors">
|
||||
<ScrollArea className="h-[400px] w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead className="text-right">Products</TableHead>
|
||||
<TableHead className="text-right">Revenue</TableHead>
|
||||
<TableHead className="text-right">Fill Rate</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.vendors.map((vendor) => (
|
||||
<TableRow key={vendor.vendor}>
|
||||
<TableCell>
|
||||
<p className="font-medium">{vendor.vendor}</p>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{vendor.products_sold.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(vendor.revenue)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{vendor.order_fill_rate.toFixed(1)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="categories">
|
||||
<ScrollArea className="h-[400px] w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead className="text-right">Products</TableHead>
|
||||
<TableHead className="text-right">Revenue</TableHead>
|
||||
<TableHead className="text-right">Growth</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.categories.map((category) => (
|
||||
<TableRow key={category.category_id}>
|
||||
<TableCell>
|
||||
<p className="font-medium">{category.name}</p>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{category.products_sold.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(category.revenue)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{category.growth_rate.toFixed(1)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
125
inventory/src/components/dashboard/ForecastMetrics.tsx
Normal file
125
inventory/src/components/dashboard/ForecastMetrics.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { useState } from "react"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface ForecastData {
|
||||
forecastSales: number
|
||||
forecastRevenue: number
|
||||
dailyForecast: {
|
||||
date: string
|
||||
sales: number
|
||||
revenue: number
|
||||
}[]
|
||||
}
|
||||
|
||||
const periods = [
|
||||
{ value: "7", label: "7 Days" },
|
||||
{ value: "14", label: "14 Days" },
|
||||
{ value: "30", label: "30 Days" },
|
||||
{ value: "60", label: "60 Days" },
|
||||
{ value: "90", label: "90 Days" },
|
||||
]
|
||||
|
||||
export function ForecastMetrics() {
|
||||
const [period, setPeriod] = useState("30")
|
||||
|
||||
const { data } = useQuery<ForecastData>({
|
||||
queryKey: ["forecast-metrics", period],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?days=${period}`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch forecast metrics")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-lg font-medium">Sales Forecast</CardTitle>
|
||||
<Select value={period} onValueChange={setPeriod}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Select period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{periods.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Forecast Sales</p>
|
||||
<p className="text-2xl font-bold">{data?.forecastSales.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Forecast Revenue</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.forecastRevenue || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data?.dailyForecast || []}>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => value.toLocaleString()}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => formatCurrency(value)}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => [
|
||||
name === "revenue" ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === "revenue" ? "Revenue" : "Sales"
|
||||
]}
|
||||
labelFormatter={(label) => `Date: ${label}`}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="sales"
|
||||
name="Sales"
|
||||
stroke="#0088FE"
|
||||
fill="#0088FE"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
name="Revenue"
|
||||
stroke="#00C49F"
|
||||
fill="#00C49F"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
103
inventory/src/components/dashboard/OverstockMetrics.tsx
Normal file
103
inventory/src/components/dashboard/OverstockMetrics.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { BarChart, Bar, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface OverstockMetricsData {
|
||||
overstockedProducts: number
|
||||
overstockedUnits: number
|
||||
overstockedCost: number
|
||||
overstockedRetail: number
|
||||
overstockByCategory: {
|
||||
category: string
|
||||
products: number
|
||||
units: number
|
||||
cost: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export function OverstockMetrics() {
|
||||
const { data } = useQuery<OverstockMetricsData>({
|
||||
queryKey: ["overstock-metrics"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/overstock/metrics`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch overstock metrics")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Overstock Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Overstocked Products</p>
|
||||
<p className="text-2xl font-bold">{data?.overstockedProducts.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Overstocked Units</p>
|
||||
<p className="text-2xl font-bold">{data?.overstockedUnits.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Cost</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.overstockedCost || 0)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Retail</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.overstockedRetail || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data?.overstockByCategory || []}>
|
||||
<XAxis
|
||||
dataKey="category"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => value.toLocaleString()}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => [
|
||||
name === "cost" ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === "cost" ? "Cost" : name === "products" ? "Products" : "Units"
|
||||
]}
|
||||
labelFormatter={(label) => `Category: ${label}`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="products"
|
||||
name="Products"
|
||||
fill="#0088FE"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="units"
|
||||
name="Units"
|
||||
fill="#00C49F"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="cost"
|
||||
name="Cost"
|
||||
fill="#FFBB28"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
101
inventory/src/components/dashboard/PurchaseMetrics.tsx
Normal file
101
inventory/src/components/dashboard/PurchaseMetrics.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { PieChart, Pie, ResponsiveContainer, Cell, Tooltip } from "recharts"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface PurchaseMetricsData {
|
||||
activePurchaseOrders: number
|
||||
overduePurchaseOrders: number
|
||||
onOrderUnits: number
|
||||
onOrderCost: number
|
||||
onOrderRetail: number
|
||||
vendorOrderValue: {
|
||||
vendor: string
|
||||
value: number
|
||||
}[]
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
"#0088FE",
|
||||
"#00C49F",
|
||||
"#FFBB28",
|
||||
"#FF8042",
|
||||
"#8884D8",
|
||||
"#82CA9D",
|
||||
"#FFC658",
|
||||
"#FF7C43",
|
||||
]
|
||||
|
||||
export function PurchaseMetrics() {
|
||||
const { data } = useQuery<PurchaseMetricsData>({
|
||||
queryKey: ["purchase-metrics"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch purchase metrics")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Purchase Orders Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Active POs</p>
|
||||
<p className="text-2xl font-bold">{data?.activePurchaseOrders.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Overdue POs</p>
|
||||
<p className="text-2xl font-bold text-destructive">{data?.overduePurchaseOrders.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">On Order Units</p>
|
||||
<p className="text-2xl font-bold">{data?.onOrderUnits.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">On Order Cost</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.onOrderCost || 0)}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">On Order Retail</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.onOrderRetail || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data?.vendorOrderValue || []}
|
||||
dataKey="value"
|
||||
nameKey="vendor"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
>
|
||||
{data?.vendorOrderValue.map((entry, index) => (
|
||||
<Cell
|
||||
key={entry.vendor}
|
||||
fill={COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelFormatter={(label: string) => `Vendor: ${label}`}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
91
inventory/src/components/dashboard/ReplenishmentMetrics.tsx
Normal file
91
inventory/src/components/dashboard/ReplenishmentMetrics.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { BarChart, Bar, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface ReplenishmentMetricsData {
|
||||
totalUnitsToReplenish: number
|
||||
totalReplenishmentCost: number
|
||||
totalReplenishmentRetail: number
|
||||
replenishmentByCategory: {
|
||||
category: string
|
||||
units: number
|
||||
cost: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export function ReplenishmentMetrics() {
|
||||
const { data } = useQuery<ReplenishmentMetricsData>({
|
||||
queryKey: ["replenishment-metrics"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/replenishment/metrics`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch replenishment metrics")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Replenishment Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Units to Replenish</p>
|
||||
<p className="text-2xl font-bold">{data?.totalUnitsToReplenish.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Cost</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalReplenishmentCost || 0)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Retail</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalReplenishmentRetail || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data?.replenishmentByCategory || []}>
|
||||
<XAxis
|
||||
dataKey="category"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => value.toLocaleString()}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => [
|
||||
name === "cost" ? formatCurrency(value) : value.toLocaleString(),
|
||||
name === "cost" ? "Cost" : "Units"
|
||||
]}
|
||||
labelFormatter={(label) => `Category: ${label}`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="units"
|
||||
name="Units"
|
||||
fill="#0088FE"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="cost"
|
||||
name="Cost"
|
||||
fill="#00C49F"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
145
inventory/src/components/dashboard/SalesMetrics.tsx
Normal file
145
inventory/src/components/dashboard/SalesMetrics.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip } from "recharts"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { useState } from "react"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface SalesData {
|
||||
totalOrders: number
|
||||
totalUnitsSold: number
|
||||
totalCogs: number
|
||||
totalRevenue: number
|
||||
dailySales: {
|
||||
date: string
|
||||
units: number
|
||||
revenue: number
|
||||
cogs: number
|
||||
}[]
|
||||
}
|
||||
|
||||
const periods = [
|
||||
{ value: "7", label: "7 Days" },
|
||||
{ value: "14", label: "14 Days" },
|
||||
{ value: "30", label: "30 Days" },
|
||||
{ value: "60", label: "60 Days" },
|
||||
{ value: "90", label: "90 Days" },
|
||||
]
|
||||
|
||||
export function SalesMetrics() {
|
||||
const [period, setPeriod] = useState("30")
|
||||
|
||||
const { data } = useQuery<SalesData>({
|
||||
queryKey: ["sales-metrics", period],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/sales/metrics?days=${period}`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch sales metrics")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-lg font-medium">Sales Overview</CardTitle>
|
||||
<Select value={period} onValueChange={setPeriod}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Select period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{periods.map((p) => (
|
||||
<SelectItem key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Orders</p>
|
||||
<p className="text-2xl font-bold">{data?.totalOrders.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Units Sold</p>
|
||||
<p className="text-2xl font-bold">{data?.totalUnitsSold.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Cost of Goods</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalCogs || 0)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Revenue</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalRevenue || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data?.dailySales || []}>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => value.toLocaleString()}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => formatCurrency(value)}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => [
|
||||
name === "units" ? value.toLocaleString() : formatCurrency(value),
|
||||
name === "units" ? "Units" : name === "revenue" ? "Revenue" : "COGS"
|
||||
]}
|
||||
labelFormatter={(label) => `Date: ${label}`}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="units"
|
||||
name="Units"
|
||||
stroke="#0088FE"
|
||||
fill="#0088FE"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
name="Revenue"
|
||||
stroke="#00C49F"
|
||||
fill="#00C49F"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="cogs"
|
||||
name="COGS"
|
||||
stroke="#FF8042"
|
||||
fill="#FF8042"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
101
inventory/src/components/dashboard/StockMetrics.tsx
Normal file
101
inventory/src/components/dashboard/StockMetrics.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { PieChart, Pie, ResponsiveContainer, Cell, Tooltip } from "recharts"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface StockMetricsData {
|
||||
totalProducts: number
|
||||
productsInStock: number
|
||||
totalStockUnits: number
|
||||
totalStockCost: number
|
||||
totalStockRetail: number
|
||||
brandRetailValue: {
|
||||
brand: string
|
||||
value: number
|
||||
}[]
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
"#0088FE",
|
||||
"#00C49F",
|
||||
"#FFBB28",
|
||||
"#FF8042",
|
||||
"#8884D8",
|
||||
"#82CA9D",
|
||||
"#FFC658",
|
||||
"#FF7C43",
|
||||
]
|
||||
|
||||
export function StockMetrics() {
|
||||
const { data } = useQuery<StockMetricsData>({
|
||||
queryKey: ["stock-metrics"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch stock metrics")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Stock Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Products</p>
|
||||
<p className="text-2xl font-bold">{data?.totalProducts.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Products In Stock</p>
|
||||
<p className="text-2xl font-bold">{data?.productsInStock.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Stock Units</p>
|
||||
<p className="text-2xl font-bold">{data?.totalStockUnits.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Stock Cost</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalStockCost || 0)}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">Total Stock Retail</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalStockRetail || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data?.brandRetailValue || []}
|
||||
dataKey="value"
|
||||
nameKey="brand"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
>
|
||||
{data?.brandRetailValue.map((entry, index) => (
|
||||
<Cell
|
||||
key={entry.brand}
|
||||
fill={COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelFormatter={(label: string) => `Brand: ${label}`}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
|
||||
interface OverstockedProduct {
|
||||
product_id: number
|
||||
sku: string
|
||||
title: string
|
||||
overstocked_units: number
|
||||
overstocked_cost: number
|
||||
overstocked_retail: number
|
||||
days_of_inventory: number
|
||||
}
|
||||
|
||||
export function TopOverstockedProducts() {
|
||||
const { data } = useQuery<OverstockedProduct[]>({
|
||||
queryKey: ["top-overstocked-products"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/overstock/products?limit=50`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch overstocked products")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-medium">Top Overstocked Products</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Product</TableHead>
|
||||
<TableHead className="text-right">Units</TableHead>
|
||||
<TableHead className="text-right">Cost</TableHead>
|
||||
<TableHead className="text-right">Days</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((product) => (
|
||||
<TableRow key={product.product_id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{product.title}</p>
|
||||
<p className="text-sm text-muted-foreground">{product.sku}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.overstocked_units.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatCurrency(product.overstocked_cost)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.days_of_inventory}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,3 +4,27 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number as currency with the specified locale and currency code
|
||||
* @param value - The number to format
|
||||
* @param locale - The locale to use for formatting (defaults to 'en-US')
|
||||
* @param currency - The currency code to use (defaults to 'USD')
|
||||
* @returns Formatted currency string
|
||||
*/
|
||||
export function formatCurrency(
|
||||
value: number | null | undefined,
|
||||
locale: string = 'en-US',
|
||||
currency: string = 'USD'
|
||||
): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '$0.00';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
@@ -4,31 +4,78 @@ import { LowStockAlerts } from "@/components/dashboard/LowStockAlerts"
|
||||
import { TrendingProducts } from "@/components/dashboard/TrendingProducts"
|
||||
import { VendorPerformance } from "@/components/dashboard/VendorPerformance"
|
||||
import { KeyMetricsCharts } from "@/components/dashboard/KeyMetricsCharts"
|
||||
import { StockMetrics } from "@/components/dashboard/StockMetrics"
|
||||
import { PurchaseMetrics } from "@/components/dashboard/PurchaseMetrics"
|
||||
import { ReplenishmentMetrics } from "@/components/dashboard/ReplenishmentMetrics"
|
||||
import { ForecastMetrics } from "@/components/dashboard/ForecastMetrics"
|
||||
import { OverstockMetrics } from "@/components/dashboard/OverstockMetrics"
|
||||
import { TopOverstockedProducts } from "@/components/dashboard/TopOverstockedProducts"
|
||||
import { BestSellers } from "@/components/dashboard/BestSellers"
|
||||
import { SalesMetrics } from "@/components/dashboard/SalesMetrics"
|
||||
import { motion } from "motion/react"
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<motion.div layout 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-2 lg:grid-cols-4">
|
||||
<InventoryHealthSummary />
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="col-span-4">
|
||||
<KeyMetricsCharts />
|
||||
|
||||
{/* First row - Stock and Purchase metrics */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<StockMetrics />
|
||||
</Card>
|
||||
<Card className="col-span-3">
|
||||
<Card>
|
||||
<PurchaseMetrics />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Second row - Replenishment and Overstock */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<ReplenishmentMetrics />
|
||||
</Card>
|
||||
<Card>
|
||||
<OverstockMetrics />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Third row - Products to Replenish and Overstocked Products */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<LowStockAlerts />
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="col-span-4">
|
||||
<TrendingProducts />
|
||||
<Card>
|
||||
<TopOverstockedProducts />
|
||||
</Card>
|
||||
<Card className="col-span-3">
|
||||
</div>
|
||||
|
||||
{/* Fourth row - Sales and Forecast */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<SalesMetrics />
|
||||
</Card>
|
||||
<Card>
|
||||
<ForecastMetrics />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Fifth row - Best Sellers */}
|
||||
<div className="grid gap-4">
|
||||
<Card>
|
||||
<BestSellers />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sixth row - Vendor Performance and Trending Products */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<VendorPerformance />
|
||||
</Card>
|
||||
<Card>
|
||||
<TrendingProducts />
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user