Add new dashboard backend

This commit is contained in:
2025-01-13 00:14:15 -05:00
parent 024155d054
commit 88c51059bb
14 changed files with 1085 additions and 727 deletions

View File

@@ -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' });
}
});