Add new dashboard backend
This commit is contained in:
@@ -192,165 +192,125 @@ router.get('/sales-by-category', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get trending products
|
||||
router.get('/trending-products', async (req, res) => {
|
||||
// Get inventory health summary
|
||||
router.get('/inventory/health/summary', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
try {
|
||||
// First check what statuses exist
|
||||
const [checkStatuses] = await pool.query(`
|
||||
SELECT DISTINCT stock_status
|
||||
FROM product_metrics
|
||||
WHERE stock_status IS NOT NULL
|
||||
`);
|
||||
console.log('Available stock statuses:', checkStatuses.map(row => row.stock_status));
|
||||
|
||||
const [rows] = await pool.query(`
|
||||
WITH CurrentSales AS (
|
||||
WITH normalized_status AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.title,
|
||||
p.sku,
|
||||
p.stock_quantity,
|
||||
p.image,
|
||||
COALESCE(SUM(o.price * o.quantity), 0) as total_sales
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
AND o.canceled = false
|
||||
AND DATE(o.date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
WHERE p.visible = true
|
||||
GROUP BY p.product_id, p.title, p.sku, p.stock_quantity, p.image
|
||||
HAVING total_sales > 0
|
||||
),
|
||||
PreviousSales AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
COALESCE(SUM(o.price * o.quantity), 0) as previous_sales
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
AND o.canceled = false
|
||||
AND DATE(o.date) BETWEEN DATE_SUB(CURDATE(), INTERVAL 60 DAY) AND DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
WHERE p.visible = true
|
||||
GROUP BY p.product_id
|
||||
CASE
|
||||
WHEN stock_status = 'Overstocked' THEN 'Overstock'
|
||||
WHEN stock_status = 'New' THEN 'Healthy'
|
||||
ELSE stock_status
|
||||
END as status
|
||||
FROM product_metrics
|
||||
WHERE stock_status IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
cs.*,
|
||||
CASE
|
||||
WHEN COALESCE(ps.previous_sales, 0) = 0 THEN
|
||||
CASE WHEN cs.total_sales > 0 THEN 100 ELSE 0 END
|
||||
ELSE ((cs.total_sales - ps.previous_sales) / ps.previous_sales * 100)
|
||||
END as sales_growth
|
||||
FROM CurrentSales cs
|
||||
LEFT JOIN PreviousSales ps ON cs.product_id = ps.product_id
|
||||
ORDER BY cs.total_sales DESC
|
||||
LIMIT 5
|
||||
status as stock_status,
|
||||
COUNT(*) as count
|
||||
FROM normalized_status
|
||||
GROUP BY status
|
||||
`);
|
||||
|
||||
console.log('Raw inventory health summary:', rows);
|
||||
|
||||
// Convert array to object with lowercase keys
|
||||
const summary = {
|
||||
critical: 0,
|
||||
reorder: 0,
|
||||
healthy: 0,
|
||||
overstock: 0
|
||||
};
|
||||
|
||||
console.log('Trending products query result:', rows);
|
||||
rows.forEach(row => {
|
||||
const key = row.stock_status.toLowerCase();
|
||||
if (key in summary) {
|
||||
summary[key] = parseInt(row.count);
|
||||
}
|
||||
});
|
||||
|
||||
res.json(rows.map(row => ({
|
||||
product_id: row.product_id,
|
||||
title: row.title,
|
||||
sku: row.sku,
|
||||
total_sales: parseFloat(row.total_sales || 0),
|
||||
sales_growth: parseFloat(row.sales_growth || 0),
|
||||
stock_quantity: parseInt(row.stock_quantity || 0),
|
||||
image_url: row.image || null
|
||||
})));
|
||||
// Calculate total
|
||||
summary.total = Object.values(summary).reduce((a, b) => a + b, 0);
|
||||
|
||||
console.log('Final inventory health summary:', summary);
|
||||
res.json(summary);
|
||||
} catch (error) {
|
||||
console.error('Error in trending products:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: error.code,
|
||||
sqlState: error.sqlState,
|
||||
sqlMessage: error.sqlMessage
|
||||
});
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch trending products',
|
||||
details: error.message
|
||||
});
|
||||
console.error('Error fetching inventory health summary:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch inventory health summary' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get inventory metrics
|
||||
router.get('/inventory-metrics', async (req, res) => {
|
||||
// Get low stock alerts
|
||||
router.get('/inventory/low-stock', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
try {
|
||||
// Get global configuration values
|
||||
const [configs] = await pool.query(`
|
||||
const [rows] = await pool.query(`
|
||||
SELECT
|
||||
st.low_stock_threshold,
|
||||
tc.calculation_period_days as turnover_period
|
||||
FROM stock_thresholds st
|
||||
CROSS JOIN turnover_config tc
|
||||
WHERE st.id = 1 AND tc.id = 1
|
||||
p.product_id,
|
||||
p.sku,
|
||||
p.title,
|
||||
p.stock_quantity,
|
||||
pm.reorder_point,
|
||||
pm.days_of_inventory,
|
||||
pm.daily_sales_avg,
|
||||
pm.stock_status
|
||||
FROM product_metrics pm
|
||||
JOIN products p ON pm.product_id = p.product_id
|
||||
WHERE pm.stock_status IN ('Critical', 'Reorder')
|
||||
ORDER BY
|
||||
CASE pm.stock_status
|
||||
WHEN 'Critical' THEN 1
|
||||
WHEN 'Reorder' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
pm.days_of_inventory ASC
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
const config = configs[0] || {
|
||||
low_stock_threshold: 5,
|
||||
turnover_period: 30
|
||||
};
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching low stock alerts:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch low stock alerts' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get stock levels by category
|
||||
const [stockLevels] = await pool.query(`
|
||||
SELECT
|
||||
c.name as category,
|
||||
SUM(CASE WHEN stock_quantity > ? THEN 1 ELSE 0 END) as inStock,
|
||||
SUM(CASE WHEN stock_quantity > 0 AND stock_quantity <= ? THEN 1 ELSE 0 END) as lowStock,
|
||||
SUM(CASE WHEN stock_quantity = 0 THEN 1 ELSE 0 END) as outOfStock
|
||||
FROM products p
|
||||
JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
JOIN categories c ON pc.category_id = c.id
|
||||
WHERE visible = true
|
||||
GROUP BY c.name
|
||||
ORDER BY c.name ASC
|
||||
`, [config.low_stock_threshold, config.low_stock_threshold]);
|
||||
|
||||
// Get top vendors with product counts and average stock
|
||||
const [topVendors] = await pool.query(`
|
||||
// Get vendor performance metrics
|
||||
router.get('/vendors/metrics', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
try {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT
|
||||
vendor,
|
||||
COUNT(*) as productCount,
|
||||
AVG(stock_quantity) as averageStockLevel
|
||||
FROM products
|
||||
WHERE visible = true
|
||||
AND vendor IS NOT NULL
|
||||
AND vendor != ''
|
||||
GROUP BY vendor
|
||||
ORDER BY productCount DESC
|
||||
LIMIT 5
|
||||
avg_lead_time_days,
|
||||
on_time_delivery_rate,
|
||||
order_fill_rate,
|
||||
total_orders,
|
||||
total_late_orders,
|
||||
total_purchase_value,
|
||||
avg_order_value
|
||||
FROM vendor_metrics
|
||||
ORDER BY on_time_delivery_rate DESC
|
||||
`);
|
||||
|
||||
// Calculate stock turnover rate by category
|
||||
const [stockTurnover] = await pool.query(`
|
||||
WITH CategorySales AS (
|
||||
SELECT
|
||||
c.name as category,
|
||||
SUM(o.quantity) as units_sold
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
JOIN categories c ON pc.category_id = c.id
|
||||
WHERE o.canceled = false
|
||||
AND DATE(o.date) >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY c.name
|
||||
),
|
||||
CategoryStock AS (
|
||||
SELECT
|
||||
c.name as category,
|
||||
AVG(p.stock_quantity) as avg_stock
|
||||
FROM products p
|
||||
JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
JOIN categories c ON pc.category_id = c.id
|
||||
WHERE p.visible = true
|
||||
GROUP BY c.name
|
||||
)
|
||||
SELECT
|
||||
cs.category,
|
||||
CASE
|
||||
WHEN cst.avg_stock > 0 THEN (cs.units_sold / cst.avg_stock)
|
||||
ELSE 0
|
||||
END as rate
|
||||
FROM CategorySales cs
|
||||
JOIN CategoryStock cst ON cs.category = cst.category
|
||||
ORDER BY rate DESC
|
||||
`, [config.turnover_period]);
|
||||
|
||||
res.json({ stockLevels, topVendors, stockTurnover });
|
||||
res.json(rows.map(row => ({
|
||||
...row,
|
||||
avg_lead_time_days: parseFloat(row.avg_lead_time_days || 0),
|
||||
on_time_delivery_rate: parseFloat(row.on_time_delivery_rate || 0),
|
||||
order_fill_rate: parseFloat(row.order_fill_rate || 0),
|
||||
total_purchase_value: parseFloat(row.total_purchase_value || 0),
|
||||
avg_order_value: parseFloat(row.avg_order_value || 0)
|
||||
})));
|
||||
} catch (error) {
|
||||
console.error('Error fetching inventory metrics:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch inventory metrics' });
|
||||
console.error('Error fetching vendor metrics:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch vendor metrics' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user