From f7bdefb0a323f14e3bd9be6bdb8abd7e6ed1549a Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 1 Mar 2025 12:24:04 -0500 Subject: [PATCH] Add product search and template creation functionality to validation step --- inventory-server/src/routes/import.js | 231 +++ inventory/package-lock.json | 260 ++- inventory/package.json | 3 +- .../products/ProductSearchDialog.tsx | 1569 +++++++++++++++++ .../steps/ImageUploadStep/ImageUploadStep.tsx | 156 +- .../steps/ValidationStep/ValidationStep.tsx | 1192 +++++++------ inventory/src/pages/Import.tsx | 4 +- 7 files changed, 2855 insertions(+), 560 deletions(-) create mode 100644 inventory/src/components/products/ProductSearchDialog.tsx diff --git a/inventory-server/src/routes/import.js b/inventory-server/src/routes/import.js index 22cb6a6..f75d74d 100644 --- a/inventory-server/src/routes/import.js +++ b/inventory-server/src/routes/import.js @@ -566,6 +566,9 @@ router.get('/field-options', async (req, res) => { }; }); + // Add debugging to verify category types + console.log(`Returning ${result.categories.length} categories with types: ${Array.from(new Set(result.categories.map(c => c.type))).join(', ')}`); + res.json(result); } catch (error) { console.error('Error fetching import field options:', error); @@ -715,4 +718,232 @@ router.get('/list-uploads', (req, res) => { } }); +// Search products from production database +router.get('/search-products', async (req, res) => { + const { q, company, dateRange } = req.query; + + if (!q) { + return res.status(400).json({ error: 'Search term is required' }); + } + + try { + const { connection } = await getDbConnection(); + + // Build WHERE clause with additional filters + let whereClause = ` + WHERE ( + p.description LIKE ? OR + p.itemnumber LIKE ? OR + p.upc LIKE ? OR + pc1.name LIKE ? OR + s.companyname LIKE ? + )`; + + // Add company filter if provided + if (company) { + whereClause += ` AND p.company = ${connection.escape(company)}`; + } + + // Add date range filter if provided + if (dateRange) { + let dateCondition; + const now = new Date(); + + switch(dateRange) { + case '1week': + // Last week: date is after (current date - 7 days) + const weekAgo = new Date(now); + weekAgo.setDate(now.getDate() - 7); + dateCondition = `p.datein >= ${connection.escape(weekAgo.toISOString().slice(0, 10))}`; + break; + case '1month': + // Last month: date is after (current date - 30 days) + const monthAgo = new Date(now); + monthAgo.setDate(now.getDate() - 30); + dateCondition = `p.datein >= ${connection.escape(monthAgo.toISOString().slice(0, 10))}`; + break; + case '2months': + // Last 2 months: date is after (current date - 60 days) + const twoMonthsAgo = new Date(now); + twoMonthsAgo.setDate(now.getDate() - 60); + dateCondition = `p.datein >= ${connection.escape(twoMonthsAgo.toISOString().slice(0, 10))}`; + break; + case '3months': + // Last 3 months: date is after (current date - 90 days) + const threeMonthsAgo = new Date(now); + threeMonthsAgo.setDate(now.getDate() - 90); + dateCondition = `p.datein >= ${connection.escape(threeMonthsAgo.toISOString().slice(0, 10))}`; + break; + case '6months': + // Last 6 months: date is after (current date - 180 days) + const sixMonthsAgo = new Date(now); + sixMonthsAgo.setDate(now.getDate() - 180); + dateCondition = `p.datein >= ${connection.escape(sixMonthsAgo.toISOString().slice(0, 10))}`; + break; + case '1year': + // Last year: date is after (current date - 365 days) + const yearAgo = new Date(now); + yearAgo.setDate(now.getDate() - 365); + dateCondition = `p.datein >= ${connection.escape(yearAgo.toISOString().slice(0, 10))}`; + break; + default: + // If an unrecognized value is provided, don't add a date condition + dateCondition = null; + } + + if (dateCondition) { + whereClause += ` AND ${dateCondition}`; + } + } + + // Special case for wildcard search + const isWildcardSearch = q === '*'; + const searchPattern = isWildcardSearch ? '%' : `%${q}%`; + const exactPattern = isWildcardSearch ? '%' : q; + + // Search for products based on various fields + const query = ` + SELECT + p.pid, + p.description AS title, + p.notes AS description, + p.itemnumber AS sku, + p.upc AS barcode, + p.harmonized_tariff_code, + pcp.price_each AS price, + p.sellingprice AS regular_price, + CASE + WHEN EXISTS (SELECT 1 FROM product_inventory WHERE pid = p.pid AND count > 0) + THEN (SELECT ROUND(AVG(costeach), 5) FROM product_inventory WHERE pid = p.pid AND count > 0) + ELSE (SELECT costeach FROM product_inventory WHERE pid = p.pid ORDER BY daterec DESC LIMIT 1) + END AS cost_price, + s.companyname AS vendor, + sid.supplier_itemnumber AS vendor_reference, + sid.notions_itemnumber AS notions_reference, + pc1.name AS brand, + p.company AS brand_id, + pc2.name AS line, + p.line AS line_id, + pc3.name AS subline, + p.subline AS subline_id, + pc4.name AS artist, + p.artist AS artist_id, + COALESCE(CASE + WHEN sid.supplier_id = 92 THEN sid.notions_qty_per_unit + ELSE sid.supplier_qty_per_unit + END, sid.notions_qty_per_unit) AS moq, + p.weight, + p.length, + p.width, + p.height, + p.country_of_origin, + p.totalsold AS total_sold, + p.datein AS first_received, + pls.date_sold AS date_last_sold + FROM products p + LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1 + LEFT JOIN supplier_item_data sid ON p.pid = sid.pid + LEFT JOIN suppliers s ON sid.supplier_id = s.supplierid + LEFT JOIN product_categories pc1 ON p.company = pc1.cat_id + LEFT JOIN product_categories pc2 ON p.line = pc2.cat_id + LEFT JOIN product_categories pc3 ON p.subline = pc3.cat_id + LEFT JOIN product_categories pc4 ON p.artist = pc4.cat_id + LEFT JOIN product_last_sold pls ON p.pid = pls.pid + ${whereClause} + GROUP BY p.pid + ${isWildcardSearch ? 'ORDER BY p.datein DESC' : ` + ORDER BY + CASE + WHEN p.description LIKE ? THEN 1 + WHEN p.itemnumber = ? THEN 2 + WHEN p.upc = ? THEN 3 + WHEN pc1.name LIKE ? THEN 4 + WHEN s.companyname LIKE ? THEN 5 + ELSE 6 + END + `} + `; + + // Prepare query parameters based on whether it's a wildcard search + let queryParams; + if (isWildcardSearch) { + queryParams = [ + searchPattern, // LIKE for description + searchPattern, // LIKE for itemnumber + searchPattern, // LIKE for upc + searchPattern, // LIKE for brand name + searchPattern // LIKE for company name + ]; + } else { + queryParams = [ + searchPattern, // LIKE for description + searchPattern, // LIKE for itemnumber + searchPattern, // LIKE for upc + searchPattern, // LIKE for brand name + searchPattern, // LIKE for company name + // For ORDER BY clause + searchPattern, // LIKE for description + exactPattern, // Exact match for itemnumber + exactPattern, // Exact match for upc + searchPattern, // LIKE for brand name + searchPattern // LIKE for company name + ]; + } + + const [results] = await connection.query(query, queryParams); + + res.json(results); + } catch (error) { + console.error('Error searching products:', error); + res.status(500).json({ error: 'Failed to search products' }); + } +}); + +// Get product categories for a specific product +router.get('/product-categories/:pid', async (req, res) => { + try { + const { pid } = req.params; + + if (!pid || isNaN(parseInt(pid))) { + return res.status(400).json({ error: 'Valid product ID is required' }); + } + + // Use the getDbConnection function instead of getPool + const { connection } = await getDbConnection(); + + // Query to get categories for a specific product + const query = ` + SELECT pc.cat_id, pc.name, pc.type, pc.combined_name + FROM product_category_index pci + JOIN product_categories pc ON pci.cat_id = pc.cat_id + WHERE pci.pid = ? + ORDER BY pc.type, pc.name + `; + + const [rows] = await connection.query(query, [pid]); + + // Add debugging to log category types + const categoryTypes = rows.map(row => row.type); + const uniqueTypes = [...new Set(categoryTypes)]; + console.log(`Product ${pid} has ${rows.length} categories with types: ${uniqueTypes.join(', ')}`); + console.log('Categories:', rows.map(row => ({ id: row.cat_id, name: row.name, type: row.type }))); + + // Format the response to match the expected format in the frontend + const categories = rows.map(category => ({ + value: category.cat_id.toString(), + label: category.name, + type: category.type, + combined_name: category.combined_name + })); + + res.json(categories); + } catch (error) { + console.error('Error fetching product categories:', error); + res.status(500).json({ + error: 'Failed to fetch product categories', + details: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/inventory/package-lock.json b/inventory/package-lock.json index 7c678c6..b10f026 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -42,7 +42,7 @@ "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.6", @@ -57,6 +57,7 @@ "@tanstack/virtual-core": "^3.11.2", "@types/js-levenshtein": "^1.1.3", "@types/uuid": "^10.0.0", + "axios": "^1.8.1", "chakra-react-select": "^4.7.5", "chakra-ui-steps": "^2.0.4", "chart.js": "^4.4.7", @@ -5316,6 +5317,12 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/attr-accept": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", @@ -5363,6 +5370,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -5507,6 +5525,19 @@ "ieee754": "^1.2.1" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6185,6 +6216,18 @@ "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -6491,6 +6534,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -6528,6 +6580,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6556,6 +6622,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -7041,6 +7152,26 @@ "node": ">=10" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -7069,6 +7200,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -7193,6 +7339,30 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -7202,6 +7372,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7283,6 +7466,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -7306,6 +7501,33 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7770,6 +7992,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -7804,6 +8035,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -8474,6 +8726,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/inventory/package.json b/inventory/package.json index 822a045..d4813cf 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -44,7 +44,7 @@ "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.6", @@ -59,6 +59,7 @@ "@tanstack/virtual-core": "^3.11.2", "@types/js-levenshtein": "^1.1.3", "@types/uuid": "^10.0.0", + "axios": "^1.8.1", "chakra-react-select": "^4.7.5", "chakra-ui-steps": "^2.0.4", "chart.js": "^4.4.7", diff --git a/inventory/src/components/products/ProductSearchDialog.tsx b/inventory/src/components/products/ProductSearchDialog.tsx new file mode 100644 index 0000000..6ae7f68 --- /dev/null +++ b/inventory/src/components/products/ProductSearchDialog.tsx @@ -0,0 +1,1569 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Loader2, Search, ChevronsUpDown, Check, ChevronLeft, ChevronRight, MoreHorizontal, X, ChevronUp, ChevronDown } from 'lucide-react'; +import { toast } from 'sonner'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; +import { cn } from '@/lib/utils'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { format } from 'date-fns'; +import { Calendar } from '@/components/ui/calendar'; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Badge } from "@/components/ui/badge"; + +interface ProductSearchDialogProps { + isOpen: boolean; + onClose: () => void; + onTemplateCreated: () => void; +} + +interface Product { + pid: number; + title: string; + description: string; + sku: string; + barcode: string; + harmonized_tariff_code: string; + price: number; + regular_price: number; + cost_price: number; + vendor: string; + vendor_reference: string; + notions_reference: string; + brand: string; + brand_id: string; + line: string; + line_id: string; + subline: string; + subline_id: string; + artist: string; + artist_id: string; + moq: number; + weight: number; + length: number; + width: number; + height: number; + country_of_origin: string; + total_sold: number; + first_received: string | null; + date_last_sold: string | null; +} + +interface FieldOption { + label: string; + value: string; + type?: number; + level?: number; + hexColor?: string; +} + +interface FieldOptions { + companies: FieldOption[]; + artists: FieldOption[]; + sizes: FieldOption[]; + themes: FieldOption[]; + categories: FieldOption[]; + colors: FieldOption[]; + suppliers: FieldOption[]; + taxCategories: FieldOption[]; + shippingRestrictions: FieldOption[]; +} + +interface TemplateFormData { + company: string; + product_type: string; + supplier?: string; + msrp?: number; + cost_each?: number; + qty_per_unit?: number; + case_qty?: number; + hts_code?: string; + description?: string; + weight?: number; + length?: number; + width?: number; + height?: number; + tax_cat?: string; + size_cat?: string; + categories?: string[]; + ship_restrictions?: string; +} + +// Add sorting types +type SortDirection = 'asc' | 'desc' | null; +type SortField = 'title' | 'brand' | 'line' | 'price' | 'total_sold' | 'first_received' | 'date_last_sold' | null; + +export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: ProductSearchDialogProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [step, setStep] = useState<'search' | 'form'>('search'); + const [formData, setFormData] = useState({ + company: '', + product_type: '', + }); + const [fieldOptions, setFieldOptions] = useState(null); + const [productLines, setProductLines] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const productsPerPage = 500; + + // Filter states + const [companyFilter, setCompanyFilter] = useState('all'); + const [dateInFilter, setDateInFilter] = useState('none'); + + // Sorting states + const [sortField, setSortField] = useState(null); + const [sortDirection, setSortDirection] = useState(null); + + // Reset all search state when dialog is closed + useEffect(() => { + if (!isOpen) { + // Reset search state + setSearchTerm(''); + setSearchResults([]); + setCompanyFilter('all'); + setDateInFilter('none'); + setSortField(null); + setSortDirection(null); + setCurrentPage(1); + setStep('search'); + } + }, [isOpen]); + + // Fetch field options when component mounts + useEffect(() => { + if (isOpen) { + fetchFieldOptions(); + // Perform initial search if any filters are already applied + if (companyFilter !== 'all' || (dateInFilter && dateInFilter !== 'none')) { + performSearch(); + } + } + }, [isOpen]); + + // Reset pagination when filters change and trigger search + useEffect(() => { + setCurrentPage(1); + + // Trigger search when company filter changes + if (companyFilter !== 'all' || (dateInFilter && dateInFilter !== 'none')) { + performSearch(); + } + }, [companyFilter, dateInFilter]); + + // Fetch product lines when company changes + useEffect(() => { + if (formData.company) { + fetchProductLines(formData.company); + } + }, [formData.company]); + + const fetchFieldOptions = async () => { + try { + const response = await axios.get('/api/import/field-options'); + setFieldOptions(response.data); + } catch (error) { + console.error('Error fetching field options:', error); + toast.error('Failed to fetch field options', { + description: 'Could not retrieve field options from the server' + }); + } + }; + + const fetchProductLines = async (companyId: string) => { + try { + const response = await axios.get(`/api/import/product-lines/${companyId}`); + setProductLines(response.data); + } catch (error) { + console.error('Error fetching product lines:', error); + setProductLines([]); + } + }; + + // Common search function for all search scenarios + const performSearch = async () => { + setIsLoading(true); + + try { + // Prepare query parameters + const params: Record = {}; + + // Always include a search parameter - necessary for the API + // If there's a search term, use it, otherwise use a wildcard/empty search + params.q = searchTerm.trim() || "*"; + + // Add company filter if selected + if (companyFilter && companyFilter !== 'all') { + params.company = companyFilter; + } + + // Add date range filter if selected + if (dateInFilter && dateInFilter !== 'none') { + params.dateRange = dateInFilter; + } + + const response = await axios.get('/api/import/search-products', { params }); + + const parsedResults = response.data.map((product: Product) => { + const parsedProduct = { + ...product, + // Ensure pid is a number + pid: typeof product.pid === 'string' ? parseInt(product.pid, 10) : product.pid, + // Convert string numeric values to actual numbers + price: typeof product.price === 'string' ? parseFloat(product.price) : product.price, + regular_price: typeof product.regular_price === 'string' ? parseFloat(product.regular_price) : product.regular_price, + cost_price: product.cost_price !== null && typeof product.cost_price === 'string' ? parseFloat(product.cost_price) : product.cost_price, + brand_id: product.brand_id !== null ? String(product.brand_id) : '', + line_id: product.line_id !== null ? String(product.line_id) : '0', + subline_id: product.subline_id !== null ? String(product.subline_id) : '0', + artist_id: product.artist_id !== null ? String(product.artist_id) : '0', + moq: typeof product.moq === 'string' ? parseInt(product.moq, 10) : product.moq, + weight: typeof product.weight === 'string' ? parseFloat(product.weight) : product.weight, + length: typeof product.length === 'string' ? parseFloat(product.length) : product.length, + width: typeof product.width === 'string' ? parseFloat(product.width) : product.width, + height: typeof product.height === 'string' ? parseFloat(product.height) : product.height, + total_sold: typeof product.total_sold === 'string' ? parseInt(product.total_sold, 10) : product.total_sold + }; + + return parsedProduct; + }); + + setSearchResults(parsedResults); + } catch (error) { + console.error('Error searching products:', error); + toast.error('Failed to search products', { + description: 'Could not retrieve search results from the server' + }); + setSearchResults([]); + } finally { + setIsLoading(false); + } + }; + + const handleSearch = () => { + performSearch(); + }; + + const handleCompanyFilterChange = (value: string) => { + setCompanyFilter(value); + // The useEffect will handle the search + }; + + const handleDateFilterChange = (value: string) => { + setDateInFilter(value); + // The useEffect will handle the search + }; + + const handleProductSelect = (product: Product) => { + console.log('Selected product supplier data:', { + vendor: product.vendor, + vendor_reference: product.vendor_reference + }); + + // Ensure all values are of the correct type + setFormData({ + company: product.brand_id ? String(product.brand_id) : '', + product_type: '', + // For supplier, we need to find the supplier ID by matching the vendor name + // vendor_reference is NOT the supplier ID, it's the supplier's product identifier + supplier: undefined, // We'll set this below if we can find a match + msrp: typeof product.regular_price === 'number' ? product.regular_price : + typeof product.regular_price === 'string' ? parseFloat(product.regular_price) : undefined, + cost_each: typeof product.cost_price === 'number' ? product.cost_price : + typeof product.cost_price === 'string' ? parseFloat(product.cost_price) : undefined, + qty_per_unit: typeof product.moq === 'number' ? product.moq : + typeof product.moq === 'string' ? parseInt(product.moq, 10) : undefined, + hts_code: product.harmonized_tariff_code || undefined, + description: product.description || undefined, + weight: typeof product.weight === 'number' ? product.weight : + typeof product.weight === 'string' ? parseFloat(product.weight) : undefined, + length: typeof product.length === 'number' ? product.length : + typeof product.length === 'string' ? parseFloat(product.length) : undefined, + width: typeof product.width === 'number' ? product.width : + typeof product.width === 'string' ? parseFloat(product.width) : undefined, + height: typeof product.height === 'number' ? product.height : + typeof product.height === 'string' ? parseFloat(product.height) : undefined, + categories: [], + ship_restrictions: undefined + }); + + // Try to find the supplier ID from the vendor name + if (product.vendor && fieldOptions) { + console.log('Available suppliers:', fieldOptions.suppliers); + console.log('Looking for supplier match for vendor:', product.vendor); + + // First try exact match + let supplierOption = fieldOptions.suppliers.find( + supplier => supplier.label.toLowerCase() === product.vendor.toLowerCase() + ); + + // If no exact match, try partial match + if (!supplierOption) { + console.log('No exact match found, trying partial match'); + supplierOption = fieldOptions.suppliers.find( + supplier => supplier.label.toLowerCase().includes(product.vendor.toLowerCase()) || + product.vendor.toLowerCase().includes(supplier.label.toLowerCase()) + ); + } + + if (supplierOption) { + setFormData(prev => ({ + ...prev, + supplier: supplierOption.value + })); + console.log('Found supplier match:', { + vendorName: product.vendor, + matchedSupplier: supplierOption.label, + supplierId: supplierOption.value + }); + } else { + console.log('No supplier match found for vendor:', product.vendor); + } + } + + // Fetch product categories + if (product.pid) { + console.log('Fetching categories for product ID:', product.pid); + fetchProductCategories(product.pid); + } + + setStep('form'); + }; + + // Add a function to fetch product categories + const fetchProductCategories = async (productId: number) => { + try { + const response = await axios.get(`/api/import/product-categories/${productId}`); + console.log('Product categories:', response.data); + + if (response.data && Array.isArray(response.data)) { + // Filter out categories with type 20 (themes) and type 21 (subthemes) + const filteredCategories = response.data.filter((category: any) => + category.type !== 20 && category.type !== 21 + ); + + console.log('Filtered categories (excluding themes):', filteredCategories); + + // Extract category IDs and update form data + const categoryIds = filteredCategories.map((category: any) => category.value); + setFormData(prev => ({ + ...prev, + categories: categoryIds + })); + } + } catch (error) { + console.error('Error fetching product categories:', error); + toast.error('Failed to fetch product categories', { + description: 'Could not retrieve categories for this product' + }); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleTextAreaChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleMultiSelectChange = (name: string, value: string) => { + setFormData(prev => { + const currentValues = prev[name as keyof typeof prev] as string[] || []; + const valueSet = new Set(currentValues); + + if (valueSet.has(value)) { + valueSet.delete(value); + } else { + valueSet.add(value); + } + + return { + ...prev, + [name]: Array.from(valueSet), + }; + }); + }; + + const handleSelectChange = (name: string, value: string | string[]) => { + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleNumberInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + const numValue = value === '' ? undefined : parseFloat(value); + setFormData(prev => ({ ...prev, [name]: numValue })); + }; + + const handleSubmit = async () => { + // Validate required fields + if (!formData.company || !formData.product_type) { + toast.error('Validation Error', { + description: 'Company and Product Type are required' + }); + return; + } + + // Log supplier information for debugging + if (formData.supplier) { + const supplierOption = fieldOptions?.suppliers.find( + supplier => supplier.value === formData.supplier + ); + console.log('Submitting supplier:', { + id: formData.supplier, + name: supplierOption?.label || 'Unknown', + allSuppliers: fieldOptions?.suppliers.map(s => ({ id: s.value, name: s.label })) + }); + } else { + console.log('No supplier selected for submission'); + } + + // Log categories information for debugging + if (formData.categories && formData.categories.length > 0) { + const categoryOptions = formData.categories.map(catId => { + const category = fieldOptions?.categories.find(c => c.value === catId); + return { + id: catId, + name: category?.label || 'Unknown Category' + }; + }); + console.log('Submitting categories:', categoryOptions); + } else { + console.log('No categories selected for submission'); + } + + setIsSubmitting(true); + try { + // Prepare the data to be sent - ensure all fields are properly formatted + const dataToSend = { + company: formData.company, + product_type: formData.product_type, + supplier: formData.supplier || null, + msrp: formData.msrp !== undefined ? Number(formData.msrp) : null, + cost_each: formData.cost_each !== undefined ? Number(formData.cost_each) : null, + qty_per_unit: formData.qty_per_unit !== undefined ? Number(formData.qty_per_unit) : null, + case_qty: formData.case_qty !== undefined ? Number(formData.case_qty) : null, + hts_code: formData.hts_code || null, + description: formData.description || null, + weight: formData.weight !== undefined ? Number(formData.weight) : null, + length: formData.length !== undefined ? Number(formData.length) : null, + width: formData.width !== undefined ? Number(formData.width) : null, + height: formData.height !== undefined ? Number(formData.height) : null, + tax_cat: formData.tax_cat || null, + size_cat: formData.size_cat || null, + categories: formData.categories || [], + ship_restrictions: formData.ship_restrictions || null + }; + + console.log('Sending template data:', dataToSend); + + const response = await axios.post('/api/templates', dataToSend); + + console.log('Template creation response:', response); + + if (response.status >= 200 && response.status < 300) { + toast.success('Template created successfully'); + onTemplateCreated(); + onClose(); + } else { + throw new Error(`Server responded with status: ${response.status}`); + } + } catch (error) { + console.error('Error creating template:', error); + let errorMessage = 'Failed to create template'; + + if (axios.isAxiosError(error)) { + console.error('Axios error details:', { + status: error.response?.status, + data: error.response?.data, + headers: error.response?.headers + }); + + if (error.response?.data?.message) { + errorMessage = error.response.data.message; + } else if (error.response?.data?.error) { + errorMessage = error.response.data.error; + } else if (error.message) { + errorMessage = error.message; + } + } + + toast.error('Template Creation Failed', { + description: errorMessage + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + if (step === 'form') { + setStep('search'); + } else { + // Reset form data when closing + setFormData({ + company: '', + product_type: '', + }); + onClose(); + } + }; + + // Get current products for pagination + const indexOfLastProduct = currentPage * productsPerPage; + const indexOfFirstProduct = indexOfLastProduct - productsPerPage; + const currentProducts = searchResults.slice(indexOfFirstProduct, indexOfLastProduct); + const totalPages = Math.ceil(searchResults.length / productsPerPage); + + // Change page + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + + // Generate page numbers for pagination + const getPageNumbers = () => { + const pageNumbers = []; + const maxPagesToShow = 5; + + if (totalPages <= maxPagesToShow) { + // If we have fewer pages than the max to show, display all pages + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + // Always include first page + pageNumbers.push(1); + + // Calculate start and end of middle pages + let startPage = Math.max(2, currentPage - 1); + let endPage = Math.min(totalPages - 1, currentPage + 1); + + // Adjust if we're near the beginning + if (currentPage <= 3) { + endPage = Math.min(totalPages - 1, 4); + } + + // Adjust if we're near the end + if (currentPage >= totalPages - 2) { + startPage = Math.max(2, totalPages - 3); + } + + // Add ellipsis after first page if needed + if (startPage > 2) { + pageNumbers.push('ellipsis-start'); + } + + // Add middle pages + for (let i = startPage; i <= endPage; i++) { + pageNumbers.push(i); + } + + // Add ellipsis before last page if needed + if (endPage < totalPages - 1) { + pageNumbers.push('ellipsis-end'); + } + + // Always include last page + pageNumbers.push(totalPages); + } + + return pageNumbers; + }; + + // Sort function + const toggleSort = (field: SortField) => { + if (sortField === field) { + // Toggle direction if already sorting by this field + if (sortDirection === 'asc') { + setSortDirection('desc'); + } else if (sortDirection === 'desc') { + setSortField(null); + setSortDirection(null); + } + } else { + // Set new sort field and direction + setSortField(field); + setSortDirection('asc'); + } + + // Reset to page 1 when sorting changes + setCurrentPage(1); + }; + + // Apply sorting to results + const getSortedResults = (results: Product[]) => { + if (!sortField || !sortDirection) return results; + + return [...results].sort((a, b) => { + let valueA: any; + let valueB: any; + + // Extract the correct field values + switch (sortField) { + case 'title': + valueA = a.title?.toLowerCase() || ''; + valueB = b.title?.toLowerCase() || ''; + break; + case 'brand': + valueA = a.brand?.toLowerCase() || ''; + valueB = b.brand?.toLowerCase() || ''; + break; + case 'line': + valueA = a.line?.toLowerCase() || ''; + valueB = b.line?.toLowerCase() || ''; + break; + case 'price': + valueA = typeof a.price === 'number' ? a.price : parseFloat(String(a.price) || '0'); + valueB = typeof b.price === 'number' ? b.price : parseFloat(String(b.price) || '0'); + break; + case 'total_sold': + valueA = typeof a.total_sold === 'number' ? a.total_sold : parseInt(String(a.total_sold) || '0', 10); + valueB = typeof b.total_sold === 'number' ? b.total_sold : parseInt(String(b.total_sold) || '0', 10); + break; + case 'first_received': + valueA = a.first_received ? new Date(a.first_received).getTime() : 0; + valueB = b.first_received ? new Date(b.first_received).getTime() : 0; + break; + case 'date_last_sold': + valueA = a.date_last_sold ? new Date(a.date_last_sold).getTime() : 0; + valueB = b.date_last_sold ? new Date(b.date_last_sold).getTime() : 0; + break; + default: + return 0; + } + + // Compare the values + if (valueA < valueB) { + return sortDirection === 'asc' ? -1 : 1; + } + if (valueA > valueB) { + return sortDirection === 'asc' ? 1 : -1; + } + return 0; + }); + }; + + // Apply filters to search results + const getFilteredResults = () => { + if (!searchResults) return []; + + return searchResults.filter(product => { + // Apply company filter if set + if (companyFilter && companyFilter !== "all" && product.brand_id !== companyFilter) { + return false; + } + + // The date filtering is now handled on the server side with the dateRange parameter + // No need to filter dates on the client side anymore + + return true; + }); + }; + + const filteredResults = getFilteredResults(); + const sortedResults = getSortedResults(filteredResults); + + // Get current products for pagination + const indexOfLastProductFiltered = currentPage * productsPerPage; + const indexOfFirstProductFiltered = indexOfLastProductFiltered - productsPerPage; + const currentProductsFiltered = sortedResults.slice(indexOfFirstProductFiltered, indexOfLastProductFiltered); + const totalPagesFiltered = Math.ceil(sortedResults.length / productsPerPage); + + const clearFilters = () => { + setCompanyFilter('all'); + setDateInFilter('none'); + + // If search term is empty, clear results completely + if (!searchTerm.trim()) { + setSearchResults([]); + } else { + // Otherwise, perform a search with just the search term + performSearch(); + } + }; + + // Get sort icon + const getSortIcon = (field: SortField) => { + if (sortField !== field) { + return ; + } + + return sortDirection === 'asc' + ? + : ; + }; + + // Sortable table header component + const SortableTableHead = ({ field, children }: { field: SortField, children: React.ReactNode }) => ( + toggleSort(field)} + > +
+ {children} + {getSortIcon(field)} +
+
+ ); + + // Render search step + const renderSearchStep = () => ( + <> + + Search Products + + Search for a product you want to use as a template. + + + +
+
+
+
+ { + // Just update the search term without triggering search + setSearchTerm(e.target.value); + }} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="pr-8" + /> + {searchTerm && ( + + )} +
+ +
+ + {fieldOptions ? ( +
+
+ + +
+ +
+ + +
+
+ ) : ( +
Loading filter options...
+ )} + + {fieldOptions && ((companyFilter && companyFilter !== 'all') || dateInFilter !== 'none') && ( +
+
Active filters:
+
+ {companyFilter && companyFilter !== "all" && ( + + Company: {fieldOptions?.companies?.find(c => c.value === companyFilter)?.label || 'Unknown'} + + + )} + {dateInFilter && dateInFilter !== 'none' && ( + + Date: {(() => { + switch(dateInFilter) { + case '1week': return 'Last week'; + case '1month': return 'Last month'; + case '2months': return 'Last 2 months'; + case '3months': return 'Last 3 months'; + case '6months': return 'Last 6 months'; + case '1year': return 'Last year'; + default: return 'Custom range'; + } + })()} + + + )} + +
+
+ )} +
+ +
+ + {isLoading ? ( +
+ Searching... +
+ ) : searchResults.length === 0 ? ( +
+ {companyFilter !== 'all' || dateInFilter !== 'none' || searchTerm.trim() + ? 'No products found matching your criteria' + : 'Use the search field or filters to find products'} +
+ ) : ( + <> +
+ {sortedResults.length} products found +
+
+ + + + Name + {(!companyFilter || companyFilter === "all") && ( + Company + )} + Line + Price + Total Sold + Date In + Last Sold + + + + {currentProductsFiltered.map((product) => ( + handleProductSelect(product)} + > + {product.title} + {(!companyFilter || companyFilter === "all") && ( + {product.brand || '-'} + )} + {product.line || '-'} + + {product.price !== null && product.price !== undefined + ? `$${typeof product.price === 'number' + ? product.price.toFixed(2) + : parseFloat(String(product.price)).toFixed(2)}` + : '-'} + + + {product.total_sold !== null && product.total_sold !== undefined + ? typeof product.total_sold === 'number' + ? product.total_sold + : parseInt(String(product.total_sold), 10) + : 0} + + + {product.first_received + ? (() => { + try { + return new Date(product.first_received).toLocaleDateString(); + } catch (e) { + console.error('Error formatting first_received date:', e); + return '-'; + } + })() + : '-'} + + + {product.date_last_sold + ? (() => { + try { + return new Date(product.date_last_sold).toLocaleDateString(); + } catch (e) { + console.error('Error formatting date_last_sold date:', e); + return '-'; + } + })() + : '-'} + + + ))} + +
+
+ + {totalPagesFiltered > 1 && ( +
+ + + + currentPage > 1 && paginate(currentPage - 1)} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {getPageNumbers().map((page, index) => ( + + {page === 'ellipsis-start' || page === 'ellipsis-end' ? ( + + ) : ( + typeof page === 'number' && paginate(page)} + className={typeof page === 'number' ? "cursor-pointer" : ""} + > + {page} + + )} + + ))} + + + currentPage < totalPagesFiltered && paginate(currentPage + 1)} + className={currentPage === totalPagesFiltered ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} + + )} +
+
+
+ + + + + + ); + + const renderFormStep = () => { + if (!fieldOptions) return
Loading...
; + + // Helper function to sort options with selected value at top + const getSortedOptions = (options: FieldOption[], selectedValue?: string | string[]) => { + return [...options].sort((a, b) => { + if (Array.isArray(selectedValue)) { + const aSelected = selectedValue.includes(a.value); + const bSelected = selectedValue.includes(b.value); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + } + if (a.value === selectedValue) return -1; + if (b.value === selectedValue) return 1; + return 0; + }); + }; + + // Helper function to get selected category labels + const getSelectedCategoryLabels = () => { + if (!formData.categories?.length) return ""; + const labels = formData.categories + .map(value => fieldOptions.categories.find(cat => cat.value === value)?.label) + .filter(Boolean); + return labels.join(", "); + }; + + // Sort categories with selected ones first and respect levels + const getSortedCategories = () => { + const selected = new Set(formData.categories || []); + + // Filter categories to only include types 10, 11, 12, and 13 (proper categories, not themes) + const validCategoryTypes = [10, 11, 12, 13]; + const filteredCategories = fieldOptions.categories.filter( + category => category.type && validCategoryTypes.includes(category.type) + ); + + console.log('Filtered categories for dropdown:', filteredCategories.length); + + return [...filteredCategories].sort((a, b) => { + const aSelected = selected.has(a.value); + const bSelected = selected.has(b.value); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + }; + + return ( + <> + + Create Template from Product + + Create a new template for importing products. Company and Product Type combination must be unique. + + + +
{ e.preventDefault(); handleSubmit(); }} className="flex flex-col flex-1 overflow-hidden"> + +
+
+
+ + + + + + + + + { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }}> + No companies found. + + {getSortedOptions(fieldOptions.companies, formData.company).map((company) => ( + handleSelectChange('company', company.value)} + > + + {company.label} + + ))} + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }}> + No suppliers found. + + {getSortedOptions(fieldOptions.suppliers, formData.supplier).map((supplier) => ( + { + // Make sure we're setting the ID (value), not the label + handleSelectChange('supplier', supplier.value); + console.log('Selected supplier from dropdown:', { + label: supplier.label, + value: supplier.value + }); + }} + > + + {supplier.label} + + ))} + + + + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + + + + + + + + { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }}> + No restrictions found. + + {getSortedOptions(fieldOptions.shippingRestrictions, formData.ship_restrictions).map((restriction) => ( + handleSelectChange('ship_restrictions', restriction.value)} + > + + {restriction.label} + + ))} + + + + + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + + + + + + + + { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }}> + No tax categories found. + + {getSortedOptions(fieldOptions.taxCategories, formData.tax_cat).map((category) => ( + handleSelectChange('tax_cat', category.value)} + > + + {category.label} + + ))} + + + + + +
+
+ + + + + + + + + { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }}> + No sizes found. + + {getSortedOptions(fieldOptions.sizes, formData.size_cat).map((size) => ( + handleSelectChange('size_cat', size.value)} + > + + {size.label} + + ))} + + + + + +
+
+ +
+ + + + + + + + + { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }}> + No categories found. + + {getSortedCategories().map((category) => ( + handleMultiSelectChange('categories', category.value)} + > +
+ + + {category.label} + +
+
+ ))} +
+
+
+
+
+
+ +
+ +