Fix some backend issues, get dashboard loading without crashing

This commit is contained in:
2025-01-17 17:09:26 -05:00
parent 118344b730
commit 6b7a62ffaf
4 changed files with 381 additions and 210 deletions

View File

@@ -15,34 +15,45 @@ async function executeQuery(sql, params = []) {
// Returns brand-level stock metrics // Returns brand-level stock metrics
router.get('/stock/metrics', async (req, res) => { router.get('/stock/metrics', async (req, res) => {
try { try {
const [rows] = await executeQuery(` // Get stock metrics
const [stockMetrics] = await executeQuery(`
SELECT SELECT
bm.*, COALESCE(COUNT(*), 0) as total_products,
COALESCE( COALESCE(COUNT(CASE WHEN stock_quantity > 0 THEN 1 END), 0) as products_in_stock,
SUM(CASE COALESCE(SUM(stock_quantity), 0) as total_units,
WHEN pm.stock_status = 'Critical' THEN 1 COALESCE(SUM(stock_quantity * cost_price), 0) as total_cost,
ELSE 0 COALESCE(SUM(stock_quantity * price), 0) as total_retail
END) FROM products
, 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
`); `);
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) { } catch (err) {
console.error('Error fetching stock metrics:', err); console.error('Error fetching stock metrics:', err);
res.status(500).json({ error: 'Failed to fetch stock metrics' }); 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 // Returns purchase order metrics by vendor
router.get('/purchase/metrics', async (req, res) => { router.get('/purchase/metrics', async (req, res) => {
try { try {
const [rows] = await executeQuery(` const [poMetrics] = await executeQuery(`
SELECT SELECT
vm.*, COALESCE(COUNT(DISTINCT CASE WHEN po.status = 'open' THEN po.po_id END), 0) as active_pos,
COUNT(DISTINCT CASE COALESCE(COUNT(DISTINCT CASE
WHEN po.status = 'open' THEN po.po_id WHEN po.status = 'open' AND po.expected_date < CURDATE()
END) as active_orders,
COUNT(DISTINCT CASE
WHEN po.status = 'open'
AND po.expected_date < CURDATE()
THEN po.po_id THEN po.po_id
END) as overdue_orders, END), 0) as overdue_pos,
SUM(CASE COALESCE(SUM(CASE WHEN po.status = 'open' THEN po.ordered ELSE 0 END), 0) as total_units,
COALESCE(SUM(CASE
WHEN po.status = 'open' WHEN po.status = 'open'
THEN po.ordered * po.cost_price THEN po.ordered * po.cost_price
ELSE 0 ELSE 0
END) as active_order_value END), 0) as total_cost,
FROM vendor_metrics vm COALESCE(SUM(CASE
LEFT JOIN purchase_orders po ON vm.vendor = po.vendor WHEN po.status = 'open'
GROUP BY vm.vendor THEN po.ordered * p.price
ORDER BY vm.total_purchase_value DESC 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) { } catch (err) {
console.error('Error fetching purchase metrics:', err); console.error('Error fetching purchase metrics:', err);
res.status(500).json({ error: 'Failed to fetch purchase metrics' }); 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 // Returns replenishment needs by category
router.get('/replenishment/metrics', async (req, res) => { router.get('/replenishment/metrics', async (req, res) => {
try { try {
const [rows] = await executeQuery(` // Get summary metrics
WITH category_replenishment AS ( const [metrics] = await executeQuery(`
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
)
SELECT SELECT
cr.*, COUNT(DISTINCT CASE
cm.total_value as category_total_value, WHEN pm.stock_status IN ('Critical', 'Reorder')
cm.turnover_rate as category_turnover_rate THEN p.product_id
FROM category_replenishment cr END) as products_to_replenish,
LEFT JOIN category_metrics cm ON cr.category_id = cm.category_id SUM(CASE
WHERE cr.products_to_replenish > 0 WHEN pm.stock_status IN ('Critical', 'Reorder')
ORDER BY cr.total_replenishment_cost DESC 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) { } catch (err) {
console.error('Error fetching replenishment metrics:', err); console.error('Error fetching replenishment metrics:', err);
res.status(500).json({ error: 'Failed to fetch replenishment metrics' }); 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) => { router.get('/forecast/metrics', async (req, res) => {
const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30)); const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30));
try { try {
const [rows] = await executeQuery(` // Get summary metrics
WITH daily_forecasts AS ( const [metrics] = await executeQuery(`
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
)
SELECT SELECT
SUM(df.total_units) as total_forecast_units, COALESCE(SUM(forecast_units), 0) as total_forecast_units,
SUM(df.total_revenue) as total_forecast_revenue, COALESCE(SUM(forecast_revenue), 0) as total_forecast_revenue,
AVG(df.avg_confidence) as overall_confidence, COALESCE(AVG(confidence_level), 0) as overall_confidence
JSON_ARRAYAGG( FROM sales_forecasts
JSON_OBJECT( WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
'date', df.forecast_date, `, [days]);
'units', df.total_units,
'revenue', df.total_revenue, // Get daily forecasts
'confidence', df.avg_confidence const [dailyForecasts] = await executeQuery(`
) SELECT
) as daily_data, forecast_date as date,
JSON_ARRAYAGG( COALESCE(SUM(forecast_units), 0) as units,
JSON_OBJECT( COALESCE(SUM(forecast_revenue), 0) as revenue,
'category', cfs.category_name, COALESCE(AVG(confidence_level), 0) as confidence
'units', cfs.category_units, FROM sales_forecasts
'revenue', cfs.category_revenue, WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
'confidence', cfs.category_confidence GROUP BY forecast_date
) ORDER BY forecast_date
) as category_data `, [days]);
FROM daily_forecasts df
CROSS JOIN category_forecasts_summary cfs // Get category forecasts
`, [days, days]); const [categoryForecasts] = await executeQuery(`
res.json(rows[0]); 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) { } catch (err) {
console.error('Error fetching forecast metrics:', err); console.error('Error fetching forecast metrics:', err);
res.status(500).json({ error: 'Failed to fetch forecast metrics' }); 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 GROUP BY c.id, c.name
) )
SELECT SELECT
co.*, SUM(overstocked_products) as total_overstocked,
cm.total_value as category_total_value, SUM(total_excess_units) as total_excess_units,
cm.turnover_rate as category_turnover_rate SUM(total_excess_cost) as total_excess_cost,
FROM category_overstock co SUM(total_excess_retail) as total_excess_retail,
LEFT JOIN category_metrics cm ON co.category_id = cm.category_id CAST(JSON_ARRAYAGG(
WHERE co.overstocked_products > 0 JSON_OBJECT(
ORDER BY co.total_excess_cost DESC '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) { } catch (err) {
console.error('Error fetching overstock metrics:', err); console.error('Error fetching overstock metrics:', err);
res.status(500).json({ error: 'Failed to fetch overstock metrics' }); 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.total_revenue,
pm.daily_sales_avg, pm.daily_sales_avg,
pm.number_of_orders, pm.number_of_orders,
SUM(o.quantity) as units_sold,
GROUP_CONCAT(c.name) as categories GROUP_CONCAT(c.name) as categories
FROM products p FROM products p
JOIN product_metrics pm ON p.product_id = pm.product_id 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 product_categories pc ON p.product_id = pc.product_id
LEFT JOIN categories c ON pc.category_id = c.id LEFT JOIN categories c ON pc.category_id = c.id
GROUP BY p.product_id GROUP BY p.product_id
@@ -302,8 +412,11 @@ router.get('/best-sellers', async (req, res) => {
const [vendors] = await executeQuery(` const [vendors] = await executeQuery(`
SELECT SELECT
vm.* vm.*,
COALESCE(SUM(o.quantity), 0) as products_sold
FROM vendor_metrics vm 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 ORDER BY vm.total_revenue DESC
LIMIT 10 LIMIT 10
`); `);
@@ -318,8 +431,18 @@ router.get('/best-sellers', async (req, res) => {
LIMIT 10 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({ res.json({
products, products: formattedProducts,
vendors, vendors,
categories categories
}); });
@@ -334,8 +457,18 @@ router.get('/best-sellers', async (req, res) => {
router.get('/sales/metrics', async (req, res) => { router.get('/sales/metrics', async (req, res) => {
const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30)); const days = Math.max(1, Math.min(365, parseInt(req.query.days) || 30));
try { try {
const [rows] = await executeQuery(` const [dailyData] = await executeQuery(`
WITH daily_sales AS ( 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 SELECT
DATE(o.date) as sale_date, DATE(o.date) as sale_date,
COUNT(DISTINCT o.order_number) as total_orders, COUNT(DISTINCT o.order_number) as total_orders,
@@ -348,8 +481,19 @@ router.get('/sales/metrics', async (req, res) => {
WHERE o.canceled = false WHERE o.canceled = false
AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY DATE(o.date) GROUP BY DATE(o.date)
), ) d
category_sales AS ( `, [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 SELECT
c.name as category_name, c.name as category_name,
COUNT(DISTINCT o.order_number) as category_orders, COUNT(DISTINCT o.order_number) as category_orders,
@@ -362,39 +506,50 @@ router.get('/sales/metrics', async (req, res) => {
WHERE o.canceled = false WHERE o.canceled = false
AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY c.id, c.name GROUP BY c.id, c.name
) ) c
`, [days]);
const [metrics] = await executeQuery(`
SELECT SELECT
COUNT(DISTINCT ds.sale_date) as days_with_sales, COALESCE(COUNT(DISTINCT DATE(o.date)), 0) as days_with_sales,
SUM(ds.total_orders) as total_orders, COALESCE(COUNT(DISTINCT o.order_number), 0) as total_orders,
SUM(ds.total_units) as total_units, COALESCE(SUM(o.quantity), 0) as total_units,
SUM(ds.total_revenue) as total_revenue, COALESCE(SUM(o.price * o.quantity), 0) as total_revenue,
SUM(ds.total_cogs) as total_cogs, COALESCE(SUM(p.cost_price * o.quantity), 0) as total_cogs,
SUM(ds.total_profit) as total_profit, COALESCE(SUM((o.price - p.cost_price) * o.quantity), 0) as total_profit,
AVG(ds.total_orders) as avg_daily_orders, COALESCE(AVG(daily.orders), 0) as avg_daily_orders,
AVG(ds.total_units) as avg_daily_units, COALESCE(AVG(daily.units), 0) as avg_daily_units,
AVG(ds.total_revenue) as avg_daily_revenue, COALESCE(AVG(daily.revenue), 0) as avg_daily_revenue
JSON_ARRAYAGG( FROM orders o
JSON_OBJECT( JOIN products p ON o.product_id = p.product_id
'date', ds.sale_date, LEFT JOIN (
'orders', ds.total_orders, SELECT
'units', ds.total_units, DATE(date) as sale_date,
'revenue', ds.total_revenue, COUNT(DISTINCT order_number) as orders,
'cogs', ds.total_cogs, SUM(quantity) as units,
'profit', ds.total_profit SUM(price * quantity) as revenue
) FROM orders
) as daily_data, WHERE canceled = false
JSON_ARRAYAGG( GROUP BY DATE(date)
JSON_OBJECT( ) daily ON DATE(o.date) = daily.sale_date
'category', cs.category_name, WHERE o.canceled = false
'orders', cs.category_orders, AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
'units', cs.category_units, `, [days]);
'revenue', cs.category_revenue
) const response = {
) as category_data totalOrders: parseInt(metrics.total_orders) || 0,
FROM daily_sales ds totalUnitsSold: parseInt(metrics.total_units) || 0,
CROSS JOIN category_sales cs totalRevenue: parseFloat(metrics.total_revenue) || 0,
`, [days, days]); totalCogs: parseFloat(metrics.total_cogs) || 0,
res.json(rows[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) { } catch (err) {
console.error('Error fetching sales metrics:', err); console.error('Error fetching sales metrics:', err);
res.status(500).json({ error: 'Failed to fetch sales metrics' }); res.status(500).json({ error: 'Failed to fetch sales metrics' });
@@ -502,38 +657,53 @@ router.get('/vendor/performance', async (req, res) => {
SELECT SELECT
po.vendor, po.vendor,
COUNT(DISTINCT po.po_id) as total_orders, COUNT(DISTINCT po.po_id) as total_orders,
AVG(DATEDIFF(po.delivery_date, po.order_date)) as avg_lead_time, CAST(AVG(DATEDIFF(po.received_date, po.date)) AS DECIMAL(10,2)) as avg_lead_time,
AVG(CASE CAST(AVG(CASE
WHEN po.status = 'completed' WHEN po.status = 'completed'
THEN DATEDIFF(po.delivery_date, po.expected_date) THEN DATEDIFF(po.received_date, po.expected_date)
END) as avg_delay, END) AS DECIMAL(10,2)) as avg_delay,
SUM(CASE CAST(SUM(CASE
WHEN po.status = 'completed' AND po.delivery_date <= po.expected_date WHEN po.status = 'completed' AND po.received_date <= po.expected_date
THEN 1 THEN 1
ELSE 0 ELSE 0
END) * 100.0 / COUNT(*) as on_time_delivery_rate, END) * 100.0 / COUNT(*) AS DECIMAL(10,2)) as on_time_delivery_rate,
AVG(po.fill_rate) as avg_fill_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 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 GROUP BY po.vendor
) )
SELECT SELECT
v.*, vd.vendor,
vo.total_orders, vd.contact_name,
vd.status,
CAST(vo.total_orders AS SIGNED) as total_orders,
vo.avg_lead_time, vo.avg_lead_time,
vo.avg_delay, vo.avg_delay,
vo.on_time_delivery_rate, vo.on_time_delivery_rate,
vo.avg_fill_rate, vo.avg_fill_rate
vm.total_purchase_value, FROM vendor_details vd
vm.total_revenue, JOIN vendor_orders vo ON vd.vendor = vo.vendor
vm.product_count, WHERE vd.status = 'active'
vm.active_products ORDER BY vo.on_time_delivery_rate DESC
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
`); `);
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) { } catch (err) {
console.error('Error fetching vendor performance:', err); console.error('Error fetching vendor performance:', err);
res.status(500).json({ error: 'Failed to fetch vendor performance' }); res.status(500).json({ error: 'Failed to fetch vendor performance' });

View File

@@ -14,10 +14,10 @@ import config from "@/config"
interface LowStockProduct { interface LowStockProduct {
product_id: number product_id: number
sku: string SKU: string
title: string title: string
stock_quantity: number stock_quantity: number
reorder_point: number reorder_qty: number
days_of_inventory: number days_of_inventory: number
stock_status: "Critical" | "Reorder" stock_status: "Critical" | "Reorder"
daily_sales_avg: number daily_sales_avg: number
@@ -27,7 +27,7 @@ export function LowStockAlerts() {
const { data: products } = useQuery<LowStockProduct[]>({ const { data: products } = useQuery<LowStockProduct[]>({
queryKey: ["low-stock"], queryKey: ["low-stock"],
queryFn: async () => { 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) { if (!response.ok) {
throw new Error("Failed to fetch low stock products") throw new Error("Failed to fetch low stock products")
} }
@@ -54,10 +54,10 @@ export function LowStockAlerts() {
<TableBody> <TableBody>
{products?.map((product) => ( {products?.map((product) => (
<TableRow key={product.product_id}> <TableRow key={product.product_id}>
<TableCell className="font-medium">{product.sku}</TableCell> <TableCell className="font-medium">{product.SKU}</TableCell>
<TableCell>{product.title}</TableCell> <TableCell>{product.title}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{product.stock_quantity} / {product.reorder_point} {product.stock_quantity} / {product.reorder_qty}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Badge <Badge

View File

@@ -7,11 +7,11 @@ import { formatCurrency } from "@/lib/utils"
interface OverstockedProduct { interface OverstockedProduct {
product_id: number product_id: number
sku: string SKU: string
title: string title: string
overstocked_units: number overstocked_amt: number
overstocked_cost: number excess_cost: number
overstocked_retail: number excess_retail: number
days_of_inventory: number days_of_inventory: number
} }
@@ -49,14 +49,14 @@ export function TopOverstockedProducts() {
<TableCell> <TableCell>
<div> <div>
<p className="font-medium">{product.title}</p> <p className="font-medium">{product.title}</p>
<p className="text-sm text-muted-foreground">{product.sku}</p> <p className="text-sm text-muted-foreground">{product.SKU}</p>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{product.overstocked_units.toLocaleString()} {product.overstocked_amt.toLocaleString()}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{formatCurrency(product.overstocked_cost)} {formatCurrency(product.excess_cost)}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{product.days_of_inventory} {product.days_of_inventory}

View File

@@ -13,18 +13,19 @@ import config from "@/config"
interface VendorMetrics { interface VendorMetrics {
vendor: string vendor: string
avg_lead_time_days: number avg_lead_time: number
on_time_delivery_rate: number on_time_delivery_rate: number
order_fill_rate: number avg_fill_rate: number
total_orders: number total_orders: number
total_late_orders: number active_orders: number
overdue_orders: number
} }
export function VendorPerformance() { export function VendorPerformance() {
const { data: vendors } = useQuery<VendorMetrics[]>({ const { data: vendors } = useQuery<VendorMetrics[]>({
queryKey: ["vendor-metrics"], queryKey: ["vendor-metrics"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/vendors/metrics`) const response = await fetch(`${config.apiUrl}/dashboard/vendor/performance`)
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch vendor metrics") throw new Error("Failed to fetch vendor metrics")
} }
@@ -66,7 +67,7 @@ export function VendorPerformance() {
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{vendor.order_fill_rate.toFixed(0)}% {vendor.avg_fill_rate.toFixed(0)}%
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}