diff --git a/inventory-server/src/routes/analytics.js b/inventory-server/src/routes/analytics.js index 92fddda..c0caf96 100644 --- a/inventory-server/src/routes/analytics.js +++ b/inventory-server/src/routes/analytics.js @@ -137,47 +137,107 @@ router.get('/vendors', async (req, res) => { try { const pool = req.app.locals.pool; + console.log('Fetching vendor performance data...'); + + // First check if we have any vendors with sales + const [checkData] = await pool.query(` + 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 + WHERE p.vendor IS NOT NULL + `); + + console.log('Vendor data check:', checkData[0]); + // Get vendor performance metrics const [performance] = await pool.query(` + WITH monthly_sales AS ( + SELECT + p.vendor, + SUM(CASE + WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + THEN o.price * o.quantity + ELSE 0 + END) as current_month, + 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 + FROM products p + LEFT JOIN orders o ON p.product_id = o.product_id + 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, - ROUND( + COALESCE(ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 - ) as profitMargin, - ROUND( + ), 0) as profitMargin, + COALESCE(ROUND( SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0), 1 - ) as stockTurnover, - COUNT(DISTINCT p.product_id) as productCount + ), 0) as stockTurnover, + COUNT(DISTINCT p.product_id) 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 - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - AND p.vendor IS NOT NULL - GROUP BY p.vendor - HAVING salesVolume > 0 + 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) + GROUP BY p.vendor, ms.current_month, ms.previous_month ORDER BY salesVolume DESC LIMIT 10 `); + console.log('Performance data:', performance); + // Get vendor comparison data const [comparison] = await pool.query(` SELECT p.vendor, - ROUND(SUM(o.price * o.quantity) / NULLIF(COUNT(DISTINCT p.product_id), 0), 2) as salesPerProduct, - ROUND(AVG((o.price - p.cost_price) / NULLIF(o.price, 0) * 100), 1) as averageMargin, + COALESCE(ROUND(SUM(o.price * o.quantity) / NULLIF(COUNT(DISTINCT p.product_id), 0), 2), 0) 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 FROM products p - LEFT JOIN orders o ON p.product_id = o.product_id - WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - AND p.vendor IS NOT NULL + LEFT JOIN orders o ON p.product_id = o.product_id AND o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + WHERE p.vendor IS NOT NULL GROUP BY p.vendor - HAVING salesPerProduct > 0 ORDER BY salesPerProduct DESC LIMIT 20 `); - res.json({ performance, comparison }); + console.log('Comparison data:', comparison); + + // Get vendor sales trends + const [trends] = await pool.query(` + SELECT + p.vendor, + DATE_FORMAT(o.date, '%b %Y') as month, + COALESCE(SUM(o.price * o.quantity), 0) as sales + FROM products p + LEFT JOIN orders o ON p.product_id = o.product_id + WHERE p.vendor IS NOT NULL + AND o.date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + GROUP BY + p.vendor, + DATE_FORMAT(o.date, '%b %Y'), + DATE_FORMAT(o.date, '%Y-%m') + ORDER BY + p.vendor, + DATE_FORMAT(o.date, '%Y-%m') + `); + + console.log('Trends data:', trends); + + res.json({ performance, comparison, trends }); } catch (error) { console.error('Error fetching vendor performance:', error); res.status(500).json({ error: 'Failed to fetch vendor performance' }); @@ -382,22 +442,33 @@ router.get('/categories', async (req, res) => { // Get category performance metrics const [performance] = await pool.query(` + WITH monthly_sales AS ( + SELECT + c.name, + SUM(CASE + WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + THEN o.price * o.quantity + ELSE 0 + END) as current_month, + 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 + 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.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) + GROUP BY c.name + ) SELECT c.name as category, SUM(o.price * o.quantity) as revenue, SUM(o.price * o.quantity - p.cost_price * o.quantity) as profit, ROUND( - ((SUM(CASE - WHEN o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - THEN o.price * o.quantity - ELSE 0 - END) / - NULLIF(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), 0)) - 1) * 100, + ((ms.current_month / NULLIF(ms.previous_month, 0)) - 1) * 100, 1 ) as growth, COUNT(DISTINCT p.product_id) as productCount @@ -405,8 +476,9 @@ router.get('/categories', async (req, res) => { 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 monthly_sales ms ON c.name = ms.name WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) - GROUP BY c.name + GROUP BY c.name, ms.current_month, ms.previous_month HAVING revenue > 0 ORDER BY revenue DESC LIMIT 10 @@ -428,7 +500,27 @@ router.get('/categories', async (req, res) => { LIMIT 6 `); - res.json({ performance, distribution }); + // Get category sales trends + const [trends] = await pool.query(` + SELECT + c.name as category, + 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 + WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + GROUP BY + c.name, + DATE_FORMAT(o.date, '%b %Y'), + DATE_FORMAT(o.date, '%Y-%m') + ORDER BY + c.name, + DATE_FORMAT(o.date, '%Y-%m') + `); + + res.json({ performance, distribution, trends }); } catch (error) { console.error('Error fetching category performance:', error); res.status(500).json({ error: 'Failed to fetch category performance' }); diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index ffe9091..f3c8cb6 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -287,6 +287,7 @@ router.get('/inventory/low-stock', async (req, res) => { router.get('/vendors/metrics', async (req, res) => { const pool = req.app.locals.pool; try { + console.log('Fetching vendor metrics...'); const [rows] = await pool.query(` SELECT vendor, @@ -300,18 +301,74 @@ router.get('/vendors/metrics', async (req, res) => { FROM vendor_metrics ORDER BY on_time_delivery_rate DESC `); - res.json(rows.map(row => ({ + console.log('Found vendor metrics:', rows.length, 'rows'); + console.log('First row sample:', rows[0]); + + const mappedRows = 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) - }))); + })); + console.log('First mapped row sample:', mappedRows[0]); + + res.json(mappedRows); } catch (error) { console.error('Error fetching vendor metrics:', error); res.status(500).json({ error: 'Failed to fetch vendor metrics' }); } }); +// Get trending products +router.get('/products/trending', async (req, res) => { + const pool = req.app.locals.pool; + try { + // First check if we have any data + const [checkData] = await pool.query(` + 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 + FROM product_metrics + WHERE total_revenue > 0 OR daily_sales_avg > 0 + `); + console.log('Product metrics stats:', checkData[0]); + + if (checkData[0].count === 0) { + console.log('No products with metrics found'); + return res.json([]); + } + + // Get trending products + const [rows] = await pool.query(` + SELECT + p.product_id, + p.sku, + p.title, + COALESCE(pm.daily_sales_avg, 0) as daily_sales_avg, + COALESCE(pm.weekly_sales_avg, 0) as weekly_sales_avg, + CASE + WHEN pm.weekly_sales_avg > 0 AND pm.daily_sales_avg > 0 + THEN ((pm.daily_sales_avg - pm.weekly_sales_avg) / pm.weekly_sales_avg) * 100 + ELSE 0 + 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 + WHERE (pm.total_revenue > 0 OR pm.daily_sales_avg > 0) + AND p.visible = true + ORDER BY growth_rate DESC + LIMIT 50 + `); + + console.log('Trending products:', rows); + res.json(rows); + } catch (error) { + console.error('Error fetching trending products:', error); + res.status(500).json({ error: 'Failed to fetch trending products' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/inventory/src/components/dashboard/TrendingProducts.tsx b/inventory/src/components/dashboard/TrendingProducts.tsx index c8e86a5..2fed059 100644 --- a/inventory/src/components/dashboard/TrendingProducts.tsx +++ b/inventory/src/components/dashboard/TrendingProducts.tsx @@ -25,7 +25,7 @@ export function TrendingProducts() { const { data: products } = useQuery({ queryKey: ["trending-products"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/products/trending`) + const response = await fetch(`${config.apiUrl}/products/trending`) if (!response.ok) { throw new Error("Failed to fetch trending products") }