Fix some backend issues, get dashboard loading without crashing
This commit is contained in:
@@ -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,11 +123,9 @@ 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
|
SELECT
|
||||||
c.id as category_id,
|
|
||||||
c.name as category_name,
|
|
||||||
COUNT(DISTINCT CASE
|
COUNT(DISTINCT CASE
|
||||||
WHEN pm.stock_status IN ('Critical', 'Reorder')
|
WHEN pm.stock_status IN ('Critical', 'Reorder')
|
||||||
THEN p.product_id
|
THEN p.product_id
|
||||||
@@ -103,29 +139,67 @@ router.get('/replenishment/metrics', async (req, res) => {
|
|||||||
WHEN pm.stock_status IN ('Critical', 'Reorder')
|
WHEN pm.stock_status IN ('Critical', 'Reorder')
|
||||||
THEN pm.reorder_qty * p.cost_price
|
THEN pm.reorder_qty * p.cost_price
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END) as total_replenishment_cost,
|
END) as total_cost,
|
||||||
SUM(CASE
|
SUM(CASE
|
||||||
WHEN pm.stock_status IN ('Critical', 'Reorder')
|
WHEN pm.stock_status IN ('Critical', 'Reorder')
|
||||||
THEN pm.reorder_qty * p.price
|
THEN pm.reorder_qty * p.price
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END) as total_replenishment_retail
|
END) as total_retail
|
||||||
|
FROM products p
|
||||||
|
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||||
|
WHERE p.replenishable = true
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 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
|
FROM categories c
|
||||||
JOIN product_categories pc ON c.id = pc.category_id
|
JOIN product_categories pc ON c.id = pc.category_id
|
||||||
JOIN products p ON pc.product_id = p.product_id
|
JOIN products p ON pc.product_id = p.product_id
|
||||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||||
WHERE p.replenishable = true
|
WHERE p.replenishable = true
|
||||||
GROUP BY c.id, c.name
|
GROUP BY c.id, c.name
|
||||||
)
|
HAVING products > 0
|
||||||
SELECT
|
ORDER BY cost DESC
|
||||||
cr.*,
|
LIMIT 8
|
||||||
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
|
|
||||||
`);
|
`);
|
||||||
res.json(rows);
|
|
||||||
|
// 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
|
SELECT
|
||||||
forecast_date,
|
COALESCE(SUM(forecast_units), 0) as total_forecast_units,
|
||||||
SUM(forecast_units) as total_units,
|
COALESCE(SUM(forecast_revenue), 0) as total_forecast_revenue,
|
||||||
SUM(forecast_revenue) as total_revenue,
|
COALESCE(AVG(confidence_level), 0) as overall_confidence
|
||||||
AVG(confidence_level) as avg_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
|
FROM sales_forecasts
|
||||||
WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
WHERE forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
||||||
GROUP BY forecast_date
|
GROUP BY forecast_date
|
||||||
),
|
ORDER BY forecast_date
|
||||||
category_forecasts_summary AS (
|
`, [days]);
|
||||||
|
|
||||||
|
// Get category forecasts
|
||||||
|
const [categoryForecasts] = await executeQuery(`
|
||||||
SELECT
|
SELECT
|
||||||
c.name as category_name,
|
c.name as category,
|
||||||
SUM(cf.forecast_units) as category_units,
|
COALESCE(SUM(cf.forecast_units), 0) as units,
|
||||||
SUM(cf.forecast_revenue) as category_revenue,
|
COALESCE(SUM(cf.forecast_revenue), 0) as revenue,
|
||||||
AVG(cf.confidence_level) as category_confidence
|
COALESCE(AVG(cf.confidence_level), 0) as confidence
|
||||||
FROM category_forecasts cf
|
FROM category_forecasts cf
|
||||||
JOIN categories c ON cf.category_id = c.id
|
JOIN categories c ON cf.category_id = c.id
|
||||||
WHERE cf.forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
WHERE cf.forecast_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
||||||
GROUP BY c.id, c.name
|
GROUP BY c.id, c.name
|
||||||
)
|
ORDER BY revenue DESC
|
||||||
SELECT
|
`, [days]);
|
||||||
SUM(df.total_units) as total_forecast_units,
|
|
||||||
SUM(df.total_revenue) as total_forecast_revenue,
|
// Format response
|
||||||
AVG(df.avg_confidence) as overall_confidence,
|
const response = {
|
||||||
JSON_ARRAYAGG(
|
forecastSales: parseInt(metrics.total_forecast_units) || 0,
|
||||||
JSON_OBJECT(
|
forecastRevenue: parseFloat(metrics.total_forecast_revenue) || 0,
|
||||||
'date', df.forecast_date,
|
confidenceLevel: parseFloat(metrics.overall_confidence) || 0,
|
||||||
'units', df.total_units,
|
dailyForecasts: dailyForecasts.map(d => ({
|
||||||
'revenue', df.total_revenue,
|
date: d.date,
|
||||||
'confidence', df.avg_confidence
|
units: parseInt(d.units) || 0,
|
||||||
)
|
revenue: parseFloat(d.revenue) || 0,
|
||||||
) as daily_data,
|
confidence: parseFloat(d.confidence) || 0
|
||||||
JSON_ARRAYAGG(
|
})),
|
||||||
JSON_OBJECT(
|
categoryForecasts: categoryForecasts.map(c => ({
|
||||||
'category', cfs.category_name,
|
category: c.category,
|
||||||
'units', cfs.category_units,
|
units: parseInt(c.units) || 0,
|
||||||
'revenue', cfs.category_revenue,
|
revenue: parseFloat(c.revenue) || 0,
|
||||||
'confidence', cfs.category_confidence
|
confidence: parseFloat(c.confidence) || 0
|
||||||
)
|
}))
|
||||||
) as category_data
|
};
|
||||||
FROM daily_forecasts df
|
|
||||||
CROSS JOIN category_forecasts_summary cfs
|
res.json(response);
|
||||||
`, [days, days]);
|
|
||||||
res.json(rows[0]);
|
|
||||||
} 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' });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user