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

@@ -367,15 +367,129 @@ router.get('/trending', async (req, res) => {
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
const pool = req.app.locals.pool; const pool = req.app.locals.pool;
try { try {
// Get basic product data with metrics
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM products WHERE product_id = ? AND visible = true', `SELECT
p.*,
GROUP_CONCAT(DISTINCT c.name) as categories,
pm.daily_sales_avg,
pm.weekly_sales_avg,
pm.monthly_sales_avg,
pm.days_of_inventory,
pm.reorder_point,
pm.safety_stock,
pm.avg_margin_percent,
pm.total_revenue,
pm.inventory_value,
pm.turnover_rate,
pm.abc_class,
pm.stock_status,
pm.avg_lead_time_days,
pm.current_lead_time,
pm.target_lead_time,
pm.lead_time_status,
pm.gmroi,
pm.cost_of_goods_sold,
pm.gross_profit
FROM products p
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
LEFT JOIN product_categories pc ON p.product_id = pc.product_id
LEFT JOIN categories c ON pc.category_id = c.id
WHERE p.product_id = ? AND p.visible = true
GROUP BY p.product_id`,
[req.params.id] [req.params.id]
); );
if (rows.length === 0) { if (rows.length === 0) {
return res.status(404).json({ error: 'Product not found' }); return res.status(404).json({ error: 'Product not found' });
} }
res.json(rows[0]);
// Get vendor performance metrics
const [vendorMetrics] = await pool.query(
`SELECT * FROM vendor_metrics WHERE vendor = ?`,
[rows[0].vendor]
);
// Transform the data to match frontend expectations
const product = {
// Basic product info
product_id: rows[0].product_id,
title: rows[0].title,
SKU: rows[0].SKU,
barcode: rows[0].barcode,
created_at: rows[0].created_at,
updated_at: rows[0].updated_at,
// Inventory fields
stock_quantity: parseInt(rows[0].stock_quantity),
moq: parseInt(rows[0].moq),
uom: parseInt(rows[0].uom),
managing_stock: Boolean(rows[0].managing_stock),
replenishable: Boolean(rows[0].replenishable),
// Pricing fields
price: parseFloat(rows[0].price),
regular_price: parseFloat(rows[0].regular_price),
cost_price: parseFloat(rows[0].cost_price),
landing_cost_price: parseFloat(rows[0].landing_cost_price),
// Categorization
categories: rows[0].categories ? rows[0].categories.split(',') : [],
tags: rows[0].tags ? rows[0].tags.split(',') : [],
options: rows[0].options ? JSON.parse(rows[0].options) : {},
// Vendor info
vendor: rows[0].vendor,
vendor_reference: rows[0].vendor_reference,
brand: rows[0].brand,
// URLs
permalink: rows[0].permalink,
image: rows[0].image,
// Metrics
metrics: {
// Sales metrics
daily_sales_avg: parseFloat(rows[0].daily_sales_avg) || 0,
weekly_sales_avg: parseFloat(rows[0].weekly_sales_avg) || 0,
monthly_sales_avg: parseFloat(rows[0].monthly_sales_avg) || 0,
// Inventory metrics
days_of_inventory: parseInt(rows[0].days_of_inventory) || 0,
reorder_point: parseInt(rows[0].reorder_point) || 0,
safety_stock: parseInt(rows[0].safety_stock) || 0,
stock_status: rows[0].stock_status || 'Unknown',
abc_class: rows[0].abc_class || 'C',
// Financial metrics
avg_margin_percent: parseFloat(rows[0].avg_margin_percent) || 0,
total_revenue: parseFloat(rows[0].total_revenue) || 0,
inventory_value: parseFloat(rows[0].inventory_value) || 0,
turnover_rate: parseFloat(rows[0].turnover_rate) || 0,
gmroi: parseFloat(rows[0].gmroi) || 0,
cost_of_goods_sold: parseFloat(rows[0].cost_of_goods_sold) || 0,
gross_profit: parseFloat(rows[0].gross_profit) || 0,
// Lead time metrics
avg_lead_time_days: parseInt(rows[0].avg_lead_time_days) || 0,
current_lead_time: parseInt(rows[0].current_lead_time) || 0,
target_lead_time: parseInt(rows[0].target_lead_time) || 14,
lead_time_status: rows[0].lead_time_status || 'Unknown'
},
// Vendor performance (if available)
vendor_performance: vendorMetrics.length ? {
avg_lead_time_days: parseFloat(vendorMetrics[0].avg_lead_time_days) || 0,
on_time_delivery_rate: parseFloat(vendorMetrics[0].on_time_delivery_rate) || 0,
order_fill_rate: parseFloat(vendorMetrics[0].order_fill_rate) || 0,
total_orders: parseInt(vendorMetrics[0].total_orders) || 0,
total_late_orders: parseInt(vendorMetrics[0].total_late_orders) || 0,
total_purchase_value: parseFloat(vendorMetrics[0].total_purchase_value) || 0,
avg_order_value: parseFloat(vendorMetrics[0].avg_order_value) || 0
} : null
};
res.json(product);
} catch (error) { } catch (error) {
console.error('Error fetching product:', error); console.error('Error fetching product:', error);
res.status(500).json({ error: 'Failed to fetch product' }); res.status(500).json({ error: 'Failed to fetch product' });
@@ -458,4 +572,202 @@ router.put('/:id', async (req, res) => {
} }
}); });
// Get product metrics
router.get('/:id/metrics', async (req, res) => {
const pool = req.app.locals.pool;
try {
const { id } = req.params;
// Get metrics from product_metrics table with inventory health data
const [metrics] = await pool.query(`
WITH inventory_status AS (
SELECT
p.product_id,
CASE
WHEN pm.daily_sales_avg = 0 THEN 'New'
WHEN p.stock_quantity <= CEIL(pm.daily_sales_avg * 7) THEN 'Critical'
WHEN p.stock_quantity <= CEIL(pm.daily_sales_avg * 14) THEN 'Reorder'
WHEN p.stock_quantity > (pm.daily_sales_avg * 90) THEN 'Overstocked'
ELSE 'Healthy'
END as calculated_status
FROM products p
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
WHERE p.product_id = ?
)
SELECT
COALESCE(pm.daily_sales_avg, 0) as daily_sales_avg,
COALESCE(pm.weekly_sales_avg, 0) as weekly_sales_avg,
COALESCE(pm.monthly_sales_avg, 0) as monthly_sales_avg,
COALESCE(pm.days_of_inventory, 0) as days_of_inventory,
COALESCE(pm.reorder_point, CEIL(COALESCE(pm.daily_sales_avg, 0) * 14)) as reorder_point,
COALESCE(pm.safety_stock, CEIL(COALESCE(pm.daily_sales_avg, 0) * 7)) as safety_stock,
COALESCE(pm.avg_margin_percent,
((p.price - COALESCE(p.cost_price, 0)) / NULLIF(p.price, 0)) * 100
) as avg_margin_percent,
COALESCE(pm.total_revenue, 0) as total_revenue,
COALESCE(pm.inventory_value, p.stock_quantity * COALESCE(p.cost_price, 0)) as inventory_value,
COALESCE(pm.turnover_rate, 0) as turnover_rate,
COALESCE(pm.abc_class, 'C') as abc_class,
COALESCE(pm.stock_status, is.calculated_status) as stock_status,
COALESCE(pm.avg_lead_time_days, 0) as avg_lead_time_days,
COALESCE(pm.current_lead_time, 0) as current_lead_time,
COALESCE(pm.target_lead_time, 14) as target_lead_time,
COALESCE(pm.lead_time_status, 'Unknown') as lead_time_status
FROM products p
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
LEFT JOIN inventory_status is ON p.product_id = is.product_id
WHERE p.product_id = ?
`, [id, id]);
if (!metrics.length) {
// Return default metrics structure if no data found
res.json({
daily_sales_avg: 0,
weekly_sales_avg: 0,
monthly_sales_avg: 0,
days_of_inventory: 0,
reorder_point: 0,
safety_stock: 0,
avg_margin_percent: 0,
total_revenue: 0,
inventory_value: 0,
turnover_rate: 0,
abc_class: 'C',
stock_status: 'New',
avg_lead_time_days: 0,
current_lead_time: 0,
target_lead_time: 14,
lead_time_status: 'Unknown'
});
return;
}
res.json(metrics[0]);
} catch (error) {
console.error('Error fetching product metrics:', error);
res.status(500).json({ error: 'Failed to fetch product metrics' });
}
});
// Get product time series data
router.get('/:id/time-series', async (req, res) => {
const pool = req.app.locals.pool;
try {
const { id } = req.params;
const months = parseInt(req.query.months) || 12;
// Get monthly sales data with running totals and growth rates
const [monthlySales] = await pool.query(`
WITH monthly_data AS (
SELECT
CONCAT(year, '-', LPAD(month, 2, '0')) as month,
total_quantity_sold as quantity,
total_revenue as revenue,
total_cost as cost,
avg_price,
profit_margin,
inventory_value
FROM product_time_aggregates
WHERE product_id = ?
ORDER BY year DESC, month DESC
LIMIT ?
)
SELECT
month,
quantity,
revenue,
cost,
avg_price,
profit_margin,
inventory_value,
LAG(quantity) OVER (ORDER BY month) as prev_month_quantity,
LAG(revenue) OVER (ORDER BY month) as prev_month_revenue
FROM monthly_data
ORDER BY month ASC
`, [id, months]);
// Calculate growth rates and format data
const formattedMonthlySales = monthlySales.map(row => ({
month: row.month,
quantity: parseInt(row.quantity) || 0,
revenue: parseFloat(row.revenue) || 0,
cost: parseFloat(row.cost) || 0,
avg_price: parseFloat(row.avg_price) || 0,
profit_margin: parseFloat(row.profit_margin) || 0,
inventory_value: parseFloat(row.inventory_value) || 0,
quantity_growth: row.prev_month_quantity ?
((row.quantity - row.prev_month_quantity) / row.prev_month_quantity) * 100 : 0,
revenue_growth: row.prev_month_revenue ?
((row.revenue - row.prev_month_revenue) / row.prev_month_revenue) * 100 : 0
}));
// Get recent orders with customer info and status
const [recentOrders] = await pool.query(`
SELECT
DATE_FORMAT(date, '%Y-%m-%d') as date,
order_number,
quantity,
price,
discount,
tax,
shipping,
customer,
status,
payment_method
FROM orders
WHERE product_id = ?
AND canceled = false
ORDER BY date DESC
LIMIT 10
`, [id]);
// Get recent purchase orders with detailed status
const [recentPurchases] = await pool.query(`
SELECT
DATE_FORMAT(date, '%Y-%m-%d') as date,
DATE_FORMAT(expected_date, '%Y-%m-%d') as expected_date,
DATE_FORMAT(received_date, '%Y-%m-%d') as received_date,
po_id,
ordered,
received,
status,
cost_price,
notes,
CASE
WHEN received_date IS NOT NULL THEN
DATEDIFF(received_date, date)
WHEN expected_date < CURDATE() AND status != 'received' THEN
DATEDIFF(CURDATE(), expected_date)
ELSE NULL
END as lead_time_days
FROM purchase_orders
WHERE product_id = ?
ORDER BY date DESC
LIMIT 10
`, [id]);
res.json({
monthly_sales: formattedMonthlySales,
recent_orders: recentOrders.map(order => ({
...order,
price: parseFloat(order.price),
discount: parseFloat(order.discount),
tax: parseFloat(order.tax),
shipping: parseFloat(order.shipping),
quantity: parseInt(order.quantity)
})),
recent_purchases: recentPurchases.map(po => ({
...po,
ordered: parseInt(po.ordered),
received: parseInt(po.received),
cost_price: parseFloat(po.cost_price),
lead_time_days: po.lead_time_days ? parseInt(po.lead_time_days) : null
}))
});
} catch (error) {
console.error('Error fetching product time series:', error);
res.status(500).json({ error: 'Failed to fetch product time series' });
}
});
module.exports = router; module.exports = router;

View File

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