diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 456029d..5a2bbed 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -15,34 +15,45 @@ async function executeQuery(sql, params = []) { // Returns brand-level stock metrics router.get('/stock/metrics', async (req, res) => { try { - const [rows] = await executeQuery(` + // Get stock metrics + const [stockMetrics] = await executeQuery(` SELECT - bm.*, - COALESCE( - SUM(CASE - WHEN pm.stock_status = 'Critical' THEN 1 - ELSE 0 - END) - , 0) as critical_stock_count, - COALESCE( - SUM(CASE - WHEN pm.stock_status = 'Reorder' THEN 1 - ELSE 0 - END) - , 0) as reorder_count, - COALESCE( - SUM(CASE - WHEN pm.stock_status = 'Overstocked' THEN 1 - ELSE 0 - END) - , 0) as overstock_count - FROM brand_metrics bm - LEFT JOIN products p ON p.brand = bm.brand - LEFT JOIN product_metrics pm ON p.product_id = pm.product_id - GROUP BY bm.brand - ORDER BY bm.total_revenue DESC + COALESCE(COUNT(*), 0) as total_products, + COALESCE(COUNT(CASE WHEN stock_quantity > 0 THEN 1 END), 0) as products_in_stock, + COALESCE(SUM(stock_quantity), 0) as total_units, + COALESCE(SUM(stock_quantity * cost_price), 0) as total_cost, + COALESCE(SUM(stock_quantity * price), 0) as total_retail + FROM products `); - res.json(rows); + + // Get brand values in a separate query + const [brandValues] = await executeQuery(` + SELECT + brand, + COALESCE(SUM(stock_quantity * price), 0) as value + FROM products + WHERE brand IS NOT NULL + AND stock_quantity > 0 + GROUP BY brand + HAVING value > 0 + ORDER BY value DESC + LIMIT 8 + `); + + // Format the response with explicit type conversion + const response = { + totalProducts: parseInt(stockMetrics.total_products) || 0, + productsInStock: parseInt(stockMetrics.products_in_stock) || 0, + totalStockUnits: parseInt(stockMetrics.total_units) || 0, + totalStockCost: parseFloat(stockMetrics.total_cost) || 0, + totalStockRetail: parseFloat(stockMetrics.total_retail) || 0, + brandRetailValue: brandValues.map(b => ({ + brand: b.brand, + value: parseFloat(b.value) || 0 + })) + }; + + res.json(response); } catch (err) { console.error('Error fetching stock metrics:', err); res.status(500).json({ error: 'Failed to fetch stock metrics' }); @@ -53,28 +64,55 @@ router.get('/stock/metrics', async (req, res) => { // Returns purchase order metrics by vendor router.get('/purchase/metrics', async (req, res) => { try { - const [rows] = await executeQuery(` + const [poMetrics] = await executeQuery(` SELECT - vm.*, - COUNT(DISTINCT CASE - WHEN po.status = 'open' THEN po.po_id - END) as active_orders, - COUNT(DISTINCT CASE - WHEN po.status = 'open' - AND po.expected_date < CURDATE() + COALESCE(COUNT(DISTINCT CASE WHEN po.status = 'open' THEN po.po_id END), 0) as active_pos, + COALESCE(COUNT(DISTINCT CASE + WHEN po.status = 'open' AND po.expected_date < CURDATE() THEN po.po_id - END) as overdue_orders, - SUM(CASE + END), 0) as overdue_pos, + COALESCE(SUM(CASE WHEN po.status = 'open' THEN po.ordered ELSE 0 END), 0) as total_units, + COALESCE(SUM(CASE WHEN po.status = 'open' THEN po.ordered * po.cost_price ELSE 0 - END) as active_order_value - FROM vendor_metrics vm - LEFT JOIN purchase_orders po ON vm.vendor = po.vendor - GROUP BY vm.vendor - ORDER BY vm.total_purchase_value DESC + END), 0) as total_cost, + COALESCE(SUM(CASE + WHEN po.status = 'open' + THEN po.ordered * p.price + ELSE 0 + END), 0) as total_retail + FROM purchase_orders po + JOIN products p ON po.product_id = p.product_id `); - res.json(rows); + + const [vendorValues] = await executeQuery(` + SELECT + po.vendor, + COALESCE(SUM(CASE + WHEN po.status = 'open' + THEN po.ordered * po.cost_price + ELSE 0 + END), 0) as value + FROM purchase_orders po + WHERE po.status = 'open' + GROUP BY po.vendor + HAVING value > 0 + ORDER BY value DESC + LIMIT 8 + `); + + res.json({ + activePurchaseOrders: parseInt(poMetrics.active_pos) || 0, + overduePurchaseOrders: parseInt(poMetrics.overdue_pos) || 0, + onOrderUnits: parseInt(poMetrics.total_units) || 0, + onOrderCost: parseFloat(poMetrics.total_cost) || 0, + onOrderRetail: parseFloat(poMetrics.total_retail) || 0, + vendorOrderValue: vendorValues.map(v => ({ + vendor: v.vendor, + value: parseFloat(v.value) || 0 + })) + }); } catch (err) { console.error('Error fetching purchase metrics:', err); res.status(500).json({ error: 'Failed to fetch purchase metrics' }); @@ -85,47 +123,83 @@ router.get('/purchase/metrics', async (req, res) => { // Returns replenishment needs by category router.get('/replenishment/metrics', async (req, res) => { try { - const [rows] = await executeQuery(` - WITH category_replenishment AS ( - SELECT - c.id as category_id, - c.name as category_name, - COUNT(DISTINCT CASE - WHEN pm.stock_status IN ('Critical', 'Reorder') - THEN p.product_id - END) as products_to_replenish, - SUM(CASE - WHEN pm.stock_status IN ('Critical', 'Reorder') - THEN pm.reorder_qty - ELSE 0 - END) as total_units_needed, - SUM(CASE - WHEN pm.stock_status IN ('Critical', 'Reorder') - THEN pm.reorder_qty * p.cost_price - ELSE 0 - END) as total_replenishment_cost, - SUM(CASE - WHEN pm.stock_status IN ('Critical', 'Reorder') - THEN pm.reorder_qty * p.price - ELSE 0 - END) as total_replenishment_retail - FROM categories c - JOIN product_categories pc ON c.id = pc.category_id - JOIN products p ON pc.product_id = p.product_id - JOIN product_metrics pm ON p.product_id = pm.product_id - WHERE p.replenishable = true - GROUP BY c.id, c.name - ) + // Get summary metrics + const [metrics] = await executeQuery(` SELECT - cr.*, - cm.total_value as category_total_value, - cm.turnover_rate as category_turnover_rate - FROM category_replenishment cr - LEFT JOIN category_metrics cm ON cr.category_id = cm.category_id - WHERE cr.products_to_replenish > 0 - ORDER BY cr.total_replenishment_cost DESC + COUNT(DISTINCT CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN p.product_id + END) as products_to_replenish, + SUM(CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN pm.reorder_qty + ELSE 0 + END) as total_units_needed, + SUM(CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN pm.reorder_qty * p.cost_price + ELSE 0 + END) as total_cost, + SUM(CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN pm.reorder_qty * p.price + ELSE 0 + END) as total_retail + FROM products p + JOIN product_metrics pm ON p.product_id = pm.product_id + WHERE p.replenishable = true `); - res.json(rows); + + // Get category breakdown + const [categories] = await executeQuery(` + SELECT + c.name as category, + COUNT(DISTINCT CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN p.product_id + END) as products, + SUM(CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN pm.reorder_qty + ELSE 0 + END) as units, + SUM(CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN pm.reorder_qty * p.cost_price + ELSE 0 + END) as cost, + SUM(CASE + WHEN pm.stock_status IN ('Critical', 'Reorder') + THEN pm.reorder_qty * p.price + ELSE 0 + END) as retail + FROM categories c + JOIN product_categories pc ON c.id = pc.category_id + JOIN products p ON pc.product_id = p.product_id + JOIN product_metrics pm ON p.product_id = pm.product_id + WHERE p.replenishable = true + GROUP BY c.id, c.name + HAVING products > 0 + ORDER BY cost DESC + LIMIT 8 + `); + + // Format response + const response = { + productsToReplenish: parseInt(metrics.products_to_replenish) || 0, + totalUnitsToReplenish: parseInt(metrics.total_units_needed) || 0, + totalReplenishmentCost: parseFloat(metrics.total_cost) || 0, + totalReplenishmentRetail: parseFloat(metrics.total_retail) || 0, + categoryData: categories.map(c => ({ + category: c.category, + products: parseInt(c.products) || 0, + units: parseInt(c.units) || 0, + cost: parseFloat(c.cost) || 0, + retail: parseFloat(c.retail) || 0 + })) + }; + + res.json(response); } catch (err) { console.error('Error fetching replenishment metrics:', err); res.status(500).json({ error: 'Failed to fetch replenishment metrics' }); @@ -137,52 +211,63 @@ router.get('/replenishment/metrics', async (req, res) => { router.get('/forecast/metrics', async (req, res) => { const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30)); try { - const [rows] = await executeQuery(` - WITH daily_forecasts AS ( - SELECT - forecast_date, - SUM(forecast_units) as total_units, - SUM(forecast_revenue) as total_revenue, - AVG(confidence_level) as avg_confidence - FROM sales_forecasts - WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) - GROUP BY forecast_date - ), - category_forecasts_summary AS ( - SELECT - c.name as category_name, - SUM(cf.forecast_units) as category_units, - SUM(cf.forecast_revenue) as category_revenue, - AVG(cf.confidence_level) as category_confidence - FROM category_forecasts cf - JOIN categories c ON cf.category_id = c.id - WHERE cf.forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) - GROUP BY c.id, c.name - ) + // Get summary metrics + const [metrics] = await executeQuery(` SELECT - SUM(df.total_units) as total_forecast_units, - SUM(df.total_revenue) as total_forecast_revenue, - AVG(df.avg_confidence) as overall_confidence, - JSON_ARRAYAGG( - JSON_OBJECT( - 'date', df.forecast_date, - 'units', df.total_units, - 'revenue', df.total_revenue, - 'confidence', df.avg_confidence - ) - ) as daily_data, - JSON_ARRAYAGG( - JSON_OBJECT( - 'category', cfs.category_name, - 'units', cfs.category_units, - 'revenue', cfs.category_revenue, - 'confidence', cfs.category_confidence - ) - ) as category_data - FROM daily_forecasts df - CROSS JOIN category_forecasts_summary cfs - `, [days, days]); - res.json(rows[0]); + COALESCE(SUM(forecast_units), 0) as total_forecast_units, + COALESCE(SUM(forecast_revenue), 0) as total_forecast_revenue, + COALESCE(AVG(confidence_level), 0) as overall_confidence + FROM sales_forecasts + WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) + `, [days]); + + // Get daily forecasts + const [dailyForecasts] = await executeQuery(` + SELECT + forecast_date as date, + COALESCE(SUM(forecast_units), 0) as units, + COALESCE(SUM(forecast_revenue), 0) as revenue, + COALESCE(AVG(confidence_level), 0) as confidence + FROM sales_forecasts + WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) + GROUP BY forecast_date + ORDER BY forecast_date + `, [days]); + + // Get category forecasts + const [categoryForecasts] = await executeQuery(` + SELECT + c.name as category, + COALESCE(SUM(cf.forecast_units), 0) as units, + COALESCE(SUM(cf.forecast_revenue), 0) as revenue, + COALESCE(AVG(cf.confidence_level), 0) as confidence + FROM category_forecasts cf + JOIN categories c ON cf.category_id = c.id + WHERE cf.forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) + GROUP BY c.id, c.name + ORDER BY revenue DESC + `, [days]); + + // Format response + const response = { + forecastSales: parseInt(metrics.total_forecast_units) || 0, + forecastRevenue: parseFloat(metrics.total_forecast_revenue) || 0, + confidenceLevel: parseFloat(metrics.overall_confidence) || 0, + dailyForecasts: dailyForecasts.map(d => ({ + date: d.date, + units: parseInt(d.units) || 0, + revenue: parseFloat(d.revenue) || 0, + confidence: parseFloat(d.confidence) || 0 + })), + categoryForecasts: categoryForecasts.map(c => ({ + category: c.category, + units: parseInt(c.units) || 0, + revenue: parseFloat(c.revenue) || 0, + confidence: parseFloat(c.confidence) || 0 + })) + }; + + res.json(response); } catch (err) { console.error('Error fetching forecast metrics:', err); res.status(500).json({ error: 'Failed to fetch forecast metrics' }); @@ -224,15 +309,38 @@ router.get('/overstock/metrics', async (req, res) => { GROUP BY c.id, c.name ) SELECT - co.*, - cm.total_value as category_total_value, - cm.turnover_rate as category_turnover_rate - FROM category_overstock co - LEFT JOIN category_metrics cm ON co.category_id = cm.category_id - WHERE co.overstocked_products > 0 - ORDER BY co.total_excess_cost DESC + SUM(overstocked_products) as total_overstocked, + SUM(total_excess_units) as total_excess_units, + SUM(total_excess_cost) as total_excess_cost, + SUM(total_excess_retail) as total_excess_retail, + CAST(JSON_ARRAYAGG( + JSON_OBJECT( + 'category', category_name, + 'products', overstocked_products, + 'units', total_excess_units, + 'cost', total_excess_cost, + 'retail', total_excess_retail + ) + ) AS JSON) as category_data + FROM ( + SELECT * + FROM category_overstock + WHERE overstocked_products > 0 + ORDER BY total_excess_cost DESC + LIMIT 8 + ) filtered_categories `); - res.json(rows); + + // Format response with explicit type conversion + const response = { + overstockedProducts: parseInt(rows[0].total_overstocked) || 0, + excessUnits: parseInt(rows[0].total_excess_units) || 0, + excessCost: parseFloat(rows[0].total_excess_cost) || 0, + excessRetail: parseFloat(rows[0].total_excess_retail) || 0, + categoryData: rows[0].category_data ? JSON.parse(rows[0].category_data) : [] + }; + + res.json(response); } catch (err) { console.error('Error fetching overstock metrics:', err); res.status(500).json({ error: 'Failed to fetch overstock metrics' }); @@ -290,9 +398,11 @@ router.get('/best-sellers', async (req, res) => { pm.total_revenue, pm.daily_sales_avg, pm.number_of_orders, + SUM(o.quantity) as units_sold, GROUP_CONCAT(c.name) as categories FROM products p JOIN product_metrics pm ON p.product_id = pm.product_id + LEFT JOIN orders o ON p.product_id = o.product_id AND o.canceled = false LEFT JOIN product_categories pc ON p.product_id = pc.product_id LEFT JOIN categories c ON pc.category_id = c.id GROUP BY p.product_id @@ -302,8 +412,11 @@ router.get('/best-sellers', async (req, res) => { const [vendors] = await executeQuery(` SELECT - vm.* + vm.*, + COALESCE(SUM(o.quantity), 0) as products_sold FROM vendor_metrics vm + LEFT JOIN orders o ON vm.vendor = o.vendor AND o.canceled = false + GROUP BY vm.vendor ORDER BY vm.total_revenue DESC LIMIT 10 `); @@ -318,8 +431,18 @@ router.get('/best-sellers', async (req, res) => { LIMIT 10 `); + // Format response with explicit type conversion + const formattedProducts = products.map(p => ({ + ...p, + total_revenue: parseFloat(p.total_revenue) || 0, + daily_sales_avg: parseFloat(p.daily_sales_avg) || 0, + number_of_orders: parseInt(p.number_of_orders) || 0, + units_sold: parseInt(p.units_sold) || 0, + categories: p.categories ? p.categories.split(',') : [] + })); + res.json({ - products, + products: formattedProducts, vendors, categories }); @@ -334,8 +457,18 @@ router.get('/best-sellers', async (req, res) => { router.get('/sales/metrics', async (req, res) => { const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30)); try { - const [rows] = await executeQuery(` - WITH daily_sales AS ( + const [dailyData] = await executeQuery(` + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'date', sale_date, + 'orders', COALESCE(total_orders, 0), + 'units', COALESCE(total_units, 0), + 'revenue', COALESCE(total_revenue, 0), + 'cogs', COALESCE(total_cogs, 0), + 'profit', COALESCE(total_profit, 0) + ) + ) as daily_data + FROM ( SELECT DATE(o.date) as sale_date, COUNT(DISTINCT o.order_number) as total_orders, @@ -348,8 +481,19 @@ router.get('/sales/metrics', async (req, res) => { WHERE o.canceled = false AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) GROUP BY DATE(o.date) - ), - category_sales AS ( + ) d + `, [days]); + + const [categoryData] = await executeQuery(` + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'category', category_name, + 'orders', COALESCE(category_orders, 0), + 'units', COALESCE(category_units, 0), + 'revenue', COALESCE(category_revenue, 0) + ) + ) as category_data + FROM ( SELECT c.name as category_name, COUNT(DISTINCT o.order_number) as category_orders, @@ -362,39 +506,50 @@ router.get('/sales/metrics', async (req, res) => { WHERE o.canceled = false AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) GROUP BY c.id, c.name - ) + ) c + `, [days]); + + const [metrics] = await executeQuery(` SELECT - COUNT(DISTINCT ds.sale_date) as days_with_sales, - SUM(ds.total_orders) as total_orders, - SUM(ds.total_units) as total_units, - SUM(ds.total_revenue) as total_revenue, - SUM(ds.total_cogs) as total_cogs, - SUM(ds.total_profit) as total_profit, - AVG(ds.total_orders) as avg_daily_orders, - AVG(ds.total_units) as avg_daily_units, - AVG(ds.total_revenue) as avg_daily_revenue, - JSON_ARRAYAGG( - JSON_OBJECT( - 'date', ds.sale_date, - 'orders', ds.total_orders, - 'units', ds.total_units, - 'revenue', ds.total_revenue, - 'cogs', ds.total_cogs, - 'profit', ds.total_profit - ) - ) as daily_data, - JSON_ARRAYAGG( - JSON_OBJECT( - 'category', cs.category_name, - 'orders', cs.category_orders, - 'units', cs.category_units, - 'revenue', cs.category_revenue - ) - ) as category_data - FROM daily_sales ds - CROSS JOIN category_sales cs - `, [days, days]); - res.json(rows[0]); + COALESCE(COUNT(DISTINCT DATE(o.date)), 0) as days_with_sales, + COALESCE(COUNT(DISTINCT o.order_number), 0) as total_orders, + COALESCE(SUM(o.quantity), 0) as total_units, + COALESCE(SUM(o.price * o.quantity), 0) as total_revenue, + COALESCE(SUM(p.cost_price * o.quantity), 0) as total_cogs, + COALESCE(SUM((o.price - p.cost_price) * o.quantity), 0) as total_profit, + COALESCE(AVG(daily.orders), 0) as avg_daily_orders, + COALESCE(AVG(daily.units), 0) as avg_daily_units, + COALESCE(AVG(daily.revenue), 0) as avg_daily_revenue + FROM orders o + JOIN products p ON o.product_id = p.product_id + LEFT JOIN ( + SELECT + DATE(date) as sale_date, + COUNT(DISTINCT order_number) as orders, + SUM(quantity) as units, + SUM(price * quantity) as revenue + FROM orders + WHERE canceled = false + GROUP BY DATE(date) + ) daily ON DATE(o.date) = daily.sale_date + WHERE o.canceled = false + AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + `, [days]); + + const response = { + totalOrders: parseInt(metrics.total_orders) || 0, + totalUnitsSold: parseInt(metrics.total_units) || 0, + totalRevenue: parseFloat(metrics.total_revenue) || 0, + totalCogs: parseFloat(metrics.total_cogs) || 0, + dailySales: JSON.parse(dailyData.daily_data || '[]').map(day => ({ + date: day.date, + units: parseInt(day.units) || 0, + revenue: parseFloat(day.revenue) || 0, + cogs: parseFloat(day.cogs) || 0 + })) + }; + + res.json(response); } catch (err) { console.error('Error fetching sales metrics:', err); res.status(500).json({ error: 'Failed to fetch sales metrics' }); @@ -502,38 +657,53 @@ router.get('/vendor/performance', async (req, res) => { SELECT po.vendor, COUNT(DISTINCT po.po_id) as total_orders, - AVG(DATEDIFF(po.delivery_date, po.order_date)) as avg_lead_time, - AVG(CASE + CAST(AVG(DATEDIFF(po.received_date, po.date)) AS DECIMAL(10,2)) as avg_lead_time, + CAST(AVG(CASE WHEN po.status = 'completed' - THEN DATEDIFF(po.delivery_date, po.expected_date) - END) as avg_delay, - SUM(CASE - WHEN po.status = 'completed' AND po.delivery_date <= po.expected_date + THEN DATEDIFF(po.received_date, po.expected_date) + END) AS DECIMAL(10,2)) as avg_delay, + CAST(SUM(CASE + WHEN po.status = 'completed' AND po.received_date <= po.expected_date THEN 1 ELSE 0 - END) * 100.0 / COUNT(*) as on_time_delivery_rate, - AVG(po.fill_rate) as avg_fill_rate + END) * 100.0 / COUNT(*) AS DECIMAL(10,2)) as on_time_delivery_rate, + CAST(AVG(CASE + WHEN po.status = 'completed' + THEN po.received / po.ordered * 100 + ELSE NULL + END) AS DECIMAL(10,2)) as avg_fill_rate FROM purchase_orders po - WHERE po.order_date >= DATE_SUB(CURDATE(), INTERVAL 180 DAY) + WHERE po.date >= DATE_SUB(CURDATE(), INTERVAL 180 DAY) GROUP BY po.vendor ) SELECT - v.*, - vo.total_orders, + vd.vendor, + vd.contact_name, + vd.status, + CAST(vo.total_orders AS SIGNED) as total_orders, vo.avg_lead_time, vo.avg_delay, vo.on_time_delivery_rate, - vo.avg_fill_rate, - vm.total_purchase_value, - vm.total_revenue, - vm.product_count, - vm.active_products - FROM vendors v - JOIN vendor_orders vo ON v.vendor = vo.vendor - JOIN vendor_metrics vm ON v.vendor = vm.vendor - ORDER BY vm.total_revenue DESC + vo.avg_fill_rate + FROM vendor_details vd + JOIN vendor_orders vo ON vd.vendor = vo.vendor + WHERE vd.status = 'active' + ORDER BY vo.on_time_delivery_rate DESC `); - res.json(rows); + + // Format response with explicit number parsing + const formattedRows = rows.map(row => ({ + vendor: row.vendor, + contact_name: row.contact_name, + status: row.status, + total_orders: parseInt(row.total_orders) || 0, + avg_lead_time: parseFloat(row.avg_lead_time) || 0, + avg_delay: parseFloat(row.avg_delay) || 0, + on_time_delivery_rate: parseFloat(row.on_time_delivery_rate) || 0, + avg_fill_rate: parseFloat(row.avg_fill_rate) || 0 + })); + + res.json(formattedRows); } catch (err) { console.error('Error fetching vendor performance:', err); res.status(500).json({ error: 'Failed to fetch vendor performance' }); diff --git a/inventory/src/components/dashboard/LowStockAlerts.tsx b/inventory/src/components/dashboard/LowStockAlerts.tsx index 7e98db7..25b6991 100644 --- a/inventory/src/components/dashboard/LowStockAlerts.tsx +++ b/inventory/src/components/dashboard/LowStockAlerts.tsx @@ -14,10 +14,10 @@ import config from "@/config" interface LowStockProduct { product_id: number - sku: string + SKU: string title: string stock_quantity: number - reorder_point: number + reorder_qty: number days_of_inventory: number stock_status: "Critical" | "Reorder" daily_sales_avg: number @@ -27,7 +27,7 @@ export function LowStockAlerts() { const { data: products } = useQuery({ queryKey: ["low-stock"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/inventory/low-stock`) + const response = await fetch(`${config.apiUrl}/dashboard/low-stock/products`) if (!response.ok) { throw new Error("Failed to fetch low stock products") } @@ -54,10 +54,10 @@ export function LowStockAlerts() { {products?.map((product) => ( - {product.sku} + {product.SKU} {product.title} - {product.stock_quantity} / {product.reorder_point} + {product.stock_quantity} / {product.reorder_qty}

{product.title}

-

{product.sku}

+

{product.SKU}

- {product.overstocked_units.toLocaleString()} + {product.overstocked_amt.toLocaleString()} - {formatCurrency(product.overstocked_cost)} + {formatCurrency(product.excess_cost)} {product.days_of_inventory} diff --git a/inventory/src/components/dashboard/VendorPerformance.tsx b/inventory/src/components/dashboard/VendorPerformance.tsx index 91a676c..52a0443 100644 --- a/inventory/src/components/dashboard/VendorPerformance.tsx +++ b/inventory/src/components/dashboard/VendorPerformance.tsx @@ -13,18 +13,19 @@ import config from "@/config" interface VendorMetrics { vendor: string - avg_lead_time_days: number + avg_lead_time: number on_time_delivery_rate: number - order_fill_rate: number + avg_fill_rate: number total_orders: number - total_late_orders: number + active_orders: number + overdue_orders: number } export function VendorPerformance() { const { data: vendors } = useQuery({ queryKey: ["vendor-metrics"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/vendors/metrics`) + const response = await fetch(`${config.apiUrl}/dashboard/vendor/performance`) if (!response.ok) { throw new Error("Failed to fetch vendor metrics") } @@ -66,7 +67,7 @@ export function VendorPerformance() { - {vendor.order_fill_rate.toFixed(0)}% + {vendor.avg_fill_rate.toFixed(0)}%
))}