Get frontend pages loading data again, remove unused components

This commit is contained in:
2025-03-26 21:47:24 -04:00
parent 8e19e6cd74
commit e5c4f617c5
16 changed files with 2170 additions and 1314 deletions

View File

@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, ScatterChart, Scatter, ZAxis } from 'recharts';
import config from '../../config';
import { useState, useEffect } from 'react';
interface VendorData {
performance: {
@@ -10,14 +11,15 @@ interface VendorData {
profitMargin: number;
stockTurnover: number;
productCount: number;
growth: number;
}[];
comparison: {
comparison?: {
vendor: string;
salesPerProduct: number;
averageMargin: number;
size: number;
}[];
trends: {
trends?: {
vendor: string;
month: string;
sales: number;
@@ -25,40 +27,86 @@ interface VendorData {
}
export function VendorPerformance() {
const { data, isLoading } = useQuery<VendorData>({
queryKey: ['vendor-performance'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/analytics/vendors`);
if (!response.ok) {
throw new Error('Failed to fetch vendor performance');
}
const rawData = await response.json();
return {
performance: rawData.performance.map((vendor: any) => ({
...vendor,
salesVolume: Number(vendor.salesVolume) || 0,
profitMargin: Number(vendor.profitMargin) || 0,
stockTurnover: Number(vendor.stockTurnover) || 0,
productCount: Number(vendor.productCount) || 0
})),
comparison: rawData.comparison.map((vendor: any) => ({
...vendor,
salesPerProduct: Number(vendor.salesPerProduct) || 0,
averageMargin: Number(vendor.averageMargin) || 0,
size: Number(vendor.size) || 0
})),
trends: rawData.trends.map((vendor: any) => ({
...vendor,
sales: Number(vendor.sales) || 0
}))
};
},
});
const [vendorData, setVendorData] = useState<VendorData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
if (isLoading || !data) {
useEffect(() => {
// Use plain fetch to bypass cache issues with React Query
const fetchData = async () => {
try {
setIsLoading(true);
// Add cache-busting parameter
const response = await fetch(`${config.apiUrl}/analytics/vendors?nocache=${Date.now()}`, {
headers: {
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
}
});
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status}`);
}
const rawData = await response.json();
if (!rawData || !rawData.performance) {
throw new Error('Invalid response format');
}
// Create a complete structure even if some parts are missing
const data: VendorData = {
performance: rawData.performance.map((vendor: any) => ({
vendor: vendor.vendor,
salesVolume: Number(vendor.salesVolume) || 0,
profitMargin: Number(vendor.profitMargin) || 0,
stockTurnover: Number(vendor.stockTurnover) || 0,
productCount: Number(vendor.productCount) || 0,
growth: Number(vendor.growth) || 0
})),
comparison: rawData.comparison?.map((vendor: any) => ({
vendor: vendor.vendor,
salesPerProduct: Number(vendor.salesPerProduct) || 0,
averageMargin: Number(vendor.averageMargin) || 0,
size: Number(vendor.size) || 0
})) || [],
trends: rawData.trends?.map((vendor: any) => ({
vendor: vendor.vendor,
month: vendor.month,
sales: Number(vendor.sales) || 0
})) || []
};
setVendorData(data);
} catch (err) {
console.error('Error fetching vendor data:', err);
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading) {
return <div>Loading vendor performance...</div>;
}
if (error || !vendorData) {
return <div className="text-red-500">Error loading vendor data: {error}</div>;
}
// Ensure we have at least the performance data
const sortedPerformance = vendorData.performance
.sort((a, b) => b.salesVolume - a.salesVolume)
.slice(0, 10);
// Use simplified version if comparison data is missing
const hasComparisonData = vendorData.comparison && vendorData.comparison.length > 0;
return (
<div className="grid gap-4">
<div className="grid gap-4 md:grid-cols-2">
@@ -68,7 +116,7 @@ export function VendorPerformance() {
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data.performance}>
<BarChart data={sortedPerformance}>
<XAxis dataKey="vendor" />
<YAxis tickFormatter={(value) => `$${(value / 1000).toFixed(0)}k`} />
<Tooltip
@@ -84,44 +132,68 @@ export function VendorPerformance() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Vendor Performance Matrix</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<XAxis
dataKey="salesPerProduct"
name="Sales per Product"
tickFormatter={(value) => `$${(value / 1000).toFixed(0)}k`}
/>
<YAxis
dataKey="averageMargin"
name="Average Margin"
tickFormatter={(value) => `${value.toFixed(0)}%`}
/>
<ZAxis
dataKey="size"
range={[50, 400]}
name="Product Count"
/>
<Tooltip
formatter={(value: number, name: string) => {
if (name === 'Sales per Product') return [`$${value.toLocaleString()}`, name];
if (name === 'Average Margin') return [`${value.toFixed(1)}%`, name];
return [value, name];
}}
/>
<Scatter
data={data.comparison}
fill="#60a5fa"
name="Vendors"
/>
</ScatterChart>
</ResponsiveContainer>
</CardContent>
</Card>
{hasComparisonData ? (
<Card>
<CardHeader>
<CardTitle>Vendor Performance Matrix</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<XAxis
dataKey="salesPerProduct"
name="Sales per Product"
tickFormatter={(value) => `$${(value / 1000).toFixed(0)}k`}
/>
<YAxis
dataKey="averageMargin"
name="Average Margin"
tickFormatter={(value) => `${value.toFixed(0)}%`}
/>
<ZAxis
dataKey="size"
range={[50, 400]}
name="Product Count"
/>
<Tooltip
formatter={(value: number, name: string) => {
if (name === 'Sales per Product') return [`$${value.toLocaleString()}`, name];
if (name === 'Average Margin') return [`${value.toFixed(1)}%`, name];
return [value, name];
}}
/>
<Scatter
data={vendorData.comparison}
fill="#60a5fa"
name="Vendors"
/>
</ScatterChart>
</ResponsiveContainer>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle>Vendor Profit Margins</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={sortedPerformance}>
<XAxis dataKey="vendor" />
<YAxis tickFormatter={(value) => `${value}%`} />
<Tooltip
formatter={(value: number) => [`${value.toFixed(1)}%`, 'Profit Margin']}
/>
<Bar
dataKey="profitMargin"
fill="#4ade80"
name="Profit Margin"
/>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
)}
</div>
<Card>
@@ -130,7 +202,7 @@ export function VendorPerformance() {
</CardHeader>
<CardContent>
<div className="space-y-4">
{data.performance.map((vendor) => (
{sortedPerformance.map((vendor) => (
<div key={`${vendor.vendor}-${vendor.salesVolume}`} className="flex items-center">
<div className="flex-1">
<p className="text-sm font-medium">{vendor.vendor}</p>

View File

@@ -1,88 +0,0 @@
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"
import { useNavigate } from "react-router-dom"
import { cn } from "@/lib/utils"
interface InventoryHealth {
critical: number
reorder: number
healthy: number
overstock: number
total: number
}
export function InventoryHealthSummary() {
const navigate = useNavigate();
const { data: summary } = useQuery<InventoryHealth>({
queryKey: ["inventory-health"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/inventory/health/summary`)
if (!response.ok) {
throw new Error("Failed to fetch inventory health")
}
return response.json()
},
})
const stats = [
{
title: "Critical Stock",
value: summary?.critical || 0,
description: "Products needing immediate attention",
icon: AlertCircle,
className: "bg-destructive/10",
iconClassName: "text-destructive",
view: "critical"
},
{
title: "Reorder Soon",
value: summary?.reorder || 0,
description: "Products approaching reorder point",
icon: AlertTriangle,
className: "bg-warning/10",
iconClassName: "text-warning",
view: "reorder"
},
{
title: "Healthy Stock",
value: summary?.healthy || 0,
description: "Products at optimal levels",
icon: CheckCircle2,
className: "bg-success/10",
iconClassName: "text-success",
view: "healthy"
},
{
title: "Overstock",
value: summary?.overstock || 0,
description: "Products exceeding optimal levels",
icon: PackageSearch,
className: "bg-muted",
iconClassName: "text-muted-foreground",
view: "overstocked"
},
]
return (
<>
{stats.map((stat) => (
<Card
key={stat.title}
className={cn(stat.className, "cursor-pointer hover:opacity-90 transition-opacity")}
onClick={() => navigate(`/products?view=${stat.view}`)}
>
<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,106 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recharts';
import config from '../../config';
interface InventoryMetrics {
stockLevels: {
category: string;
inStock: number;
lowStock: number;
outOfStock: number;
}[];
topVendors: {
vendor: string;
productCount: number;
averageStockLevel: string;
}[];
stockTurnover: {
category: string;
rate: string;
}[];
}
export function InventoryStats() {
const { data, isLoading, error } = useQuery<InventoryMetrics>({
queryKey: ['inventory-metrics'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/inventory-metrics`);
if (!response.ok) {
throw new Error('Failed to fetch inventory metrics');
}
return response.json();
},
});
if (isLoading) {
return <div>Loading inventory metrics...</div>;
}
if (error) {
return <div className="text-red-500">Error loading inventory metrics</div>;
}
return (
<div className="grid gap-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Stock Levels by Category</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data?.stockLevels}>
<XAxis dataKey="category" />
<YAxis />
<Tooltip />
<Bar dataKey="inStock" name="In Stock" fill="#4ade80" />
<Bar dataKey="lowStock" name="Low Stock" fill="#fbbf24" />
<Bar dataKey="outOfStock" name="Out of Stock" fill="#f87171" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Stock Turnover Rate</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data?.stockTurnover}>
<XAxis dataKey="category" />
<YAxis />
<Tooltip formatter={(value: string) => [Number(value).toFixed(2), "Rate"]} />
<Bar dataKey="rate" name="Turnover Rate" fill="#60a5fa" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Top Vendors</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{data?.topVendors.map((vendor) => (
<div key={vendor.vendor} className="flex items-center">
<div className="flex-1">
<p className="text-sm font-medium">{vendor.vendor}</p>
<p className="text-sm text-muted-foreground">
{vendor.productCount} products
</p>
</div>
<div className="ml-4 text-right">
<p className="text-sm font-medium">
Avg. Stock: {Number(vendor.averageStockLevel).toFixed(0)}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,232 +0,0 @@
import { useQuery } from "@tanstack/react-query"
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import config from "@/config"
interface MetricDataPoint {
date: string
value: number
}
interface KeyMetrics {
revenue: MetricDataPoint[]
inventory_value: MetricDataPoint[]
gmroi: MetricDataPoint[]
}
export function KeyMetricsCharts() {
const { data: metrics } = useQuery<KeyMetrics>({
queryKey: ["key-metrics"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/metrics/trends`)
if (!response.ok) {
throw new Error("Failed to fetch metrics trends")
}
return response.json()
},
})
const formatCurrency = (value: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value)
return (
<>
<CardHeader>
<CardTitle className="text-lg font-medium">Key Metrics</CardTitle>
</CardHeader>
<CardContent>
<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>
</>
)
}

View File

@@ -1,108 +0,0 @@
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 config from "@/config"
import { format } from "date-fns"
interface Product {
pid: number;
sku: string;
title: string;
stock_quantity: number;
daily_sales_avg: string;
days_of_inventory: string;
reorder_qty: number;
last_purchase_date: string | null;
lead_time_status: string;
}
// Helper functions
const formatDate = (dateString: string) => {
return format(new Date(dateString), 'MMM dd, yyyy')
}
const getLeadTimeVariant = (status: string) => {
switch (status.toLowerCase()) {
case 'critical':
return 'destructive'
case 'warning':
return 'secondary'
case 'good':
return 'secondary'
default:
return 'secondary'
}
}
export function LowStockAlerts() {
const { data: products } = useQuery<Product[]>({
queryKey: ["low-stock"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/low-stock/products`)
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>Product</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 Qty</TableHead>
<TableHead>Last Purchase</TableHead>
<TableHead>Lead Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products?.map((product) => (
<TableRow key={product.pid}>
<TableCell>
<a
href={`https://backend.acherryontop.com/product/${product.pid}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{product.title}
</a>
<div className="text-sm text-muted-foreground">{product.sku}</div>
</TableCell>
<TableCell className="text-right">{product.stock_quantity}</TableCell>
<TableCell className="text-right">{Number(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right">{Number(product.days_of_inventory).toFixed(1)}</TableCell>
<TableCell className="text-right">{product.reorder_qty}</TableCell>
<TableCell>{product.last_purchase_date ? formatDate(product.last_purchase_date) : '-'}</TableCell>
<TableCell>
<Badge variant={getLeadTimeVariant(product.lead_time_status)}>
{product.lead_time_status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</>
)
}

View File

@@ -1,66 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import config from '../../config';
interface SalesData {
date: string;
total: number;
}
export function Overview() {
const { data, isLoading, error } = useQuery<SalesData[]>({
queryKey: ['sales-overview'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/sales-overview`);
if (!response.ok) {
throw new Error('Failed to fetch sales overview');
}
const rawData = await response.json();
return rawData.map((item: SalesData) => ({
...item,
total: parseFloat(item.total.toString()),
date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}));
},
});
if (isLoading) {
return <div>Loading chart...</div>;
}
if (error) {
return <div className="text-red-500">Error loading sales overview</div>;
}
return (
<ResponsiveContainer width="100%" height={350}>
<LineChart data={data}>
<XAxis
dataKey="date"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value.toLocaleString()}`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, 'Sales']}
labelFormatter={(label) => `Date: ${label}`}
/>
<Line
type="monotone"
dataKey="total"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
);
}

View File

@@ -1,63 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import config from '../../config';
interface RecentOrder {
order_id: string;
customer_name: string;
total_amount: number;
order_date: string;
}
export function RecentSales() {
const { data: recentOrders, isLoading, error } = useQuery<RecentOrder[]>({
queryKey: ['recent-orders'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/recent-orders`);
if (!response.ok) {
throw new Error('Failed to fetch recent orders');
}
const data = await response.json();
return data.map((order: RecentOrder) => ({
...order,
total_amount: parseFloat(order.total_amount.toString())
}));
},
});
if (isLoading) {
return <div>Loading recent sales...</div>;
}
if (error) {
return <div className="text-red-500">Error loading recent sales</div>;
}
return (
<div className="space-y-8">
{recentOrders?.map((order) => (
<div key={order.order_id} className="flex items-center">
<Avatar className="h-9 w-9">
<AvatarFallback>
{order.customer_name?.split(' ').map(n => n[0]).join('') || '??'}
</AvatarFallback>
</Avatar>
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">Order #{order.order_id}</p>
<p className="text-sm text-muted-foreground">
{new Date(order.order_date).toLocaleDateString()}
</p>
</div>
<div className="ml-auto font-medium">
${order.total_amount.toFixed(2)}
</div>
</div>
))}
{!recentOrders?.length && (
<div className="text-center text-muted-foreground">
No recent orders found
</div>
)}
</div>
);
}

View File

@@ -1,58 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import config from '../../config';
interface CategorySales {
category: string;
total: number;
percentage: number;
}
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
export function SalesByCategory() {
const { data, isLoading, error } = useQuery<CategorySales[]>({
queryKey: ['sales-by-category'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/sales-by-category`);
if (!response.ok) {
throw new Error('Failed to fetch category sales');
}
return response.json();
},
});
if (isLoading) {
return <div>Loading chart...</div>;
}
if (error) {
return <div className="text-red-500">Error loading category sales</div>;
}
return (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="total"
nameKey="category"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{data?.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, 'Sales']}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
);
}

View File

@@ -1,95 +0,0 @@
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 { TrendingUp, TrendingDown } from "lucide-react"
import config from "@/config"
interface Product {
pid: number;
sku: string;
title: string;
daily_sales_avg: string;
weekly_sales_avg: string;
growth_rate: string;
total_revenue: string;
}
export function TrendingProducts() {
const { data: products } = useQuery<Product[]>({
queryKey: ["trending-products"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/products/trending`)
if (!response.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 (
<>
<CardHeader>
<CardTitle className="text-lg font-medium">Trending Products</CardTitle>
</CardHeader>
<CardContent>
<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.pid}>
<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>{Number(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{Number(product.growth_rate) > 0 ? (
<TrendingUp className="h-4 w-4 text-success" />
) : (
<TrendingDown className="h-4 w-4 text-destructive" />
)}
<span
className={
Number(product.growth_rate) > 0 ? "text-success" : "text-destructive"
}
>
{formatPercent(Number(product.growth_rate))}
</span>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</>
)
}

View File

@@ -1,79 +0,0 @@
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
avg_lead_time: number
on_time_delivery_rate: number
avg_fill_rate: number
total_orders: number
active_orders: number
overdue_orders: number
}
export function VendorPerformance() {
const { data: vendors } = useQuery<VendorMetrics[]>({
queryKey: ["vendor-metrics"],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/vendor/performance`)
if (!response.ok) {
throw new Error("Failed to fetch vendor metrics")
}
return response.json()
},
})
// Sort vendors by on-time delivery rate
const sortedVendors = vendors
?.sort((a, b) => b.on_time_delivery_rate - a.on_time_delivery_rate)
return (
<>
<CardHeader>
<CardTitle className="text-lg font-medium">Top Vendor Performance</CardTitle>
</CardHeader>
<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.avg_fill_rate.toFixed(0)}%
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</>
)
}