Update frontend to match part 1
This commit is contained in:
@@ -36,7 +36,7 @@ router.get('/stats', async (req, res) => {
|
||||
0
|
||||
) as averageOrderValue
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
`);
|
||||
|
||||
@@ -70,12 +70,12 @@ router.get('/profit', async (req, res) => {
|
||||
(SUM(o.price * o.quantity - p.cost_price * o.quantity) /
|
||||
NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1
|
||||
) as profitMargin,
|
||||
SUM(o.price * o.quantity) as revenue,
|
||||
SUM(p.cost_price * o.quantity) as cost
|
||||
CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue,
|
||||
CAST(SUM(p.cost_price * o.quantity) AS DECIMAL(15,3)) as cost
|
||||
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
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY c.name
|
||||
ORDER BY profitMargin DESC
|
||||
@@ -90,10 +90,10 @@ router.get('/profit', async (req, res) => {
|
||||
(SUM(o.price * o.quantity - p.cost_price * o.quantity) /
|
||||
NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1
|
||||
) as profitMargin,
|
||||
SUM(o.price * o.quantity) as revenue,
|
||||
SUM(p.cost_price * o.quantity) as cost
|
||||
CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue,
|
||||
CAST(SUM(p.cost_price * o.quantity) AS DECIMAL(15,3)) as cost
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
CROSS JOIN (
|
||||
SELECT DATE_FORMAT(o.date, '%Y-%m-%d') as formatted_date
|
||||
FROM orders o
|
||||
@@ -114,12 +114,12 @@ router.get('/profit', async (req, res) => {
|
||||
(SUM(o.price * o.quantity - p.cost_price * o.quantity) /
|
||||
NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1
|
||||
) as profitMargin,
|
||||
SUM(o.price * o.quantity) as revenue,
|
||||
SUM(p.cost_price * o.quantity) as cost
|
||||
CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as revenue,
|
||||
CAST(SUM(p.cost_price * o.quantity) AS DECIMAL(15,3)) as cost
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY p.product_id, p.title
|
||||
GROUP BY p.pid, p.title
|
||||
HAVING revenue > 0
|
||||
ORDER BY profitMargin DESC
|
||||
LIMIT 10
|
||||
@@ -144,7 +144,7 @@ router.get('/vendors', async (req, res) => {
|
||||
SELECT COUNT(DISTINCT p.vendor) as vendor_count,
|
||||
COUNT(DISTINCT o.order_number) as order_count
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE p.vendor IS NOT NULL
|
||||
`);
|
||||
|
||||
@@ -155,26 +155,26 @@ router.get('/vendors', async (req, res) => {
|
||||
WITH monthly_sales AS (
|
||||
SELECT
|
||||
p.vendor,
|
||||
SUM(CASE
|
||||
CAST(SUM(CASE
|
||||
WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
THEN o.price * o.quantity
|
||||
ELSE 0
|
||||
END) as current_month,
|
||||
SUM(CASE
|
||||
END) AS DECIMAL(15,3)) as current_month,
|
||||
CAST(SUM(CASE
|
||||
WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||||
AND o.date < DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
THEN o.price * o.quantity
|
||||
ELSE 0
|
||||
END) as previous_month
|
||||
END) AS DECIMAL(15,3)) as previous_month
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE p.vendor IS NOT NULL
|
||||
AND o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||||
GROUP BY p.vendor
|
||||
)
|
||||
SELECT
|
||||
p.vendor,
|
||||
SUM(o.price * o.quantity) as salesVolume,
|
||||
CAST(SUM(o.price * o.quantity) AS DECIMAL(15,3)) as salesVolume,
|
||||
COALESCE(ROUND(
|
||||
(SUM(o.price * o.quantity - p.cost_price * o.quantity) /
|
||||
NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1
|
||||
@@ -182,13 +182,13 @@ router.get('/vendors', async (req, res) => {
|
||||
COALESCE(ROUND(
|
||||
SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0), 1
|
||||
), 0) as stockTurnover,
|
||||
COUNT(DISTINCT p.product_id) as productCount,
|
||||
COUNT(DISTINCT p.pid) as productCount,
|
||||
ROUND(
|
||||
((ms.current_month / NULLIF(ms.previous_month, 0)) - 1) * 100,
|
||||
1
|
||||
) as growth
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
LEFT JOIN monthly_sales ms ON p.vendor = ms.vendor
|
||||
WHERE p.vendor IS NOT NULL
|
||||
AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
@@ -203,11 +203,11 @@ router.get('/vendors', async (req, res) => {
|
||||
const [comparison] = await pool.query(`
|
||||
SELECT
|
||||
p.vendor,
|
||||
COALESCE(ROUND(SUM(o.price * o.quantity) / NULLIF(COUNT(DISTINCT p.product_id), 0), 2), 0) as salesPerProduct,
|
||||
CAST(COALESCE(ROUND(SUM(o.price * o.quantity) / NULLIF(COUNT(DISTINCT p.pid), 0), 2), 0) AS DECIMAL(15,3)) as salesPerProduct,
|
||||
COALESCE(ROUND(AVG((o.price - p.cost_price) / NULLIF(o.price, 0) * 100), 1), 0) as averageMargin,
|
||||
COUNT(DISTINCT p.product_id) as size
|
||||
COUNT(DISTINCT p.pid) as size
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
LEFT JOIN orders o ON p.pid = o.pid AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
WHERE p.vendor IS NOT NULL
|
||||
GROUP BY p.vendor
|
||||
ORDER BY salesPerProduct DESC
|
||||
@@ -221,9 +221,9 @@ router.get('/vendors', async (req, res) => {
|
||||
SELECT
|
||||
p.vendor,
|
||||
DATE_FORMAT(o.date, '%b %Y') as month,
|
||||
COALESCE(SUM(o.price * o.quantity), 0) as sales
|
||||
CAST(COALESCE(SUM(o.price * o.quantity), 0) AS DECIMAL(15,3)) as sales
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE p.vendor IS NOT NULL
|
||||
AND o.date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY
|
||||
@@ -272,9 +272,9 @@ router.get('/stock', async (req, res) => {
|
||||
ROUND(AVG(p.stock_quantity), 0) as averageStock,
|
||||
SUM(o.quantity) as totalSales
|
||||
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
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY c.name
|
||||
HAVING turnoverRate > 0
|
||||
@@ -290,7 +290,7 @@ router.get('/stock', async (req, res) => {
|
||||
SUM(CASE WHEN p.stock_quantity <= ? AND p.stock_quantity > 0 THEN 1 ELSE 0 END) as lowStock,
|
||||
SUM(CASE WHEN p.stock_quantity = 0 THEN 1 ELSE 0 END) as outOfStock
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY DATE_FORMAT(o.date, '%Y-%m-%d')
|
||||
ORDER BY date
|
||||
@@ -304,25 +304,25 @@ router.get('/stock', async (req, res) => {
|
||||
const [criticalItems] = await pool.query(`
|
||||
WITH product_thresholds AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
COALESCE(
|
||||
(SELECT reorder_days
|
||||
FROM stock_thresholds st
|
||||
JOIN product_categories pc ON st.category_id = pc.category_id
|
||||
WHERE pc.product_id = p.product_id
|
||||
JOIN product_categories pc ON st.cat_id = pc.cat_id
|
||||
WHERE pc.pid = p.pid
|
||||
AND st.vendor = p.vendor LIMIT 1),
|
||||
(SELECT reorder_days
|
||||
FROM stock_thresholds st
|
||||
JOIN product_categories pc ON st.category_id = pc.category_id
|
||||
WHERE pc.product_id = p.product_id
|
||||
JOIN product_categories pc ON st.cat_id = pc.cat_id
|
||||
WHERE pc.pid = p.pid
|
||||
AND st.vendor IS NULL LIMIT 1),
|
||||
(SELECT reorder_days
|
||||
FROM stock_thresholds st
|
||||
WHERE st.category_id IS NULL
|
||||
WHERE st.cat_id IS NULL
|
||||
AND st.vendor = p.vendor LIMIT 1),
|
||||
(SELECT reorder_days
|
||||
FROM stock_thresholds st
|
||||
WHERE st.category_id IS NULL
|
||||
WHERE st.cat_id IS NULL
|
||||
AND st.vendor IS NULL LIMIT 1),
|
||||
14
|
||||
) as reorder_days
|
||||
@@ -339,11 +339,11 @@ router.get('/stock', async (req, res) => {
|
||||
ELSE ROUND(p.stock_quantity / NULLIF((SUM(o.quantity) / ?), 0))
|
||||
END as daysUntilStockout
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN product_thresholds pt ON p.product_id = pt.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_thresholds pt ON p.pid = pt.pid
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
AND p.managing_stock = true
|
||||
GROUP BY p.product_id
|
||||
GROUP BY p.pid
|
||||
HAVING daysUntilStockout < ? AND daysUntilStockout >= 0
|
||||
ORDER BY daysUntilStockout
|
||||
LIMIT 10
|
||||
@@ -374,7 +374,7 @@ router.get('/pricing', async (req, res) => {
|
||||
SUM(o.price * o.quantity) as revenue,
|
||||
p.categories as category
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY p.price, p.categories
|
||||
HAVING salesVolume > 0
|
||||
@@ -420,9 +420,9 @@ router.get('/pricing', async (req, res) => {
|
||||
ELSE 65
|
||||
END as confidence
|
||||
FROM products p
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY p.product_id
|
||||
GROUP BY p.pid
|
||||
HAVING ABS(recommendedPrice - currentPrice) > 0
|
||||
ORDER BY potentialRevenue - SUM(o.price * o.quantity) DESC
|
||||
LIMIT 10
|
||||
@@ -457,9 +457,9 @@ router.get('/categories', async (req, res) => {
|
||||
ELSE 0
|
||||
END) as previous_month
|
||||
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
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||||
GROUP BY c.name
|
||||
)
|
||||
@@ -471,11 +471,11 @@ router.get('/categories', async (req, res) => {
|
||||
((ms.current_month / NULLIF(ms.previous_month, 0)) - 1) * 100,
|
||||
1
|
||||
) as growth,
|
||||
COUNT(DISTINCT p.product_id) as productCount
|
||||
COUNT(DISTINCT p.pid) as productCount
|
||||
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
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
LEFT JOIN monthly_sales ms ON c.name = ms.name
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
|
||||
GROUP BY c.name, ms.current_month, ms.previous_month
|
||||
@@ -490,9 +490,9 @@ router.get('/categories', async (req, res) => {
|
||||
c.name as category,
|
||||
SUM(o.price * o.quantity) as value
|
||||
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
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY c.name
|
||||
HAVING value > 0
|
||||
@@ -507,9 +507,9 @@ router.get('/categories', async (req, res) => {
|
||||
DATE_FORMAT(o.date, '%b %Y') as month,
|
||||
SUM(o.price * o.quantity) as sales
|
||||
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
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY
|
||||
c.name,
|
||||
@@ -536,52 +536,52 @@ router.get('/forecast', async (req, res) => {
|
||||
const [results] = await pool.query(`
|
||||
WITH category_metrics AS (
|
||||
SELECT
|
||||
c.id as category_id,
|
||||
c.cat_id as category_id,
|
||||
c.name as category_name,
|
||||
p.brand,
|
||||
COUNT(DISTINCT p.product_id) as num_products,
|
||||
COUNT(DISTINCT p.pid) as num_products,
|
||||
COALESCE(ROUND(SUM(o.quantity) / DATEDIFF(?, ?), 2), 0) as avg_daily_sales,
|
||||
COALESCE(SUM(o.quantity), 0) as total_sold,
|
||||
COALESCE(ROUND(SUM(o.quantity) / COUNT(DISTINCT p.product_id), 2), 0) as avgTotalSold,
|
||||
COALESCE(ROUND(SUM(o.quantity) / COUNT(DISTINCT p.pid), 2), 0) as avgTotalSold,
|
||||
COALESCE(ROUND(AVG(o.price), 2), 0) as avg_price
|
||||
FROM categories c
|
||||
JOIN product_categories pc ON c.id = pc.category_id
|
||||
JOIN products p ON pc.product_id = p.product_id
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN product_categories pc ON c.cat_id = pc.cat_id
|
||||
JOIN products p ON pc.pid = p.pid
|
||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
AND o.date BETWEEN ? AND ?
|
||||
AND o.canceled = false
|
||||
WHERE p.brand = ?
|
||||
AND pm.first_received_date BETWEEN ? AND ?
|
||||
GROUP BY c.id, c.name, p.brand
|
||||
GROUP BY c.cat_id, c.name, p.brand
|
||||
),
|
||||
product_metrics AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.title,
|
||||
p.sku,
|
||||
p.SKU,
|
||||
p.stock_quantity,
|
||||
pc.category_id,
|
||||
pc.cat_id,
|
||||
pm.first_received_date,
|
||||
COALESCE(SUM(o.quantity), 0) as total_sold,
|
||||
COALESCE(ROUND(AVG(o.price), 2), 0) as avg_price
|
||||
FROM products p
|
||||
JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN product_categories pc ON p.pid = pc.pid
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN orders o ON p.pid = o.pid
|
||||
AND o.date BETWEEN ? AND ?
|
||||
AND o.canceled = false
|
||||
WHERE p.brand = ?
|
||||
AND pm.first_received_date BETWEEN ? AND ?
|
||||
GROUP BY p.product_id, p.title, p.sku, p.stock_quantity, pc.category_id, pm.first_received_date
|
||||
GROUP BY p.pid, p.title, p.SKU, p.stock_quantity, pc.cat_id, pm.first_received_date
|
||||
)
|
||||
SELECT
|
||||
cm.*,
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'product_id', pm.product_id,
|
||||
'pid', pm.pid,
|
||||
'title', pm.title,
|
||||
'sku', pm.sku,
|
||||
'SKU', pm.SKU,
|
||||
'stock_quantity', pm.stock_quantity,
|
||||
'total_sold', pm.total_sold,
|
||||
'avg_price', pm.avg_price,
|
||||
@@ -589,7 +589,7 @@ router.get('/forecast', async (req, res) => {
|
||||
)
|
||||
) as products
|
||||
FROM category_metrics cm
|
||||
JOIN product_metrics pm ON cm.category_id = pm.category_id
|
||||
JOIN product_metrics pm ON cm.category_id = pm.cat_id
|
||||
GROUP BY cm.category_id, cm.category_name, cm.brand, cm.num_products, cm.avg_daily_sales, cm.total_sold, cm.avgTotalSold, cm.avg_price
|
||||
ORDER BY cm.total_sold DESC
|
||||
`, [startDate, endDate, startDate, endDate, brand, startDate, endDate, startDate, endDate, brand, startDate, endDate]);
|
||||
|
||||
@@ -9,58 +9,62 @@ router.get('/', async (req, res) => {
|
||||
const [parentCategories] = await pool.query(`
|
||||
SELECT DISTINCT c2.name as parent_name
|
||||
FROM categories c1
|
||||
JOIN categories c2 ON c1.parent_id = c2.id
|
||||
WHERE c1.parent_id IS NOT NULL
|
||||
JOIN categories c2 ON c1.parent_cat_id = c2.cat_id
|
||||
WHERE c1.parent_cat_id IS NOT NULL
|
||||
ORDER BY c2.name
|
||||
`);
|
||||
|
||||
// Get all categories with metrics
|
||||
const [categories] = await pool.query(`
|
||||
SELECT
|
||||
c.id as category_id,
|
||||
c.cat_id,
|
||||
c.name,
|
||||
c.description,
|
||||
COALESCE(p.name, '') as parent_name,
|
||||
cm.product_count,
|
||||
cm.total_value,
|
||||
cm.avg_margin,
|
||||
cm.turnover_rate,
|
||||
cm.growth_rate,
|
||||
cm.status
|
||||
COALESCE(cm.product_count, 0) as product_count,
|
||||
CAST(COALESCE(cm.total_value, 0) AS DECIMAL(15,3)) as total_value,
|
||||
COALESCE(cm.avg_margin, 0) as avg_margin,
|
||||
COALESCE(cm.turnover_rate, 0) as turnover_rate,
|
||||
COALESCE(cm.growth_rate, 0) as growth_rate,
|
||||
COALESCE(cm.status, 'inactive') as status
|
||||
FROM categories c
|
||||
LEFT JOIN categories p ON c.parent_id = p.id
|
||||
LEFT JOIN category_metrics cm ON c.id = cm.category_id
|
||||
LEFT JOIN categories p ON c.parent_cat_id = p.cat_id
|
||||
LEFT JOIN category_metrics cm ON c.cat_id = cm.cat_id
|
||||
ORDER BY c.name ASC
|
||||
`);
|
||||
|
||||
// Get overall stats
|
||||
const [stats] = await pool.query(`
|
||||
SELECT
|
||||
COUNT(DISTINCT c.id) as totalCategories,
|
||||
COUNT(DISTINCT CASE WHEN cm.status = 'active' THEN c.id END) as activeCategories,
|
||||
COALESCE(SUM(cm.total_value), 0) as totalValue,
|
||||
COUNT(DISTINCT c.cat_id) as totalCategories,
|
||||
COUNT(DISTINCT CASE WHEN cm.status = 'active' THEN c.cat_id END) as activeCategories,
|
||||
CAST(COALESCE(SUM(cm.total_value), 0) AS DECIMAL(15,3)) as totalValue,
|
||||
COALESCE(ROUND(AVG(NULLIF(cm.avg_margin, 0)), 1), 0) as avgMargin,
|
||||
COALESCE(ROUND(AVG(NULLIF(cm.growth_rate, 0)), 1), 0) as avgGrowth
|
||||
FROM categories c
|
||||
LEFT JOIN category_metrics cm ON c.id = cm.category_id
|
||||
LEFT JOIN category_metrics cm ON c.cat_id = cm.cat_id
|
||||
`);
|
||||
|
||||
res.json({
|
||||
categories: categories.map(cat => ({
|
||||
...cat,
|
||||
parent_category: cat.parent_name, // Map parent_name to parent_category for frontend compatibility
|
||||
product_count: parseInt(cat.product_count || 0),
|
||||
total_value: parseFloat(cat.total_value || 0),
|
||||
avg_margin: parseFloat(cat.avg_margin || 0),
|
||||
turnover_rate: parseFloat(cat.turnover_rate || 0),
|
||||
growth_rate: parseFloat(cat.growth_rate || 0)
|
||||
id: cat.cat_id,
|
||||
name: cat.name,
|
||||
description: cat.description,
|
||||
parent_category: cat.parent_name,
|
||||
product_count: parseInt(cat.product_count),
|
||||
total_value: parseFloat(cat.total_value),
|
||||
avg_margin: parseFloat(cat.avg_margin),
|
||||
turnover_rate: parseFloat(cat.turnover_rate),
|
||||
growth_rate: parseFloat(cat.growth_rate),
|
||||
status: cat.status
|
||||
})),
|
||||
parentCategories: parentCategories.map(p => p.parent_name),
|
||||
stats: {
|
||||
...stats[0],
|
||||
totalValue: parseFloat(stats[0].totalValue || 0),
|
||||
avgMargin: parseFloat(stats[0].avgMargin || 0),
|
||||
avgGrowth: parseFloat(stats[0].avgGrowth || 0)
|
||||
totalCategories: parseInt(stats[0].totalCategories),
|
||||
activeCategories: parseInt(stats[0].activeCategories),
|
||||
totalValue: parseFloat(stats[0].totalValue),
|
||||
avgMargin: parseFloat(stats[0].avgMargin),
|
||||
avgGrowth: parseFloat(stats[0].avgGrowth)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -38,15 +38,14 @@ router.get('/stock/metrics', async (req, res) => {
|
||||
const [brandValues] = await executeQuery(`
|
||||
WITH brand_totals AS (
|
||||
SELECT
|
||||
brand,
|
||||
COUNT(DISTINCT product_id) as variant_count,
|
||||
COALESCE(brand, 'Unbranded') as brand,
|
||||
COUNT(DISTINCT pid) as variant_count,
|
||||
COALESCE(SUM(stock_quantity), 0) as stock_units,
|
||||
COALESCE(SUM(stock_quantity * cost_price), 0) as stock_cost,
|
||||
COALESCE(SUM(stock_quantity * price), 0) as stock_retail
|
||||
CAST(COALESCE(SUM(stock_quantity * cost_price), 0) AS DECIMAL(15,3)) as stock_cost,
|
||||
CAST(COALESCE(SUM(stock_quantity * price), 0) AS DECIMAL(15,3)) as stock_retail
|
||||
FROM products
|
||||
WHERE brand IS NOT NULL
|
||||
AND stock_quantity > 0
|
||||
GROUP BY brand
|
||||
WHERE stock_quantity > 0
|
||||
GROUP BY COALESCE(brand, 'Unbranded')
|
||||
HAVING stock_cost > 0
|
||||
),
|
||||
other_brands AS (
|
||||
@@ -54,8 +53,8 @@ router.get('/stock/metrics', async (req, res) => {
|
||||
'Other' as brand,
|
||||
SUM(variant_count) as variant_count,
|
||||
SUM(stock_units) as stock_units,
|
||||
SUM(stock_cost) as stock_cost,
|
||||
SUM(stock_retail) as stock_retail
|
||||
CAST(SUM(stock_cost) AS DECIMAL(15,3)) as stock_cost,
|
||||
CAST(SUM(stock_retail) AS DECIMAL(15,3)) as stock_retail
|
||||
FROM brand_totals
|
||||
WHERE stock_cost <= 5000
|
||||
),
|
||||
@@ -101,24 +100,24 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await executeQuery(`
|
||||
SELECT
|
||||
COALESCE(COUNT(DISTINCT CASE WHEN po.status = 'open' THEN po.po_id END), 0) as active_pos,
|
||||
COALESCE(COUNT(DISTINCT CASE WHEN po.receiving_status < 30 THEN po.po_id END), 0) as active_pos,
|
||||
COALESCE(COUNT(DISTINCT CASE
|
||||
WHEN po.status = 'open' AND po.expected_date < CURDATE()
|
||||
WHEN po.receiving_status < 30 AND po.expected_date < CURDATE()
|
||||
THEN po.po_id
|
||||
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'
|
||||
COALESCE(SUM(CASE WHEN po.receiving_status < 30 THEN po.ordered ELSE 0 END), 0) as total_units,
|
||||
CAST(COALESCE(SUM(CASE
|
||||
WHEN po.receiving_status < 30
|
||||
THEN po.ordered * po.cost_price
|
||||
ELSE 0
|
||||
END), 0) as total_cost,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN po.status = 'open'
|
||||
END), 0) AS DECIMAL(15,3)) as total_cost,
|
||||
CAST(COALESCE(SUM(CASE
|
||||
WHEN po.receiving_status < 30
|
||||
THEN po.ordered * p.price
|
||||
ELSE 0
|
||||
END), 0) as total_retail
|
||||
END), 0) AS DECIMAL(15,3)) as total_retail
|
||||
FROM purchase_orders po
|
||||
JOIN products p ON po.product_id = p.product_id
|
||||
JOIN products p ON po.pid = p.pid
|
||||
`);
|
||||
const poMetrics = rows[0];
|
||||
|
||||
@@ -134,11 +133,11 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
po.vendor,
|
||||
COUNT(DISTINCT po.po_id) as order_count,
|
||||
COALESCE(SUM(po.ordered), 0) as ordered_units,
|
||||
COALESCE(SUM(po.ordered * po.cost_price), 0) as order_cost,
|
||||
COALESCE(SUM(po.ordered * p.price), 0) as order_retail
|
||||
CAST(COALESCE(SUM(po.ordered * po.cost_price), 0) AS DECIMAL(15,3)) as order_cost,
|
||||
CAST(COALESCE(SUM(po.ordered * p.price), 0) AS DECIMAL(15,3)) as order_retail
|
||||
FROM purchase_orders po
|
||||
JOIN products p ON po.product_id = p.product_id
|
||||
WHERE po.status = 'open'
|
||||
JOIN products p ON po.pid = p.pid
|
||||
WHERE po.receiving_status < 30
|
||||
GROUP BY po.vendor
|
||||
HAVING order_cost > 0
|
||||
ORDER BY order_cost DESC
|
||||
@@ -173,21 +172,21 @@ router.get('/replenishment/metrics', async (req, res) => {
|
||||
// Get summary metrics
|
||||
const [metrics] = await executeQuery(`
|
||||
SELECT
|
||||
COUNT(DISTINCT p.product_id) as products_to_replenish,
|
||||
COUNT(DISTINCT p.pid) as products_to_replenish,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN p.stock_quantity < 0 THEN ABS(p.stock_quantity) + pm.reorder_qty
|
||||
ELSE pm.reorder_qty
|
||||
END), 0) as total_units_needed,
|
||||
COALESCE(SUM(CASE
|
||||
CAST(COALESCE(SUM(CASE
|
||||
WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.cost_price
|
||||
ELSE pm.reorder_qty * p.cost_price
|
||||
END), 0) as total_cost,
|
||||
COALESCE(SUM(CASE
|
||||
END), 0) AS DECIMAL(15,3)) as total_cost,
|
||||
CAST(COALESCE(SUM(CASE
|
||||
WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.price
|
||||
ELSE pm.reorder_qty * p.price
|
||||
END), 0) as total_retail
|
||||
END), 0) AS DECIMAL(15,3)) as total_retail
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE p.replenishable = true
|
||||
AND (pm.stock_status IN ('Critical', 'Reorder')
|
||||
OR p.stock_quantity < 0)
|
||||
@@ -197,24 +196,24 @@ router.get('/replenishment/metrics', async (req, res) => {
|
||||
// Get top variants to replenish
|
||||
const [variants] = await executeQuery(`
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.title,
|
||||
p.stock_quantity as current_stock,
|
||||
CASE
|
||||
WHEN p.stock_quantity < 0 THEN ABS(p.stock_quantity) + pm.reorder_qty
|
||||
ELSE pm.reorder_qty
|
||||
END as replenish_qty,
|
||||
CASE
|
||||
CAST(CASE
|
||||
WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.cost_price
|
||||
ELSE pm.reorder_qty * p.cost_price
|
||||
END as replenish_cost,
|
||||
CASE
|
||||
END AS DECIMAL(15,3)) as replenish_cost,
|
||||
CAST(CASE
|
||||
WHEN p.stock_quantity < 0 THEN (ABS(p.stock_quantity) + pm.reorder_qty) * p.price
|
||||
ELSE pm.reorder_qty * p.price
|
||||
END as replenish_retail,
|
||||
END AS DECIMAL(15,3)) as replenish_retail,
|
||||
pm.stock_status
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE p.replenishable = true
|
||||
AND (pm.stock_status IN ('Critical', 'Reorder')
|
||||
OR p.stock_quantity < 0)
|
||||
@@ -235,7 +234,7 @@ router.get('/replenishment/metrics', async (req, res) => {
|
||||
replenishmentCost: parseFloat(metrics[0].total_cost) || 0,
|
||||
replenishmentRetail: parseFloat(metrics[0].total_retail) || 0,
|
||||
topVariants: variants.map(v => ({
|
||||
id: v.product_id,
|
||||
id: v.pid,
|
||||
title: v.title,
|
||||
currentStock: parseInt(v.current_stock) || 0,
|
||||
replenishQty: parseInt(v.replenish_qty) || 0,
|
||||
@@ -287,9 +286,9 @@ router.get('/forecast/metrics', async (req, res) => {
|
||||
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
|
||||
JOIN categories c ON cf.cat_id = c.cat_id
|
||||
WHERE cf.forecast_date BETWEEN ? AND ?
|
||||
GROUP BY c.id, c.name
|
||||
GROUP BY c.cat_id, c.name
|
||||
ORDER BY revenue DESC
|
||||
`, [startDate, endDate]);
|
||||
|
||||
@@ -325,11 +324,11 @@ router.get('/overstock/metrics', async (req, res) => {
|
||||
const [rows] = await executeQuery(`
|
||||
WITH category_overstock AS (
|
||||
SELECT
|
||||
c.id as category_id,
|
||||
c.cat_id,
|
||||
c.name as category_name,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN pm.stock_status = 'Overstocked'
|
||||
THEN p.product_id
|
||||
THEN p.pid
|
||||
END) as overstocked_products,
|
||||
SUM(CASE
|
||||
WHEN pm.stock_status = 'Overstocked'
|
||||
@@ -347,10 +346,10 @@ router.get('/overstock/metrics', async (req, res) => {
|
||||
ELSE 0
|
||||
END) as total_excess_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
|
||||
GROUP BY c.id, c.name
|
||||
JOIN product_categories pc ON c.cat_id = pc.cat_id
|
||||
JOIN products p ON pc.pid = p.pid
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
GROUP BY c.cat_id, c.name
|
||||
)
|
||||
SELECT
|
||||
SUM(overstocked_products) as total_overstocked,
|
||||
@@ -405,7 +404,7 @@ router.get('/overstock/products', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await executeQuery(`
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.SKU,
|
||||
p.title,
|
||||
p.brand,
|
||||
@@ -420,11 +419,11 @@ router.get('/overstock/products', async (req, res) => {
|
||||
(pm.overstocked_amt * p.price) as excess_retail,
|
||||
GROUP_CONCAT(c.name) as categories
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
LEFT JOIN categories c ON pc.category_id = c.id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
||||
LEFT JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE pm.stock_status = 'Overstocked'
|
||||
GROUP BY p.product_id
|
||||
GROUP BY p.pid
|
||||
ORDER BY excess_cost DESC
|
||||
LIMIT ?
|
||||
`, [limit]);
|
||||
@@ -442,7 +441,7 @@ router.get('/best-sellers', async (req, res) => {
|
||||
const [products] = await executeQuery(`
|
||||
WITH product_sales AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.SKU as sku,
|
||||
p.title,
|
||||
-- Current period (last 30 days)
|
||||
@@ -468,13 +467,13 @@ router.get('/best-sellers', async (req, res) => {
|
||||
ELSE 0
|
||||
END) as previous_revenue
|
||||
FROM products p
|
||||
JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.canceled = false
|
||||
AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY)
|
||||
GROUP BY p.product_id, p.SKU, p.title
|
||||
GROUP BY p.pid, p.SKU, p.title
|
||||
)
|
||||
SELECT
|
||||
product_id,
|
||||
pid,
|
||||
sku,
|
||||
title,
|
||||
units_sold,
|
||||
@@ -520,7 +519,7 @@ router.get('/best-sellers', async (req, res) => {
|
||||
ELSE 0
|
||||
END) as previous_revenue
|
||||
FROM products p
|
||||
JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.canceled = false
|
||||
AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY)
|
||||
AND p.brand IS NOT NULL
|
||||
@@ -547,7 +546,7 @@ router.get('/best-sellers', async (req, res) => {
|
||||
const [categories] = await executeQuery(`
|
||||
WITH category_sales AS (
|
||||
SELECT
|
||||
c.id as category_id,
|
||||
c.cat_id,
|
||||
c.name,
|
||||
-- Current period (last 30 days)
|
||||
SUM(CASE
|
||||
@@ -572,15 +571,15 @@ router.get('/best-sellers', async (req, res) => {
|
||||
ELSE 0
|
||||
END) as previous_revenue
|
||||
FROM categories c
|
||||
JOIN product_categories pc ON c.id = pc.category_id
|
||||
JOIN products p ON pc.product_id = p.product_id
|
||||
JOIN orders o ON p.product_id = o.product_id
|
||||
JOIN product_categories pc ON c.cat_id = pc.cat_id
|
||||
JOIN products p ON pc.pid = p.pid
|
||||
JOIN orders o ON p.pid = o.pid
|
||||
WHERE o.canceled = false
|
||||
AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY)
|
||||
GROUP BY c.id, c.name
|
||||
GROUP BY c.cat_id, c.name
|
||||
)
|
||||
SELECT
|
||||
category_id,
|
||||
cat_id as category_id,
|
||||
name,
|
||||
units_sold,
|
||||
revenue,
|
||||
@@ -616,7 +615,7 @@ router.get('/best-sellers', async (req, res) => {
|
||||
}));
|
||||
|
||||
const formattedCategories = categories.map(c => ({
|
||||
category_id: c.category_id,
|
||||
category_id: c.cat_id,
|
||||
name: c.name,
|
||||
units_sold: parseInt(c.units_sold) || 0,
|
||||
revenue: parseFloat(c.revenue) || 0,
|
||||
@@ -650,7 +649,7 @@ router.get('/sales/metrics', async (req, res) => {
|
||||
SUM(p.cost_price * o.quantity) as total_cogs,
|
||||
SUM((o.price - p.cost_price) * o.quantity) as total_profit
|
||||
FROM orders o
|
||||
JOIN products p ON o.product_id = p.product_id
|
||||
JOIN products p ON o.pid = p.pid
|
||||
WHERE o.canceled = false
|
||||
AND o.date BETWEEN ? AND ?
|
||||
GROUP BY DATE(o.date)
|
||||
@@ -666,7 +665,7 @@ router.get('/sales/metrics', async (req, res) => {
|
||||
SUM(p.cost_price * o.quantity) as total_cogs,
|
||||
SUM((o.price - p.cost_price) * o.quantity) as total_profit
|
||||
FROM orders o
|
||||
JOIN products p ON o.product_id = p.product_id
|
||||
JOIN products p ON o.pid = p.pid
|
||||
WHERE o.canceled = false
|
||||
AND o.date BETWEEN ? AND ?
|
||||
`, [startDate, endDate]);
|
||||
@@ -698,7 +697,7 @@ router.get('/low-stock/products', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await executeQuery(`
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.SKU,
|
||||
p.title,
|
||||
p.brand,
|
||||
@@ -712,12 +711,12 @@ router.get('/low-stock/products', async (req, res) => {
|
||||
(pm.reorder_qty * p.cost_price) as reorder_cost,
|
||||
GROUP_CONCAT(c.name) as categories
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
LEFT JOIN categories c ON pc.category_id = c.id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
||||
LEFT JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE pm.stock_status IN ('Critical', 'Reorder')
|
||||
AND p.replenishable = true
|
||||
GROUP BY p.product_id
|
||||
GROUP BY p.pid
|
||||
ORDER BY
|
||||
CASE pm.stock_status
|
||||
WHEN 'Critical' THEN 1
|
||||
@@ -742,17 +741,17 @@ router.get('/trending/products', async (req, res) => {
|
||||
const [rows] = await executeQuery(`
|
||||
WITH recent_sales AS (
|
||||
SELECT
|
||||
o.product_id,
|
||||
o.pid,
|
||||
COUNT(DISTINCT o.order_number) as recent_orders,
|
||||
SUM(o.quantity) as recent_units,
|
||||
SUM(o.price * o.quantity) as recent_revenue
|
||||
FROM orders o
|
||||
WHERE o.canceled = false
|
||||
AND o.date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
GROUP BY o.product_id
|
||||
GROUP BY o.pid
|
||||
)
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.SKU,
|
||||
p.title,
|
||||
p.brand,
|
||||
@@ -767,11 +766,11 @@ router.get('/trending/products', async (req, res) => {
|
||||
((rs.recent_units / ?) - pm.daily_sales_avg) / pm.daily_sales_avg * 100 as velocity_change,
|
||||
GROUP_CONCAT(c.name) as categories
|
||||
FROM recent_sales rs
|
||||
JOIN products p ON rs.product_id = p.product_id
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
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
|
||||
JOIN products p ON rs.pid = p.pid
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
||||
LEFT JOIN categories c ON pc.cat_id = c.cat_id
|
||||
GROUP BY p.pid
|
||||
HAVING velocity_change > 0
|
||||
ORDER BY velocity_change DESC
|
||||
LIMIT ?
|
||||
@@ -859,7 +858,7 @@ router.get('/key-metrics', async (req, res) => {
|
||||
COUNT(CASE WHEN pm.stock_status = 'Critical' THEN 1 END) as critical_stock_count,
|
||||
COUNT(CASE WHEN pm.stock_status = 'Overstocked' THEN 1 END) as overstock_count
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
),
|
||||
sales_summary AS (
|
||||
SELECT
|
||||
@@ -909,7 +908,7 @@ router.get('/inventory-health', async (req, res) => {
|
||||
AVG(pm.turnover_rate) as avg_turnover_rate,
|
||||
AVG(pm.days_of_inventory) as avg_days_inventory
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE p.replenishable = true
|
||||
),
|
||||
value_distribution AS (
|
||||
@@ -931,7 +930,7 @@ router.get('/inventory-health', async (req, res) => {
|
||||
ELSE 0
|
||||
END) * 100.0 / SUM(p.stock_quantity * p.cost_price) as overstock_value_percent
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
),
|
||||
category_health AS (
|
||||
SELECT
|
||||
@@ -940,11 +939,11 @@ router.get('/inventory-health', async (req, res) => {
|
||||
SUM(CASE WHEN pm.stock_status = 'Healthy' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as category_healthy_percent,
|
||||
AVG(pm.turnover_rate) as category_turnover_rate
|
||||
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
|
||||
JOIN product_categories pc ON c.cat_id = pc.cat_id
|
||||
JOIN products p ON pc.pid = p.pid
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE p.replenishable = true
|
||||
GROUP BY c.id, c.name
|
||||
GROUP BY c.cat_id, c.name
|
||||
)
|
||||
SELECT
|
||||
sd.*,
|
||||
@@ -975,20 +974,15 @@ router.get('/replenish/products', async (req, res) => {
|
||||
try {
|
||||
const [products] = await executeQuery(`
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.SKU,
|
||||
p.pid,
|
||||
p.SKU as sku,
|
||||
p.title,
|
||||
p.stock_quantity as current_stock,
|
||||
pm.reorder_qty as replenish_qty,
|
||||
(pm.reorder_qty * p.cost_price) as replenish_cost,
|
||||
(pm.reorder_qty * p.price) as replenish_retail,
|
||||
CASE
|
||||
WHEN pm.daily_sales_avg > 0
|
||||
THEN FLOOR(p.stock_quantity / pm.daily_sales_avg)
|
||||
ELSE NULL
|
||||
END as days_until_stockout
|
||||
p.stock_quantity,
|
||||
pm.daily_sales_avg,
|
||||
pm.reorder_qty,
|
||||
pm.last_purchase_date
|
||||
FROM products p
|
||||
JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE p.replenishable = true
|
||||
AND pm.stock_status IN ('Critical', 'Reorder')
|
||||
AND pm.reorder_qty > 0
|
||||
@@ -997,23 +991,16 @@ router.get('/replenish/products', async (req, res) => {
|
||||
WHEN 'Critical' THEN 1
|
||||
WHEN 'Reorder' THEN 2
|
||||
END,
|
||||
replenish_cost DESC
|
||||
pm.reorder_qty * p.cost_price DESC
|
||||
LIMIT ?
|
||||
`, [limit]);
|
||||
|
||||
// Format response
|
||||
const response = products.map(p => ({
|
||||
product_id: p.product_id,
|
||||
SKU: p.SKU,
|
||||
title: p.title,
|
||||
current_stock: parseInt(p.current_stock) || 0,
|
||||
replenish_qty: parseInt(p.replenish_qty) || 0,
|
||||
replenish_cost: parseFloat(p.replenish_cost) || 0,
|
||||
replenish_retail: parseFloat(p.replenish_retail) || 0,
|
||||
days_until_stockout: p.days_until_stockout
|
||||
}));
|
||||
|
||||
res.json(response);
|
||||
res.json(products.map(p => ({
|
||||
...p,
|
||||
stock_quantity: parseInt(p.stock_quantity) || 0,
|
||||
daily_sales_avg: parseFloat(p.daily_sales_avg) || 0,
|
||||
reorder_qty: parseInt(p.reorder_qty) || 0
|
||||
})));
|
||||
} catch (err) {
|
||||
console.error('Error fetching products to replenish:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch products to replenish' });
|
||||
|
||||
@@ -9,25 +9,25 @@ router.get('/trends', async (req, res) => {
|
||||
WITH MonthlyMetrics AS (
|
||||
SELECT
|
||||
DATE(CONCAT(pta.year, '-', LPAD(pta.month, 2, '0'), '-01')) as date,
|
||||
SUM(pta.total_revenue) as revenue,
|
||||
SUM(pta.total_cost) as cost,
|
||||
SUM(pm.inventory_value) as inventory_value,
|
||||
CAST(COALESCE(SUM(pta.total_revenue), 0) AS DECIMAL(15,3)) as revenue,
|
||||
CAST(COALESCE(SUM(pta.total_cost), 0) AS DECIMAL(15,3)) as cost,
|
||||
CAST(COALESCE(SUM(pm.inventory_value), 0) AS DECIMAL(15,3)) as inventory_value,
|
||||
CASE
|
||||
WHEN SUM(pm.inventory_value) > 0
|
||||
THEN (SUM(pta.total_revenue - pta.total_cost) / SUM(pm.inventory_value)) * 100
|
||||
THEN CAST((SUM(pta.total_revenue - pta.total_cost) / SUM(pm.inventory_value)) * 100 AS DECIMAL(15,3))
|
||||
ELSE 0
|
||||
END as gmroi
|
||||
FROM product_time_aggregates pta
|
||||
JOIN product_metrics pm ON pta.product_id = pm.product_id
|
||||
JOIN product_metrics pm ON pta.pid = pm.pid
|
||||
WHERE (pta.year * 100 + pta.month) >= DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 12 MONTH), '%Y%m')
|
||||
GROUP BY pta.year, pta.month
|
||||
ORDER BY date ASC
|
||||
)
|
||||
SELECT
|
||||
DATE_FORMAT(date, '%b %y') as date,
|
||||
ROUND(revenue, 2) as revenue,
|
||||
ROUND(inventory_value, 2) as inventory_value,
|
||||
ROUND(gmroi, 2) as gmroi
|
||||
revenue,
|
||||
inventory_value,
|
||||
gmroi
|
||||
FROM MonthlyMetrics
|
||||
`);
|
||||
|
||||
@@ -37,15 +37,15 @@ router.get('/trends', async (req, res) => {
|
||||
const transformedData = {
|
||||
revenue: rows.map(row => ({
|
||||
date: row.date,
|
||||
value: parseFloat(row.revenue || 0)
|
||||
value: parseFloat(row.revenue)
|
||||
})),
|
||||
inventory_value: rows.map(row => ({
|
||||
date: row.date,
|
||||
value: parseFloat(row.inventory_value || 0)
|
||||
value: parseFloat(row.inventory_value)
|
||||
})),
|
||||
gmroi: rows.map(row => ({
|
||||
date: row.date,
|
||||
value: parseFloat(row.gmroi || 0)
|
||||
value: parseFloat(row.gmroi)
|
||||
}))
|
||||
};
|
||||
|
||||
|
||||
@@ -74,8 +74,8 @@ router.get('/', async (req, res) => {
|
||||
o1.status,
|
||||
o1.payment_method,
|
||||
o1.shipping_method,
|
||||
COUNT(o2.product_id) as items_count,
|
||||
SUM(o2.price * o2.quantity) as total_amount
|
||||
COUNT(o2.pid) as items_count,
|
||||
CAST(SUM(o2.price * o2.quantity) AS DECIMAL(15,3)) as total_amount
|
||||
FROM orders o1
|
||||
JOIN orders o2 ON o1.order_number = o2.order_number
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
@@ -101,7 +101,7 @@ router.get('/', async (req, res) => {
|
||||
WITH CurrentStats AS (
|
||||
SELECT
|
||||
COUNT(DISTINCT order_number) as total_orders,
|
||||
SUM(price * quantity) as total_revenue
|
||||
CAST(SUM(price * quantity) AS DECIMAL(15,3)) as total_revenue
|
||||
FROM orders
|
||||
WHERE canceled = false
|
||||
AND DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
@@ -109,7 +109,7 @@ router.get('/', async (req, res) => {
|
||||
PreviousStats AS (
|
||||
SELECT
|
||||
COUNT(DISTINCT order_number) as prev_orders,
|
||||
SUM(price * quantity) as prev_revenue
|
||||
CAST(SUM(price * quantity) AS DECIMAL(15,3)) as prev_revenue
|
||||
FROM orders
|
||||
WHERE canceled = false
|
||||
AND DATE(date) BETWEEN DATE_SUB(CURDATE(), INTERVAL 60 DAY) AND DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
@@ -117,7 +117,7 @@ router.get('/', async (req, res) => {
|
||||
OrderValues AS (
|
||||
SELECT
|
||||
order_number,
|
||||
SUM(price * quantity) as order_value
|
||||
CAST(SUM(price * quantity) AS DECIMAL(15,3)) as order_value
|
||||
FROM orders
|
||||
WHERE canceled = false
|
||||
AND DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
@@ -138,12 +138,12 @@ router.get('/', async (req, res) => {
|
||||
END as revenue_growth,
|
||||
CASE
|
||||
WHEN cs.total_orders > 0
|
||||
THEN (cs.total_revenue / cs.total_orders)
|
||||
THEN CAST((cs.total_revenue / cs.total_orders) AS DECIMAL(15,3))
|
||||
ELSE 0
|
||||
END as average_order_value,
|
||||
CASE
|
||||
WHEN ps.prev_orders > 0
|
||||
THEN (ps.prev_revenue / ps.prev_orders)
|
||||
THEN CAST((ps.prev_revenue / ps.prev_orders) AS DECIMAL(15,3))
|
||||
ELSE 0
|
||||
END as prev_average_order_value
|
||||
FROM CurrentStats cs
|
||||
@@ -199,8 +199,8 @@ router.get('/:orderNumber', async (req, res) => {
|
||||
o1.shipping_method,
|
||||
o1.shipping_address,
|
||||
o1.billing_address,
|
||||
COUNT(o2.product_id) as items_count,
|
||||
SUM(o2.price * o2.quantity) as total_amount
|
||||
COUNT(o2.pid) as items_count,
|
||||
CAST(SUM(o2.price * o2.quantity) AS DECIMAL(15,3)) as total_amount
|
||||
FROM orders o1
|
||||
JOIN orders o2 ON o1.order_number = o2.order_number
|
||||
WHERE o1.order_number = ? AND o1.canceled = false
|
||||
@@ -222,14 +222,14 @@ router.get('/:orderNumber', async (req, res) => {
|
||||
// Get order items
|
||||
const [itemRows] = await pool.query(`
|
||||
SELECT
|
||||
o.product_id,
|
||||
o.pid,
|
||||
p.title,
|
||||
p.sku,
|
||||
p.SKU,
|
||||
o.quantity,
|
||||
o.price,
|
||||
(o.price * o.quantity) as total
|
||||
CAST((o.price * o.quantity) AS DECIMAL(15,3)) as total
|
||||
FROM orders o
|
||||
JOIN products p ON o.product_id = p.product_id
|
||||
JOIN products p ON o.pid = p.pid
|
||||
WHERE o.order_number = ? AND o.canceled = false
|
||||
`, [req.params.orderNumber]);
|
||||
|
||||
|
||||
@@ -20,15 +20,13 @@ router.get('/brands', async (req, res) => {
|
||||
console.log('Fetching brands from database...');
|
||||
|
||||
const [results] = await pool.query(`
|
||||
SELECT DISTINCT p.brand
|
||||
SELECT DISTINCT COALESCE(p.brand, 'Unbranded') as brand
|
||||
FROM products p
|
||||
JOIN purchase_orders po ON p.product_id = po.product_id
|
||||
WHERE p.brand IS NOT NULL
|
||||
AND p.brand != ''
|
||||
AND p.visible = true
|
||||
GROUP BY p.brand
|
||||
JOIN purchase_orders po ON p.pid = po.pid
|
||||
WHERE p.visible = true
|
||||
GROUP BY COALESCE(p.brand, 'Unbranded')
|
||||
HAVING SUM(po.cost_price * po.received) >= 500
|
||||
ORDER BY p.brand
|
||||
ORDER BY COALESCE(p.brand, 'Unbranded')
|
||||
`);
|
||||
|
||||
console.log(`Found ${results.length} brands:`, results.slice(0, 3));
|
||||
@@ -147,9 +145,9 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// Get total count for pagination
|
||||
const countQuery = `
|
||||
SELECT COUNT(DISTINCT p.product_id) as total
|
||||
SELECT COUNT(DISTINCT p.pid) as total
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||
${whereClause}
|
||||
`;
|
||||
const [countResult] = await pool.query(countQuery, params);
|
||||
@@ -163,26 +161,26 @@ router.get('/', async (req, res) => {
|
||||
'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor'
|
||||
);
|
||||
const [brands] = await pool.query(
|
||||
'SELECT DISTINCT brand FROM products WHERE visible = true AND brand IS NOT NULL AND brand != "" ORDER BY brand'
|
||||
'SELECT DISTINCT COALESCE(brand, \'Unbranded\') as brand FROM products WHERE visible = true ORDER BY brand'
|
||||
);
|
||||
|
||||
// Main query with all fields
|
||||
const query = `
|
||||
WITH product_thresholds AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
COALESCE(
|
||||
(SELECT overstock_days FROM stock_thresholds st
|
||||
WHERE st.category_id IN (
|
||||
SELECT pc.category_id
|
||||
WHERE st.cat_id IN (
|
||||
SELECT pc.cat_id
|
||||
FROM product_categories pc
|
||||
WHERE pc.product_id = p.product_id
|
||||
WHERE pc.pid = p.pid
|
||||
)
|
||||
AND (st.vendor = p.vendor OR st.vendor IS NULL)
|
||||
ORDER BY st.vendor IS NULL
|
||||
LIMIT 1),
|
||||
(SELECT overstock_days FROM stock_thresholds st
|
||||
WHERE st.category_id IS NULL
|
||||
WHERE st.cat_id IS NULL
|
||||
AND (st.vendor = p.vendor OR st.vendor IS NULL)
|
||||
ORDER BY st.vendor IS NULL
|
||||
LIMIT 1),
|
||||
@@ -192,6 +190,7 @@ router.get('/', async (req, res) => {
|
||||
)
|
||||
SELECT
|
||||
p.*,
|
||||
COALESCE(p.brand, 'Unbranded') as brand,
|
||||
GROUP_CONCAT(DISTINCT c.name) as categories,
|
||||
pm.daily_sales_avg,
|
||||
pm.weekly_sales_avg,
|
||||
@@ -205,10 +204,10 @@ router.get('/', async (req, res) => {
|
||||
pm.reorder_point,
|
||||
pm.safety_stock,
|
||||
pm.avg_margin_percent,
|
||||
pm.total_revenue,
|
||||
pm.inventory_value,
|
||||
pm.cost_of_goods_sold,
|
||||
pm.gross_profit,
|
||||
CAST(pm.total_revenue AS DECIMAL(15,3)) as total_revenue,
|
||||
CAST(pm.inventory_value AS DECIMAL(15,3)) as inventory_value,
|
||||
CAST(pm.cost_of_goods_sold AS DECIMAL(15,3)) as cost_of_goods_sold,
|
||||
CAST(pm.gross_profit AS DECIMAL(15,3)) as gross_profit,
|
||||
pm.gmroi,
|
||||
pm.avg_lead_time_days,
|
||||
pm.last_purchase_date,
|
||||
@@ -223,12 +222,12 @@ router.get('/', async (req, res) => {
|
||||
pm.overstocked_amt,
|
||||
COALESCE(pm.days_of_inventory / NULLIF(pt.target_days, 0), 0) as stock_coverage_ratio
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.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 product_thresholds pt ON p.product_id = pt.product_id
|
||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
||||
LEFT JOIN categories c ON pc.cat_id = c.cat_id
|
||||
LEFT JOIN product_thresholds pt ON p.pid = pt.pid
|
||||
${whereClause}
|
||||
GROUP BY p.product_id
|
||||
GROUP BY p.pid
|
||||
ORDER BY ${sortColumn} ${sortDirection}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
@@ -308,7 +307,7 @@ router.get('/trending', async (req, res) => {
|
||||
SELECT COUNT(*) as count,
|
||||
MAX(total_revenue) as max_revenue,
|
||||
MAX(daily_sales_avg) as max_daily_sales,
|
||||
COUNT(DISTINCT product_id) as products_with_metrics
|
||||
COUNT(DISTINCT pid) as products_with_metrics
|
||||
FROM product_metrics
|
||||
WHERE total_revenue > 0 OR daily_sales_avg > 0
|
||||
`);
|
||||
@@ -322,7 +321,7 @@ router.get('/trending', async (req, res) => {
|
||||
// Get trending products
|
||||
const [rows] = await pool.query(`
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
p.sku,
|
||||
p.title,
|
||||
COALESCE(pm.daily_sales_avg, 0) as daily_sales_avg,
|
||||
@@ -334,7 +333,7 @@ router.get('/trending', async (req, res) => {
|
||||
END as growth_rate,
|
||||
COALESCE(pm.total_revenue, 0) as total_revenue
|
||||
FROM products p
|
||||
INNER JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
INNER JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE (pm.total_revenue > 0 OR pm.daily_sales_avg > 0)
|
||||
AND p.visible = true
|
||||
ORDER BY growth_rate DESC
|
||||
@@ -378,11 +377,11 @@ router.get('/:id', async (req, res) => {
|
||||
pm.cost_of_goods_sold,
|
||||
pm.gross_profit
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
LEFT JOIN categories c ON pc.category_id = c.id
|
||||
WHERE p.product_id = ? AND p.visible = true
|
||||
GROUP BY p.product_id`,
|
||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN product_categories pc ON p.pid = pc.pid
|
||||
LEFT JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE p.pid = ? AND p.visible = true
|
||||
GROUP BY p.pid`,
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
@@ -399,7 +398,7 @@ router.get('/:id', async (req, res) => {
|
||||
// Transform the data to match frontend expectations
|
||||
const product = {
|
||||
// Basic product info
|
||||
product_id: rows[0].product_id,
|
||||
pid: rows[0].pid,
|
||||
title: rows[0].title,
|
||||
SKU: rows[0].SKU,
|
||||
barcode: rows[0].barcode,
|
||||
@@ -532,7 +531,7 @@ router.put('/:id', async (req, res) => {
|
||||
categories = ?,
|
||||
visible = ?,
|
||||
managing_stock = ?
|
||||
WHERE product_id = ?`,
|
||||
WHERE pid = ?`,
|
||||
[
|
||||
title,
|
||||
sku,
|
||||
@@ -570,7 +569,7 @@ router.get('/:id/metrics', async (req, res) => {
|
||||
const [metrics] = await pool.query(`
|
||||
WITH inventory_status AS (
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid,
|
||||
CASE
|
||||
WHEN pm.daily_sales_avg = 0 THEN 'New'
|
||||
WHEN p.stock_quantity <= CEIL(pm.daily_sales_avg * 7) THEN 'Critical'
|
||||
@@ -579,8 +578,8 @@ router.get('/:id/metrics', async (req, res) => {
|
||||
ELSE 'Healthy'
|
||||
END as calculated_status
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
WHERE p.product_id = ?
|
||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||
WHERE p.pid = ?
|
||||
)
|
||||
SELECT
|
||||
COALESCE(pm.daily_sales_avg, 0) as daily_sales_avg,
|
||||
@@ -604,9 +603,9 @@ router.get('/:id/metrics', async (req, res) => {
|
||||
COALESCE(pm.reorder_qty, 0) as reorder_qty,
|
||||
COALESCE(pm.overstocked_amt, 0) as overstocked_amt
|
||||
FROM products p
|
||||
LEFT JOIN product_metrics pm ON p.product_id = pm.product_id
|
||||
LEFT JOIN inventory_status is ON p.product_id = is.product_id
|
||||
WHERE p.product_id = ?
|
||||
LEFT JOIN product_metrics pm ON p.pid = pm.pid
|
||||
LEFT JOIN inventory_status is ON p.pid = is.pid
|
||||
WHERE p.pid = ?
|
||||
`, [id]);
|
||||
|
||||
if (!metrics.length) {
|
||||
@@ -660,7 +659,7 @@ router.get('/:id/time-series', async (req, res) => {
|
||||
profit_margin,
|
||||
inventory_value
|
||||
FROM product_time_aggregates
|
||||
WHERE product_id = ?
|
||||
WHERE pid = ?
|
||||
ORDER BY year DESC, month DESC
|
||||
LIMIT ?
|
||||
)
|
||||
@@ -707,7 +706,7 @@ router.get('/:id/time-series', async (req, res) => {
|
||||
status,
|
||||
payment_method
|
||||
FROM orders
|
||||
WHERE product_id = ?
|
||||
WHERE pid = ?
|
||||
AND canceled = false
|
||||
ORDER BY date DESC
|
||||
LIMIT 10
|
||||
@@ -733,7 +732,7 @@ router.get('/:id/time-series', async (req, res) => {
|
||||
ELSE NULL
|
||||
END as lead_time_days
|
||||
FROM purchase_orders
|
||||
WHERE product_id = ?
|
||||
WHERE pid = ?
|
||||
ORDER BY date DESC
|
||||
LIMIT 10
|
||||
`, [id]);
|
||||
|
||||
@@ -42,7 +42,7 @@ router.get('/', async (req, res) => {
|
||||
po_id,
|
||||
SUM(ordered) as total_ordered,
|
||||
SUM(received) as total_received,
|
||||
SUM(ordered * cost_price) as total_cost
|
||||
CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost
|
||||
FROM purchase_orders po
|
||||
WHERE ${whereClause}
|
||||
GROUP BY po_id
|
||||
@@ -54,8 +54,8 @@ router.get('/', async (req, res) => {
|
||||
ROUND(
|
||||
SUM(total_received) / NULLIF(SUM(total_ordered), 0), 3
|
||||
) as fulfillment_rate,
|
||||
SUM(total_cost) as total_value,
|
||||
ROUND(AVG(total_cost), 2) as avg_cost
|
||||
CAST(SUM(total_cost) AS DECIMAL(15,3)) as total_value,
|
||||
CAST(AVG(total_cost) AS DECIMAL(15,3)) as avg_cost
|
||||
FROM po_totals
|
||||
`, params);
|
||||
|
||||
@@ -78,9 +78,9 @@ router.get('/', async (req, res) => {
|
||||
vendor,
|
||||
date,
|
||||
status,
|
||||
COUNT(DISTINCT product_id) as total_items,
|
||||
COUNT(DISTINCT pid) as total_items,
|
||||
SUM(ordered) as total_quantity,
|
||||
SUM(ordered * cost_price) as total_cost,
|
||||
CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost,
|
||||
SUM(received) as total_received,
|
||||
ROUND(
|
||||
SUM(received) / NULLIF(SUM(ordered), 0), 3
|
||||
@@ -104,8 +104,8 @@ router.get('/', async (req, res) => {
|
||||
CASE
|
||||
WHEN ? = 'order_date' THEN date
|
||||
WHEN ? = 'vendor_name' THEN vendor
|
||||
WHEN ? = 'total_cost' THEN CAST(total_cost AS DECIMAL(15,2))
|
||||
WHEN ? = 'total_received' THEN CAST(total_received AS DECIMAL(15,2))
|
||||
WHEN ? = 'total_cost' THEN CAST(total_cost AS DECIMAL(15,3))
|
||||
WHEN ? = 'total_received' THEN CAST(total_received AS DECIMAL(15,3))
|
||||
WHEN ? = 'total_items' THEN CAST(total_items AS SIGNED)
|
||||
WHEN ? = 'total_quantity' THEN CAST(total_quantity AS SIGNED)
|
||||
WHEN ? = 'fulfillment_rate' THEN CAST(fulfillment_rate AS DECIMAL(5,3))
|
||||
@@ -203,10 +203,10 @@ router.get('/vendor-metrics', async (req, res) => {
|
||||
ROUND(
|
||||
SUM(received) / NULLIF(SUM(ordered), 0), 3
|
||||
) as fulfillment_rate,
|
||||
ROUND(
|
||||
CAST(ROUND(
|
||||
SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2
|
||||
) as avg_unit_cost,
|
||||
SUM(ordered * cost_price) as total_spend,
|
||||
) AS DECIMAL(15,3)) as avg_unit_cost,
|
||||
CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend,
|
||||
ROUND(
|
||||
AVG(NULLIF(delivery_days, 0)), 1
|
||||
) as avg_delivery_days
|
||||
@@ -244,18 +244,15 @@ router.get('/cost-analysis', async (req, res) => {
|
||||
const [analysis] = await pool.query(`
|
||||
SELECT
|
||||
c.name as categories,
|
||||
COUNT(DISTINCT po.product_id) as unique_products,
|
||||
ROUND(AVG(po.cost_price), 2) as avg_cost,
|
||||
MIN(po.cost_price) as min_cost,
|
||||
MAX(po.cost_price) as max_cost,
|
||||
ROUND(
|
||||
STDDEV(po.cost_price), 2
|
||||
) as cost_variance,
|
||||
SUM(po.ordered * po.cost_price) as total_spend
|
||||
COUNT(DISTINCT po.pid) as unique_products,
|
||||
CAST(AVG(po.cost_price) AS DECIMAL(15,3)) as avg_cost,
|
||||
CAST(MIN(po.cost_price) AS DECIMAL(15,3)) as min_cost,
|
||||
CAST(MAX(po.cost_price) AS DECIMAL(15,3)) as max_cost,
|
||||
CAST(STDDEV(po.cost_price) AS DECIMAL(15,3)) as cost_std_dev,
|
||||
CAST(SUM(po.ordered * po.cost_price) AS DECIMAL(15,3)) as total_spend
|
||||
FROM purchase_orders po
|
||||
JOIN products p ON po.product_id = p.product_id
|
||||
JOIN product_categories pc ON p.product_id = pc.product_id
|
||||
JOIN categories c ON pc.category_id = c.id
|
||||
JOIN product_categories pc ON po.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
GROUP BY c.name
|
||||
ORDER BY total_spend DESC
|
||||
`);
|
||||
|
||||
@@ -29,8 +29,8 @@ router.get('/', async (req, res) => {
|
||||
const [costMetrics] = await pool.query(`
|
||||
SELECT
|
||||
vendor,
|
||||
ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) as avg_unit_cost,
|
||||
SUM(ordered * cost_price) as total_spend
|
||||
CAST(ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) AS DECIMAL(15,3)) as avg_unit_cost,
|
||||
CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend
|
||||
FROM purchase_orders
|
||||
WHERE status = 'closed'
|
||||
AND cost_price IS NOT NULL
|
||||
@@ -56,9 +56,9 @@ router.get('/', async (req, res) => {
|
||||
WHEN COALESCE(vm.total_orders, 0) > 0 AND COALESCE(vm.order_fill_rate, 0) >= 75
|
||||
THEN p.vendor
|
||||
END) as activeVendors,
|
||||
ROUND(AVG(NULLIF(vm.avg_lead_time_days, 0)), 1) as avgLeadTime,
|
||||
ROUND(AVG(NULLIF(vm.order_fill_rate, 0)), 1) as avgFillRate,
|
||||
ROUND(AVG(NULLIF(vm.on_time_delivery_rate, 0)), 1) as avgOnTimeDelivery
|
||||
COALESCE(ROUND(AVG(NULLIF(vm.avg_lead_time_days, 0)), 1), 0) as avgLeadTime,
|
||||
COALESCE(ROUND(AVG(NULLIF(vm.order_fill_rate, 0)), 1), 0) as avgFillRate,
|
||||
COALESCE(ROUND(AVG(NULLIF(vm.on_time_delivery_rate, 0)), 1), 0) as avgOnTimeDelivery
|
||||
FROM products p
|
||||
LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor
|
||||
WHERE p.vendor IS NOT NULL AND p.vendor != ''
|
||||
@@ -67,8 +67,8 @@ router.get('/', async (req, res) => {
|
||||
// Get overall cost metrics
|
||||
const [overallCostMetrics] = await pool.query(`
|
||||
SELECT
|
||||
ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) as avg_unit_cost,
|
||||
SUM(ordered * cost_price) as total_spend
|
||||
CAST(ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) AS DECIMAL(15,3)) as avg_unit_cost,
|
||||
CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend
|
||||
FROM purchase_orders
|
||||
WHERE status = 'closed'
|
||||
AND cost_price IS NOT NULL
|
||||
@@ -78,25 +78,25 @@ router.get('/', async (req, res) => {
|
||||
|
||||
res.json({
|
||||
vendors: vendors.map(vendor => ({
|
||||
vendor_id: vendor.vendor_id || vendor.name,
|
||||
vendor_id: vendor.name,
|
||||
name: vendor.name,
|
||||
status: vendor.status,
|
||||
avg_lead_time_days: parseFloat(vendor.avg_lead_time_days || 0),
|
||||
on_time_delivery_rate: parseFloat(vendor.on_time_delivery_rate || 0),
|
||||
order_fill_rate: parseFloat(vendor.order_fill_rate || 0),
|
||||
total_orders: parseInt(vendor.total_orders || 0),
|
||||
active_products: parseInt(vendor.active_products || 0),
|
||||
avg_lead_time_days: parseFloat(vendor.avg_lead_time_days),
|
||||
on_time_delivery_rate: parseFloat(vendor.on_time_delivery_rate),
|
||||
order_fill_rate: parseFloat(vendor.order_fill_rate),
|
||||
total_orders: parseInt(vendor.total_orders),
|
||||
active_products: parseInt(vendor.active_products),
|
||||
avg_unit_cost: parseFloat(costMetricsMap[vendor.name]?.avg_unit_cost || 0),
|
||||
total_spend: parseFloat(costMetricsMap[vendor.name]?.total_spend || 0)
|
||||
})),
|
||||
stats: {
|
||||
totalVendors: parseInt(stats[0].totalVendors || 0),
|
||||
activeVendors: parseInt(stats[0].activeVendors || 0),
|
||||
avgLeadTime: parseFloat(stats[0].avgLeadTime || 0),
|
||||
avgFillRate: parseFloat(stats[0].avgFillRate || 0),
|
||||
avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery || 0),
|
||||
avgUnitCost: parseFloat(overallCostMetrics[0].avg_unit_cost || 0),
|
||||
totalSpend: parseFloat(overallCostMetrics[0].total_spend || 0)
|
||||
totalVendors: parseInt(stats[0].totalVendors),
|
||||
activeVendors: parseInt(stats[0].activeVendors),
|
||||
avgLeadTime: parseFloat(stats[0].avgLeadTime),
|
||||
avgFillRate: parseFloat(stats[0].avgFillRate),
|
||||
avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery),
|
||||
avgUnitCost: parseFloat(overallCostMetrics[0].avg_unit_cost),
|
||||
totalSpend: parseFloat(overallCostMetrics[0].total_spend)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user