diff --git a/inventory-server/package-lock.json b/inventory-server/package-lock.json index a0e42a4..f71d63b 100755 --- a/inventory-server/package-lock.json +++ b/inventory-server/package-lock.json @@ -15,6 +15,7 @@ "express": "^4.18.2", "multer": "^1.4.5-lts.1", "mysql2": "^3.12.0", + "openai": "^4.85.3", "pm2": "^5.3.0", "ssh2": "^1.16.0", "uuid": "^9.0.1" @@ -100,6 +101,27 @@ "node": ">=10" } }, + "node_modules/@pm2/agent/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@pm2/io": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz", @@ -238,6 +260,27 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/@pm2/js-api/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@pm2/pm2-version-check": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", @@ -276,6 +319,37 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "18.19.76", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", + "integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -298,6 +372,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -401,6 +487,12 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "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/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -648,6 +740,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "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": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -801,6 +905,15 @@ "node": ">= 14" } }, + "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/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -912,6 +1025,21 @@ "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/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -991,6 +1119,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", @@ -1114,6 +1251,40 @@ } } }, + "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/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1295,6 +1466,21 @@ "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", @@ -1395,6 +1581,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1857,6 +2052,45 @@ "node": ">= 0.4.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", @@ -1995,6 +2229,36 @@ "node": ">= 0.8" } }, + "node_modules/openai": { + "version": "4.85.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.85.3.tgz", + "integrity": "sha512-KTMXAK6FPd2IvsPtglMt0J1GyVrjMxCYzu/mVbCPabzzquSJoZlYpHtE0p0ScZPyt11XTc757xSO4j39j5g+Xw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/pac-proxy-agent": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", @@ -2996,6 +3260,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -3062,6 +3332,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3132,17 +3408,44 @@ "lodash": "^4.17.14" } }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { diff --git a/inventory-server/package.json b/inventory-server/package.json index e776547..fb36c53 100755 --- a/inventory-server/package.json +++ b/inventory-server/package.json @@ -24,6 +24,7 @@ "express": "^4.18.2", "multer": "^1.4.5-lts.1", "mysql2": "^3.12.0", + "openai": "^4.85.3", "pm2": "^5.3.0", "ssh2": "^1.16.0", "uuid": "^9.0.1" diff --git a/inventory-server/src/routes/ai-validation.js b/inventory-server/src/routes/ai-validation.js new file mode 100644 index 0000000..d54afbd --- /dev/null +++ b/inventory-server/src/routes/ai-validation.js @@ -0,0 +1,75 @@ +const express = require('express'); +const router = express.Router(); +const OpenAI = require('openai'); + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY +}); + +// Helper function to create the prompt for product validation +function createValidationPrompt(products) { + return `You are a product data validation assistant. Please review the following product data and suggest corrections or improvements. Focus on: +1. Standardizing product names and descriptions +2. Fixing any obvious errors in measurements, prices, or quantities +3. Ensuring consistency in formatting +4. Flagging any suspicious or invalid values + +Here is the product data to validate: +${JSON.stringify(products, null, 2)} + +Please respond with: +1. The corrected product data in the exact same JSON format +2. A list of changes made and why +3. Any warnings or suggestions for manual review + +Respond in the following JSON format: +{ + "correctedData": [], // Array of corrected products + "changes": [], // Array of changes made + "warnings": [] // Array of warnings or suggestions +}`; +} + +router.post('/validate', async (req, res) => { + try { + const { products } = req.body; + + if (!Array.isArray(products)) { + return res.status(400).json({ error: 'Products must be an array' }); + } + + const prompt = createValidationPrompt(products); + + const completion = await openai.chat.completions.create({ + model: "gpt-4-turbo-preview", + messages: [ + { + role: "system", + content: "You are a product data validation assistant that helps ensure product data is accurate, consistent, and properly formatted." + }, + { + role: "user", + content: prompt + } + ], + temperature: 0.3, // Lower temperature for more consistent results + max_tokens: 4000, + response_format: { type: "json_object" } + }); + + const aiResponse = JSON.parse(completion.choices[0].message.content); + + res.json({ + success: true, + ...aiResponse + }); + } catch (error) { + console.error('AI Validation Error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Error during AI validation' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index 7c1f5ca..f167683 100755 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -18,6 +18,7 @@ const vendorsRouter = require('./routes/vendors'); const categoriesRouter = require('./routes/categories'); const testConnectionRouter = require('./routes/test-connection'); const importRouter = require('./routes/import'); +const aiValidationRouter = require('./routes/ai-validation'); // Get the absolute path to the .env file const envPath = path.resolve(process.cwd(), '.env'); @@ -93,6 +94,7 @@ async function startServer() { app.use('/api/vendors', vendorsRouter); app.use('/api/categories', categoriesRouter); app.use('/api/import', importRouter); + app.use('/api/ai-validation', aiValidationRouter); app.use('/api', testConnectionRouter); // Basic health check route diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index 826cd35..6bed3e4 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -15,7 +15,7 @@ import { RequireAuth } from './components/auth/RequireAuth'; import Forecasting from "@/pages/Forecasting"; import { Vendors } from '@/pages/Vendors'; import { Categories } from '@/pages/Categories'; -import { Import } from '@/pages/import/Import'; +import { Import } from '@/pages/Import'; import { ChakraProvider } from '@chakra-ui/react'; const queryClient = new QueryClient(); diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/UserTableColumn.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/UserTableColumn.tsx index 0d83635..b1236ae 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/UserTableColumn.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/UserTableColumn.tsx @@ -35,7 +35,7 @@ export const UserTableColumn = (props: UserTableColumnProps
{header}
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index d8cc2db..a0a9850 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -1,9 +1,9 @@ -import { useCallback, useMemo, useState, useEffect } from "react" +import { useCallback, useMemo, useState, useEffect, memo } from "react" import { useRsi } from "../../hooks/useRsi" import type { Meta } from "./types" import { addErrorsAndRunHooks } from "./utils/dataMutations" -import type { Data, Field, SelectOption, MultiInput } from "../../types" -import { Check, ChevronsUpDown, ArrowDown, AlertCircle } from "lucide-react" +import type { Data, SelectOption } from "../../types" +import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2 } from "lucide-react" import { cn } from "@/lib/utils" import { Command, @@ -56,12 +56,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" -import * as Select from "@radix-ui/react-select" -import { - Dialog, - DialogContent, - DialogTrigger, -} from "@/components/ui/dialog" +import config from "@/config" type Props = { initialData: (Data & Meta)[] @@ -69,19 +64,80 @@ type Props = { onBack?: () => void } -type CellProps = { - value: any, - onChange: (value: any) => void, - error?: { level: string, message: string }, - field: Field +type BaseFieldType = { + multiline?: boolean; + price?: boolean; } -const EditableCell = ({ value, onChange, error, field }: CellProps) => { +type InputFieldType = BaseFieldType & { + type: "input"; +} + +type MultiInputFieldType = BaseFieldType & { + type: "multi-input"; + separator?: string; +} + +type SelectFieldType = { + type: "select" | "multi-select"; + options: SelectOption[]; +} + +type CheckboxFieldType = { + type: "checkbox"; + booleanMatches?: { [key: string]: boolean }; +} + +type FieldType = InputFieldType | MultiInputFieldType | SelectFieldType | CheckboxFieldType; + +type Field = { + label: string; + key: T; + description?: string; + alternateMatches?: string[]; + validations?: ({ rule: string } & Record)[]; + fieldType: FieldType; + width?: number; + disabled?: boolean; + onChange?: (value: string) => void; +} + +type CellProps = { + value: any; + onChange: (value: any) => void; + error?: { level: string; message: string }; + field: Field; +} + +// Define ValidationIcon before EditableCell +const ValidationIcon = memo(({ error }: { error: { level: string, message: string } }) => ( + + + +
+ +
+
+ +

{error.message}

+
+
+
+)) + +// Wrap EditableCell with memo to avoid unnecessary re-renders +const EditableCell = memo(({ value, onChange, error, field }: CellProps) => { const [isEditing, setIsEditing] = useState(false) const [inputValue, setInputValue] = useState(value ?? "") const [searchQuery, setSearchQuery] = useState("") const [localValues, setLocalValues] = useState([]) + const handleWheel = useCallback((e: React.WheelEvent) => { + const commandList = e.currentTarget; + commandList.scrollTop += e.deltaY; + e.stopPropagation(); + }, []); + // Update input value when external value changes and we're not editing useEffect(() => { if (!isEditing) { @@ -97,6 +153,12 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { } }, [value, field.fieldType.type]) + const formatPrice = (value: string) => { + if (!value) return value + // Remove dollar signs and trim + return value.replace(/^\$/, '').trim() + } + const validateRegex = (val: any) => { // Handle non-string values if (val === undefined || val === null) return undefined @@ -115,8 +177,14 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { const regexValidation = field.validations?.find(v => v.rule === "regex") if (regexValidation) { + let testValue = strVal + // For price fields, remove dollar sign before testing + if (isPriceField(field.fieldType)) { + testValue = formatPrice(strVal) + } + const regex = new RegExp(regexValidation.value, regexValidation.flags) - if (!regex.test(strVal)) { + if (!regex.test(testValue)) { return { level: regexValidation.level || "error", message: regexValidation.errorMessage } } } @@ -134,10 +202,10 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { const currentError = getValidationError() const getDisplayValue = (value: any, fieldType: Field["fieldType"]) => { - if (fieldType.type === "select") { - return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value - } - if (fieldType.type === "multi-select") { + if (fieldType.type === "select" || fieldType.type === "multi-select") { + if (fieldType.type === "select") { + return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value + } if (Array.isArray(value)) { return value.map(v => fieldType.options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ") } @@ -160,6 +228,11 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { return true } + // Handle price fields + if (isPriceField(field.fieldType)) { + newValue = formatPrice(newValue) + } + // Always commit the value onChange(newValue) @@ -172,27 +245,6 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { setIsEditing(false) } - const ValidationIcon = ({ error }: { error: { level: string, message: string } }) => ( - - - -
- -
-
- -

{error.message}

-
-
-
- ) - - const handleWheel = (e: React.WheelEvent) => { - const commandList = e.currentTarget; - commandList.scrollTop += e.deltaY; - e.stopPropagation(); - }; - if (isEditing) { switch (field.fieldType.type) { case "select": @@ -353,8 +405,59 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { {currentError && } ) + case "input": case "multi-input": - default: + if (field.fieldType.multiline) { + return ( +
+ { + if (!open) { + validateAndCommit(inputValue) + setIsEditing(false) + } + }} + > + + + + +
+

{field.label}

+