From d805e49449c3605ea85244ac6efdc46eeacd725d Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 11 Jan 2025 01:25:52 -0500 Subject: [PATCH] Use new categories correctly in existing components and handle category names with commas --- inventory-server/scripts/import-csv.js | 32 +- inventory-server/src/routes/analytics.js | 24 +- inventory-server/src/routes/dashboard.js | 46 +- inventory-server/src/routes/products.js | 52 ++- .../src/routes/purchase-orders.js | 6 +- inventory/package-lock.json | 433 ++++++++++++++++++ inventory/package.json | 1 + .../src/components/products/ProductTable.tsx | 10 +- inventory/src/pages/Products.tsx | 5 +- 9 files changed, 556 insertions(+), 53 deletions(-) diff --git a/inventory-server/scripts/import-csv.js b/inventory-server/scripts/import-csv.js index 68d3674..6aa9c3f 100644 --- a/inventory-server/scripts/import-csv.js +++ b/inventory-server/scripts/import-csv.js @@ -83,10 +83,36 @@ async function handleCategories(connection, productId, categoriesStr) { return; } - // Split categories and clean them - const categories = categoriesStr.split(',') + // Special cases that should not be split + const specialCategories = [ + 'Paint, Dyes & Chalk', + 'Fabric Paint, Markers, and Dye', + 'Crystals, Gems & Rhinestones', + 'Pens, Pencils & Markers' + ]; + + // Split categories and clean them, preserving special cases + const categories = []; + let remainingStr = categoriesStr; + + // First check for special categories + for (const special of specialCategories) { + if (remainingStr.includes(special)) { + categories.push(special); + // Remove the special category from the string + remainingStr = remainingStr.replace(special, ''); + } + } + + // Then process any remaining regular categories + remainingStr.split(',') .map(cat => cat.trim()) - .filter(cat => cat.length > 0); + .filter(cat => cat.length > 0) + .forEach(cat => { + if (!categories.includes(cat)) { + categories.push(cat); + } + }); // Remove existing category relationships for this product await connection.query( diff --git a/inventory-server/src/routes/analytics.js b/inventory-server/src/routes/analytics.js index 05e3c83..c23b8a7 100644 --- a/inventory-server/src/routes/analytics.js +++ b/inventory-server/src/routes/analytics.js @@ -65,7 +65,7 @@ router.get('/profit', async (req, res) => { // Get profit margins by category const [byCategory] = await pool.query(` SELECT - COALESCE(p.categories, 'Uncategorized') as category, + c.name as category, ROUND( (SUM(o.price * o.quantity - p.cost_price * o.quantity) / NULLIF(SUM(o.price * o.quantity), 0)) * 100, 1 @@ -74,8 +74,10 @@ router.get('/profit', async (req, res) => { SUM(p.cost_price * o.quantity) 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 WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY p.categories + GROUP BY c.name ORDER BY profitMargin DESC LIMIT 10 `); @@ -190,14 +192,16 @@ router.get('/stock', async (req, res) => { // Get turnover by category const [turnoverByCategory] = await pool.query(` SELECT - COALESCE(p.categories, 'Uncategorized') as category, + c.name as category, ROUND(SUM(o.quantity) / NULLIF(AVG(p.stock_quantity), 0), 1) as turnoverRate, 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 WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY p.categories + GROUP BY c.name HAVING turnoverRate > 0 ORDER BY turnoverRate DESC LIMIT 10 @@ -328,7 +332,7 @@ router.get('/categories', async (req, res) => { // Get category performance metrics const [performance] = await pool.query(` SELECT - COALESCE(p.categories, 'Uncategorized') as category, + c.name as category, SUM(o.price * o.quantity) as revenue, SUM(o.price * o.quantity - p.cost_price * o.quantity) as profit, ROUND( @@ -348,8 +352,10 @@ router.get('/categories', async (req, res) => { COUNT(DISTINCT p.product_id) 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 WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) - GROUP BY p.categories + GROUP BY c.name HAVING revenue > 0 ORDER BY revenue DESC LIMIT 10 @@ -358,12 +364,14 @@ router.get('/categories', async (req, res) => { // Get category revenue distribution const [distribution] = await pool.query(` SELECT - COALESCE(p.categories, 'Uncategorized') as category, + 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 WHERE o.date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY p.categories + GROUP BY c.name HAVING value > 0 ORDER BY value DESC LIMIT 6 diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 70046ac..79a819f 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -123,11 +123,13 @@ router.get('/category-stats', async (req, res) => { try { const [rows] = await pool.query(` SELECT - categories, - COUNT(*) as count - FROM products - WHERE visible = true - GROUP BY categories + c.name as category, + COUNT(DISTINCT pc.product_id) as count + FROM categories c + LEFT JOIN product_categories pc ON c.id = pc.category_id + LEFT JOIN products p ON pc.product_id = p.product_id + WHERE p.visible = true + GROUP BY c.name ORDER BY count DESC LIMIT 10 `); @@ -164,13 +166,15 @@ router.get('/sales-by-category', async (req, res) => { try { const [rows] = await pool.query(` SELECT - p.categories as category, + c.name as category, SUM(o.price * o.quantity) as total FROM orders o JOIN products p ON o.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 WHERE o.canceled = false AND DATE(o.date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY p.categories + GROUP BY c.name ORDER BY total DESC LIMIT 6 `); @@ -266,14 +270,16 @@ router.get('/inventory-metrics', async (req, res) => { // Get stock levels by category const [stockLevels] = await pool.query(` SELECT - categories as category, + c.name as category, SUM(CASE WHEN stock_quantity > 5 THEN 1 ELSE 0 END) as inStock, SUM(CASE WHEN stock_quantity > 0 AND stock_quantity <= 5 THEN 1 ELSE 0 END) as lowStock, SUM(CASE WHEN stock_quantity = 0 THEN 1 ELSE 0 END) as outOfStock - FROM products + FROM products p + JOIN product_categories pc ON p.product_id = pc.product_id + JOIN categories c ON pc.category_id = c.id WHERE visible = true - GROUP BY categories - ORDER BY categories ASC + GROUP BY c.name + ORDER BY c.name ASC `); // Get top vendors with product counts and average stock @@ -296,21 +302,25 @@ router.get('/inventory-metrics', async (req, res) => { const [stockTurnover] = await pool.query(` WITH CategorySales AS ( SELECT - p.categories as category, + c.name as category, SUM(o.quantity) as units_sold FROM products p LEFT JOIN orders o ON p.product_id = o.product_id + JOIN product_categories pc ON p.product_id = pc.product_id + JOIN categories c ON pc.category_id = c.id WHERE o.canceled = false AND DATE(o.date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY p.categories + GROUP BY c.name ), CategoryStock AS ( SELECT - categories as category, - AVG(stock_quantity) as avg_stock - FROM products - WHERE visible = true - GROUP BY categories + c.name as category, + AVG(p.stock_quantity) as avg_stock + FROM products p + JOIN product_categories pc ON p.product_id = pc.product_id + JOIN categories c ON pc.category_id = c.id + WHERE p.visible = true + GROUP BY c.name ) SELECT cs.category, diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index 5f70a57..6fa6cfd 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -32,7 +32,14 @@ router.get('/', async (req, res) => { } if (category !== 'all') { - conditions.push('categories = ?'); + conditions.push(` + product_id IN ( + SELECT pc.product_id + FROM product_categories pc + JOIN categories c ON pc.category_id = c.id + WHERE c.name = ? + ) + `); params.push(category); } @@ -75,37 +82,46 @@ router.get('/', async (req, res) => { // Get paginated results const query = ` SELECT - product_id, - title, - SKU, - stock_quantity, - price, - regular_price, - cost_price, - vendor, - brand, - categories, - visible, - managing_stock, - image - FROM products + p.product_id, + p.title, + p.SKU, + p.stock_quantity, + p.price, + p.regular_price, + p.cost_price, + p.vendor, + p.brand, + p.visible, + p.managing_stock, + p.image, + GROUP_CONCAT(c.name) as categories + FROM products p + LEFT JOIN product_categories pc ON p.product_id = pc.product_id + LEFT JOIN categories c ON pc.category_id = c.id WHERE ${conditions.join(' AND ')} + GROUP BY p.product_id ORDER BY ${sortColumn} ${sortDirection} LIMIT ? OFFSET ? `; const [rows] = await pool.query(query, [...params, limit, offset]); + // Transform the categories string into an array + const productsWithCategories = rows.map(product => ({ + ...product, + categories: product.categories ? product.categories.split(',') : [] + })); + // Get unique categories and vendors for filters const [categories] = await pool.query( - 'SELECT DISTINCT categories FROM products WHERE visible = true AND categories IS NOT NULL AND categories != "" ORDER BY categories' + 'SELECT name FROM categories ORDER BY name' ); const [vendors] = await pool.query( 'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor' ); res.json({ - products: rows, + products: productsWithCategories, pagination: { total, pages: Math.ceil(total / limit), @@ -113,7 +129,7 @@ router.get('/', async (req, res) => { limit }, filters: { - categories: categories.map(c => c.categories), + categories: categories.map(c => c.name), vendors: vendors.map(v => v.vendor) } }); diff --git a/inventory-server/src/routes/purchase-orders.js b/inventory-server/src/routes/purchase-orders.js index cf89a88..f18cea8 100644 --- a/inventory-server/src/routes/purchase-orders.js +++ b/inventory-server/src/routes/purchase-orders.js @@ -243,7 +243,7 @@ router.get('/cost-analysis', async (req, res) => { const [analysis] = await pool.query(` SELECT - p.categories, + 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, @@ -254,7 +254,9 @@ router.get('/cost-analysis', async (req, res) => { SUM(po.ordered * po.cost_price) as total_spend FROM purchase_orders po JOIN products p ON po.product_id = p.product_id - GROUP BY p.categories + JOIN product_categories pc ON p.product_id = pc.product_id + JOIN categories c ON pc.category_id = c.id + GROUP BY c.name ORDER BY total_spend DESC `); diff --git a/inventory/package-lock.json b/inventory/package-lock.json index a8695b2..346037d 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -28,6 +28,7 @@ "@tanstack/virtual-core": "^3.11.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "lucide-react": "^0.469.0", "next-themes": "^0.4.4", @@ -3302,6 +3303,438 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/inventory/package.json b/inventory/package.json index f510983..4d19b2b 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -30,6 +30,7 @@ "@tanstack/virtual-core": "^3.11.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "lucide-react": "^0.469.0", "next-themes": "^0.4.4", diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx index c9aee90..fde26a6 100644 --- a/inventory/src/components/products/ProductTable.tsx +++ b/inventory/src/components/products/ProductTable.tsx @@ -21,7 +21,7 @@ interface Product { cost_price: number; vendor: string; brand: string; - categories: string; + categories: string[]; visible: boolean; managing_stock: boolean; image?: string; @@ -173,7 +173,13 @@ export function ProductTable({ ${product.cost_price.toFixed(2)} {product.vendor || '-'} {product.brand || '-'} - {product.categories || '-'} + +
+ {product.categories?.map((category) => ( + {category} + )) || '-'} +
+
{product.visible ? ( Active diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index e9437b0..91c89a8 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -24,7 +24,7 @@ interface Product { cost_price: number; vendor: string; brand: string; - categories: string; + categories: string[]; visible: boolean; managing_stock: boolean; image: string | null; @@ -108,7 +108,8 @@ export function Products() { cost_price: parseFloat(product.cost_price) || 0, stock_quantity: parseInt(product.stock_quantity) || 0, sku: product.SKU || product.sku || '', - image: product.image || null + image: product.image || null, + categories: Array.isArray(product.categories) ? product.categories : [] })) }; },