Fix data in product detail

This commit is contained in:
2025-01-14 00:45:04 -05:00
parent dbd3f6b490
commit 14ece7e244
2 changed files with 657 additions and 101 deletions

View File

@@ -14,47 +14,118 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai
import config from "@/config";
interface Product {
product_id: string;
product_id: number;
title: string;
sku: string;
SKU: string;
barcode: string;
created_at: string;
updated_at: string;
// Inventory fields
stock_quantity: number;
price: number;
regular_price: number;
cost_price: number;
vendor: string;
brand: string;
moq: number;
uom: number;
managing_stock: boolean;
replenishable: boolean;
// Pricing fields
price: string | number;
regular_price: string | number;
cost_price: string | number;
landing_cost_price: string | number | null;
// Categorization
categories: string[];
tags: string[];
options: Record<string, any>;
// Vendor info
vendor: string;
vendor_reference: string;
brand: string;
// URLs
permalink: string;
image: string;
// Metrics
daily_sales_avg: number;
weekly_sales_avg: number;
monthly_sales_avg: number;
days_of_inventory: number;
reorder_point: number;
safety_stock: number;
avg_margin_percent: number;
total_revenue: number;
inventory_value: number;
turnover_rate: number;
abc_class: string;
stock_status: string;
metrics: {
// Sales metrics
daily_sales_avg: number;
weekly_sales_avg: number;
monthly_sales_avg: number;
// Inventory metrics
days_of_inventory: number;
reorder_point: number;
safety_stock: number;
stock_status: string;
abc_class: string;
// Financial metrics
avg_margin_percent: number;
total_revenue: number;
inventory_value: number;
turnover_rate: number;
gmroi: number;
cost_of_goods_sold: number;
gross_profit: number;
// Lead time metrics
avg_lead_time_days: number;
current_lead_time: number;
target_lead_time: number;
lead_time_status: string;
};
// Vendor performance
vendor_performance?: {
avg_lead_time_days: number;
on_time_delivery_rate: number;
order_fill_rate: number;
total_orders: number;
total_late_orders: number;
total_purchase_value: number;
avg_order_value: number;
};
// Time series data
monthly_sales: Array<{
monthly_sales?: Array<{
month: string;
quantity: number;
revenue: number;
cost: number;
avg_price: number;
profit_margin: number;
inventory_value: number;
quantity_growth: number;
revenue_growth: number;
}>;
recent_orders: Array<{
recent_orders?: Array<{
date: string;
order_number: string;
quantity: number;
price: number;
discount: number;
tax: number;
shipping: number;
customer: string;
status: string;
payment_method: string;
}>;
recent_purchases: Array<{
recent_purchases?: Array<{
date: string;
expected_date: string;
received_date: string | null;
po_id: string;
ordered: number;
received: number;
status: string;
cost_price: number;
notes: string;
lead_time_days: number | null;
}>;
}
@@ -64,24 +135,61 @@ interface ProductDetailProps {
}
export function ProductDetail({ productId, onClose }: ProductDetailProps) {
const { data: product, isLoading } = useQuery<Product>({
const { data: product, isLoading: isLoadingProduct } = useQuery<Product>({
queryKey: ["product", productId],
queryFn: async () => {
if (!productId) return null;
console.log('Fetching product details for:', productId);
const response = await fetch(`${config.apiUrl}/products/${productId}`);
if (!response.ok) {
throw new Error("Failed to fetch product details");
}
return response.json();
const data = await response.json();
console.log('Product data:', data);
return data;
},
enabled: !!productId,
});
// Separate query for time series data
const { data: timeSeriesData, isLoading: isLoadingTimeSeries } = useQuery({
queryKey: ["product-time-series", productId],
queryFn: async () => {
if (!productId) return null;
const response = await fetch(`${config.apiUrl}/products/${productId}/time-series`);
if (!response.ok) {
throw new Error("Failed to fetch time series data");
}
const data = await response.json();
console.log('Time series data:', data);
return data;
},
enabled: !!productId,
});
const isLoading = isLoadingProduct || isLoadingTimeSeries;
// Helper function to format price values
const formatPrice = (price: string | number | null | undefined): string => {
if (price === null || price === undefined) return 'N/A';
const numericPrice = typeof price === 'string' ? parseFloat(price) : price;
return typeof numericPrice === 'number' ? numericPrice.toFixed(2) : 'N/A';
};
// Combine product and time series data
const combinedData = product && timeSeriesData ? {
...product,
monthly_sales: timeSeriesData.monthly_sales,
recent_orders: timeSeriesData.recent_orders,
recent_purchases: timeSeriesData.recent_purchases
} : product;
if (!productId) return null;
return (
<Drawer open={!!productId} onOpenChange={(open) => !open && onClose()}>
<DrawerContent className="h-[85vh]">
<DrawerContent className="h-[90vh] md:h-[90vh] overflow-y-auto">
<DrawerHeader>
<DrawerTitle>
{isLoading ? (
@@ -92,21 +200,22 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
</DrawerTitle>
<DrawerDescription>
{isLoading ? (
"\u00A0" // Non-breaking space for loading state
"\u00A0"
) : (
`SKU: ${product?.sku}`
`SKU: ${product?.SKU} | Stock: ${product?.stock_quantity}`
)}
</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<div className="px-4 pb-8">
<Tabs defaultValue="overview" className="w-full">
<TabsList className="w-full justify-start">
<TabsList className="w-full justify-start mb-4 sticky top-0 bg-background z-10">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="inventory">Inventory</TabsTrigger>
<TabsTrigger value="sales">Sales</TabsTrigger>
<TabsTrigger value="purchase">Purchase History</TabsTrigger>
<TabsTrigger value="metrics">Performance Metrics</TabsTrigger>
<TabsTrigger value="financial">Financial</TabsTrigger>
<TabsTrigger value="vendor">Vendor</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
@@ -130,7 +239,23 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
</div>
<div>
<dt className="text-sm text-muted-foreground">Categories</dt>
<dd>{Array.isArray(product?.categories) ? product.categories.join(", ") : "N/A"}</dd>
<dd className="flex flex-wrap gap-2">
{product?.categories?.map(category => (
<span key={category} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
{category}
</span>
)) || "N/A"}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Tags</dt>
<dd className="flex flex-wrap gap-2">
{product?.tags?.map(tag => (
<span key={tag} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
{tag}
</span>
)) || "N/A"}
</dd>
</div>
</dl>
</Card>
@@ -140,18 +265,74 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Price</dt>
<dd>${typeof product?.price === 'number' ? product.price.toFixed(2) : 'N/A'}</dd>
<dd>${formatPrice(product?.price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Regular Price</dt>
<dd>${typeof product?.regular_price === 'number' ? product.regular_price.toFixed(2) : 'N/A'}</dd>
<dd>${formatPrice(product?.regular_price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Cost Price</dt>
<dd>${typeof product?.cost_price === 'number' ? product.cost_price.toFixed(2) : 'N/A'}</dd>
<dd>${formatPrice(product?.cost_price)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
<dd>${formatPrice(product?.landing_cost_price)}</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Stock Status</h3>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Current Stock</dt>
<dd className="text-2xl font-semibold">{product?.stock_quantity}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd>{product?.metrics?.stock_status}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Days of Stock</dt>
<dd>{product?.metrics?.days_of_inventory} days</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Velocity</h3>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Daily Sales</dt>
<dd>{product?.metrics?.daily_sales_avg?.toFixed(1)} units</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Weekly Sales</dt>
<dd>{product?.metrics?.weekly_sales_avg?.toFixed(1)} units</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Monthly Sales</dt>
<dd>{product?.metrics?.monthly_sales_avg?.toFixed(1)} units</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="quantity" stroke="#8884d8" name="Quantity" />
<Line type="monotone" dataKey="revenue" stroke="#82ca9d" name="Revenue" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
</div>
)}
</TabsContent>
@@ -170,11 +351,11 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
</div>
<div>
<dt className="text-sm text-muted-foreground">Days of Inventory</dt>
<dd className="text-2xl font-semibold">{product?.days_of_inventory || 0}</dd>
<dd className="text-2xl font-semibold">{product?.metrics?.days_of_inventory || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd className="text-2xl font-semibold">{product?.stock_status || "N/A"}</dd>
<dd className="text-2xl font-semibold">{product?.metrics?.stock_status || "N/A"}</dd>
</div>
</dl>
</Card>
@@ -184,15 +365,15 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Reorder Point</dt>
<dd>{product?.reorder_point || 0}</dd>
<dd>{product?.metrics?.reorder_point || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Safety Stock</dt>
<dd>{product?.safety_stock || 0}</dd>
<dd>{product?.metrics?.safety_stock || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">ABC Class</dt>
<dd>{product?.abc_class || "N/A"}</dd>
<dd>{product?.metrics?.abc_class || "N/A"}</dd>
</div>
</dl>
</Card>
@@ -206,28 +387,45 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
) : (
<div className="space-y-4">
<Card className="p-4">
<h3 className="font-semibold mb-2">Sales Metrics</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Daily Sales Avg</dt>
<dd>{product?.daily_sales_avg?.toFixed(2) || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Weekly Sales Avg</dt>
<dd>{product?.weekly_sales_avg?.toFixed(2) || 0}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Monthly Sales Avg</dt>
<dd>{product?.monthly_sales_avg?.toFixed(2) || 0}</dd>
</div>
</dl>
<h3 className="font-semibold mb-2">Recent Orders</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Order #</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{combinedData?.recent_orders?.map((order: NonNullable<Product['recent_orders']>[number]) => (
<TableRow key={order.order_number}>
<TableCell>{order.date}</TableCell>
<TableCell>{order.order_number}</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell>{order.quantity}</TableCell>
<TableCell>${formatPrice(order.price)}</TableCell>
<TableCell>{order.status}</TableCell>
</TableRow>
))}
{(!combinedData?.recent_orders || combinedData.recent_orders.length === 0) && (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No recent orders
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Monthly Sales Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={product?.monthly_sales || []}>
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis yAxisId="left" />
@@ -239,30 +437,6 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
</ResponsiveContainer>
</div>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Recent Orders</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Order #</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{product?.recent_orders?.map((order) => (
<TableRow key={order.order_number}>
<TableCell>{order.date}</TableCell>
<TableCell>{order.order_number}</TableCell>
<TableCell>{order.quantity}</TableCell>
<TableCell>${order.price.toFixed(2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</div>
)}
</TabsContent>
@@ -282,18 +456,27 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
<TableHead>Ordered</TableHead>
<TableHead>Received</TableHead>
<TableHead>Status</TableHead>
<TableHead>Lead Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{product?.recent_purchases?.map((po) => (
{combinedData?.recent_purchases?.map((po: NonNullable<Product['recent_purchases']>[number]) => (
<TableRow key={po.po_id}>
<TableCell>{po.date}</TableCell>
<TableCell>{po.po_id}</TableCell>
<TableCell>{po.ordered}</TableCell>
<TableCell>{po.received}</TableCell>
<TableCell>{po.status}</TableCell>
<TableCell>{po.lead_time_days ? `${po.lead_time_days} days` : 'N/A'}</TableCell>
</TableRow>
))}
{(!combinedData?.recent_purchases || combinedData.recent_purchases.length === 0) && (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No recent purchase orders
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
@@ -301,47 +484,108 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
)}
</TabsContent>
<TabsContent value="metrics" className="space-y-4">
<TabsContent value="financial" className="space-y-4">
{isLoading ? (
<Skeleton className="h-48 w-full" />
) : (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-4">
<Card className="p-4">
<h3 className="font-semibold mb-2">Financial Metrics</h3>
<dl className="space-y-2">
<h3 className="font-semibold mb-2">Financial Overview</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Total Revenue</dt>
<dd>${product?.total_revenue?.toFixed(2) || '0.00'}</dd>
<dt className="text-sm text-muted-foreground">Gross Profit</dt>
<dd className="text-2xl font-semibold">${formatPrice(product?.metrics.gross_profit)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">GMROI</dt>
<dd className="text-2xl font-semibold">{product?.metrics.gmroi.toFixed(2)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Margin %</dt>
<dd>{product?.avg_margin_percent?.toFixed(2) || '0.00'}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Inventory Value</dt>
<dd>${product?.inventory_value?.toFixed(2) || '0.00'}</dd>
<dd className="text-2xl font-semibold">{product?.metrics.avg_margin_percent.toFixed(2)}%</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Performance Metrics</h3>
<dl className="space-y-2">
<h3 className="font-semibold mb-2">Cost Breakdown</h3>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Turnover Rate</dt>
<dd>{product?.turnover_rate?.toFixed(2) || '0.00'}</dd>
<dt className="text-sm text-muted-foreground">Cost of Goods Sold</dt>
<dd>${formatPrice(product?.metrics.cost_of_goods_sold)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">ABC Classification</dt>
<dd>Class {product?.abc_class || 'N/A'}</dd>
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
<dd>${formatPrice(product?.landing_cost_price)}</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Profit Margin Trend</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={combinedData?.monthly_sales || []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Line type="monotone" dataKey="profit_margin" stroke="#82ca9d" name="Profit Margin %" />
</LineChart>
</ResponsiveContainer>
</div>
</Card>
</div>
)}
</TabsContent>
<TabsContent value="vendor" className="space-y-4">
{isLoading ? (
<Skeleton className="h-48 w-full" />
) : product?.vendor_performance ? (
<div className="space-y-4">
<Card className="p-4">
<h3 className="font-semibold mb-2">Vendor Performance</h3>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-muted-foreground">On-Time Delivery</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.on_time_delivery_rate.toFixed(1)}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Stock Status</dt>
<dd>{product?.stock_status || 'N/A'}</dd>
<dt className="text-sm text-muted-foreground">Order Fill Rate</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.order_fill_rate.toFixed(1)}%</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Avg Lead Time</dt>
<dd className="text-2xl font-semibold">{product.vendor_performance.avg_lead_time_days} days</dd>
</div>
</dl>
</Card>
<Card className="p-4">
<h3 className="font-semibold mb-2">Order History</h3>
<dl className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-muted-foreground">Total Orders</dt>
<dd>{product.vendor_performance.total_orders}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Late Orders</dt>
<dd>{product.vendor_performance.total_late_orders}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Total Purchase Value</dt>
<dd>${formatPrice(product.vendor_performance.total_purchase_value)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Avg Order Value</dt>
<dd>${formatPrice(product.vendor_performance.avg_order_value)}</dd>
</div>
</dl>
</Card>
</div>
) : (
<div className="text-center text-muted-foreground">No vendor performance data available</div>
)}
</TabsContent>
</Tabs>