From 64d9ab2f83a5b0a7369a1ab75350e3db1ce9fe7b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Jan 2025 01:30:48 -0500 Subject: [PATCH] Update frontend to match part 1 --- docs/schema-update-changes.md | 270 ++++++++++++++++++ inventory-server/src/routes/analytics.js | 150 +++++----- inventory-server/src/routes/categories.js | 56 ++-- inventory-server/src/routes/dashboard.js | 205 +++++++------ inventory-server/src/routes/metrics.js | 22 +- inventory-server/src/routes/orders.js | 26 +- inventory-server/src/routes/products.js | 85 +++--- .../src/routes/purchase-orders.js | 39 ++- inventory-server/src/routes/vendors.js | 40 +-- .../src/components/dashboard/BestSellers.tsx | 117 ++++---- .../components/dashboard/LowStockAlerts.tsx | 67 ++--- .../dashboard/TopOverstockedProducts.tsx | 64 ++--- .../dashboard/TopReplenishProducts.tsx | 63 ++-- .../components/dashboard/TrendingProducts.tsx | 29 +- .../src/components/forecasting/columns.tsx | 54 ++-- .../src/components/products/ProductDetail.tsx | 4 +- .../src/components/products/ProductTable.tsx | 6 +- .../settings/CalculationSettings.tsx | 4 +- .../src/components/settings/Configuration.tsx | 21 +- .../settings/PerformanceMetrics.tsx | 153 ++++------ .../components/settings/StockManagement.tsx | 57 +++- inventory/src/pages/Categories.tsx | 82 +++--- inventory/src/pages/Forecasting.tsx | 2 +- inventory/src/pages/Products.tsx | 2 +- inventory/src/types/products.ts | 4 +- 25 files changed, 936 insertions(+), 686 deletions(-) create mode 100644 docs/schema-update-changes.md diff --git a/docs/schema-update-changes.md b/docs/schema-update-changes.md new file mode 100644 index 0000000..69c8728 --- /dev/null +++ b/docs/schema-update-changes.md @@ -0,0 +1,270 @@ +# Schema Update Changes Required + +## Core Field Name Changes + +### Global Changes +- Update all references from `product_id` to `pid` in all tables and queries + - This includes foreign key references in related tables + - Update TypeScript interfaces and types (e.g., `interface Product { pid: number; ... }`) + - Update API request/response types +- Update all references from `category_id` to `cat_id` in category-related queries + - This affects the `categories` table and all tables with category foreign keys +- Update purchase order status to use numeric codes instead of strings + - Status codes: 0=canceled, 1=created, 10=electronically_ready_send, 11=ordered, 12=preordered, 13=electronically_sent, 15=receiving_started, 50=done + - Receiving status codes: 0=canceled, 1=created, 30=partial_received, 40=full_received, 50=paid +- Handle NULL brand values as 'Unbranded' + - Add COALESCE(brand, 'Unbranded') in all brand-related queries + - Update frontend brand filters to handle 'Unbranded' as a valid value + +## Backend Route Changes + +### Product Routes +1. Update ID field references in all product routes: + - `/api/products/:id` -> `/api/products/:pid` + - All query parameters using `product_id` should be changed to `pid` + - Update all SQL queries to use `pid` instead of `product_id` + - Update `/api/products/:id/metrics` -> `/api/products/:pid/metrics` + - Update `/api/products/:id/time-series` -> `/api/products/:pid/time-series` + - Update request parameter validation in routes + - Example query change: + ```sql + -- Old + SELECT * FROM products WHERE product_id = ? + -- New + SELECT * FROM products WHERE pid = ? + ``` + +2. Update purchase order status checks: + - Change `status = 'closed'` to `receiving_status >= 30` in all relevant queries + - Update any route logic that checks PO status to use the new numeric status codes + - Example status check change: + ```sql + -- Old + WHERE po.status = 'closed' + -- New + WHERE po.receiving_status >= 30 -- Partial or fully received + ``` + +### Category Routes +1. Update ID references: + - `/api/categories/:id` -> `/api/categories/:cat_id` + - Update all SQL queries to use `cat_id` instead of `category_id` + - Update join conditions in category-related queries + - Example join change: + ```sql + -- Old + JOIN categories c ON p.category_id = c.category_id + -- New + JOIN categories c ON p.cat_id = c.cat_id + ``` + +2. Update category metrics queries: + - Modify field size handling for financial fields (DECIMAL(15,3) instead of DECIMAL(10,3)) + - Update category performance calculations to use new field sizes + - Example field size change: + ```sql + -- Old + total_value DECIMAL(10,3) + -- New + total_value DECIMAL(15,3) + ``` + +### Vendor Routes +1. Update product references: + - Change all queries to use `pid` instead of `product_id` + - Update purchase order status checks to use new numeric codes + - Example vendor query change: + ```sql + -- Old + SELECT v.*, p.product_id FROM vendors v + JOIN products p ON p.vendor = v.name + WHERE p.product_id = ? + -- New + SELECT v.*, p.pid FROM vendors v + JOIN products p ON p.vendor = v.name + WHERE p.pid = ? + ``` + +2. Update vendor metrics queries: + - Add COALESCE for NULL brand handling: + ```sql + -- Old + GROUP BY brand + -- New + GROUP BY COALESCE(brand, 'Unbranded') + ``` + - Update field references in performance metrics calculations + +### Dashboard Routes +1. Update all dashboard endpoints: + - `/dashboard/best-sellers`: + ```typescript + interface BestSellerProduct { + pid: number; // Changed from product_id + sku: string; + title: string; + units_sold: number; + revenue: number; // Now handles larger decimals + profit: number; // Now handles larger decimals + } + ``` + - `/dashboard/overstock/products`: + ```typescript + interface OverstockedProduct { + pid: number; // Changed from product_id + sku: string; + title: string; + stock_quantity: number; + overstocked_amt: number; + excess_cost: number; // Now DECIMAL(15,3) + excess_retail: number; // Now DECIMAL(15,3) + } + ``` + +### Analytics Routes +1. Update analytics endpoints: + - `/analytics/stats` - Update all ID references and decimal handling + - `/analytics/profit` - Update decimal precision in calculations + - `/analytics/vendors` - Add brand NULL handling + - Example analytics query change: + ```sql + -- Old + SELECT product_id, SUM(price * quantity) as revenue + FROM orders + GROUP BY product_id + -- New + SELECT pid, CAST(SUM(price * quantity) AS DECIMAL(15,3)) as revenue + FROM orders + GROUP BY pid + ``` + +## Frontend Component Changes + +### Product Components +1. Update API calls: + ```typescript + // Old + fetch(`/api/products/${product_id}`) + // New + fetch(`/api/products/${pid}`) + ``` + - Update route parameters in React Router: + ```typescript + // Old + + // New + + ``` + - Update useParams usage: + ```typescript + // Old + const { product_id } = useParams(); + // New + const { pid } = useParams(); + ``` + +2. Update data display: + ```typescript + // Old + {formatCurrency(product.price)} + // New + {formatCurrency(Number(product.price))} + ``` + +### Dashboard Components +1. Update metrics displays: + ```typescript + // Old + interface ProductMetrics { + product_id: number; + total_value: number; + } + // New + interface ProductMetrics { + pid: number; + total_value: string; // Handle as string due to DECIMAL(15,3) + } + ``` + +2. Update stock status displays: + ```typescript + // Old + const isReceived = po.status === 'closed'; + // New + const isReceived = po.receiving_status >= 30; + ``` + +### Product Filters Component +1. Update filter options: + ```typescript + // Old + const productFilter = { id: 'product_id', value: id }; + // New + const productFilter = { id: 'pid', value: id }; + ``` + +2. Update status filters: + ```typescript + // Old + const poStatusOptions = [ + { label: 'Closed', value: 'closed' } + ]; + // New + const poStatusOptions = [ + { label: 'Received', value: '30' } // Using numeric codes + ]; + ``` + +## Data Type Considerations + +### Financial Fields +- Update TypeScript types: + ```typescript + // Old + price: number; + // New + price: string; // Handle as string due to DECIMAL(15,3) + ``` +- Update formatting: + ```typescript + // Old + formatCurrency(value) + // New + formatCurrency(Number(value)) + ``` + +### Status Codes +- Add status code mapping: + ```typescript + const PO_STATUS_MAP = { + 0: 'Canceled', + 1: 'Created', + 10: 'Ready to Send', + 11: 'Ordered', + 12: 'Preordered', + 13: 'Sent', + 15: 'Receiving Started', + 50: 'Done' + }; + ``` + +## Testing Requirements + +1. API Route Testing: + ```typescript + // Test decimal handling + expect(typeof response.total_value).toBe('string'); + expect(response.total_value).toMatch(/^\d+\.\d{3}$/); + + // Test status codes + expect(response.receiving_status).toBeGreaterThanOrEqual(30); + + // Test brand handling + expect(response.brand || 'Unbranded').toBeDefined(); + ``` + +## Notes +- All numeric status code comparisons should use >= for status checks to handle future status codes +- All financial values should be handled as strings in TypeScript/JavaScript to preserve precision +- Brand grouping should always use COALESCE(brand, 'Unbranded') in SQL queries +- All ID parameters in routes should be validated as numbers \ No newline at end of file diff --git a/inventory-server/src/routes/analytics.js b/inventory-server/src/routes/analytics.js index e29693e..f367895 100644 --- a/inventory-server/src/routes/analytics.js +++ b/inventory-server/src/routes/analytics.js @@ -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]); diff --git a/inventory-server/src/routes/categories.js b/inventory-server/src/routes/categories.js index 43c2668..d6f42f1 100644 --- a/inventory-server/src/routes/categories.js +++ b/inventory-server/src/routes/categories.js @@ -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) { diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 1bdb86d..33b619f 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -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' }); diff --git a/inventory-server/src/routes/metrics.js b/inventory-server/src/routes/metrics.js index e3635b0..4b7d0d2 100644 --- a/inventory-server/src/routes/metrics.js +++ b/inventory-server/src/routes/metrics.js @@ -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) })) }; diff --git a/inventory-server/src/routes/orders.js b/inventory-server/src/routes/orders.js index 1309726..d34e87b 100644 --- a/inventory-server/src/routes/orders.js +++ b/inventory-server/src/routes/orders.js @@ -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]); diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index e333d40..d8cc84f 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -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]); diff --git a/inventory-server/src/routes/purchase-orders.js b/inventory-server/src/routes/purchase-orders.js index f18cea8..2fac225 100644 --- a/inventory-server/src/routes/purchase-orders.js +++ b/inventory-server/src/routes/purchase-orders.js @@ -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 `); diff --git a/inventory-server/src/routes/vendors.js b/inventory-server/src/routes/vendors.js index c44c518..9ecbab1 100644 --- a/inventory-server/src/routes/vendors.js +++ b/inventory-server/src/routes/vendors.js @@ -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) { diff --git a/inventory/src/components/dashboard/BestSellers.tsx b/inventory/src/components/dashboard/BestSellers.tsx index ad17942..0fa55ff 100644 --- a/inventory/src/components/dashboard/BestSellers.tsx +++ b/inventory/src/components/dashboard/BestSellers.tsx @@ -6,14 +6,21 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import config from "@/config" import { formatCurrency } from "@/lib/utils" -interface BestSellerProduct { - product_id: number - sku: string - title: string - units_sold: number - revenue: number - profit: number - growth_rate: number +interface Product { + pid: number; + sku: string; + title: string; + units_sold: number; + revenue: number; + profit: number; +} + +interface Category { + cat_id: number; + name: string; + total_revenue: number; + total_profit: number; + total_units: number; } interface BestSellerBrand { @@ -25,18 +32,18 @@ interface BestSellerBrand { } interface BestSellerCategory { - category_id: number - name: string - units_sold: number - revenue: number - profit: number - growth_rate: number + cat_id: number; + name: string; + units_sold: number; + revenue: number; + profit: number; + growth_rate: number; } interface BestSellersData { - products: BestSellerProduct[] + products: Product[] brands: BestSellerBrand[] - categories: BestSellerCategory[] + categories: Category[] } export function BestSellers() { @@ -70,41 +77,29 @@ export function BestSellers() { - Product - Sales - Revenue - Profit - Growth + Product + Units Sold + Revenue + Profit {data?.products.map((product) => ( - - -
- - {product.title} - -

{product.sku}

-
-
- - {product.units_sold.toLocaleString()} - - - {formatCurrency(product.revenue)} - - - {formatCurrency(product.profit)} - - - {product.growth_rate > 0 ? '+' : ''}{product.growth_rate.toFixed(1)}% + + + + {product.title} + +
{product.sku}
+ {product.units_sold} + {formatCurrency(product.revenue)} + {formatCurrency(product.profit)}
))}
@@ -154,31 +149,19 @@ export function BestSellers() {
- Category - Sales - Revenue - Profit - Growth + Category + Units Sold + Revenue + Profit {data?.categories.map((category) => ( - - -

{category.name}

-
- - {category.units_sold.toLocaleString()} - - - {formatCurrency(category.revenue)} - - - {formatCurrency(category.profit)} - - - {category.growth_rate > 0 ? '+' : ''}{category.growth_rate.toFixed(1)}% - + + {category.name} + {category.total_units} + {formatCurrency(category.total_revenue)} + {formatCurrency(category.total_profit)} ))}
diff --git a/inventory/src/components/dashboard/LowStockAlerts.tsx b/inventory/src/components/dashboard/LowStockAlerts.tsx index 25b6991..6f995e0 100644 --- a/inventory/src/components/dashboard/LowStockAlerts.tsx +++ b/inventory/src/components/dashboard/LowStockAlerts.tsx @@ -12,19 +12,20 @@ import { Badge } from "@/components/ui/badge" import { AlertCircle, AlertTriangle } from "lucide-react" import config from "@/config" -interface LowStockProduct { - product_id: number - SKU: string - title: string - stock_quantity: number - reorder_qty: number - days_of_inventory: number - stock_status: "Critical" | "Reorder" - daily_sales_avg: number +interface Product { + pid: number; + sku: string; + title: string; + stock_quantity: number; + daily_sales_avg: number; + days_of_inventory: number; + reorder_qty: number; + last_purchase_date: string | null; + lead_time_status: string; } export function LowStockAlerts() { - const { data: products } = useQuery({ + const { data: products } = useQuery({ queryKey: ["low-stock"], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/low-stock/products`) @@ -45,35 +46,37 @@ export function LowStockAlerts() {
- SKU Product Stock - Status + Daily Sales + Days Left + Reorder Qty + Last Purchase + Lead Time {products?.map((product) => ( - - {product.SKU} - {product.title} - - {product.stock_quantity} / {product.reorder_qty} - - - + + - {product.stock_status === "Critical" ? ( - - ) : ( - - )} - {product.stock_status} + {product.title} + +
{product.sku}
+
+ {product.stock_quantity} + {product.daily_sales_avg.toFixed(1)} + {product.days_of_inventory.toFixed(1)} + {product.reorder_qty} + {product.last_purchase_date ? formatDate(product.last_purchase_date) : '-'} + + + {product.lead_time_status}
diff --git a/inventory/src/components/dashboard/TopOverstockedProducts.tsx b/inventory/src/components/dashboard/TopOverstockedProducts.tsx index f963504..7de9154 100644 --- a/inventory/src/components/dashboard/TopOverstockedProducts.tsx +++ b/inventory/src/components/dashboard/TopOverstockedProducts.tsx @@ -5,18 +5,18 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import config from "@/config" import { formatCurrency } from "@/lib/utils" -interface OverstockedProduct { - product_id: number - SKU: string - title: string - stock_quantity: number - overstocked_amt: number - excess_cost: number - excess_retail: number +interface Product { + pid: number; + sku: string; + title: string; + stock_quantity: number; + overstocked_amt: number; + excess_cost: number; + excess_retail: number; } export function TopOverstockedProducts() { - const { data } = useQuery({ + const { data } = useQuery({ queryKey: ["top-overstocked-products"], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/overstock/products?limit=50`) @@ -38,40 +38,30 @@ export function TopOverstockedProducts() { Product - Current Stock - Overstock Amt - Overstock Cost - Overstock Retail + Stock + Excess + Cost + Retail {data?.map((product) => ( - + -
- - {product.title} - -

{product.SKU}

-
-
- - {product.stock_quantity.toLocaleString()} - - - {product.overstocked_amt.toLocaleString()} - - - {formatCurrency(product.excess_cost)} - - - {formatCurrency(product.excess_retail)} + + {product.title} + +
{product.sku}
+ {product.stock_quantity} + {product.overstocked_amt} + {formatCurrency(product.excess_cost)} + {formatCurrency(product.excess_retail)}
))}
diff --git a/inventory/src/components/dashboard/TopReplenishProducts.tsx b/inventory/src/components/dashboard/TopReplenishProducts.tsx index 41df022..4701ed5 100644 --- a/inventory/src/components/dashboard/TopReplenishProducts.tsx +++ b/inventory/src/components/dashboard/TopReplenishProducts.tsx @@ -3,20 +3,19 @@ import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import config from "@/config" -import { formatCurrency } from "@/lib/utils" -interface ReplenishProduct { - product_id: number - SKU: string - title: string - current_stock: number - replenish_qty: number - replenish_cost: number - replenish_retail: number +interface Product { + pid: number; + sku: string; + title: string; + stock_quantity: number; + daily_sales_avg: number; + reorder_qty: number; + last_purchase_date: string | null; } export function TopReplenishProducts() { - const { data } = useQuery({ + const { data } = useQuery({ queryKey: ["top-replenish-products"], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/replenish/products?limit=50`) @@ -39,39 +38,29 @@ export function TopReplenishProducts() { Product Stock - Replenish - Cost - Retail + Daily Sales + Reorder Qty + Last Purchase {data?.map((product) => ( - + -
- - {product.title} - -

{product.SKU}

-
-
- - {product.current_stock.toLocaleString()} - - - {product.replenish_qty.toLocaleString()} - - - {formatCurrency(product.replenish_cost)} - - - {formatCurrency(product.replenish_retail)} + + {product.title} + +
{product.sku}
+ {product.stock_quantity} + {product.daily_sales_avg.toFixed(1)} + {product.reorder_qty} + {product.last_purchase_date ? product.last_purchase_date : '-'}
))}
diff --git a/inventory/src/components/dashboard/TrendingProducts.tsx b/inventory/src/components/dashboard/TrendingProducts.tsx index 2fed059..5ba568a 100644 --- a/inventory/src/components/dashboard/TrendingProducts.tsx +++ b/inventory/src/components/dashboard/TrendingProducts.tsx @@ -11,18 +11,18 @@ import { import { TrendingUp, TrendingDown } from "lucide-react" import config from "@/config" -interface TrendingProduct { - product_id: number - sku: string - title: string - daily_sales_avg: string - weekly_sales_avg: string - growth_rate: string - total_revenue: string +interface Product { + pid: number; + sku: string; + title: string; + daily_sales_avg: number; + weekly_sales_avg: number; + growth_rate: number; + total_revenue: number; } export function TrendingProducts() { - const { data: products } = useQuery({ + const { data: products } = useQuery({ queryKey: ["trending-products"], queryFn: async () => { const response = await fetch(`${config.apiUrl}/products/trending`) @@ -33,7 +33,6 @@ export function TrendingProducts() { }, }) - const formatPercent = (value: number) => new Intl.NumberFormat("en-US", { style: "percent", @@ -42,6 +41,14 @@ export function TrendingProducts() { signDisplay: "exceptZero", }).format(value / 100) + const formatCurrency = (value: number) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value) + return ( <> @@ -59,7 +66,7 @@ export function TrendingProducts() { {products?.map((product) => ( - +
{product.title} diff --git a/inventory/src/components/forecasting/columns.tsx b/inventory/src/components/forecasting/columns.tsx index e3fe4f7..3f1c41a 100644 --- a/inventory/src/components/forecasting/columns.tsx +++ b/inventory/src/components/forecasting/columns.tsx @@ -3,14 +3,16 @@ import { ArrowUpDown, ChevronDown, ChevronRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -interface ProductDetail { - product_id: string; - name: string; + +interface Product { + pid: string; sku: string; + title: string; stock_quantity: number; - total_sold: number; - avg_price: number; - first_received_date: string; + daily_sales_avg: number; + forecast_units: number; + forecast_revenue: number; + confidence_level: number; } export interface ForecastItem { @@ -20,7 +22,7 @@ export interface ForecastItem { numProducts: number; avgPrice: number; avgTotalSold: number; - products?: ProductDetail[]; + products?: Product[]; } export const columns: ColumnDef[] = [ @@ -147,23 +149,33 @@ export const renderSubComponent = ({ row }: { row: any }) => {
- Product Name - SKU - First Received - Stock Quantity - Total Sold - Average Price + Product + Stock + Daily Sales + Forecast Units + Forecast Revenue + Confidence - {products.map((product: ProductDetail) => ( - - {product.name} - {product.sku} - {product.first_received_date} - {product.stock_quantity.toLocaleString()} - {product.total_sold.toLocaleString()} - ${product.avg_price.toFixed(2)} + {products.map((product) => ( + + + + {product.title} + +
{product.sku}
+
+ {product.stock_quantity} + {product.daily_sales_avg.toFixed(1)} + {product.forecast_units.toFixed(1)} + {product.forecast_revenue.toFixed(2)} + {product.confidence_level.toFixed(1)}%
))}
diff --git a/inventory/src/components/products/ProductDetail.tsx b/inventory/src/components/products/ProductDetail.tsx index cfe392c..5469aeb 100644 --- a/inventory/src/components/products/ProductDetail.tsx +++ b/inventory/src/components/products/ProductDetail.tsx @@ -10,7 +10,7 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai import config from "@/config"; interface Product { - product_id: number; + pid: number; title: string; SKU: string; barcode: string; @@ -38,7 +38,7 @@ interface Product { // Vendor info vendor: string; vendor_reference: string; - brand: string; + brand: string | 'Unbranded'; // URLs permalink: string; diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx index 184bfb3..a7b9e65 100644 --- a/inventory/src/components/products/ProductTable.tsx +++ b/inventory/src/components/products/ProductTable.tsx @@ -230,7 +230,7 @@ export function ProductTable({ return (
{Array.from(new Set(value as string[])).map((category) => ( - {category} + {category} )) || '-'}
); @@ -297,12 +297,12 @@ export function ProductTable({ {products.map((product) => ( onRowClick?.(product)} className="cursor-pointer" > {orderedColumns.map((column) => ( - + {formatColumnValue(product, column)} ))} diff --git a/inventory/src/components/settings/CalculationSettings.tsx b/inventory/src/components/settings/CalculationSettings.tsx index 85cc12f..24d83fd 100644 --- a/inventory/src/components/settings/CalculationSettings.tsx +++ b/inventory/src/components/settings/CalculationSettings.tsx @@ -8,7 +8,7 @@ import config from '../../config'; interface SalesVelocityConfig { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; daily_window_days: number; weekly_window_days: number; @@ -18,7 +18,7 @@ interface SalesVelocityConfig { export function CalculationSettings() { const [salesVelocityConfig, setSalesVelocityConfig] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, daily_window_days: 30, weekly_window_days: 7, diff --git a/inventory/src/components/settings/Configuration.tsx b/inventory/src/components/settings/Configuration.tsx index 0d7a319..faaa1ff 100644 --- a/inventory/src/components/settings/Configuration.tsx +++ b/inventory/src/components/settings/Configuration.tsx @@ -6,10 +6,11 @@ import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { toast } from "sonner"; import config from '../../config'; +import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table"; interface StockThreshold { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; critical_days: number; reorder_days: number; @@ -22,7 +23,7 @@ interface StockThreshold { interface LeadTimeThreshold { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; target_days: number; warning_days: number; @@ -31,7 +32,7 @@ interface LeadTimeThreshold { interface SalesVelocityConfig { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; daily_window_days: number; weekly_window_days: number; @@ -47,7 +48,7 @@ interface ABCClassificationConfig { interface SafetyStockConfig { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; coverage_days: number; service_level: number; @@ -55,7 +56,7 @@ interface SafetyStockConfig { interface TurnoverConfig { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; calculation_period_days: number; target_rate: number; @@ -64,7 +65,7 @@ interface TurnoverConfig { export function Configuration() { const [stockThresholds, setStockThresholds] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, critical_days: 7, reorder_days: 14, @@ -75,7 +76,7 @@ export function Configuration() { const [leadTimeThresholds, setLeadTimeThresholds] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, target_days: 14, warning_days: 21, @@ -84,7 +85,7 @@ export function Configuration() { const [salesVelocityConfig, setSalesVelocityConfig] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, daily_window_days: 30, weekly_window_days: 7, @@ -100,7 +101,7 @@ export function Configuration() { const [safetyStockConfig, setSafetyStockConfig] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, coverage_days: 14, service_level: 95.0 @@ -108,7 +109,7 @@ export function Configuration() { const [turnoverConfig, setTurnoverConfig] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, calculation_period_days: 30, target_rate: 1.0 diff --git a/inventory/src/components/settings/PerformanceMetrics.tsx b/inventory/src/components/settings/PerformanceMetrics.tsx index c2df4eb..74bb1f0 100644 --- a/inventory/src/components/settings/PerformanceMetrics.tsx +++ b/inventory/src/components/settings/PerformanceMetrics.tsx @@ -5,10 +5,11 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; import config from '../../config'; +import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table"; interface LeadTimeThreshold { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; target_days: number; warning_days: number; @@ -17,6 +18,8 @@ interface LeadTimeThreshold { interface ABCClassificationConfig { id: number; + cat_id: number | null; + vendor: string | null; a_threshold: number; b_threshold: number; classification_period_days: number; @@ -24,7 +27,7 @@ interface ABCClassificationConfig { interface TurnoverConfig { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; calculation_period_days: number; target_rate: number; @@ -33,27 +36,16 @@ interface TurnoverConfig { export function PerformanceMetrics() { const [leadTimeThresholds, setLeadTimeThresholds] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, target_days: 14, warning_days: 21, critical_days: 30 }); - const [abcConfig, setAbcConfig] = useState({ - id: 1, - a_threshold: 20.0, - b_threshold: 50.0, - classification_period_days: 90 - }); + const [abcConfigs, setAbcConfigs] = useState([]); - const [turnoverConfig, setTurnoverConfig] = useState({ - id: 1, - category_id: null, - vendor: null, - calculation_period_days: 30, - target_rate: 1.0 - }); + const [turnoverConfigs, setTurnoverConfigs] = useState([]); useEffect(() => { const loadConfig = async () => { @@ -66,8 +58,8 @@ export function PerformanceMetrics() { } const data = await response.json(); setLeadTimeThresholds(data.leadTimeThresholds); - setAbcConfig(data.abcConfig); - setTurnoverConfig(data.turnoverConfig); + setAbcConfigs(data.abcConfigs); + setTurnoverConfigs(data.turnoverConfigs); } catch (error) { toast.error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); } @@ -105,7 +97,7 @@ export function PerformanceMetrics() { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify(abcConfig) + body: JSON.stringify(abcConfigs) }); if (!response.ok) { @@ -127,7 +119,7 @@ export function PerformanceMetrics() { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify(turnoverConfig) + body: JSON.stringify(turnoverConfigs) }); if (!response.ok) { @@ -210,54 +202,28 @@ export function PerformanceMetrics() {
-
-
- - setAbcConfig(prev => ({ - ...prev, - a_threshold: parseFloat(e.target.value) || 0 - }))} - /> -
-
- - setAbcConfig(prev => ({ - ...prev, - b_threshold: parseFloat(e.target.value) || 0 - }))} - /> -
-
- - setAbcConfig(prev => ({ - ...prev, - classification_period_days: parseInt(e.target.value) || 1 - }))} - /> -
-
+
+ + + Category + Vendor + A Threshold + B Threshold + Period Days + + + + {abcConfigs.map((config) => ( + + {config.cat_id ? getCategoryName(config.cat_id) : 'Global'} + {config.vendor || 'All Vendors'} + {config.a_threshold}% + {config.b_threshold}% + {config.classification_period_days} + + ))} + +
@@ -273,37 +239,26 @@ export function PerformanceMetrics() {
-
-
- - setTurnoverConfig(prev => ({ - ...prev, - calculation_period_days: parseInt(e.target.value) || 1 - }))} - /> -
-
- - setTurnoverConfig(prev => ({ - ...prev, - target_rate: parseFloat(e.target.value) || 0 - }))} - /> -
-
+ + + + Category + Vendor + Period Days + Target Rate + + + + {turnoverConfigs.map((config) => ( + + {config.cat_id ? getCategoryName(config.cat_id) : 'Global'} + {config.vendor || 'All Vendors'} + {config.calculation_period_days} + {config.target_rate.toFixed(2)} + + ))} + +
diff --git a/inventory/src/components/settings/StockManagement.tsx b/inventory/src/components/settings/StockManagement.tsx index f892fcf..6a88ee5 100644 --- a/inventory/src/components/settings/StockManagement.tsx +++ b/inventory/src/components/settings/StockManagement.tsx @@ -5,10 +5,11 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; import config from '../../config'; +import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table"; interface StockThreshold { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; critical_days: number; reorder_days: number; @@ -19,7 +20,7 @@ interface StockThreshold { interface SafetyStockConfig { id: number; - category_id: number | null; + cat_id: number | null; vendor: string | null; coverage_days: number; service_level: number; @@ -28,7 +29,7 @@ interface SafetyStockConfig { export function StockManagement() { const [stockThresholds, setStockThresholds] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, critical_days: 7, reorder_days: 14, @@ -39,7 +40,7 @@ export function StockManagement() { const [safetyStockConfig, setSafetyStockConfig] = useState({ id: 1, - category_id: null, + cat_id: null, vendor: null, coverage_days: 14, service_level: 95.0 @@ -243,6 +244,54 @@ export function StockManagement() {
+ + + + + Category + Vendor + Critical Days + Reorder Days + Overstock Days + Low Stock + Min Reorder + + + + {stockThresholds.map((threshold) => ( + + {threshold.cat_id ? getCategoryName(threshold.cat_id) : 'Global'} + {threshold.vendor || 'All Vendors'} + {threshold.critical_days} + {threshold.reorder_days} + {threshold.overstock_days} + {threshold.low_stock_threshold} + {threshold.min_reorder_quantity} + + ))} + +
+ + + + + Category + Vendor + Coverage Days + Service Level + + + + {safetyStockConfigs.map((config) => ( + + {config.cat_id ? getCategoryName(config.cat_id) : 'Global'} + {config.vendor || 'All Vendors'} + {config.coverage_days} + {config.service_level}% + + ))} + +
); } \ No newline at end of file diff --git a/inventory/src/pages/Categories.tsx b/inventory/src/pages/Categories.tsx index 4b86eeb..eb16664 100644 --- a/inventory/src/pages/Categories.tsx +++ b/inventory/src/pages/Categories.tsx @@ -10,16 +10,22 @@ import { motion } from "motion/react"; import config from "../config"; interface Category { - category_id: number; + cat_id: number; name: string; - description: string; - parent_category?: string; - product_count: number; - total_value: number; - avg_margin: number; - turnover_rate: number; - growth_rate: number; + type: number; + parent_id: number | null; + description: string | null; + created_at: string; + updated_at: string; status: string; + metrics?: { + product_count: number; + active_products: number; + total_value: number; + avg_margin: number; + turnover_rate: number; + growth_rate: number; + }; } interface CategoryFilters { @@ -71,16 +77,16 @@ export function Categories() { // Apply parent filter if (filters.parent !== 'all') { if (filters.parent === 'none') { - filtered = filtered.filter(category => !category.parent_category); + filtered = filtered.filter(category => !category.parent_id); } else { - filtered = filtered.filter(category => category.parent_category === filters.parent); + filtered = filtered.filter(category => category.parent_id === Number(filters.parent)); } } // Apply performance filter if (filters.performance !== 'all') { filtered = filtered.filter(category => { - const growth = category.growth_rate ?? 0; + const growth = category.metrics?.growth_rate ?? 0; switch (filters.performance) { case 'high_growth': return growth >= 20; case 'growing': return growth >= 5 && growth < 20; @@ -123,9 +129,9 @@ export function Categories() { if (!filteredData.length) return data?.stats; const activeCategories = filteredData.filter(c => c.status === 'active').length; - const totalValue = filteredData.reduce((sum, c) => sum + (c.total_value || 0), 0); - const margins = filteredData.map(c => c.avg_margin || 0).filter(m => m !== 0); - const growthRates = filteredData.map(c => c.growth_rate || 0).filter(g => g !== 0); + const totalValue = filteredData.reduce((sum, c) => sum + (c.metrics?.total_value || 0), 0); + const margins = filteredData.map(c => c.metrics?.avg_margin || 0).filter(m => m !== 0); + const growthRates = filteredData.map(c => c.metrics?.growth_rate || 0).filter(g => g !== 0); return { totalCategories: filteredData.length, @@ -281,14 +287,16 @@ export function Categories() { - handleSort("name")} className="cursor-pointer">Name - handleSort("parent_category")} className="cursor-pointer">Parent - handleSort("product_count")} className="cursor-pointer">Products - handleSort("total_value")} className="cursor-pointer">Value - handleSort("avg_margin")} className="cursor-pointer">Margin - handleSort("turnover_rate")} className="cursor-pointer">Turnover - handleSort("growth_rate")} className="cursor-pointer">Growth - handleSort("status")} className="cursor-pointer">Status + Name + Type + Parent + Products + Active + Value + Margin + Turnover + Growth + Status @@ -299,25 +307,21 @@ export function Categories() { ) : paginatedData.map((category: Category) => ( - + + {category.name} + {getPerformanceBadge(category.metrics?.growth_rate ?? 0)} + {category.parent_id ? getParentName(category.parent_id) : '-'} + {category.metrics?.product_count || 0} + {category.metrics?.active_products || 0} + {formatCurrency(category.metrics?.total_value || 0)} + {category.metrics?.avg_margin?.toFixed(1)}% + {category.metrics?.turnover_rate?.toFixed(2)} + {category.metrics?.growth_rate?.toFixed(1)}% -
{category.name}
-
{category.description}
+ + {category.status} +
- {category.parent_category || "—"} - {category.product_count?.toLocaleString() ?? 0} - {formatCurrency(category.total_value ?? 0)} - {typeof category.avg_margin === 'number' ? category.avg_margin.toFixed(1) : "0.0"}% - {typeof category.turnover_rate === 'number' ? category.turnover_rate.toFixed(1) : "0.0"}x - -
-
- {typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}% -
- {getPerformanceBadge(category.growth_rate ?? 0)} -
-
- {category.status}
))} {!isLoading && !paginatedData.length && ( diff --git a/inventory/src/pages/Forecasting.tsx b/inventory/src/pages/Forecasting.tsx index 5430518..e776d57 100644 --- a/inventory/src/pages/Forecasting.tsx +++ b/inventory/src/pages/Forecasting.tsx @@ -66,7 +66,7 @@ export default function Forecasting() { avgPrice: Number(item.avg_price) || 0, avgTotalSold: Number(item.avgTotalSold) || 0, products: item.products?.map((p: any) => ({ - product_id: p.product_id, + pid: p.pid, name: p.title, sku: p.sku, stock_quantity: Number(p.stock_quantity) || 0, diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index 1bba473..fb2ae65 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -503,7 +503,7 @@ export function Products() { columnDefs={AVAILABLE_COLUMNS} columnOrder={columnOrder} onColumnOrderChange={handleColumnOrderChange} - onRowClick={(product) => setSelectedProductId(product.product_id)} + onRowClick={(product) => setSelectedProductId(product.pid)} /> {totalPages > 1 && ( diff --git a/inventory/src/types/products.ts b/inventory/src/types/products.ts index a5195e0..697d48e 100644 --- a/inventory/src/types/products.ts +++ b/inventory/src/types/products.ts @@ -1,5 +1,5 @@ export interface Product { - product_id: number; + pid: number; title: string; SKU: string; stock_quantity: number; @@ -10,7 +10,7 @@ export interface Product { barcode: string; vendor: string; vendor_reference: string; - brand: string; + brand: string | 'Unbranded'; categories: string[]; tags: string[]; options: Record;