Merge branch 'add-product-upload-page'

This commit is contained in:
2025-03-22 21:11:10 -04:00
158 changed files with 26432 additions and 1170 deletions

View File

@@ -0,0 +1,41 @@
# Details
Date : 2025-03-17 16:24:17
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 26 files, 6193 codes, 1008 comments, 1017 blanks, all 8218 lines
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
## Files
| filename | language | code | comment | blank | total |
| :--- | :--- | ---: | ---: | ---: | ---: |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md) | Markdown | 39 | 0 | 19 | 58 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx) | TypeScript JSX | 230 | 10 | 8 | 248 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx) | TypeScript JSX | 18 | 0 | 3 | 21 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx) | TypeScript JSX | 273 | 19 | 37 | 329 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 374 | 42 | 44 | 460 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 730 | 126 | 106 | 962 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 499 | 48 | 54 | 601 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx) | TypeScript JSX | 112 | 12 | 21 | 145 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 232 | 31 | 32 | 295 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 407 | 56 | 52 | 515 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx) | TypeScript JSX | 193 | 23 | 22 | 238 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | 289 | 36 | 31 | 356 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx) | TypeScript JSX | 500 | 75 | 89 | 664 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 248 | 69 | 74 | 391 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx) | TypeScript JSX | 204 | 26 | 33 | 263 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 209 | 49 | 50 | 308 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 219 | 39 | 47 | 305 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 1,060 | 228 | 229 | 1,517 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx) | TypeScript JSX | 20 | 6 | 2 | 28 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts) | TypeScript | 4 | 0 | 1 | 5 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts) | TypeScript | 16 | 4 | 4 | 24 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts) | TypeScript | 124 | 4 | 14 | 142 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts) | TypeScript | 21 | 15 | 5 | 41 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts) | TypeScript | 43 | 24 | 7 | 74 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js) | JavaScript | 28 | 7 | 9 | 44 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts) | TypeScript | 101 | 59 | 24 | 184 |
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,20 @@
# Diff Details
Date : 2025-03-17 16:24:17
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 5 files, -358 codes, -15 comments, -33 blanks, all -406 lines
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
## Files
| filename | language | code | comment | blank | total |
| :--- | :--- | ---: | ---: | ---: | ---: |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx) | TypeScript JSX | -83 | 0 | -4 | -87 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx) | TypeScript JSX | -193 | -4 | -15 | -212 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | -241 | -68 | -72 | -381 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx) | TypeScript JSX | -89 | -12 | -16 | -117 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 248 | 69 | 74 | 391 |
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details

View File

@@ -0,0 +1,23 @@
# Diff Summary
Date : 2025-03-17 16:24:17
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 5 files, -358 codes, -15 comments, -33 blanks, all -406 lines
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
## Languages
| language | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| TypeScript JSX | 5 | -358 | -15 | -33 | -406 |
## Directories
| path | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| . | 5 | -358 | -15 | -33 | -406 |
| components | 3 | -517 | -72 | -91 | -680 |
| hooks | 2 | 159 | 57 | 58 | 274 |
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,31 @@
Date : 2025-03-17 16:24:17
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 5 files, -358 codes, -15 comments, -33 blanks, all -406 lines
Languages
+----------------+------------+------------+------------+------------+------------+
| language | files | code | comment | blank | total |
+----------------+------------+------------+------------+------------+------------+
| TypeScript JSX | 5 | -358 | -15 | -33 | -406 |
+----------------+------------+------------+------------+------------+------------+
Directories
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| path | files | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| . | 5 | -358 | -15 | -33 | -406 |
| components | 3 | -517 | -72 | -91 | -680 |
| hooks | 2 | 159 | 57 | 58 | 274 |
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
Files
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| filename | language | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx | TypeScript JSX | -83 | 0 | -4 | -87 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx | TypeScript JSX | -193 | -4 | -15 | -212 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | -241 | -68 | -72 | -381 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx | TypeScript JSX | -89 | -12 | -16 | -117 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 248 | 69 | 74 | 391 |
| Total | | -358 | -15 | -33 | -406 |
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
# Summary
Date : 2025-03-17 16:24:17
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 26 files, 6193 codes, 1008 comments, 1017 blanks, all 8218 lines
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
## Languages
| language | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| TypeScript JSX | 18 | 5,817 | 895 | 934 | 7,646 |
| TypeScript | 6 | 309 | 106 | 55 | 470 |
| Markdown | 1 | 39 | 0 | 19 | 58 |
| JavaScript | 1 | 28 | 7 | 9 | 44 |
## Directories
| path | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| . | 26 | 6,193 | 1,008 | 1,017 | 8,218 |
| . (Files) | 3 | 63 | 6 | 22 | 91 |
| components | 11 | 3,357 | 403 | 410 | 4,170 |
| components (Files) | 6 | 2,124 | 245 | 252 | 2,621 |
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
| hooks | 6 | 2,440 | 486 | 522 | 3,448 |
| types | 1 | 16 | 4 | 4 | 24 |
| utils | 5 | 317 | 109 | 59 | 485 |
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,60 @@
Date : 2025-03-17 16:24:17
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 26 files, 6193 codes, 1008 comments, 1017 blanks, all 8218 lines
Languages
+----------------+------------+------------+------------+------------+------------+
| language | files | code | comment | blank | total |
+----------------+------------+------------+------------+------------+------------+
| TypeScript JSX | 18 | 5,817 | 895 | 934 | 7,646 |
| TypeScript | 6 | 309 | 106 | 55 | 470 |
| Markdown | 1 | 39 | 0 | 19 | 58 |
| JavaScript | 1 | 28 | 7 | 9 | 44 |
+----------------+------------+------------+------------+------------+------------+
Directories
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| path | files | code | comment | blank | total |
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| . | 26 | 6,193 | 1,008 | 1,017 | 8,218 |
| . (Files) | 3 | 63 | 6 | 22 | 91 |
| components | 11 | 3,357 | 403 | 410 | 4,170 |
| components (Files) | 6 | 2,124 | 245 | 252 | 2,621 |
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
| hooks | 6 | 2,440 | 486 | 522 | 3,448 |
| types | 1 | 16 | 4 | 4 | 24 |
| utils | 5 | 317 | 109 | 59 | 485 |
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
Files
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| filename | language | code | comment | blank | total |
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md | Markdown | 39 | 0 | 19 | 58 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx | TypeScript JSX | 230 | 10 | 8 | 248 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx | TypeScript JSX | 18 | 0 | 3 | 21 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx | TypeScript JSX | 273 | 19 | 37 | 329 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 374 | 42 | 44 | 460 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 730 | 126 | 106 | 962 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 499 | 48 | 54 | 601 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx | TypeScript JSX | 112 | 12 | 21 | 145 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 232 | 31 | 32 | 295 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 407 | 56 | 52 | 515 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx | TypeScript JSX | 193 | 23 | 22 | 238 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | 289 | 36 | 31 | 356 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx | TypeScript JSX | 500 | 75 | 89 | 664 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 248 | 69 | 74 | 391 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx | TypeScript JSX | 204 | 26 | 33 | 263 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 209 | 49 | 50 | 308 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 219 | 39 | 47 | 305 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 1,060 | 228 | 229 | 1,517 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx | TypeScript JSX | 20 | 6 | 2 | 28 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts | TypeScript | 4 | 0 | 1 | 5 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts | TypeScript | 16 | 4 | 4 | 24 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts | TypeScript | 124 | 4 | 14 | 142 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts | TypeScript | 21 | 15 | 5 | 41 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts | TypeScript | 43 | 24 | 7 | 74 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js | JavaScript | 28 | 7 | 9 | 44 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts | TypeScript | 101 | 59 | 24 | 184 |
| Total | | 6,193 | 1,008 | 1,017 | 8,218 |
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+

View File

@@ -0,0 +1,42 @@
# Details
Date : 2025-03-18 12:39:04
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 27 files, 6925 codes, 1247 comments, 1248 blanks, all 9420 lines
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
## Files
| filename | language | code | comment | blank | total |
| :--- | :--- | ---: | ---: | ---: | ---: |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md) | Markdown | 39 | 0 | 19 | 58 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx) | TypeScript JSX | 230 | 10 | 8 | 248 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx) | TypeScript JSX | 18 | 0 | 3 | 21 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx) | TypeScript JSX | 273 | 19 | 37 | 329 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx) | TypeScript JSX | 113 | 17 | 10 | 140 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 377 | 49 | 54 | 480 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 969 | 182 | 158 | 1,309 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 509 | 50 | 57 | 616 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx) | TypeScript JSX | 112 | 12 | 21 | 145 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 233 | 34 | 33 | 300 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 420 | 66 | 59 | 545 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx) | TypeScript JSX | 193 | 23 | 22 | 238 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | 227 | 36 | 32 | 295 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx) | TypeScript JSX | 500 | 75 | 89 | 664 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 264 | 75 | 81 | 420 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx) | TypeScript JSX | 204 | 26 | 33 | 263 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 337 | 88 | 92 | 517 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 360 | 78 | 85 | 523 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx) | TypeScript JSX | 20 | 6 | 2 | 28 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts) | TypeScript | 4 | 0 | 1 | 5 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts) | TypeScript | 16 | 4 | 4 | 24 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts) | TypeScript | 124 | 4 | 14 | 142 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts) | TypeScript | 21 | 15 | 5 | 41 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts) | TypeScript | 43 | 24 | 7 | 74 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js) | JavaScript | 28 | 7 | 9 | 44 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts) | TypeScript | 101 | 59 | 24 | 184 |
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,26 @@
# Diff Details
Date : 2025-03-18 12:39:04
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 11 files, 732 codes, 239 comments, 231 blanks, all 1202 lines
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
## Files
| filename | language | code | comment | blank | total |
| :--- | :--- | ---: | ---: | ---: | ---: |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx) | TypeScript JSX | 113 | 17 | 10 | 140 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 3 | 7 | 10 | 20 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 239 | 56 | 52 | 347 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 10 | 2 | 3 | 15 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 1 | 3 | 1 | 5 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 13 | 10 | 7 | 30 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | -62 | 0 | 1 | -61 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 16 | 6 | 7 | 29 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 128 | 39 | 42 | 209 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 141 | 39 | 38 | 218 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 130 | 60 | 60 | 250 |
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details

View File

@@ -0,0 +1,25 @@
# Diff Summary
Date : 2025-03-18 12:39:04
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 11 files, 732 codes, 239 comments, 231 blanks, all 1202 lines
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
## Languages
| language | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| TypeScript JSX | 11 | 732 | 239 | 231 | 1,202 |
## Directories
| path | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| . | 11 | 732 | 239 | 231 | 1,202 |
| components | 7 | 317 | 95 | 84 | 496 |
| components (Files) | 4 | 365 | 82 | 75 | 522 |
| components/cells | 3 | -48 | 13 | 9 | -26 |
| hooks | 4 | 415 | 144 | 147 | 706 |
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,39 @@
Date : 2025-03-18 12:39:04
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 11 files, 732 codes, 239 comments, 231 blanks, all 1202 lines
Languages
+----------------+------------+------------+------------+------------+------------+
| language | files | code | comment | blank | total |
+----------------+------------+------------+------------+------------+------------+
| TypeScript JSX | 11 | 732 | 239 | 231 | 1,202 |
+----------------+------------+------------+------------+------------+------------+
Directories
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| path | files | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| . | 11 | 732 | 239 | 231 | 1,202 |
| components | 7 | 317 | 95 | 84 | 496 |
| components (Files) | 4 | 365 | 82 | 75 | 522 |
| components/cells | 3 | -48 | 13 | 9 | -26 |
| hooks | 4 | 415 | 144 | 147 | 706 |
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
Files
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| filename | language | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx | TypeScript JSX | 113 | 17 | 10 | 140 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 3 | 7 | 10 | 20 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 239 | 56 | 52 | 347 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 10 | 2 | 3 | 15 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 1 | 3 | 1 | 5 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 13 | 10 | 7 | 30 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | -62 | 0 | 1 | -61 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 16 | 6 | 7 | 29 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 128 | 39 | 42 | 209 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 141 | 39 | 38 | 218 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 130 | 60 | 60 | 250 |
| Total | | 732 | 239 | 231 | 1,202 |
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
# Summary
Date : 2025-03-18 12:39:04
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 27 files, 6925 codes, 1247 comments, 1248 blanks, all 9420 lines
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
## Languages
| language | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| TypeScript JSX | 19 | 6,549 | 1,134 | 1,165 | 8,848 |
| TypeScript | 6 | 309 | 106 | 55 | 470 |
| Markdown | 1 | 39 | 0 | 19 | 58 |
| JavaScript | 1 | 28 | 7 | 9 | 44 |
## Directories
| path | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| . | 27 | 6,925 | 1,247 | 1,248 | 9,420 |
| . (Files) | 3 | 63 | 6 | 22 | 91 |
| components | 12 | 3,674 | 498 | 494 | 4,666 |
| components (Files) | 7 | 2,489 | 327 | 327 | 3,143 |
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
| types | 1 | 16 | 4 | 4 | 24 |
| utils | 5 | 317 | 109 | 59 | 485 |
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,61 @@
Date : 2025-03-18 12:39:04
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 27 files, 6925 codes, 1247 comments, 1248 blanks, all 9420 lines
Languages
+----------------+------------+------------+------------+------------+------------+
| language | files | code | comment | blank | total |
+----------------+------------+------------+------------+------------+------------+
| TypeScript JSX | 19 | 6,549 | 1,134 | 1,165 | 8,848 |
| TypeScript | 6 | 309 | 106 | 55 | 470 |
| Markdown | 1 | 39 | 0 | 19 | 58 |
| JavaScript | 1 | 28 | 7 | 9 | 44 |
+----------------+------------+------------+------------+------------+------------+
Directories
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| path | files | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| . | 27 | 6,925 | 1,247 | 1,248 | 9,420 |
| . (Files) | 3 | 63 | 6 | 22 | 91 |
| components | 12 | 3,674 | 498 | 494 | 4,666 |
| components (Files) | 7 | 2,489 | 327 | 327 | 3,143 |
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
| types | 1 | 16 | 4 | 4 | 24 |
| utils | 5 | 317 | 109 | 59 | 485 |
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
Files
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| filename | language | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md | Markdown | 39 | 0 | 19 | 58 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx | TypeScript JSX | 230 | 10 | 8 | 248 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx | TypeScript JSX | 18 | 0 | 3 | 21 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx | TypeScript JSX | 273 | 19 | 37 | 329 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx | TypeScript JSX | 113 | 17 | 10 | 140 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 377 | 49 | 54 | 480 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 969 | 182 | 158 | 1,309 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 509 | 50 | 57 | 616 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx | TypeScript JSX | 112 | 12 | 21 | 145 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 233 | 34 | 33 | 300 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 420 | 66 | 59 | 545 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx | TypeScript JSX | 193 | 23 | 22 | 238 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | 227 | 36 | 32 | 295 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx | TypeScript JSX | 500 | 75 | 89 | 664 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 264 | 75 | 81 | 420 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx | TypeScript JSX | 204 | 26 | 33 | 263 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 337 | 88 | 92 | 517 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 360 | 78 | 85 | 523 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx | TypeScript JSX | 20 | 6 | 2 | 28 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts | TypeScript | 4 | 0 | 1 | 5 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts | TypeScript | 16 | 4 | 4 | 24 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts | TypeScript | 124 | 4 | 14 | 142 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts | TypeScript | 21 | 15 | 5 | 41 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts | TypeScript | 43 | 24 | 7 | 74 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js | JavaScript | 28 | 7 | 9 | 44 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts | TypeScript | 101 | 59 | 24 | 184 |
| Total | | 6,925 | 1,247 | 1,248 | 9,420 |
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+

View File

@@ -0,0 +1,42 @@
# Details
Date : 2025-03-18 13:49:23
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 27 files, 6961 codes, 1254 comments, 1252 blanks, all 9467 lines
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
## Files
| filename | language | code | comment | blank | total |
| :--- | :--- | ---: | ---: | ---: | ---: |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md) | Markdown | 39 | 0 | 19 | 58 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx) | TypeScript JSX | 230 | 10 | 8 | 248 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx) | TypeScript JSX | 18 | 0 | 3 | 21 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx) | TypeScript JSX | 273 | 19 | 37 | 329 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx) | TypeScript JSX | 113 | 17 | 10 | 140 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 395 | 51 | 55 | 501 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 969 | 182 | 158 | 1,309 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 527 | 55 | 60 | 642 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx) | TypeScript JSX | 112 | 12 | 21 | 145 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 233 | 34 | 33 | 300 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 420 | 66 | 59 | 545 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx) | TypeScript JSX | 193 | 23 | 22 | 238 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | 227 | 36 | 32 | 295 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx) | TypeScript JSX | 500 | 75 | 89 | 664 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 264 | 75 | 81 | 420 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx) | TypeScript JSX | 204 | 26 | 33 | 263 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 337 | 88 | 92 | 517 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 360 | 78 | 85 | 523 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx) | TypeScript JSX | 20 | 6 | 2 | 28 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts) | TypeScript | 4 | 0 | 1 | 5 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts) | TypeScript | 16 | 4 | 4 | 24 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts) | TypeScript | 124 | 4 | 14 | 142 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts) | TypeScript | 21 | 15 | 5 | 41 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts) | TypeScript | 43 | 24 | 7 | 74 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js) | JavaScript | 28 | 7 | 9 | 44 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts) | TypeScript | 101 | 59 | 24 | 184 |
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,17 @@
# Diff Details
Date : 2025-03-18 13:49:23
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 2 files, 36 codes, 7 comments, 4 blanks, all 47 lines
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
## Files
| filename | language | code | comment | blank | total |
| :--- | :--- | ---: | ---: | ---: | ---: |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 18 | 2 | 1 | 21 |
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 18 | 5 | 3 | 26 |
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details

View File

@@ -0,0 +1,22 @@
# Diff Summary
Date : 2025-03-18 13:49:23
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 2 files, 36 codes, 7 comments, 4 blanks, all 47 lines
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
## Languages
| language | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| TypeScript JSX | 2 | 36 | 7 | 4 | 47 |
## Directories
| path | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| . | 2 | 36 | 7 | 4 | 47 |
| components | 2 | 36 | 7 | 4 | 47 |
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,27 @@
Date : 2025-03-18 13:49:23
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 2 files, 36 codes, 7 comments, 4 blanks, all 47 lines
Languages
+----------------+------------+------------+------------+------------+------------+
| language | files | code | comment | blank | total |
+----------------+------------+------------+------------+------------+------------+
| TypeScript JSX | 2 | 36 | 7 | 4 | 47 |
+----------------+------------+------------+------------+------------+------------+
Directories
+---------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| path | files | code | comment | blank | total |
+---------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| . | 2 | 36 | 7 | 4 | 47 |
| components | 2 | 36 | 7 | 4 | 47 |
+---------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
Files
+---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| filename | language | code | comment | blank | total |
+---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 18 | 2 | 1 | 21 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 18 | 5 | 3 | 26 |
| Total | | 36 | 7 | 4 | 47 |
+---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
# Summary
Date : 2025-03-18 13:49:23
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 27 files, 6961 codes, 1254 comments, 1252 blanks, all 9467 lines
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
## Languages
| language | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| TypeScript JSX | 19 | 6,585 | 1,141 | 1,169 | 8,895 |
| TypeScript | 6 | 309 | 106 | 55 | 470 |
| Markdown | 1 | 39 | 0 | 19 | 58 |
| JavaScript | 1 | 28 | 7 | 9 | 44 |
## Directories
| path | files | code | comment | blank | total |
| :--- | ---: | ---: | ---: | ---: | ---: |
| . | 27 | 6,961 | 1,254 | 1,252 | 9,467 |
| . (Files) | 3 | 63 | 6 | 22 | 91 |
| components | 12 | 3,710 | 505 | 498 | 4,713 |
| components (Files) | 7 | 2,525 | 334 | 331 | 3,190 |
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
| types | 1 | 16 | 4 | 4 | 24 |
| utils | 5 | 317 | 109 | 59 | 485 |
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)

View File

@@ -0,0 +1,61 @@
Date : 2025-03-18 13:49:23
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
Total : 27 files, 6961 codes, 1254 comments, 1252 blanks, all 9467 lines
Languages
+----------------+------------+------------+------------+------------+------------+
| language | files | code | comment | blank | total |
+----------------+------------+------------+------------+------------+------------+
| TypeScript JSX | 19 | 6,585 | 1,141 | 1,169 | 8,895 |
| TypeScript | 6 | 309 | 106 | 55 | 470 |
| Markdown | 1 | 39 | 0 | 19 | 58 |
| JavaScript | 1 | 28 | 7 | 9 | 44 |
+----------------+------------+------------+------------+------------+------------+
Directories
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| path | files | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
| . | 27 | 6,961 | 1,254 | 1,252 | 9,467 |
| . (Files) | 3 | 63 | 6 | 22 | 91 |
| components | 12 | 3,710 | 505 | 498 | 4,713 |
| components (Files) | 7 | 2,525 | 334 | 331 | 3,190 |
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
| types | 1 | 16 | 4 | 4 | 24 |
| utils | 5 | 317 | 109 | 59 | 485 |
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
Files
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| filename | language | code | comment | blank | total |
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md | Markdown | 39 | 0 | 19 | 58 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx | TypeScript JSX | 230 | 10 | 8 | 248 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx | TypeScript JSX | 18 | 0 | 3 | 21 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx | TypeScript JSX | 273 | 19 | 37 | 329 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx | TypeScript JSX | 113 | 17 | 10 | 140 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 395 | 51 | 55 | 501 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 969 | 182 | 158 | 1,309 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 527 | 55 | 60 | 642 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx | TypeScript JSX | 112 | 12 | 21 | 145 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 233 | 34 | 33 | 300 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 420 | 66 | 59 | 545 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx | TypeScript JSX | 193 | 23 | 22 | 238 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | 227 | 36 | 32 | 295 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx | TypeScript JSX | 500 | 75 | 89 | 664 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 264 | 75 | 81 | 420 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx | TypeScript JSX | 204 | 26 | 33 | 263 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 337 | 88 | 92 | 517 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 360 | 78 | 85 | 523 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx | TypeScript JSX | 20 | 6 | 2 | 28 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts | TypeScript | 4 | 0 | 1 | 5 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts | TypeScript | 16 | 4 | 4 | 24 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts | TypeScript | 124 | 4 | 14 | 142 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts | TypeScript | 21 | 15 | 5 | 41 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts | TypeScript | 43 | 24 | 7 | 74 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js | JavaScript | 28 | 7 | 9 | 44 |
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts | TypeScript | 101 | 59 | 24 | 184 |
| Total | | 6,961 | 1,254 | 1,252 | 9,467 |
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+

9
.gitignore vendored
View File

@@ -50,6 +50,11 @@ dashboard-server/meta-server/._package-lock.json
dashboard-server/meta-server/._services dashboard-server/meta-server/._services
*.tsbuildinfo *.tsbuildinfo
uploads/*
uploads/**/*
**/uploads/*
**/uploads/**/*
# CSV data files # CSV data files
*.csv *.csv
csv/* csv/*
@@ -59,3 +64,7 @@ csv/**/*
!csv/.gitkeep !csv/.gitkeep
inventory/tsconfig.tsbuildinfo inventory/tsconfig.tsbuildinfo
inventory-server/scripts/.fuse_hidden00000fa20000000a inventory-server/scripts/.fuse_hidden00000fa20000000a
.VSCodeCounter/
.VSCodeCounter/*
.VSCodeCounter/**/*

View File

@@ -0,0 +1,396 @@
# ValidationStep Component Refactoring Plan
## Overview
This document outlines a comprehensive plan to refactor the current ValidationStep component (4000+ lines) into a more maintainable, modular structure. The new implementation will be developed alongside the existing component without modifying the original code. Once completed, the previous step in the workflow will offer the option to continue to either the original ValidationStep or the new implementation.
## Table of Contents
1. [Current Component Analysis](#current-component-analysis)
2. [New Architecture Design](#new-architecture-design)
3. [Component Structure](#component-structure)
4. [State Management](#state-management)
5. [Key Features Implementation](#key-features-implementation)
6. [Integration Plan](#integration-plan)
7. [Testing Strategy](#testing-strategy)
8. [Project Timeline](#project-timeline)
9. [Design Principles](#design-principles)
10. [Appendix: Function Reference](#appendix-function-reference)
## Current Component Analysis
The current ValidationStep component has several issues:
- **Size**: At over 4000 lines, it's difficult to maintain and understand
- **Multiple responsibilities**: Handles validation, UI rendering, template management, and more
- **Special cases**: Contains numerous special case handlers and exceptions
- **Complex state management**: State is distributed across multiple useState calls
- **Tightly coupled concerns**: UI, validation logic, and business rules are intertwined
### Key Features to Preserve
1. **Data Validation**
- Field-level validation (required, regex, unique)
- Row-level validation (supplier, company fields)
- UPC validation with API integration
- AI-assisted validation
2. **Template Management**
- Saving, loading, and applying templates
- Template-based validation
3. **UI Components**
- Editable table with specialized cell renderers
- Error display and management
- Filtering and sorting capabilities
- Status indicators and progress tracking
4. **Special Field Handling**
- Input fields with price formatting
- Multi-input fields with separator configuration
- Select fields with dropdown options
- Checkbox fields with boolean value mapping
- UPC fields with specialized validation
5. **User Interaction Flows**
- Tab and keyboard navigation
- Bulk operations (select all, apply template)
- Row validation on value change
- Error reporting and display
## New Architecture Design
The new architecture will follow these principles:
1. **Separation of Concerns**
- UI rendering separate from business logic
- Validation logic isolated from state management
- Clear interfaces between components
2. **Composable Components**
- Small, focused components with single responsibilities
- Reusable pattern for different field types
3. **Centralized State Management**
- Custom hooks for state management
- Clear data flow patterns
- Reduced prop drilling
4. **Consistent Error Handling**
- Standardized error structure
- Predictable error propagation
- User-friendly error display
5. **Performance Optimization**
- Virtualized table rendering
- Memoization of expensive computations
- Deferred validation for better user experience
## Component Structure
The new ValidationStepNew folder has the following structure:
```
ValidationStepNew/
├── index.tsx # Main entry point that composes all pieces
├── components/ # UI Components
│ ├── ValidationContainer.tsx # Main wrapper component
│ ├── ValidationTable.tsx # Table implementation
│ ├── ValidationCell.tsx # Cell component
│ ├── ValidationSidebar.tsx # Sidebar with controls
│ ├── ValidationToolbar.tsx # Top toolbar (removed as unnecessary)
│ ├── TemplateManager.tsx # Template management
│ ├── FilterPanel.tsx # Filtering interface (integrated into Container)
│ └── cells/ # Specialized cell renderers
│ ├── InputCell.tsx
│ ├── SelectCell.tsx
│ ├── MultiInputCell.tsx
│ └── CheckboxCell.tsx
├── hooks/ # Custom hooks
│ ├── useValidationState.tsx # Main state management
│ ├── useTemplates.tsx # Template-related logic (integrated into ValidationState)
│ ├── useFilters.tsx # Filtering logic (integrated into ValidationState)
│ └── useUpcValidation.tsx # UPC-specific validation
└── utils/ # Utility functions
├── validationUtils.ts # Validation helper functions
├── formatters.ts # Value formatting utilities
└── constants.ts # Constant values and configuration
```
### Component Responsibilities
#### ValidationContainer
- Main container component
- Coordinates between subcomponents
- Manages global state
- Handles navigation events (next, back)
- Contains filter controls
#### ValidationTable
- Displays the data in tabular form
- Manages selection state
- Handles keyboard navigation
- Integrates with TanStack Table
- Displays properly styled rows and cells
#### ValidationCell
- Factory component that renders appropriate cell type
- Manages cell-level state
- Handles validation errors display
- Manages edit mode
- Shows consistent error indicators
#### TemplateManager
- Handles template selection UI
- Provides template save/load functionality
- Manages template application to rows
#### Cell Components
- **InputCell**: Handles text input with multiline and price support
- **MultiInputCell**: Handles multiple values with separator configuration
- **SelectCell**: Command/popover component for single selection
- **CheckboxCell**: Boolean value selection with mapping support
## State Management
### Core State Interface
```typescript
interface ValidationState<T extends string> {
// Core data
data: RowData<T>[];
filteredData: RowData<T>[];
// Validation state
isValidating: boolean;
validationErrors: Map<number, Record<string, Error[]>>;
rowValidationStatus: Map<number, 'pending' | 'validating' | 'validated' | 'error'>;
// Selection state
rowSelection: RowSelectionState;
// Template state
templates: Template[];
selectedTemplateId: string | null;
// Filter state
filters: FilterState;
// Methods
updateRow: (rowIndex: number, key: T, value: any) => void;
validateRow: (rowIndex: number) => Promise<void>;
validateUpc: (rowIndex: number, upcValue: string) => Promise<void>;
applyTemplate: (templateId: string, rowIndexes: number[]) => void;
saveTemplate: (name: string, type: string) => void;
setFilters: (newFilters: Partial<FilterState>) => void;
// Additional methods...
}
```
### useValidationState Hook
The main state management hook handles:
- Data manipulation (update, sort, filter)
- Selection management
- Validation coordination
- Integration with validation utilities
- Template management
- Filtering and sorting
## Key Features Implementation
### 1. Field Type Handling
Implemented a strategy pattern for different field types:
```typescript
// In ValidationCell
const renderCellContent = () => {
const fieldType = field.fieldType.type
switch (fieldType) {
case 'input':
return <InputCell<T> field={field} value={value} onChange={onChange} ... />
case 'multi-input':
return <MultiInputCell<T> field={field} value={value} onChange={onChange} ... />
case 'select':
return <SelectCell<T> field={field} value={value} onChange={onChange} ... />
// etc.
}
}
```
### 2. Validation Logic
Validation is broken down into clear steps:
1. **Field Validation**: Apply field-level validations (required, regex, etc.)
2. **Row Validation**: Apply row-level validations and rowHook
3. **Table Validation**: Apply table-level validations (unique) and tableHook
Validation now happens automatically without explicit buttons, with immediate feedback on field blur.
### 3. UI Components
UI components follow these principles:
1. **Consistent Styling**: All components use shadcn UI for consistent look and feel
2. **Visual Feedback**: Errors are clearly indicated with icons and border styling
3. **Intuitive Editing**: Fields show outlines even when not in focus, and edit on click
4. **Proper Command Pattern**: Select and multi-select fields use command/popover pattern for better UX
5. **Focus Management**: Fields close when clicking away and perform validation on blur
## Design Principles
Based on user preferences and best practices, the following design principles guide this refactoring:
1. **Automatic Validation**
- Validation should happen automatically without explicit buttons
- All validation should run on initial data load
- Fields should validate on blur (when user clicks away)
2. **Modern UI Patterns**
- Command/popover components for all selects and multi-selects
- Consistent field outlines and borders even when not in focus
- Badge patterns for multi-select items
- Clear visual indicators for errors
3. **Reduced Complexity**
- Remove unnecessary UI elements like "validate all" buttons
- Eliminate redundant state and toast notifications
- Simplify component hierarchy where possible
- Find root causes rather than adding special cases
4. **Consistent Component Behavior**
- Fields should close when clicking away
- All inputs should follow the same editing pattern
- Error handling should be consistent across all field types
- Multi-select fields should allow selecting multiple items with clear visual feedback
## Integration Plan
### 1. Creating the New Component Structure
Folder structure has been created without modifying the existing code:
```bash
mkdir -p inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/{components,hooks,utils}
mkdir -p inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells
```
### 2. Implementing Basic Components
Core components have been implemented:
1. Created index.tsx as the main entry point
2. Implemented ValidationContainer with basic state management
3. Created ValidationTable for data display
4. Implemented basic cell rendering with specialized cell types
### 3. Implementing State Management
State management has been implemented:
1. Created useValidationState hook
2. Implemented data transformation utilities
3. Added validation logic
### 4. Integrating with Previous Step
The previous step component allows choosing between validation implementations, enabling gradual testing and adoption.
## Testing Strategy
1. **Unit Tests**
- Test individual utility functions
- Test hooks in isolation
- Test individual UI components
2. **Integration Tests**
- Test component interactions
- Test state management flow
- Test validation logic integration
3. **Comparison Tests**
- Compare output of new component with original
- Verify that all functionality works the same
4. **Performance Tests**
- Measure render times
- Evaluate memory usage
- Compare against original component
## Project Timeline
1. **Phase 1: Initial Structure (Completed)**
- Set up folder structure
- Implement basic components
- Create core state management
2. **Phase 2: Core Functionality (In Progress)**
- Implement validation logic (completed)
- Create cell renderers (completed)
- Add template management (in progress)
3. **Phase 3: Special Features (Upcoming)**
- Implement UPC validation
- Add AI validation
- Handle special cases
4. **Phase 4: UI Refinement (Ongoing)**
- Improve error display (completed)
- Enhance user interactions (completed)
- Optimize performance (in progress)
5. **Phase 5: Testing and Integration (Upcoming)**
- Write tests
- Fix bugs
- Integrate with previous step
## Appendix: Function Reference
This section documents the core functions from the original ValidationStep that need to be preserved in the new implementation.
### Validation Functions
1. **validateRegex** - Validates values against regex patterns
2. **getValidationError** - Determines field-level validation errors
3. **validateAndCommit** - Validates and commits new values
4. **validateData** - Validates all data rows
5. **validateUpcAndGenerateItemNumbers** - Validates UPC codes and generates item numbers
### Formatting Functions
1. **formatPrice** - Formats price values
2. **getDisplayValue** - Gets formatted display value based on field type
3. **isMultiInputType** - Checks if field is multi-input type
4. **getMultiInputSeparator** - Gets separator for multi-input fields
5. **isPriceField** - Checks if field should be formatted as price
### Template Functions
1. **loadTemplates** - Loads templates from storage
2. **saveTemplate** - Saves a new template
3. **applyTemplate** - Applies a template to selected rows
4. **getTemplateDisplayText** - Gets display text for a template
### AI Validation Functions
1. **handleAiValidation** - Triggers AI validation
2. **showCurrentPrompt** - Shows current AI prompt
3. **getFieldDisplayValue** - Gets display value for a field
4. **highlightDifferences** - Highlights differences between original and corrected values
5. **getFieldDisplayValueWithHighlight** - Gets display value with highlighted differences
6. **revertAiChange** - Reverts an AI-suggested change
7. **isChangeReverted** - Checks if an AI change has been reverted
### Event Handlers
1. **handleUpcValueUpdate** - Handles UPC value updates
2. **handleBlur** - Handles input blur events
3. **handleWheel** - Handles wheel events for navigation
4. **copyValueDown** - Copies a value to cells below
5. **handleSkuGeneration** - Generates SKUs
By following this refactoring plan, we continue to transform the monolithic ValidationStep component into a modular, maintainable set of components while preserving all existing functionality and aligning with user preferences for design and behavior.

View File

@@ -0,0 +1,137 @@
# ValidationStepNew Implementation Status
## Overview
This document outlines the current status of the ValidationStepNew implementation, a refactored version of the original ValidationStep component. The goal is to create a more maintainable, modular component that preserves all functionality of the original while eliminating technical debt and implementing modern UI patterns.
## Design Principles
Based on the user's preferences, we're following these core design principles:
1. **Automatic Validation**
- ✅ Validation runs automatically on data load
- ✅ No explicit "validate all" button needed
- ✅ Fields validate on blur when user clicks away
- ✅ Immediate visual feedback for validation errors
2. **Modern UI Patterns**
- ✅ Command/popover components for selects and multi-selects
- ✅ Consistent field outlines and borders even when not in focus
- ✅ Badge pattern for multi-select field items
- ✅ Visual indicators for errors with appropriate styling
3. **Reduced Complexity**
- ✅ Removed unnecessary UI elements like "validate all" button
- ✅ Eliminated redundant toast notifications
- ✅ Simplified component hierarchy
- ✅ Fixed root causes rather than adding special cases
4. **Consistent Behavior**
- ✅ Fields close when clicking away
- ✅ All inputs follow the same editing pattern
- ✅ Error handling is consistent across field types
- ✅ Multi-select fields allow selecting multiple items
## Completed Components
### Core Structure
- ✅ Main component structure
- ✅ Directory organization
- ✅ TypeScript interfaces
- ✅ Props definition and passing
### State Management
-`useValidationState` hook for centralized state
- ✅ Data validation logic
- ✅ Integration with rowHook and tableHook
- ✅ Error tracking and management
- ✅ Row selection
- ✅ Automatic validation on data load
### UI Components
- ✅ ValidationContainer with appropriate layout
- ✅ ValidationTable with shadcn UI components
- ✅ ValidationCell factory component
- ✅ Row select/deselect functionality
- ✅ Error display and indicators
- ✅ Selection action bar
### Cell Components
- ✅ InputCell with price and multiline support
- ✅ MultiInputCell with separator configuration
- ✅ SelectCell using command/popover pattern
- ✅ CheckboxCell with boolean mapping
- ✅ Consistent styling across all field types
- ✅ Proper edit/view state management
- ✅ Outlined borders in both edit and view modes
### Utility Functions
- ✅ Value formatting for display
- ✅ Field type detection
- ✅ Error creation and management
- ✅ Price formatting
### UI Improvements
- ✅ Consistent borders and field outlines
- ✅ Fields that properly close when clicking away
- ✅ Multi-select with badge UI pattern
- ✅ Command pattern for searchable select menus
- ✅ Better visual error indication
## Pending Tasks
### Enhanced Validation
- ⏳ AI validation system
- ⏳ Custom validation hooks
- ⏳ Enhanced UPC validation with API integration
- ⏳ Validation visualizations
### Advanced UI Features
- ⏳ Table virtualization for performance
- ⏳ Drag-and-drop reordering
- ⏳ Bulk operations (copy down, fill all, etc.)
- ⏳ Keyboard navigation improvements
- ⏳ Template dialogs and management UI
### Special Features
- ⏳ Image preview integration
- ⏳ SKU generation system
- ⏳ Item number generation
- ⏳ Dependent dropdown values
### Testing
- ⏳ Unit tests for utility functions
- ⏳ Component tests
- ⏳ Integration tests
- ⏳ Performance benchmarks
## Known Issues
1. TypeScript error for `validationDisabled` property in ValidationCell.tsx
2. Some type casting is needed due to complex generic types
3. Need to address edge cases for multi-select fields validation
4. Proper error handling for API calls needs implementation
## Next Steps
1. Fix TypeScript errors in ValidationCell and related components
2. Complete template management functionality
3. Implement UPC validation with API integration
4. Make multi-select field validation more robust
5. Add comprehensive tests
## Performance Improvements
We've already implemented several performance optimizations:
1. ✅ More efficient state updates by removing unnecessary re-renders
2. ✅ Better error handling to prevent cascading validations
3. ✅ Improved component isolation to prevent unnecessary re-renders
4. ✅ Automatic validation that doesn't block the UI
Additional planned improvements:
1. Virtualized table rendering for large datasets
2. Memoization of expensive calculations
3. Optimized state updates to minimize re-renders
4. Batched API calls for validation

72
docs/fix-multi-select.md Normal file
View File

@@ -0,0 +1,72 @@
# Solution: Keeping Dropdowns Open During Multiple Selections
## The Problem
When implementing a multi-select dropdown in React, a common issue occurs:
1. You select an item in the dropdown
2. The `onChange` handler is called, updating the data
3. This triggers a re-render of the parent component (in this case, the entire table)
4. During the re-render, the dropdown is unmounted and remounted
5. This causes the dropdown to close before you can make multiple selections
## The Solution: Deferred State Updates
The key insight is to **separate local state management from parent state updates**:
```typescript
// Step 1: Add local state to track selections
const [internalValue, setInternalValue] = useState<string[]>(value)
// Step 2: Handle popover open state changes
const handleOpenChange = useCallback((newOpen: boolean) => {
if (open && !newOpen) {
// Only update parent state when dropdown closes
if (JSON.stringify(internalValue) !== JSON.stringify(value)) {
onChange(internalValue);
}
}
setOpen(newOpen);
if (newOpen) {
// Sync internal state with external state when opening
setInternalValue(value);
}
}, [open, internalValue, value, onChange]);
// Step 3: Toggle selection only updates internal state
const toggleSelection = useCallback((selectedValue: string) => {
setInternalValue(prev => {
if (prev.includes(selectedValue)) {
return prev.filter(v => v !== selectedValue);
} else {
return [...prev, selectedValue];
}
});
}, []);
```
## Why This Works
1. **No parent re-renders during selection**: Since we're only updating local state, the parent component doesn't re-render during selection.
2. **Consistent UI**: The dropdown shows accurate selected states using the internal value.
3. **Data integrity**: The final selections are properly synchronized back to the parent when done.
4. **Resilient to external changes**: Initial state is synchronized when opening the dropdown.
## Implementation Steps
1. Create a local state variable to track selections inside the component
2. Only make selections against this local state while the dropdown is open
3. Defer updating the parent until the dropdown is explicitly closed
4. When opening, synchronize the internal state with the external value
## Benefits
This pattern:
- Avoids re-render cycles that would unmount the dropdown
- Maintains UI consistency during multi-selection
- Simplifies the component's interaction with parent components
- Works with existing component lifecycles rather than fighting against them
This solution is much simpler than trying to prevent event propagation or manipulating DOM events, and addresses the root cause of the issue: premature re-rendering.

View File

@@ -0,0 +1,239 @@
# Validation Display Issue Implementation
## Issue Being Addressed
**Validation Display Issue**: Validation isn't happening beyond checking if a cell is required or not. All validation rules defined in import.tsx need to be respected.
* Required fields correctly show a red border when empty (✅ ALREADY WORKING)
* Non-empty fields with validation errors (regex, unique, etc.) should show a red border AND an alert circle icon with tooltip explaining the error (❌ NOT WORKING)
## Implementation Attempts
!!!!**NOTE** All previous attempts have been reverted and are no longer part of the code, please take this into account when trying a new solution. !!!!
### Attempt 1: Fix Validation Display Logic
**Approach**: Modified `processErrors` function to separate required errors from validation errors and show alert icons only for non-empty fields with validation errors.
**Changes Made**:
```typescript
function processErrors(value: any, errors: ErrorObject[]) {
// ...existing code...
// Separate required errors from other validation errors
const requiredErrors = errors.filter(error =>
error.message?.toLowerCase().includes('required')
);
const validationErrors = errors.filter(error =>
!error.message?.toLowerCase().includes('required')
);
const isRequiredButEmpty = valueIsEmpty && requiredErrors.length > 0;
const hasValidationErrors = validationErrors.length > 0;
const shouldShowErrorIcon = hasValidationErrors && !valueIsEmpty;
// ...more code...
}
```
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 2: Comprehensive Fix for Validation Display
**Approach**: Completely rewrote `processErrors` function with consistent empty value detection, clear error separation, and improved error message extraction.
**Changes Made**:
```typescript
function processErrors(value: any, errors: ErrorObject[]) {
if (!errors || errors.length === 0) {
return { filteredErrors: [], hasError: false, isRequiredButEmpty: false,
shouldShowErrorIcon: false, errorMessages: '' };
}
const valueIsEmpty = isEmpty(value);
const requiredErrors = errors.filter(error =>
error.message?.toLowerCase().includes('required')
);
const validationErrors = errors.filter(error =>
!error.message?.toLowerCase().includes('required')
);
let filteredErrors = valueIsEmpty ? requiredErrors : validationErrors;
const isRequiredButEmpty = valueIsEmpty && requiredErrors.length > 0;
const hasValidationErrors = validationErrors.length > 0;
const hasError = isRequiredButEmpty || hasValidationErrors;
const shouldShowErrorIcon = hasValidationErrors && !valueIsEmpty;
let errorMessages = '';
if (shouldShowErrorIcon) {
errorMessages = validationErrors.map(getErrorMessage).join('\n');
}
return { filteredErrors, hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages };
}
```
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 3: Simplified Error Processing Logic
**Approach**: Refactored `processErrors` to use shared `isEmpty` function, simplified error icon logic, and made error message extraction more direct.
**Changes Made**:
```typescript
function processErrors(value: any, errors: ErrorObject[]) {
if (!errors || errors.length === 0) {
return { filteredErrors: [], hasError: false, isRequiredButEmpty: false,
shouldShowErrorIcon: false, errorMessages: '' };
}
const valueIsEmpty = isEmpty(value);
const requiredErrors = errors.filter(error =>
error.message?.toLowerCase().includes('required')
);
const validationErrors = errors.filter(error =>
!error.message?.toLowerCase().includes('required')
);
let filteredErrors = valueIsEmpty ? requiredErrors : validationErrors;
const isRequiredButEmpty = valueIsEmpty && requiredErrors.length > 0;
const hasValidationErrors = !valueIsEmpty && validationErrors.length > 0;
const hasError = isRequiredButEmpty || hasValidationErrors;
const shouldShowErrorIcon = hasValidationErrors;
let errorMessages = '';
if (shouldShowErrorIcon) {
errorMessages = validationErrors.map(getErrorMessage).join('\n');
}
return { filteredErrors, hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages };
}
```
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 4: Consistent Error Processing Across Components
**Approach**: Updated both `processErrors` function and `ValidationCell` component to ensure consistent error handling between components.
**Changes Made**:
```typescript
// In processErrors function
function processErrors(value: any, errors: ErrorObject[]) {
// Similar to Attempt 3 with consistent error handling
}
// In ValidationCell component
const ValidationCell = ({ field, value, onChange, errors, /* other props */ }) => {
// ...existing code...
// Use the processErrors function to handle validation errors
const { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages } =
React.useMemo(() => processErrors(value, errors), [value, errors]);
// ...rest of the component...
}
```
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 5: Unified Error Processing with ItemNumberCell
**Approach**: Replaced custom error processing in `ValidationCell` with the same `processErrors` utility used by `ItemNumberCell`.
**Changes Made**:
```typescript
const ValidationCell = ({ field, value, onChange, errors, /* other props */ }) => {
// State and context setup...
// For item_number fields, use the specialized component
if (fieldKey === 'item_number') {
return <ItemNumberCell {...props} />;
}
// Use the same processErrors utility function that ItemNumberCell uses
const { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages } =
React.useMemo(() => processErrors(value, errors), [value, errors]);
// Rest of component...
}
```
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 6: Standardize Error Processing Across Cell Types
**Approach**: Standardized error handling across all cell types using the shared `processErrors` utility function.
**Changes Made**: Similar to Attempt 5, with focus on standardizing the approach for determining when to show validation error icons.
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 7: Replace Custom Error Processing with Shared Utility
**Approach**: Ensured consistent error handling between `ItemNumberCell` and regular `ValidationCell` components.
**Changes Made**: Similar to Attempts 5 and 6, with focus on using the shared utility function consistently.
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
### Attempt 8: Improved Error Normalization and Deep Comparison
**Approach**: Modified `MemoizedCell` in `ValidationTable.tsx` to use deep comparison for error objects and improved error normalization.
**Changes Made**:
```typescript
// Create a memoized cell component
const MemoizedCell = React.memo(({ field, value, onChange, errors, /* other props */ }) => {
return <ValidationCell {...props} />;
}, (prev, next) => {
// Basic prop comparison
if (prev.value !== next.value) return false;
if (prev.isValidating !== next.isValidating) return false;
if (prev.itemNumber !== next.itemNumber) return false;
// Deep compare errors - critical for validation display
if (!prev.errors && next.errors) return false;
if (prev.errors && !next.errors) return false;
if (prev.errors && next.errors) {
if (prev.errors.length !== next.errors.length) return false;
// Compare each error object
for (let i = 0; i < prev.errors.length; i++) {
if (prev.errors[i].message !== next.errors[i].message) return false;
if (prev.errors[i].level !== next.errors[i].level) return false;
if (prev.errors[i].source !== next.errors[i].source) return false;
}
}
// Compare options...
return true;
});
// In the field columns definition:
cell: ({ row }) => {
const rowErrors = validationErrors.get(row.index);
const cellErrors = rowErrors?.[fieldKey] || [];
// Ensure cellErrors is always an array
const normalizedErrors = Array.isArray(cellErrors) ? cellErrors : [cellErrors];
return <MemoizedCell {...props} errors={normalizedErrors} />;
}
```
**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip.
## Root Causes (Revised Hypothesis)
After multiple attempts, the issue appears more complex than initially thought. Possible root causes:
1. **Error Object Structure**: Error objects might not have the expected structure or properties
2. **Error Propagation**: Errors might be getting filtered out before reaching cell components
3. **Validation Rules Configuration**: Validation rules in import.tsx might be incorrectly configured
4. **Error State Management**: Error state might not be properly updated or might be reset incorrectly
5. **Component Rendering Logic**: Components might not re-render when validation state changes
6. **CSS/Styling Issues**: Validation icons might be rendered but hidden due to styling issues
7. **Validation Timing**: Validation might be happening at the wrong time or getting overridden

View File

@@ -0,0 +1,138 @@
# Multiple Cell Edit Issue Implementation
## Issue Being Addressed
**Multiple Cell Edit Issue**: When you enter values in 2+ cells before validation finishes, contents from all edited cells get erased when validation finishes.
## Implementation Attempts
### Attempt 1: Fix Multiple Cell Edit Issue (First Approach)
**Approach**:
- Added a tracking mechanism using a Set to keep track of cells that are currently being edited
- Modified the `flushPendingUpdates` function to preserve values of cells being edited
- Added cleanup of editing state after validation completes
**Changes Made**:
```typescript
// Add ref to track cells currently being edited
const currentlyEditingCellsRef = useRef(new Set<string>());
// Update a row's field value
const updateRow = useCallback((rowIndex: number, key: T, value: any) => {
// Add this cell to currently editing cells
const cellKey = `${rowIndex}-${key}`;
currentlyEditingCellsRef.current.add(cellKey);
// ...existing code...
// After validation completes, remove this cell from currently editing list
setTimeout(() => {
currentlyEditingCellsRef.current.delete(cellKey);
}, 100);
}, []);
// Modify flushPendingUpdates to respect currently editing cells
const flushPendingUpdates = useCallback(() => {
// ...existing code...
if (dataUpdates.length > 0) {
setData(prev => {
// ...existing code...
dataUpdates.forEach((row, index) => {
if (index < newData.length) {
const updatedRow = { ...row };
// Check if any fields in this row are currently being edited
// If so, preserve their current values in the previous data
Object.keys(prev[index] || {}).forEach(key => {
const cellKey = `${index}-${key}`;
if (currentlyEditingCellsRef.current.has(cellKey)) {
// Keep the value from the previous state for this field
updatedRow[key] = prev[index][key];
}
});
newData[index] = updatedRow;
}
});
return newData;
});
}
}, []);
```
**Result**:
- Slight improvement - the first value entered was saved, but any subsequent values still got erased
### Attempt 2: Fix Multiple Cell Edit Issue (Second Approach)
**Approach**:
- Completely revised the cell editing tracking system
- Used a Map with timestamps to track editing cells more accurately
- Added proper Promise-based tracking for cell validation
- Increased timeout from 100ms to 1000ms
- Made cleanup more robust by checking if it's still the same editing session
**Changes Made**:
```typescript
// Add ref to track cells currently being edited with timestamps
const currentlyEditingCellsRef = useRef(new Map<string, number>());
// Add ref to track validation promises
const validationPromisesRef = useRef<Map<string, Promise<void>>>(new Map());
// Update a row's field value
const updateRow = useCallback((rowIndex: number, key: T, value: any) => {
// Mark this cell as being edited with the current timestamp
const cellKey = `${rowIndex}-${key}`;
currentlyEditingCellsRef.current.set(cellKey, Date.now());
// ...existing code...
// Create a validation promise
const validationPromise = new Promise<void>((resolve) => {
setTimeout(() => {
try {
validateRow(rowIndex);
} finally {
resolve();
}
}, 0);
});
validationPromisesRef.current.set(cellKey, validationPromise);
// When validation is complete, remove from validating cells
validationPromise.then(() => {
// ...existing code...
// Keep this cell in the editing state for a longer time
setTimeout(() => {
if (currentlyEditingCellsRef.current.has(cellKey)) {
currentlyEditingCellsRef.current.delete(cellKey);
}
}, 1000); // Keep as "editing" for 1 second
});
}, []);
```
**Result**:
- Worse than the first approach - now all values get erased, including the first one
## Root Causes (Hypothesized)
- The validation process might be updating the entire data state, causing race conditions with cell edits
- The timing of validation completions might be problematic
- State updates might be happening in a way that overwrites user changes
- The cell state tracking system is not robust enough to prevent overwrites
## Next Steps
The issue requires a more fundamental approach than just tweaking the editing logic. We need to:
1. Implement a more robust state management system for cell edits that can survive validation cycles
2. Consider disabling validation during active editing
3. Implement a proper "dirty state" tracking system for cells

View File

@@ -0,0 +1,305 @@
# Current Issues to Address
4. Validation isn't happening beyond checking if a cell is required or not - needs to respect rules in import.tsx
* Red cell outline if cell is required and it's empty
* Red outline + alert circle icon with tooltip if cell is NOT empty and isn't valid
8. When you enter a value in 2+ cells before validation finishes, contents from all edited cells get erased when validation finishes
## Do NOT change or edit
* Anything related to AI validation
* Anything about how templates or UPC validation work (only focus on specific issues described above)
* Anything outside of the ValidationStepNew folder
## Issues already fixed - do not work on these
✅FIXED 1. The red row background should go away when all cells in the row are valid and all required cells are populated
✅FIXED 2. Columns alignment with header is slightly off, gets worse the further right you go
✅FIXED 3. The copy down button is in the way of the validation error icon and the select open trigger - all three need to be in unique locations
✅FIXED 5. Description column needs to have an expanded view of some sort, maybe a popover to allow for easier editing
* Don't distort table to make it happen
✅FIXED 6. Need to ensure all cell's contents don't overflow the input (truncate). COO does this currently, probably more
✅FIXED 7. The template select cell is expanding, needs to be fixed size and truncate
✅FIXED 9. Import dialog state not fully reset when closing? (validate data step appears scrolled to the middle of the table where I left it)
✅FIXED 10. UPC column doesn't need to show loading state when Item Number is being processed, only show on item number column
✅FIXED 11. Copy down needs to show a loading state on the cells that it will copy to
✅FIXED 12. Shipping restrictions/tax category should default to ID 0 if we didn't get it elsewhere
✅FIXED 13. Header row should be sticky (both up/down and left/right)
✅FIXED 14. Need a way to scroll around table if user doesn't have mouse wheel for left/right
✅FIXED 15. Enhance copy down feature by allowing user to choose the last cell to copy to, instead of going all the way to the bottom
---------
# Validation Step Components Overview
## Core Components
### ValidationContainer
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx`
- Main wrapper component for the validation step
- Manages global state and coordinates between subcomponents
- Handles navigation events (next, back)
- Manages template application and validation state
- Coordinates UPC validation and product line loading
- Manages row selection and filtering
- Contains cache management for UPC validation results
- Maintains item number references separate from main data
### ValidationTable
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx`
- Handles data display and column configuration
- Uses TanStack Table for core functionality
- Features:
- Sticky header (both vertical and horizontal) - currently doesn't work properly
- Row selection with checkboxes
- Template selection column
- Dynamic column widths based on field types - specified in import.tsx component
- Copy down functionality for cell values
- Error highlighting for rows and cells
- Loading states for cells being validated
### ValidationCell
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx`
- Base cell component that renders different cell types based on field configuration
- Handles error display with tooltips
- Manages copy down button visibility
- Supports loading states during validation
- Cell Types:
1. InputCell: For single-value text input
2. SelectCell: For dropdown selection
3. MultiInputCell: For multiple value inputs
4. Template selection cells with SearchableTemplateSelect component
### SearchableTemplateSelect
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx`
- Advanced template selection component with search functionality
- Features:
- Real-time search filtering of templates
- Customizable display text for templates
- Support for default brand selection
- Accessible popover interface
- Keyboard navigation support
- Custom styling through className props
- Scroll event handling for nested scrollable areas
### TemplateManager
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx`
- Comprehensive template management interface
- Features:
- Template selection with search functionality
- Save template dialog with name and type inputs
- Batch template application to selected rows
- Template count tracking
- Toast notifications for user feedback
- Dialog-based interface for template operations
### AiValidationDialogs
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx`
- Manages AI-assisted validation dialogs and interactions
### SaveTemplateDialog
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx`
- Dialog component for saving new templates
## Cell Components
### InputCell
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx`
- Handles single value text input
- Features:
- Inline/edit mode switching
- Multiline support
- Price formatting
- Error state display
- Loading state during validation
- Width constraints
- Automated cleanPriceFields processing for "$" formatting
### SelectCell
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx`
- Handles dropdown selection
- Features:
- Searchable dropdown
- Custom option rendering
- Error state display
- Loading state during validation
- Width constraints
- Disabled state support
- Deferred search query handling for performance
### MultiInputCell
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx`
- Handles multiple value inputs
- Features:
- Comma-separated input support
- Multi-select dropdown for predefined options
- Custom separators
- Badge display for selected count
- Truncation for long values
- Width constraints
- Price formatting support
- Internal state management to avoid excessive re-renders
## Validation System
### useValidation Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx`
- Provides core validation logic
- Validates at multiple levels:
1. Field-level validation (required, regex, unique)
2. Row-level validation (supplier, company fields)
3. Table-level validation
4. Custom validation hooks support
- Error object structure includes message, level, and source properties
- Handles debounced validation updates to avoid UI freezing
### useAiValidation Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx`
- Manages AI-assisted validation logic and state
- Features:
- Tracks detailed changes per product
- Manages validation progress with estimated completion time
- Handles warnings and change suggestions
- Supports diff generation for changes
- Progress tracking with step indicators
- Prompt management for AI interactions
- Timer management for long-running operations
### useTemplates Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx`
- Comprehensive template management system
- Features:
- Template CRUD operations
- Template application logic
- Default value handling
- Template search and filtering
- Batch template operations
- Template validation
### useUpcValidation Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx`
- Dedicated UPC validation management
- Features:
- UPC format validation
- Supplier data validation
- Cache management for validation results
- Batch processing of UPC validations
- Item number generation logic
- Loading state management
### useFilters Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx`
- Advanced filtering system for table data
- Features:
- Multiple filter criteria support
- Dynamic filter updates
- Filter persistence
- Filter combination logic
- Performance optimized filtering
### useValidationState Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx`
- Manages global validation state
- Handles:
- Data updates
- Template management
- Error tracking using Map objects
- Row selection
- Filtering
- UPC validation with caching to prevent duplicate API calls
- Product line loading
- Batch processing of updates
- Default value application for tax_cat and ship_restrictions (defaulting to "0")
- Price field auto-formatting to remove "$" symbols
### Utility Files
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts`
- Core validation utility functions
- Includes:
- Field validation logic
- Error message formatting
- Validation rule processing
- Type checking utilities
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts`
- Error handling and formatting utilities
- Includes:
- Error object creation
- Error message formatting
- Error source tracking
- Error level management
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts`
- Data transformation and mutation utilities
- Includes:
- Row data updates
- Batch data processing
- Data structure conversions
- Change tracking
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js`
- Helper functions for validation
- Includes:
- Common validation patterns
- Validation state management
- Validation result processing
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts`
- UPC-specific validation utilities
- Includes:
- UPC format checking
- Checksum validation
- Supplier data matching
- Cache management
### Types
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts`
- Core type definitions for the validation step
### Validation Types
1. Required field validation
2. Regex pattern validation
3. Unique value validation
4. Custom field validation
5. Row-level validation
6. Table-level validation
## State Management
### useValidationState Hook
`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx`
- Manages global validation state
- Handles:
- Data updates
- Template management
- Error tracking using Map objects
- Row selection
- Filtering
- UPC validation with caching to prevent duplicate API calls
- Product line loading
- Batch processing of updates
- Default value application for tax_cat and ship_restrictions (defaulting to "0")
- Price field auto-formatting to remove "$" symbols
## UPC Validation System
### UPC Processing
- Validates UPCs against supplier data
- Cache system for UPC validation results
- Batch processing of UPC validation requests
- Auto-generation of item numbers based on UPC
- Special loading states for UPC/item number fields
- Separate state tracking to avoid unnecessary data structure updates
## Template System
### Template Management
- Supports saving and loading templates
- Template application to single/multiple rows
- Default template values
- Template search and filtering
## Performance Optimizations
1. Memoized components to prevent unnecessary renders
2. Virtualized table for large datasets
3. Deferred value updates for search inputs
4. Efficient error state management
5. Optimized cell update handling

View File

@@ -0,0 +1,131 @@
# Refactoring Plan for Validation Code
## Current Structure Analysis
- **useValidationState.tsx**: ~1650 lines - Core validation state management
- **useValidation.tsx**: ~425 lines - Field/data validation utility
- **useUpcValidation.tsx**: ~410 lines - UPC-specific validation
## Proposed New Structure
### 1. Core Types & Utilities (150-200 lines)
**File: `validation/types.ts`**
- All interfaces and types (RowData, ValidationError, FilterState, Template, etc.)
- Shared utility functions (isEmpty, getCellKey, etc.)
**File: `validation/utils.ts`**
- Generic validation utility functions
- Caching mechanism and cache clearing helpers
- API URL helpers
### 2. Field Validation (300-350 lines)
**File: `validation/hooks/useFieldValidation.ts`**
- `validateField` function
- Field-level validation logic
- Required, regex, and other field validations
### 3. Uniqueness Validation (250-300 lines)
**File: `validation/hooks/useUniquenessValidation.ts`**
- `validateUniqueField` function
- `validateUniqueItemNumbers` function
- All uniqueness checking logic
### 4. UPC Validation (300-350 lines)
**File: `validation/hooks/useUpcValidation.ts`**
- `fetchProductByUpc` function
- `validateUpc` function
- `applyItemNumbersToData` function
- UPC validation state management
### 5. Validation Status Management (300-350 lines)
**File: `validation/hooks/useValidationStatus.ts`**
- Error state management
- Row validation status tracking
- Validation indicators and refs
- Batch validation processing
### 6. Data Management (300-350 lines)
**File: `validation/hooks/useValidationData.ts`**
- Data state management
- Row updates
- Data filtering
- Initial data processing
### 7. Template Management (250-300 lines)
**File: `validation/hooks/useTemplateManagement.ts`**
- Template saving
- Template application
- Template loading
- Template display helpers
### 8. Main Validation Hook (300-350 lines)
**File: `validation/hooks/useValidation.ts`**
- Main hook that composes all other hooks
- Public API export
- Initialization logic
- Core validation flow
## Function Distribution
### Core Types & Utilities
- All interfaces (InfoWithSource, ValidationState, etc.)
- `isEmpty` utility
- `getApiUrl` helper
### Field Validation
- `validateField`
- `validateRow`
- `validateData` (partial)
- All validation result caching
### Uniqueness Validation
- `validateUniqueField`
- `validateUniqueItemNumbers`
- Uniqueness caching mechanisms
### UPC Validation
- `fetchProductByUpc`
- `validateUpc`
- `validateAllUPCs`
- `applyItemNumbersToData`
- UPC validation state tracking (cells, rows)
### Validation Status Management
- `startValidatingCell`/`stopValidatingCell`
- `startValidatingRow`/`stopValidatingRow`
- `isValidatingCell`/`isRowValidatingUpc`
- Error state management
- `revalidateRows`
### Data Management
- Initial data cleaning/processing
- `updateRow`
- `copyDown`
- Search/filter functionality
- `filteredData` calculation
### Template Management
- `saveTemplate`
- `applyTemplate`
- `applyTemplateToSelected`
- `getTemplateDisplayText`
- `loadTemplates`/`refreshTemplates`
### Main Validation Hook
- Composition of all other hooks
- Initialization logic
- Button/navigation handling
- Field options management
## Implementation Approach
1. **Start with Types**: Create the types file first, as all other files will depend on it
2. **Create Utility Functions**: Move shared utilities next
3. **Build Core Validation**: Extract the field validation and uniqueness validation
4. **Separate UPC Logic**: Move all UPC-specific code to its own module
5. **Extract State Management**: Move data and status management to separate files
6. **Move Template Logic**: Extract template functionality
7. **Create Composition Hook**: Build the main hook that uses all other hooks
This approach will give you more maintainable code with clearer separation of concerns, making it easier to understand, test, and modify each component independently.

View File

@@ -0,0 +1,354 @@
## 1. ✅ Error Filtering Logic Inconsistency (RESOLVED)
> **Note: This issue has been resolved by implementing a type-based error system.**
The filtering logic in `ValidationCell.tsx` previously relied on string matching, which was fragile:
```typescript
// Old implementation (string-based matching)
const filteredErrors = React.useMemo(() => {
return !isEmpty(value)
? errors.filter(error => !error.message?.toLowerCase().includes('required'))
: errors;
}, [value, errors]);
// New implementation (type-based filtering)
const filteredErrors = React.useMemo(() => {
return !isEmpty(value)
? errors.filter(error => error.type !== ErrorType.Required)
: errors;
}, [value, errors]);
```
The solution implemented:
- Added an `ErrorType` enum in `types.ts` to standardize error categorization
- Updated all error creation to include the appropriate error type
- Modified error filtering to use the type property instead of string matching
- Ensured consistent error handling across the application
**Guidelines for future development:**
- Always use the `ErrorType` enum when creating errors
- Never rely on string matching for error filtering
- Ensure all error objects include the `type` property
- Use the appropriate error type for each validation rule:
- `ErrorType.Required` for required field validations
- `ErrorType.Regex` for regex validations
- `ErrorType.Unique` for uniqueness validations
- `ErrorType.Custom` for custom validations
- `ErrorType.Api` for API-based validations
## 2. ⚠️ Redundant Error Processing (PARTIALLY RESOLVED)
> **Note: This issue has been partially resolved by the re-rendering optimizations.**
The system still processes errors in multiple places:
- In `ValidationCell.tsx`, errors are filtered by the optimized `processErrors` function
- In `useValidation.tsx`, errors are generated at the field level
- In `ValidationContainer.tsx`, errors are manipulated at the container level
While the error processing has been optimized to be more efficient, there is still some redundancy in how errors are handled across components. However, the current implementation has mitigated the performance impact.
**Improvements made:**
- Created a central `processErrors` function in ValidationCell that efficiently handles error filtering
- Implemented a batched update system to reduce redundant error processing
- Added better memoization to avoid reprocessing errors when not needed
**Future improvement opportunities:**
- Further consolidate error processing logic into a single location
- Create a dedicated error handling service or hook
- Implement a more declarative approach to error handling
## 3. Race Conditions in Async Validation
async validations could create race conditions:
- If a user types quickly, multiple validation requests might be in flight
- Later responses could overwrite more recent ones if they complete out of order
- The debouncing helps but doesn't fully solve this issue
## 4. Memory Leaks in Timeout Management
The validation timeouts are stored in refs:
```typescript
const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({});
```
While there is cleanup on unmount, if rows are added/removed dynamically, timeouts for deleted rows might not be properly cleared.
## 5. ✅ Inefficient Error Storage (RESOLVED)
**Status: RESOLVED**
### Problem
Previously, validation errors were stored in multiple locations:
- In the `validationErrors` Map in `useValidationState`
- In the row data itself as `__errors`
This redundancy caused several issues:
- Inconsistent error states between the two storage locations
- Increased memory usage by storing the same information twice
- Complex state management to keep both sources in sync
- Difficulty reasoning about where errors should be accessed from
### Solution
We've implemented a unified error storage approach by:
- Making the `validationErrors` Map in `useValidationState` the single source of truth for all validation errors
- Removed the `__errors` property from row data
- Updated all validation functions to interact with the central error store instead of modifying row data
- Modified UPC validation to use the central error store
- Updated all components to read errors from the `validationErrors` Map instead of row data
### Key Changes
1. Modified `dataMutations.ts` to stop storing errors in row data
2. Updated the `Meta` type to remove the `__errors` property
3. Modified the `RowData` type to remove the `__errors` property
4. Updated the `useValidation` hook to return errors separately from row data
5. Modified the `useAiValidation` hook to work with the central error store
6. Updated the `useFilters` hook to check for errors in the `validationErrors` Map
7. Modified the `ValidationTable` and `ValidationCell` components to read errors from the `validationErrors` Map
### Benefits
- **Single Source of Truth**: All validation errors are now stored in one place
- **Reduced Memory Usage**: No duplicate storage of error information
- **Simplified State Management**: Only one state to update when errors change
- **Cleaner Data Structure**: Row data no longer contains validation metadata
- **More Maintainable Code**: Clearer separation of concerns between data and validation
### Future Improvements
While this refactoring addresses the core issue of inefficient error storage, there are still opportunities for further optimization:
1.**Redundant Error Processing**: ~~The validation process still performs some redundant calculations that could be optimized.~~ This has been largely addressed by the re-rendering optimizations.
2. **Race Conditions**: Async validation can lead to race conditions when multiple validations are triggered in quick succession.
3. **Memory Leaks**: The timeout management for validation could be improved to prevent potential memory leaks.
4. **Tight Coupling**: Components are still tightly coupled to the validation state structure.
5. **Error Prioritization**: The system doesn't prioritize errors well, showing all errors at once rather than focusing on the most critical ones first.
### Validation Flow
The validation process now works as follows:
1. **Error Generation**:
- Field-level validations generate errors based on validation rules
- Row-level hooks add custom validation errors
- Table-level validations (like uniqueness checks) add errors across rows
2. **Error Storage**:
- All errors are stored in the `validationErrors` Map in `useValidationState`
- The Map uses row indices as keys and objects of field errors as values
3. **Error Display**:
- The `ValidationTable` component checks the `validationErrors` Map to highlight rows with errors
- The `ValidationCell` component receives errors for specific fields from the `validationErrors` Map
- Errors are filtered in the UI to avoid showing "required" errors for fields with values
This focused refactoring approach has successfully addressed a critical issue while keeping changes manageable and targeted.
## 6. ✅ Excessive Re-rendering (RESOLVED)
**Status: RESOLVED**
### Problem
The validation system was suffering from excessive re-renders due to several key issues:
- **Inefficient Error Filtering**: The ValidationCell component was filtering errors on every render
- **Redundant Error Processing**: The same validation work was repeated in multiple components
- **Poor Memoization**: Components were inadequately memoized, causing unnecessary re-renders
- **Inefficient Batch Updates**: The state update system wasn't optimally batching changes
These issues led to performance problems, especially with large datasets, and affected the user experience.
### Solution
We've implemented a comprehensive optimization approach:
- **Optimized Error Processing**: Created an efficient `processErrors` function in ValidationCell that calculates all derived state in one pass
- **Enhanced Memoization**: Improved memo comparison functions to avoid unnecessary rerenders
- **Improved Batch Updates**: Redesigned the batching system to aggregate multiple changes before state updates
- **Single Update Pattern**: Implemented a queue-based update mechanism that applies multiple state changes at once
### Key Changes
1. Added a more efficient error processing function in ValidationCell
2. Created an enhanced error comparison function to properly compare error arrays
3. Improved the memo comparison function in ValidationCell
4. Added a batch update system in useValidationState
5. Implemented a queue-based update mechanism for row modifications
### Benefits
- **Improved Performance**: Reduced render cycles = faster UI response
- **Better User Experience**: Less lag when editing large datasets
- **Reduced Memory Usage**: Fewer component instantiations and temporary objects
- **Increased Scalability**: The application can now handle larger datasets without slowdown
- **Maintainable Code**: More predictable update flow that's easier to debug and extend
### Guidelines for future development
- Use the `processErrors` function for error filtering and processing
- Ensure React.memo components have proper comparison functions
- Use the batched update system for state changes
- Maintain stable references to objects and functions
- Use appropriate React hooks (useMemo, useCallback) with correct dependencies
- Avoid unnecessary recreations of arrays, objects, and functions
## 7. Complex Error Merging Logic
When merging errors from different sources, the logic is complex and potentially error-prone:
```typescript
// Merge field errors and row hook errors
const mergedErrors: Record<string, InfoWithSource> = {}
// Convert field errors to InfoWithSource
Object.entries(fieldErrors).forEach(([key, errors]) => {
if (errors.length > 0) {
mergedErrors[key] = {
message: errors[0].message,
level: errors[0].level,
source: ErrorSources.Row,
type: errors[0].type || ErrorType.Custom
}
}
})
```
This only takes the first error for each field, potentially hiding important validation issues.
## 8. ✅ Inconsistent Error Handling for Empty Values (PARTIALLY RESOLVED)
> **Note: This issue has been partially resolved by standardizing the isEmpty function and error type system.**
The system previously had different approaches to handling empty values:
- Some validations skipped empty values unless they're required
- Others processed empty values differently
- The `isEmpty` function was defined multiple times with slight variations
The solution implemented:
- Standardized the `isEmpty` function implementation
- Ensured consistent error type usage for required field validations
- Made error filtering consistent across the application
**Guidelines for future development:**
- Always use the shared `isEmpty` function for checking empty values
- Ensure consistent handling of empty values across all validation rules
- Use the `ErrorType.Required` type for all required field validations
## 9. Tight Coupling Between Components
The validation system is tightly coupled across components:
- `ValidationCell` needs to understand the structure of errors
- `ValidationTable` needs to extract and pass the right errors
- `ValidationContainer` directly manipulates the error structure
This makes it harder to refactor or reuse components independently.
## 10. Limited Error Prioritization
There's no clear prioritization of errors:
- When multiple errors exist for a field, which one should be shown first?
- Are some errors more important than others?
- The current system mostly shows the first error it finds
A more robust approach would be to have a consistent error source identification system and a clear prioritization strategy for displaying errors.
------------
Let me explain how these hooks fit together to create the validation errors that eventually get filtered in the `ValidationCell` component:
## The Validation Flow
1. **useValidationState Hook**:
This is the main state management hook used by the `ValidationContainer` component. It:
- Manages the core data state (`data`)
- Tracks validation errors in a Map (`validationErrors`)
- Provides functions to update and validate rows
2. **useValidation Hook**:
This is a utility hook that provides the core validation logic:
- `validateField`: Validates a single field against its validation rules
- `validateRow`: Validates an entire row, field by field
- `validateTable`: Runs table-level validations
- `validateUnique`: Checks for uniqueness constraints
- `validateData`: Orchestrates the complete validation process
## How Errors Are Generated
Validation errors come from multiple sources:
1. **Field-Level Validations**:
In `useValidation.tsx`, the `validateField` function checks individual fields against rules like:
- `required`: Field must have a value
- `regex`: Value must match a pattern
- `min`/`max`: Numeric constraints
2. **Row-Level Validations**:
The `validateRow` function in `useValidation.tsx` runs:
- Field validations for each field in the row
- Special validations for required fields like supplier and company
- Custom row hooks provided by the application
3. **Table-Level Validations**:
- `validateUnique` checks for duplicate values in fields marked as unique
- `validateTable` runs custom table hooks for cross-row validations
4. **API-Based Validations**:
In `useValidationState.tsx` and `ValidationContainer.tsx`:
- UPC validation via API calls
- Item number uniqueness checks
## The Error Flow
1. Errors are collected in the `validationErrors` Map in `useValidationState`
2. This Map is passed to `ValidationTable` as a prop
3. `ValidationTable` extracts the relevant errors for each cell and passes them to `ValidationCell`
4. In `ValidationCell`, the errors are filtered based on whether the cell has a value:
```typescript
// Updated implementation using type-based filtering
const filteredErrors = React.useMemo(() => {
return !isEmpty(value)
? errors.filter(error => error.type !== ErrorType.Required)
: errors;
}, [value, errors]);
```
## Key Insights
1. **Error Structure**:
Errors now have a consistent structure with type information:
```typescript
type ErrorObject = {
message: string;
level: string; // 'error', 'warning', etc.
source?: ErrorSources; // Where the error came from
type: ErrorType; // The type of error (Required, Regex, Unique, etc.)
}
```
2. **Error Sources**:
Errors can come from:
- Field validations (required, regex, etc.)
- Row validations (custom business logic)
- Table validations (uniqueness checks)
- API validations (UPC checks)
3. **Error Types**:
Errors are now categorized by type:
- `ErrorType.Required`: Field is required but empty
- `ErrorType.Regex`: Value doesn't match the regex pattern
- `ErrorType.Unique`: Value must be unique across rows
- `ErrorType.Custom`: Custom validation errors
- `ErrorType.Api`: Errors from API calls
4. **Error Filtering**:
The filtering in `ValidationCell` is now more robust:
- When a field has a value, errors of type `ErrorType.Required` are filtered out
- When a field is empty, all errors are shown
5. **Performance Optimizations**:
- Batch processing of validations
- Debounced updates to avoid excessive re-renders
- Memoization of computed values

View File

@@ -0,0 +1,538 @@
# ValidationTable Scroll Position Issue
## Problem Description
The `ValidationTable` component in the inventory application suffers from a persistent scroll position issue. When the table content updates or re-renders, the scroll position resets to the top left corner. This creates a poor user experience, especially when users are working with large datasets and need to maintain their position while making edits or filtering data.
Specific behaviors:
- Scroll position resets to the top left corner during re-renders
- User loses their place in the table when data is updated
- The table does not preserve vertical or horizontal scroll position
## Relevant Files
- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx`**
- Main component that renders the validation table
- Handles scroll position management
- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx`**
- Parent component that wraps ValidationTable
- Creates an EnhancedValidationTable wrapper component
- Manages data and state for the validation table
- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx`**
- Provides state management and data manipulation functions
- Contains scroll-related code in the `updateRow` function
- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx`**
- Renders individual cells in the table
- May influence re-renders that affect scroll position
## Failed Attempts
We've tried multiple approaches to fix the scroll position issue, none of which have been successful:
### 1. Using Refs for Scroll Position
```typescript
const scrollPosition = useRef({
left: 0,
top: 0
});
// Capture position on scroll
const handleScroll = useCallback(() => {
if (tableContainerRef.current) {
scrollPosition.current = {
left: tableContainerRef.current.scrollLeft,
top: tableContainerRef.current.scrollTop
};
}
}, []);
// Restore in useLayoutEffect
useLayoutEffect(() => {
const container = tableContainerRef.current;
if (container) {
const { left, top } = scrollPosition.current;
if (left || top) {
container.scrollLeft = left;
container.scrollTop = top;
}
}
});
```
Result: Scroll position was still lost during updates.
### 2. Multiple Restoration Attempts with Timeouts
```typescript
// Multiple timeouts at different intervals
setTimeout(() => {
if (tableContainerRef.current) {
tableContainerRef.current.scrollTop = savedPosition.top;
tableContainerRef.current.scrollLeft = savedPosition.left;
}
}, 0);
setTimeout(() => {
if (tableContainerRef.current) {
tableContainerRef.current.scrollTop = savedPosition.top;
tableContainerRef.current.scrollLeft = savedPosition.left;
}
}, 50);
// Additional timeouts at 100ms, 300ms
```
Result: Still not reliable, scroll position would reset between timeouts or after all timeouts completed.
### 3. Using MutationObserver and ResizeObserver
```typescript
// Create a mutation observer to detect DOM changes
const mutationObserver = new MutationObserver(() => {
if (shouldPreserveScroll) {
restoreScrollPosition();
}
});
// Start observing the table for DOM changes
mutationObserver.observe(scrollableContainer, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});
// Create a resize observer
const resizeObserver = new ResizeObserver(() => {
if (shouldPreserveScroll) {
restoreScrollPosition();
}
});
// Observe the table container
resizeObserver.observe(scrollableContainer);
```
Result: Did not reliably maintain scroll position, and sometimes caused other rendering issues.
### 4. Recursive Restoration Approach
```typescript
let attempts = 0;
const maxAttempts = 5;
const restore = () => {
if (tableContainerRef.current) {
tableContainerRef.current.scrollTop = y;
tableContainerRef.current.scrollLeft = x;
attempts++;
if (attempts < maxAttempts) {
setTimeout(restore, 50 * attempts);
}
}
};
restore();
```
Result: No improvement, scroll position still reset.
### 5. Using React State for Scroll Position
```typescript
const [scrollPos, setScrollPos] = useState<{top: number; left: number}>({top: 0, left: 0});
// Track the scroll event
useEffect(() => {
const handleScroll = () => {
if (scrollContainerRef.current) {
setScrollPos({
top: scrollContainerRef.current.scrollTop,
left: scrollContainerRef.current.scrollLeft
});
}
};
// Add scroll listener...
}, []);
// Restore scroll position
useLayoutEffect(() => {
const container = scrollContainerRef.current;
const { top, left } = scrollPos;
if (top > 0 || left > 0) {
requestAnimationFrame(() => {
if (container) {
container.scrollTop = top;
container.scrollLeft = left;
}
});
}
}, [scrollPos, data]);
```
Result: Caused the screen to shake violently when scrolling and did not preserve position.
### 6. Using Key Attribute for Stability
```typescript
return (
<div
key="validation-table-container"
ref={scrollContainerRef}
className="overflow-auto max-h-[calc(100vh-300px)]"
>
{/* Table content */}
</div>
);
```
Result: Did not resolve the issue and may have contributed to rendering instability.
### 7. Removing Scroll Management from Other Components
We removed scroll position management code from:
- `useValidationState.tsx` (in the updateRow function)
- `ValidationContainer.tsx` (in the enhancedUpdateRow function)
Result: This did not fix the issue either.
### 8. Simple Scroll Position Management with Event Listeners
```typescript
// Create a ref to store scroll position
const scrollPosition = useRef({ left: 0, top: 0 });
const tableContainerRef = useRef<HTMLDivElement>(null);
// Save scroll position when scrolling
const handleScroll = useCallback(() => {
if (tableContainerRef.current) {
scrollPosition.current = {
left: tableContainerRef.current.scrollLeft,
top: tableContainerRef.current.scrollTop
};
}
}, []);
// Add scroll listener
useEffect(() => {
const container = tableContainerRef.current;
if (container) {
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
// Restore scroll position after data changes
useLayoutEffect(() => {
const container = tableContainerRef.current;
if (container) {
const { left, top } = scrollPosition.current;
if (left > 0 || top > 0) {
container.scrollLeft = left;
container.scrollTop = top;
}
}
}, [data]);
```
Result: Still did not maintain scroll position during updates.
### 9. Memoized Scroll Container Component
```typescript
// Create a stable scroll container that won't re-render with the table
const ScrollContainer = React.memo(({ children }: { children: React.ReactNode }) => {
const containerRef = useRef<HTMLDivElement>(null);
const scrollPosition = useRef({ left: 0, top: 0 });
const handleScroll = useCallback(() => {
if (containerRef.current) {
scrollPosition.current = {
left: containerRef.current.scrollLeft,
top: containerRef.current.scrollTop
};
}
}, []);
useEffect(() => {
const container = containerRef.current;
if (container) {
// Set initial scroll position if it exists
if (scrollPosition.current.left > 0 || scrollPosition.current.top > 0) {
container.scrollLeft = scrollPosition.current.left;
container.scrollTop = scrollPosition.current.top;
}
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
return (
<div ref={containerRef} className="overflow-auto max-h-[calc(100vh-300px)]">
{children}
</div>
);
});
```
Result: Still did not maintain scroll position during updates, even with a memoized container.
### 10. Using TanStack Table State Management
```typescript
// Track scroll state in the table instance
const [scrollState, setScrollState] = useState({ scrollLeft: 0, scrollTop: 0 });
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
state: {
rowSelection,
// Include scroll position in table state
scrollLeft: scrollState.scrollLeft,
scrollTop: scrollState.scrollTop
},
onStateChange: (updater) => {
if (typeof updater === 'function') {
const newState = updater({
rowSelection,
scrollLeft: scrollState.scrollLeft,
scrollTop: scrollState.scrollTop
});
if ('scrollLeft' in newState || 'scrollTop' in newState) {
setScrollState({
scrollLeft: newState.scrollLeft ?? scrollState.scrollLeft,
scrollTop: newState.scrollTop ?? scrollState.scrollTop
});
}
}
}
});
// Handle scroll events
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
const target = event.target as HTMLDivElement;
setScrollState({
scrollLeft: target.scrollLeft,
scrollTop: target.scrollTop
});
}, []);
// Restore scroll position after updates
useLayoutEffect(() => {
if (tableContainerRef.current) {
tableContainerRef.current.scrollLeft = scrollState.scrollLeft;
tableContainerRef.current.scrollTop = scrollState.scrollTop;
}
}, [data, scrollState]);
```
Result: Still did not maintain scroll position during updates, even with table state management.
### 11. Using CSS Sticky Positioning
```typescript
return (
<div className="relative max-h-[calc(100vh-300px)] overflow-auto">
<Table>
<TableHeader className="sticky top-0 z-10 bg-background">
<TableRow>
{table.getFlatHeaders().map((header) => (
<TableHead
key={header.id}
style={{
width: `${header.getSize()}px`,
minWidth: `${header.getSize()}px`,
position: 'sticky',
top: 0,
backgroundColor: 'inherit'
}}
>
{/* Header content */}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{/* Table body content */}
</TableBody>
</Table>
</div>
);
```
Result: Still did not maintain scroll position during updates, even with native CSS scrolling.
### 12. Optimized Memoization with Object.is
```typescript
// Memoize data structures to prevent unnecessary re-renders
const memoizedData = useMemo(() => data, [data]);
const memoizedValidationErrors = useMemo(() => validationErrors, [validationErrors]);
const memoizedValidatingCells = useMemo(() => validatingCells, [validatingCells]);
const memoizedItemNumbers = useMemo(() => itemNumbers, [itemNumbers]);
// Use Object.is for more efficient comparisons
export default React.memo(ValidationTable, (prev, next) => {
if (!Object.is(prev.data.length, next.data.length)) return false;
if (prev.validationErrors.size !== next.validationErrors.size) return false;
for (const [key, value] of prev.validationErrors) {
if (!next.validationErrors.has(key)) return false;
if (!Object.is(value, next.validationErrors.get(key))) return false;
}
// ... more optimized comparisons ...
});
```
Result: Caused the page to crash with "TypeError: undefined has no properties" in the MemoizedCell component.
### 13. Simplified Component Structure
```typescript
const ValidationTable = <T extends string>({
data,
fields,
rowSelection,
setRowSelection,
updateRow,
validationErrors,
// ... other props
}) => {
const tableContainerRef = useRef<HTMLDivElement>(null);
const lastScrollPosition = useRef({ left: 0, top: 0 });
// Simple scroll position management
const handleScroll = useCallback(() => {
if (tableContainerRef.current) {
lastScrollPosition.current = {
left: tableContainerRef.current.scrollLeft,
top: tableContainerRef.current.scrollTop
};
}
}, []);
useEffect(() => {
const container = tableContainerRef.current;
if (container) {
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
useLayoutEffect(() => {
const container = tableContainerRef.current;
if (container) {
const { left, top } = lastScrollPosition.current;
if (left > 0 || top > 0) {
requestAnimationFrame(() => {
if (container) {
container.scrollLeft = left;
container.scrollTop = top;
}
});
}
}
}, [data]);
return (
<div ref={tableContainerRef} className="overflow-auto max-h-[calc(100vh-300px)]">
<Table>
{/* ... table content ... */}
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className={cn(
"hover:bg-muted/50",
row.getIsSelected() ? "bg-muted/50" : "",
validationErrors.get(data.indexOf(row.original)) ? "bg-red-50/40" : ""
)}
>
{/* ... row content ... */}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
```
Result: Still did not maintain scroll position during updates. However, this implementation restored the subtle red highlight on rows with validation errors, which is a useful visual indicator that should be preserved in future attempts.
### 14. Portal-Based Scroll Container
```typescript
// Create a stable container outside of React's control
const createStableContainer = () => {
const containerId = 'validation-table-container';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.className = 'overflow-auto';
container.style.maxHeight = 'calc(100vh - 300px)';
document.body.appendChild(container);
}
return container;
};
const ValidationTable = <T extends string>({...props}) => {
const [container] = useState(createStableContainer);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => {
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
};
}, [container]);
// ... table configuration ...
return createPortal(content, container);
};
```
Result: The table contents failed to render at all. The portal-based approach to maintain scroll position by moving the scroll container outside of React's control was unsuccessful.
## Current Understanding
The scroll position issue appears to be complex and likely stems from multiple factors:
1. React's virtual DOM reconciliation may be replacing the scroll container element during updates
2. The table uses complex memo patterns with custom equality checks that may not be working as expected
3. The data structure may be changing in ways that cause complete re-renders
4. The component hierarchy (with EnhancedValidationTable wrapper) may be affecting DOM stability
## Next Steps to Consider
At this point, we have tried multiple approaches without success:
1. Various scroll position management techniques
2. Memoization and optimization strategies
3. Different component structures
4. Portal-based rendering
Given that none of these approaches have fully resolved the issue, it may be worth:
1. Investigating if there are any parent component updates forcing re-renders
2. Profiling the application to identify the exact timing of scroll position resets
3. Considering if the current table implementation could be simplified
4. Exploring if the data update patterns could be optimized to reduce re-renders
## Conclusion
The scroll position issue has proven resistant to multiple solution attempts. Each approach has either failed to maintain scroll position, introduced new issues, or in some cases (like the portal-based approach) prevented the table from rendering entirely. A deeper investigation into the component lifecycle and data flow may be necessary to identify the root cause.

View File

@@ -0,0 +1,53 @@
-- Templates table for storing import templates
CREATE TABLE IF NOT EXISTS templates (
id SERIAL PRIMARY KEY,
company TEXT NOT NULL,
product_type TEXT NOT NULL,
supplier TEXT,
msrp DECIMAL(10,2),
cost_each DECIMAL(10,2),
qty_per_unit INTEGER,
case_qty INTEGER,
hts_code TEXT,
description TEXT,
weight DECIMAL(10,2),
length DECIMAL(10,2),
width DECIMAL(10,2),
height DECIMAL(10,2),
tax_cat TEXT,
size_cat TEXT,
categories TEXT[],
ship_restrictions TEXT[],
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(company, product_type)
);
-- AI Validation Performance Tracking
CREATE TABLE IF NOT EXISTS ai_validation_performance (
id SERIAL PRIMARY KEY,
prompt_length INTEGER NOT NULL,
product_count INTEGER NOT NULL,
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
end_time TIMESTAMP WITH TIME ZONE NOT NULL,
duration_seconds DECIMAL(10,2) GENERATED ALWAYS AS (EXTRACT(EPOCH FROM (end_time - start_time))) STORED,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create index on prompt_length for efficient querying
CREATE INDEX IF NOT EXISTS idx_ai_validation_prompt_length ON ai_validation_performance(prompt_length);
-- Function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger to automatically update the updated_at column
CREATE TRIGGER update_templates_updated_at
BEFORE UPDATE ON templates
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -9,13 +9,17 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/diff": "^7.0.1",
"axios": "^1.8.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"diff": "^7.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"openai": "^4.85.3",
"pg": "^8.13.3", "pg": "^8.13.3",
"pm2": "^5.3.0", "pm2": "^5.3.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",
@@ -170,6 +174,27 @@
"node": ">=10" "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": { "node_modules/@pm2/io": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz",
@@ -308,6 +333,27 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "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": { "node_modules/@pm2/pm2-version-check": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz",
@@ -346,12 +392,49 @@
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/diff": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz",
"integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==",
"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/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC" "license": "ISC"
}, },
"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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -374,6 +457,18 @@
"node": ">= 14" "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": { "node_modules/amp": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
@@ -520,6 +615,12 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "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": { "node_modules/aws-ssl-profiles": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@@ -529,6 +630,17 @@
"node": ">= 6.0.0" "node": ">= 6.0.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/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -797,6 +909,18 @@
"color-support": "bin.js" "color-support": "bin.js"
} }
}, },
"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": { "node_modules/commander": {
"version": "2.15.1", "version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
@@ -955,6 +1079,15 @@
"node": ">= 14" "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/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -998,6 +1131,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -1087,6 +1229,21 @@
"node": ">= 0.4" "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": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1166,6 +1323,15 @@
"node": ">= 0.6" "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": { "node_modules/eventemitter2": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
@@ -1289,6 +1455,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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1337,20 +1537,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1542,6 +1728,21 @@
"url": "https://github.com/sponsors/ljharb" "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/has-unicode": { "node_modules/has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1648,6 +1849,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "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": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -2193,6 +2403,25 @@
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT" "license": "MIT"
}, },
"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": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -2388,6 +2617,36 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"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": { "node_modules/pac-proxy-agent": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz",
@@ -3672,6 +3931,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3742,6 +4007,15 @@
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
}, },
"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": ">= 14"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -3774,16 +4048,18 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "7.5.10", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=8.3.0" "node": ">=10.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"bufferutil": "^4.0.1", "bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2" "utf-8-validate": ">=5.0.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"bufferutil": { "bufferutil": {

View File

@@ -18,13 +18,17 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/diff": "^7.0.1",
"axios": "^1.8.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"diff": "^7.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"openai": "^4.85.3",
"pg": "^8.13.3", "pg": "^8.13.3",
"pm2": "^5.3.0", "pm2": "^5.3.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",

View File

@@ -0,0 +1,226 @@
I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response (e.g. do not include its key or any value) unless the specific field guidelines below say otherwise. If a product appears to be from an empty or entirely invalid line, do not include it in your response.
Your response should be a JSON object with the following structure:
{
"correctedData": [], // Array of corrected products
"changes": [], // Array of strings describing each change made
"warnings": [] // Array of strings with warnings or suggestions for manual review (see below for details)
}
IMPORTANT: For all fields that use IDs (categories, supplier, company, line, subline, ship_restrictions, tax_cat, artist, themes, etc.), you MUST return the ID values, not the display names. The system will handle converting IDs to display names.
Using the provided guidelines, focus on:
1. Correcting typos and any incorrect spelling or grammar
2. Standardizing product names
3. Correcting and enhancing descriptions by adding details, keywords, and SEO-friendly language
4. Fixing any obvious errors or inconsistencies between similar products in measurements, prices, or quantities
5. Adding correct categories, themes, and colors
Use only the provided data and your own knowledge to make changes. Do not make assumptions or make up information that you're not sure about. If you're unable to make a change you're confident about, leave the field as is. All data passed in should be validated, corrected, and returned. All values returned should be strings, not numbers. Do not leave out any fields that were present in the original data.
Possible reasons for including a warning in the warnings array:
- If you're unable to make a change you're confident about but you believe one needs to be made
- If there are inconsistencies in the data that could be valid but need to be reviewed
- If not enough information is provided to make a change that you believe is needed
- If you infer a value for a required field based on context
----------PRODUCT FIELD GUIDELINES----------
Fields: supplier, private_notes, company, line, subline, artist
Changes: Not allowed
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, return these fields exactly as provided with no changes
Fields: upc, supplier_no, notions_no, item_number
Changes: Formatting only
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, trim outside white space and return these fields exactly as provided with no other changes
Fields: hts_code
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, trim white space and any non-numeric characters, then return as a string. Do not validate in any other way.
Fields: image_url
Changes: Formatting only
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, convert all comma-separated values to valid https:// URLs and return
Fields: msrp, cost_each
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, strip any currency symbols and return as a string with exactly two decimal places, even if the last place is a 0.
Fields: qty_per_unit, case_qty
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, strip non-numeric characters and return
Fields: ship_restrictions
Changes: Only add a value if it's not already present
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0.
Instructions: Always return a value exactly as provided, or return 0 if no value is provided.
Fields: eta
Changes: Minimal, you can correct formatting, obvious errors or inconsistencies
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, return a full month name, day is optional, no year ever (e.g. “January” or “March 3”). This value is not required if not provided.
Fields: name
Changes: Allowed to conform to guidelines, to fix typos or formatting
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most reasonable value possible based on the naming guidelines and the other information you have.
Instructions: Always return a value that is corrected and enhanced per additional guidelines below
Fields: description
Changes: Full creative control allowed within guidelines
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most accurate description possible based on the description guidelines and the other information you have.
Instructions: Always return a value that is corrected and enhanced per additional guidelines below
Fields: weight, length, width, height
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return your best guess based on the other information you have or the dimensions for similar products.
Instructions: Always return a reasonable value (weights in ounces and dimensions in inches) that is validated against similar provided products and your knowledge of general object measurements (e.g. a sheet of paper is not going to be 3 inches thick, a pack of stickers is not going to be 250 ounces, this sheet of paper is very likely going to be the same size as that other sheet of paper from the same line). If a value is unusual or unreasonable, even wildly so, change it to match similar products or to be more reasonable. When correcting unreasonable weights or dimensions, prioritize comparisons to products from the same company and product line first, then broader category matches or common knowledge if necessary.Do not return 0 or null for any of these fields.
Fields: coo
Changes: Formatting only
Required: Return if present in the original data. Do not return if not present.
Instructions: If present, convert all country names and abbreviations to the official ISO 3166-1 alpha-2 two-character country code. Convert any value with more than two characters to two characters only (e.g. "United States" or "USA" should both return "US").
Fields: tax_cat
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0.
Instructions: Always return a valid numerical tax code ID from the Available Tax Codes array below. Give preference to the value provided, but correct it if another value is more accurate. You must return a value for this field. 0 should be the default value in most cases.
Fields: size_cat
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable. Do not return if not applicable (e.g. if no size categories apply based on what you know about the product).
Instructions: If present or if applicable, return one valid numerical size category ID from the Available Size Categories array below. Give preference to the value provided, but correct it if another value is more accurate. If the product name contains a match for one of the size categories (such as 12x12, 6x6, 2oz, etc) you MUST return that size category with the results. A value is not required if none of the size categories apply.
Fields: themes
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no themes apply based on what you know about the product).
Instructions: If present, confirm that each provided theme matches what you understand to be a theme of the product. Remove any themes that do not match and add any themes that are missing. Most products will have zero or one theme. Return a comma-separated list of numerical theme IDs from the Available Themes array below. If you choose a sub-theme, you do not need to include its parent theme in the list.
Fields: colors
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no colors apply based on what you know about the product).
Instructions: If present or if applicable, return a comma-separated list of numerical color IDs from the Available Colors array below, using the product name as the primary guide (e.g. if the name contains Blue or a blue variant, you should return the blue color ID). A value is not required if none of the colors apply. Most products will have zero colors.
Fields: categories
Changes: Allowed to correct obvious errors or inconsistencies or to add missing values
Required: You must always return at least one value for this field, even if it's not provided in the original data. If no value is provided, return the most appropriate category or categories based on the other information you have.
Instructions: Always return a comma-separated list of one or more valid numerical category IDs from the Available Categories array below. Give preference to the values provided, particularly if the other information isn't enough to determine a category, but correct them or add new categories if another value is more accurate. Do not return categories in the Deals or Black Friday categories, and strip these from the list if present. If you choose a subcategory at any level, you do not need to include its parent categories in the list. You must return at least one category and you can return multiple categories if applicable. All categories have equal value so their order is not important. Always try to return the most specific categories possible (e.g. one in the third level of the category hierarchy is better than one in the second level).
----------PRODUCT NAMING GUIDELINES----------
If there's only one of this type of product in a line: [Line Name] [Product Name] - [Company]
Example: "Cosmos Infinity Chipboard - Stamperia"
Example: "Serene Petals 6x6 Paper Pad - Prima"
Multiple similar products in a line: [Differentiator] [Product Type] - [Line Name] - [Company]
Example: "Ice & Shells Stencil - Arctic Antarctic - Stamperia"
Example: "Astronomy Paper - Cosmos Infinity - Stamperia"
Standalone products: [Product Name] - [Company]
Example: "Hedwig Puffy Stickers - Paper House Productions"
Example: "Heart Tree Dies - Lawn Fawn"
Color-based products: [Color] [Product Name] - [Company]
Example: "Green Valley Enamel Dots - Altenew"
Example: "Magenta Aqua Pigment - Brutus Monroe"
Complex products: [Differentiator] [Line] [Product Type] - [Company]
Example: "Size 6 Round Black Velvet Watercolor Brush - Silver Brush Limited" (Size 6 Round is the differentiator, Black Velvet is the line, Watercolor Brush is the product type)
These should not be included in the name, unless there are multiple products that are otherwise identical:
- Product size
- Product weight
- Number of pages
- How many are in the package
Naming Conventions:
- Paper sizes: Use "12x12", "8x8", "6x6" (no spaces or units of measure)
- Company names must match backend exactly
- Always capitalize every word in the name, including short articles like "The" and "An"
- Use "Idea-ology" (not "idea-ology" or "Ideaology")
- All stamps are "Stamp Set" (not "Clear Stamps" or "Rubber Stamps")
- All dies are "Dies" or "Die" (not "Die Set")
- Brands with their own naming conventions should be respected, such as "Doodle Cuts" for dies from Doodlebug
Special Brand Rules - Ranger:
Format: [Product Name] - [Designer Line] - Ranger
Possible Designers: Dylusions, Dina Wakley MEdia, Simon Hurley create., Wendy Vecchi
Example: "Stacked Stencil - Dina Wakley MEdia - Ranger"
Special Brand Rules - Tim Holtz products from Ranger:
Format: [Color] [Product Name/Type] - Tim Holtz Distress - Ranger
Example: "Mermaid Lagoon Tim Holtz Distress Oxide Ink Pad - Ranger"
Special Brand Rules - Tim Holtz products from Sizzix or Stampers Anonymous:
Format: [Product Name] [Product Type] by Tim Holtz - [Company]
Example: "Leaf Fragments Thinlits Dies by Tim Holtz - Sizzix"
Special Brand Rules - Tim Holtz products from Advantus/Idea-ology:
Format: [Product Name] - Tim Holtz Idea-ology
Example: "Tiny Vials - Tim Holtz Idea-ology"
Special Brand Rules - Dies from Sizzix:
Include die type plus "Dies" or "Die"
Examples:
"Art Nouveau 3-D Textured Impressions Embossing Folder - Sizzix"
"Pocket Pals Thinlits Dies - Sizzix"
"Butterfly Wishes Framelits Dies & Stamps - Sizzix"
Important Notes
- Ensure that product names are consistent across all products of the same type
- Use the minimum amount of information needed to uniquely identify the product
- Put detailed specifications in the product description, not its name
Edge Cases
- If the product is missing a company name, infer one from the other products included in the data
- If the product is missing a clear differentiator and needs one to be unique, infer and add one from the other data provided (e.g. the description, existing size categories, etc.)
Incorrect example: MVP Rugby - Collection Pack - Photoplay
Notes: there should be no dash between the line and the product
Incorrect Example: A2 Easel Cards - Black - Photoplay
Notes: the differentiating factor should come first: “Black A2 Easel Cards - Photoplay”. Size is ok to include here because this is the name printed on the package.
Incorrect Example: 6” - Scriber Needle Modeling Tool
Notes: this product only comes in one size, so 6” isnt needed. The company name should also be included.
Incorrect Example: Slick - White - Tulip Dimensional Fabric Paint 4oz
Notes: color should be first, then type, then product, then company, so “White Slick Dimensional Fabric Paint - Tulip”. It appears theres only one size available so no need to differentiate in the name.
Incorrect Example: Silhouette Adhesive Cork Sheets 5”X7” 8/Pkg
Notes: should be “Adhesive Cork Sheets - Silhouette”
Incorrect Example: Galaxy - Opaque - American Crafts Color Pour Resin Dyes
Notes: “Galaxy Opaque Dye Set - Color Pour Resin - American Crafts”
Incorrect Example: Slate - Lion Brand Truboo Yarn
Notes: [Differentiator] [Line] [Product Type] - [Company] : “Slate Truboo Yarn - Lion Brand”
Incorrect Example: Rose Quartz Dylusions Shimmer Paint
Notes: “Rose Quartz Shimmer Paint - Dylusions - Ranger”
----------PRODUCT DESCRIPTION GUIDELINES----------
Product descriptions are an extremely important part of the listing and are the most important part of your response. Care should be taken to ensure they are correct, helpful, and SEO-friendly.
If a description is provided in the data, use it as a starting point. Correct any spelling errors, typos, poor grammar, or awkward phrasing. If necessary and you have the information, add more details, describe how the customer could use it, etc. Use complete sentences and keep SEO in mind.
If no description is provided, make one up using the product name, the information you have, and the other provided guidelines. At minimum, a description should be one complete sentence that starts with a capital letter and ends with a period. Unless the product is extremely complex, 2-4 sentences is usually sufficient if you have enough information.
Important Notes:
- Every description should state exactly what's included in the product (e.g. "Includes one 12x12 sheet of patterned cardstock." or "Includes one 6x12 sheet with 27 unique stickers." or "Includes 55 pieces." or "Package includes machine, power cord, 12 sheets of cardstock, 3 dies, and project instructions.")
- Do not use the word "our" in the description (this usually shows up when we copy a description from the manufacturer). Instead use "these" or "[Company name] [product]" or similar. (e.g. don't use "Our journals are hand-made in the USA", instead use "These journals are hand made..." or "Archer & Olive journals are handmade...")
- Don't include statements that add no value like “this is perfect for all your paper crafts”. If the product helps to solve a unique problem or has a unique feature, by all means describe it, but if its just a normal sheet of paper or pack of stickers, you dont have to pretend like its the best thing ever. At the same time, ensure that you add enough copy to ensure good SEO.
- State as many facts as you can about the product, considering the viewpoint of the customer and what they would want to know when looking at it. They probably want to know dimensions, what products its compatible with, how thick the paper is, how many sheets are included, whether the sheets are double-sided or not, which items are in the kit, etc. Say as much as you possibly can with the information that you have.
- !!DO NOT make up information if you aren't sure about it. A minimal correct description is better than a long incorrect one!!
Avoid/remove:
- The word "Imported"
- Any warnings about Prop 65, choking hazards, etc
- The manufacturer's name if it's included as the very first thing in the description
- Any statement similar to "comes in a variety of colors, each sold separately"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
const express = require('express');
const { getPool } = require('../utils/db');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, "../../.env") });
const router = express.Router();
// Get all templates
router.get('/', async (req, res) => {
try {
const pool = getPool();
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
SELECT * FROM templates
ORDER BY company ASC, product_type ASC
`);
res.json(result.rows);
} catch (error) {
console.error('Error fetching templates:', error);
res.status(500).json({
error: 'Failed to fetch templates',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Get template by company and product type
router.get('/:company/:productType', async (req, res) => {
try {
const { company, productType } = req.params;
const pool = getPool();
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
SELECT * FROM templates
WHERE company = $1 AND product_type = $2
`, [company, productType]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Template not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching template:', error);
res.status(500).json({
error: 'Failed to fetch template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Create new template
router.post('/', async (req, res) => {
try {
const {
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
} = req.body;
// Validate required fields
if (!company || !product_type) {
return res.status(400).json({ error: 'Company and Product Type are required' });
}
const pool = getPool();
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
INSERT INTO templates (
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING *
`, [
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
]);
res.status(201).json(result.rows[0]);
} catch (error) {
console.error('Error creating template:', error);
// Check for unique constraint violation
if (error instanceof Error && error.message.includes('unique constraint')) {
return res.status(409).json({
error: 'Template already exists for this company and product type',
details: error.message
});
}
res.status(500).json({
error: 'Failed to create template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Update template
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const {
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions
} = req.body;
// Validate required fields
if (!company || !product_type) {
return res.status(400).json({ error: 'Company and Product Type are required' });
}
const pool = getPool();
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
UPDATE templates
SET
company = $1,
product_type = $2,
supplier = $3,
msrp = $4,
cost_each = $5,
qty_per_unit = $6,
case_qty = $7,
hts_code = $8,
description = $9,
weight = $10,
length = $11,
width = $12,
height = $13,
tax_cat = $14,
size_cat = $15,
categories = $16,
ship_restrictions = $17
WHERE id = $18
RETURNING *
`, [
company,
product_type,
supplier,
msrp,
cost_each,
qty_per_unit,
case_qty,
hts_code,
description,
weight,
length,
width,
height,
tax_cat,
size_cat,
categories,
ship_restrictions,
id
]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Template not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Error updating template:', error);
// Check for unique constraint violation
if (error instanceof Error && error.message.includes('unique constraint')) {
return res.status(409).json({
error: 'Template already exists for this company and product type',
details: error.message
});
}
res.status(500).json({
error: 'Failed to update template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Delete template
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const pool = getPool();
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query('DELETE FROM templates WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Template not found' });
}
res.json({ message: 'Template deleted successfully' });
} catch (error) {
console.error('Error deleting template:', error);
res.status(500).json({
error: 'Failed to delete template',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Error handling middleware
router.use((err, req, res, next) => {
console.error('Template route error:', err);
res.status(500).json({
error: 'Internal server error',
details: err.message
});
});
module.exports = router;

View File

@@ -15,6 +15,9 @@ const configRouter = require('./routes/config');
const metricsRouter = require('./routes/metrics'); const metricsRouter = require('./routes/metrics');
const vendorsRouter = require('./routes/vendors'); const vendorsRouter = require('./routes/vendors');
const categoriesRouter = require('./routes/categories'); const categoriesRouter = require('./routes/categories');
const importRouter = require('./routes/import');
const aiValidationRouter = require('./routes/ai-validation');
const templatesRouter = require('./routes/templates');
// Get the absolute path to the .env file // Get the absolute path to the .env file
const envPath = '/var/www/html/inventory/.env'; const envPath = '/var/www/html/inventory/.env';
@@ -62,67 +65,78 @@ app.use((req, res, next) => {
app.use(corsMiddleware); app.use(corsMiddleware);
// Body parser middleware // Body parser middleware
app.use(express.json()); app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Initialize database pool // Initialize database pool and start server
const poolPromise = initPool({ async function startServer() {
host: process.env.DB_HOST, try {
user: process.env.DB_USER, // Initialize database pool
password: process.env.DB_PASSWORD, const pool = await initPool({
database: process.env.DB_NAME, host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432, user: process.env.DB_USER,
max: process.env.NODE_ENV === 'production' ? 20 : 10, password: process.env.DB_PASSWORD,
idleTimeoutMillis: 30000, database: process.env.DB_NAME,
connectionTimeoutMillis: 2000, port: process.env.DB_PORT || 5432,
ssl: process.env.DB_SSL === 'true' ? { max: process.env.NODE_ENV === 'production' ? 20 : 10,
rejectUnauthorized: false idleTimeoutMillis: 30000,
} : false connectionTimeoutMillis: 2000,
}); ssl: process.env.DB_SSL === 'true' ? {
rejectUnauthorized: false
} : false
});
// Make pool available to routes once initialized // Make pool available to routes
poolPromise.then(pool => { app.locals.pool = pool;
app.locals.pool = pool;
}).catch(err => {
console.error('[Database] Failed to initialize pool:', err);
process.exit(1);
});
// Routes // Set up routes after pool is initialized
app.use('/api/products', productsRouter); app.use('/api/products', productsRouter);
app.use('/api/dashboard', dashboardRouter); app.use('/api/dashboard', dashboardRouter);
app.use('/api/orders', ordersRouter); app.use('/api/orders', ordersRouter);
app.use('/api/csv', csvRouter); app.use('/api/csv', csvRouter);
app.use('/api/analytics', analyticsRouter); app.use('/api/analytics', analyticsRouter);
app.use('/api/purchase-orders', purchaseOrdersRouter); app.use('/api/purchase-orders', purchaseOrdersRouter);
app.use('/api/config', configRouter); app.use('/api/config', configRouter);
app.use('/api/metrics', metricsRouter); app.use('/api/metrics', metricsRouter);
app.use('/api/vendors', vendorsRouter); app.use('/api/vendors', vendorsRouter);
app.use('/api/categories', categoriesRouter); app.use('/api/categories', categoriesRouter);
app.use('/api/import', importRouter);
app.use('/api/ai-validation', aiValidationRouter);
app.use('/api/templates', templatesRouter);
// Basic health check route // Basic health check route
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ res.json({
status: 'ok', status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV environment: process.env.NODE_ENV
}); });
}); });
// CORS error handler - must be before other error handlers // CORS error handler - must be before other error handlers
app.use(corsErrorHandler); app.use(corsErrorHandler);
// Error handling middleware - MUST be after routes and CORS error handler // Error handling middleware - MUST be after routes and CORS error handler
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, err); console.error(`[${new Date().toISOString()}] Error:`, err);
// Send detailed error in development, generic in production // Send detailed error in development, generic in production
const error = process.env.NODE_ENV === 'production' const error = process.env.NODE_ENV === 'production'
? 'An internal server error occurred' ? 'An internal server error occurred'
: err.message || err; : err.message || err;
res.status(err.status || 500).json({ error }); res.status(err.status || 500).json({ error });
}); });
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`[Server] Running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// Handle uncaught exceptions // Handle uncaught exceptions
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
@@ -184,62 +198,5 @@ const setupSSE = (req, res) => {
} }
}; };
// Update the status endpoint to include reset-metrics // Start the server
app.get('/csv/status', (req, res) => { startServer();
res.json({
active: !!currentOperation,
type: currentOperation?.type || null,
progress: currentOperation ? {
status: currentOperation.status,
operation: currentOperation.operation,
current: currentOperation.current,
total: currentOperation.total,
percentage: currentOperation.percentage
} : null
});
});
// Update progress endpoint mapping
app.get('/csv/:type/progress', (req, res) => {
const { type } = req.params;
if (!['import', 'update', 'reset', 'reset-metrics'].includes(type)) {
res.status(400).json({ error: 'Invalid operation type' });
return;
}
setupSSE(req, res);
});
// Update the cancel endpoint to handle reset-metrics
app.post('/csv/cancel', (req, res) => {
const { operation } = req.query;
if (!currentOperation) {
res.status(400).json({ error: 'No operation in progress' });
return;
}
if (operation && operation.toLowerCase() !== currentOperation.type) {
res.status(400).json({ error: 'Operation type mismatch' });
return;
}
try {
// Handle cancellation based on operation type
if (currentOperation.type === 'reset-metrics') {
// Reset metrics doesn't need special cleanup
currentOperation = null;
res.json({ message: 'Reset metrics cancelled' });
} else {
// ... existing cancellation logic for other operations ...
}
} catch (error) {
console.error('Error during cancellation:', error);
res.status(500).json({ error: 'Failed to cancel operation' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`[Server] Running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);
});

View File

@@ -1,63 +1,10 @@
const { Pool, Client } = require('pg'); const mysql = require('mysql2/promise');
let pool; let pool;
function initPool(config) { function initPool(config) {
// Log config without sensitive data pool = mysql.createPool(config);
const safeConfig = { return pool;
host: config.host,
user: config.user,
database: config.database,
port: config.port,
max: config.max,
idleTimeoutMillis: config.idleTimeoutMillis,
connectionTimeoutMillis: config.connectionTimeoutMillis,
ssl: config.ssl,
password: config.password ? '[password set]' : '[no password]'
};
console.log('[Database] Initializing pool with config:', safeConfig);
// Try creating a client first to test the connection
const testClient = new Client({
host: config.host,
user: config.user,
password: config.password,
database: config.database,
port: config.port,
ssl: config.ssl
});
console.log('[Database] Testing connection with Client...');
return testClient.connect()
.then(() => {
console.log('[Database] Test connection with Client successful');
return testClient.end();
})
.then(() => {
// If client connection worked, create the pool
console.log('[Database] Creating pool...');
pool = new Pool({
host: config.host,
user: config.user,
password: config.password,
database: config.database,
port: config.port,
max: config.max,
idleTimeoutMillis: config.idleTimeoutMillis,
connectionTimeoutMillis: config.connectionTimeoutMillis,
ssl: config.ssl
});
return pool.connect();
})
.then(poolClient => {
console.log('[Database] Pool connection successful');
poolClient.release();
return pool;
})
.catch(err => {
console.error('[Database] Connection failed:', err);
throw err;
});
} }
async function getConnection() { async function getConnection() {

File diff suppressed because it is too large Load Diff

View File

@@ -10,58 +10,75 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1", "@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-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.6",
"@shadcn/ui": "^0.0.4", "@shadcn/ui": "^0.0.4",
"@tabler/icons-react": "^3.28.1", "@tabler/icons-react": "^3.28.1",
"@tanstack/react-query": "^5.63.0", "@tanstack/react-query": "^5.66.7",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.2", "@tanstack/react-virtual": "^3.11.2",
"@tanstack/virtual-core": "^3.11.2", "@tanstack/virtual-core": "^3.11.2",
"@types/js-levenshtein": "^1.1.3",
"@types/uuid": "^10.0.0",
"axios": "^1.8.1",
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"diff": "^7.0.0",
"framer-motion": "^12.4.4",
"js-levenshtein": "^1.1.6",
"lodash": "^4.17.21",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"motion": "^11.18.0", "motion": "^11.18.0",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-data-grid": "^7.0.0-beta.13",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.3.5",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.1.1",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"sonner": "^1.7.1", "sonner": "^1.7.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tanstack": "^1.0.0", "tanstack": "^1.0.0",
"vaul": "^1.1.2" "uuid": "^11.0.5",
"vaul": "^1.1.2",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.15",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",

View File

@@ -1,25 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
async function copyBuild() {
const sourcePath = path.resolve(__dirname, '../build');
const targetPath = path.resolve(__dirname, '../../inventory-server/frontend/build');
try {
// Ensure the target directory exists
await fs.ensureDir(path.dirname(targetPath));
// Remove old build if it exists
await fs.remove(targetPath);
// Copy new build
await fs.copy(sourcePath, targetPath);
console.log('Build files copied successfully to server directory!');
} catch (error) {
console.error('Error copying build files:', error);
process.exit(1);
}
}
copyBuild();

View File

@@ -15,6 +15,8 @@ import { RequireAuth } from './components/auth/RequireAuth';
import Forecasting from "@/pages/Forecasting"; import Forecasting from "@/pages/Forecasting";
import { Vendors } from '@/pages/Vendors'; import { Vendors } from '@/pages/Vendors';
import { Categories } from '@/pages/Categories'; import { Categories } from '@/pages/Categories';
import { Import } from '@/pages/Import';
import { AiValidationDebug } from "@/pages/AiValidationDebug"
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -60,6 +62,7 @@ function App() {
}> }>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Dashboard />} />
<Route path="/products" element={<Products />} /> <Route path="/products" element={<Products />} />
<Route path="/import" element={<Import />} />
<Route path="/categories" element={<Categories />} /> <Route path="/categories" element={<Categories />} />
<Route path="/vendors" element={<Vendors />} /> <Route path="/vendors" element={<Vendors />} />
<Route path="/orders" element={<Orders />} /> <Route path="/orders" element={<Orders />} />
@@ -67,6 +70,7 @@ function App() {
<Route path="/analytics" element={<Analytics />} /> <Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/forecasting" element={<Forecasting />} /> <Route path="/forecasting" element={<Forecasting />} />
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -99,7 +99,7 @@ export function CategoryPerformance() {
))} ))}
</Pie> </Pie>
<Tooltip <Tooltip
formatter={(value: number, name: string, props: any) => [ formatter={(value: number, _: string, props: any) => [
`$${value.toLocaleString()}`, `$${value.toLocaleString()}`,
<div key="tooltip"> <div key="tooltip">
<div className="font-medium">Category Path:</div> <div className="font-medium">Category Path:</div>
@@ -143,7 +143,7 @@ export function CategoryPerformance() {
/> />
<YAxis tickFormatter={(value) => `${value}%`} /> <YAxis tickFormatter={(value) => `${value}%`} />
<Tooltip <Tooltip
formatter={(value: number, name: string, props: any) => [ formatter={(value: number, _: string, props: any) => [
`${value.toFixed(1)}%`, `${value.toFixed(1)}%`,
<div key="tooltip"> <div key="tooltip">
<div className="font-medium">Category Path:</div> <div className="font-medium">Category Path:</div>

View File

@@ -96,7 +96,7 @@ export function ProfitAnalysis() {
/> />
<YAxis tickFormatter={(value) => `${value}%`} /> <YAxis tickFormatter={(value) => `${value}%`} />
<Tooltip <Tooltip
formatter={(value: number, name: string, props: any) => [ formatter={(value: number, _: string, props: any) => [
`${value.toFixed(1)}%`, `${value.toFixed(1)}%`,
<div key="tooltip"> <div key="tooltip">
<div className="font-medium">Category Path:</div> <div className="font-medium">Category Path:</div>

View File

@@ -33,15 +33,6 @@ interface BestSellerBrand {
growth_rate: string growth_rate: string
} }
interface BestSellerCategory {
cat_id: number;
name: string;
units_sold: number;
revenue: string;
profit: string;
growth_rate: string;
}
interface BestSellersData { interface BestSellersData {
products: Product[] products: Product[]
brands: BestSellerBrand[] brands: BestSellerBrand[]

View File

@@ -9,8 +9,8 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { AlertCircle, AlertTriangle } from "lucide-react"
import config from "@/config" import config from "@/config"
import { format } from "date-fns"
interface Product { interface Product {
pid: number; pid: number;
@@ -24,6 +24,24 @@ interface Product {
lead_time_status: string; lead_time_status: string;
} }
// Helper functions
const formatDate = (dateString: string) => {
return format(new Date(dateString), 'MMM dd, yyyy')
}
const getLeadTimeVariant = (status: string) => {
switch (status.toLowerCase()) {
case 'critical':
return 'destructive'
case 'warning':
return 'secondary'
case 'good':
return 'secondary'
default:
return 'secondary'
}
}
export function LowStockAlerts() { export function LowStockAlerts() {
const { data: products } = useQuery<Product[]>({ const { data: products } = useQuery<Product[]>({
queryKey: ["low-stock"], queryKey: ["low-stock"],

View File

@@ -5,7 +5,6 @@ import config from "@/config"
import { formatCurrency } from "@/lib/utils" import { formatCurrency } from "@/lib/utils"
import { ClipboardList, AlertCircle, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons import { ClipboardList, AlertCircle, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons
import { useState } from "react" import { useState } from "react"
import { PurchaseOrderStatus, ReceivingStatus } from "@/types/status-codes"
interface PurchaseMetricsData { interface PurchaseMetricsData {
activePurchaseOrders: number // Orders that are not canceled, done, or fully received activePurchaseOrders: number // Orders that are not canceled, done, or fully received

View File

@@ -41,14 +41,6 @@ export function TrendingProducts() {
signDisplay: "exceptZero", signDisplay: "exceptZero",
}).format(value / 100) }).format(value / 100)
const formatCurrency = (value: number) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value)
return ( return (
<> <>
<CardHeader> <CardHeader>

View File

@@ -169,7 +169,7 @@ export const renderSubComponent = ({ row }: { row: any }) => {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{products.map((product) => ( {products.map((product: Product) => (
<TableRow key={product.pid}> <TableRow key={product.pid}>
<TableCell> <TableCell>
<a <a

View File

@@ -8,6 +8,7 @@ import {
LogOut, LogOut,
Users, Users,
Tags, Tags,
FileSpreadsheet,
} from "lucide-react"; } from "lucide-react";
import { IconCrystalBall } from "@tabler/icons-react"; import { IconCrystalBall } from "@tabler/icons-react";
import { import {
@@ -35,6 +36,11 @@ const items = [
icon: Package, icon: Package,
url: "/products", url: "/products",
}, },
{
title: "Import",
icon: FileSpreadsheet,
url: "/import",
},
{ {
title: "Forecasting", title: "Forecasting",
icon: IconCrystalBall, icon: IconCrystalBall,

View File

@@ -0,0 +1,38 @@
import merge from "lodash/merge"
import { Steps } from "./steps/Steps"
import { Providers } from "./components/Providers"
import type { RsiProps } from "./types"
import { ModalWrapper } from "./components/ModalWrapper"
import { translations } from "./translationsRSIProps"
// Simple empty theme placeholder
export const defaultTheme = {}
export const defaultRSIProps: Partial<RsiProps<any>> = {
autoMapHeaders: true,
autoMapSelectValues: false,
allowInvalidSubmit: true,
autoMapDistance: 2,
isNavigationEnabled: false,
translations: translations,
uploadStepHook: async (value) => value,
selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }),
matchColumnsStepHook: async (table) => table,
dateFormat: "yyyy-mm-dd", // ISO 8601,
parseRaw: true,
} as const
export const ReactSpreadsheetImport = <T extends string>(propsWithoutDefaults: RsiProps<T>) => {
const props = merge({}, defaultRSIProps, propsWithoutDefaults)
const mergedTranslations =
props.translations !== translations ? merge(translations, props.translations) : translations
return (
<Providers rsiValues={{ ...props, translations: mergedTranslations }}>
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
<Steps />
</ModalWrapper>
</Providers>
)
}

View File

@@ -0,0 +1,104 @@
import type React from "react"
import {
Dialog,
DialogContent,
DialogOverlay,
DialogPortal,
DialogClose,
} from "@/components/ui/dialog"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
AlertDialogPortal,
AlertDialogOverlay,
} from "@/components/ui/alert-dialog"
import { useRsi } from "../hooks/useRsi"
import { useState, useCallback } from "react"
type Props = {
children: React.ReactNode
isOpen: boolean
onClose: () => void
}
export const ModalWrapper = ({ children, isOpen, onClose }: Props) => {
const { rtl, translations } = useRsi()
const [showCloseAlert, setShowCloseAlert] = useState(false)
// Create a handler that resets scroll positions before closing
const handleClose = useCallback(() => {
// Reset all scroll positions in the dialog
const scrollContainers = document.querySelectorAll('.overflow-auto, .overflow-scroll');
scrollContainers.forEach(container => {
if (container instanceof HTMLElement) {
// Reset scroll position to top-left
container.scrollTop = 0;
container.scrollLeft = 0;
}
});
// Call the original onClose handler
onClose();
}, [onClose]);
return (
<>
<Dialog open={isOpen} onOpenChange={() => setShowCloseAlert(true)} modal>
<DialogPortal>
<DialogOverlay className="bg-background/80 backdrop-blur-sm" />
<DialogContent
onEscapeKeyDown={(e) => {
e.preventDefault()
setShowCloseAlert(true)
}}
onPointerDownOutside={(e) => e.preventDefault()}
className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-[calc(100%-2rem)] h-[calc(100%-2rem)] max-w-[100vw] max-h-[100vh] flex flex-col overflow-hidden rounded-lg border bg-background p-0 shadow-lg sm:w-[calc(100%-3rem)] sm:h-[calc(100%-3rem)] md:w-[calc(100%-4rem)] md:h-[calc(100%-4rem)]"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<DialogClose className="absolute right-4 top-4" onClick={(e) => {
e.preventDefault()
setShowCloseAlert(true)
}} />
</AlertDialogTrigger>
</AlertDialog>
<div dir={rtl ? "rtl" : "ltr"} className="flex-1 overflow-auto">
{children}
</div>
</DialogContent>
</DialogPortal>
</Dialog>
<AlertDialog open={showCloseAlert} onOpenChange={setShowCloseAlert}>
<AlertDialogPortal>
<AlertDialogOverlay className="z-[1400]" />
<AlertDialogContent className="z-[1500]">
<AlertDialogHeader>
<AlertDialogTitle>
{translations.alerts.confirmClose.headerTitle}
</AlertDialogTitle>
<AlertDialogDescription>
{translations.alerts.confirmClose.bodyText}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setShowCloseAlert(false)}>
{translations.alerts.confirmClose.cancelButtonTitle}
</AlertDialogCancel>
<AlertDialogAction onClick={handleClose}>
{translations.alerts.confirmClose.exitButtonTitle}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialog>
</>
)
}

View File

@@ -0,0 +1,24 @@
import { createContext } from "react"
import type { RsiProps } from "../types"
export const RsiContext = createContext({} as any)
type ProvidersProps<T extends string> = {
children: React.ReactNode
rsiValues: RsiProps<T>
}
// No need for a root ID as we're not using Chakra anymore
export const rootId = "rsi-modal-root"
export const Providers = <T extends string>({ children, rsiValues }: ProvidersProps<T>) => {
if (!rsiValues.fields) {
throw new Error("Fields must be provided to react-spreadsheet-import")
}
return (
<RsiContext.Provider value={{ ...rsiValues }}>
{children}
</RsiContext.Provider>
)
}

View File

@@ -0,0 +1,23 @@
import type { DataGridProps, Column } from "react-data-grid"
import DataGrid from "react-data-grid"
import { useRsi } from "../hooks/useRsi"
export type { Column }
export type Props<TRow> = DataGridProps<TRow> & {
rowHeight?: number
hiddenHeader?: boolean
className?: string
style?: React.CSSProperties
}
export const Table = <TRow,>({ className, ...props }: Props<TRow>) => {
const { rtl } = useRsi()
return (
<DataGrid
className={"rdg-light " + (className || "")}
direction={rtl ? "rtl" : "ltr"}
{...props}
/>
)
}

View File

@@ -0,0 +1,9 @@
import { useContext } from "react"
import { RsiContext } from "../components/Providers"
import type { RsiProps } from "../types"
import type { MarkRequired } from "ts-essentials"
import type { defaultRSIProps } from "../ReactSpreadsheetImport"
import type { Translations } from "../translationsRSIProps"
export const useRsi = <T extends string>() =>
useContext<MarkRequired<RsiProps<T>, keyof typeof defaultRSIProps> & { translations: Translations }>(RsiContext)

View File

@@ -0,0 +1,3 @@
export { StepType } from "./steps/UploadFlow"
export { ReactSpreadsheetImport } from "./ReactSpreadsheetImport"
export * from "./types"

View File

@@ -0,0 +1,294 @@
import { useCallback, useState, useRef, useEffect, createRef } from "react";
import { useRsi } from "../../hooks/useRsi";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import {
DndContext,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay
} from '@dnd-kit/core';
import {
sortableKeyboardCoordinates} from '@dnd-kit/sortable';
import { Product } from "./types";
import { GenericDropzone } from "./components/GenericDropzone";
import { UnassignedImagesSection } from "./components/UnassignedImagesSection";
import { ProductCard } from "./components/ProductCard/ProductCard";
import { useDragAndDrop } from "./hooks/useDragAndDrop";
import { useProductImagesInit } from "./hooks/useProductImagesInit";
import { useProductImageOperations } from "./hooks/useProductImageOperations";
import { useBulkImageUpload } from "./hooks/useBulkImageUpload";
import { useUrlImageUpload } from "./hooks/useUrlImageUpload";
interface Props {
data: Product[];
file: File;
onBack?: () => void;
onSubmit: (data: Product[], file: File) => void | Promise<any>;
}
export const ImageUploadStep = ({
data,
file,
onBack,
onSubmit
}: Props) => {
useRsi();
const [isSubmitting, setIsSubmitting] = useState(false);
const fileInputRefs = useRef<{ [key: number]: React.RefObject<HTMLInputElement> }>({});
// Use our hook for product images initialization
const { productImages, setProductImages, getFullImageUrl } = useProductImagesInit(data);
// Use our hook for product image operations
const {
addImageToProduct,
handleImageUpload,
removeImage
} = useProductImageOperations({
data,
productImages,
setProductImages
});
// Use our hook for URL image uploads
const {
urlInputs,
processingUrls,
handleAddImageFromUrl,
updateUrlInput
} = useUrlImageUpload({
data,
setProductImages,
addImageToProduct
});
// Use our hook for bulk image uploads
const {
unassignedImages,
processingBulk,
showUnassigned,
setShowUnassigned,
handleBulkUpload,
assignImageToProduct,
removeUnassignedImage,
cleanupPreviewUrls
} = useBulkImageUpload({
data,
handleImageUpload
});
// Set up sensors for drag and drop with enhanced configuration
const sensors = useSensors(
useSensor(PointerSensor, {
// Make it responsive with less restrictive constraints
activationConstraint: {
distance: 1, // Reduced distance for more responsive drag
delay: 0, // No delay
tolerance: 5
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Use the drag and drop hook
const {
activeId,
activeImage,
activeDroppableId,
customCollisionDetection,
findContainer,
getProductContainerClasses,
handleDragStart,
handleDragOver,
handleDragEnd
} = useDragAndDrop({
productImages,
setProductImages,
data
});
// Initialize refs for each product
useEffect(() => {
// Create refs for each product's file input
data.forEach((_: Product, index: number) => {
if (!fileInputRefs.current[index]) {
fileInputRefs.current[index] = createRef<HTMLInputElement>();
}
});
}, [data]);
// Add this CSS for preventing browser drag behavior
useEffect(() => {
// Add a custom style element to the document head
const styleEl = document.createElement('style');
styleEl.textContent = `
.no-native-drag {
-webkit-user-drag: none;
user-select: none;
}
.no-native-drag img {
-webkit-user-drag: none;
}
`;
document.head.appendChild(styleEl);
return () => {
// Clean up on unmount
document.head.removeChild(styleEl);
// Clean up preview URLs
cleanupPreviewUrls();
};
}, []);
// Handle calling onSubmit with the current data
const handleSubmit = useCallback(async () => {
setIsSubmitting(true);
try {
// First, we need to ensure product_images is properly formatted for each product
const updatedData = [...data].map((product, index) => {
// Get all images for this product
const images = productImages
.filter(img => img.productIndex === index)
.map(img => img.imageUrl)
.filter(Boolean);
// Update the product with the formatted image URLs
return {
...product,
// Store as comma-separated string to ensure compatibility
product_images: images.join(',')
};
});
await onSubmit(updatedData, file);
} catch (error) {
console.error('Submit error:', error);
toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSubmitting(false);
}
}, [data, file, onSubmit, productImages]);
return (
<div className="flex flex-col h-[calc(100vh-9.5rem)] overflow-hidden">
{/* Header - fixed at top */}
<div className="px-8 py-6 bg-background shrink-0">
<h2 className="text-2xl font-semibold">Add Product Images</h2>
<p className="text-sm text-muted-foreground mt-1">
Drag images to reorder them or move them between products.
</p>
</div>
{/* Content area - only this part scrolls */}
<div className="flex-1 overflow-hidden">
<div className="h-full flex flex-col overflow-auto">
<div className="px-8 py-4 shrink-0">
<GenericDropzone
processingBulk={processingBulk}
unassignedImages={unassignedImages}
showUnassigned={showUnassigned}
onDrop={handleBulkUpload}
onShowUnassigned={() => setShowUnassigned(true)}
/>
</div>
<div className="px-8 py-2 shrink-0">
<UnassignedImagesSection
showUnassigned={showUnassigned}
unassignedImages={unassignedImages}
data={data}
onHide={() => setShowUnassigned(false)}
onAssign={assignImageToProduct}
onRemove={removeUnassignedImage}
/>
</div>
{/* Scrollable product cards */}
<div className="px-8 py-2 flex-1">
<DndContext
sensors={sensors}
collisionDetection={customCollisionDetection}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
autoScroll={{
threshold: {
x: 0,
y: 0.2,
}
}}
>
<div className="space-y-2">
{data.map((product: Product, index: number) => (
<ProductCard
key={index}
product={product}
index={index}
urlInput={urlInputs[index] || ''}
processingUrl={processingUrls[index] || false}
activeDroppableId={activeDroppableId}
activeId={activeId}
productImages={productImages}
fileInputRef={fileInputRefs.current[index] || createRef()}
onUrlInputChange={(value: string) => updateUrlInput(index, value)}
onUrlSubmit={(e: React.FormEvent) => {
e.preventDefault();
if (urlInputs[index]) {
handleAddImageFromUrl(index, urlInputs[index]);
}
}}
onImageUpload={(files: FileList | File[]) => handleImageUpload(files, index)}
onDragOver={(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}}
onRemoveImage={(imageId: string) =>
removeImage(productImages.findIndex(img => img.id === imageId))
}
getProductContainerClasses={() => getProductContainerClasses(index)}
findContainer={findContainer}
/>
))}
</div>
<DragOverlay>
{activeImage && (
<div className="relative border rounded-md overflow-hidden shadow-md bg-white">
<img
src={getFullImageUrl(activeImage.imageUrl)}
alt={activeImage.fileName}
className="w-24 h-24 object-contain "
/>
</div>
)}
</DragOverlay>
</DndContext>
</div>
</div>
</div>
{/* Footer - fixed at bottom */}
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-1 shrink-0">
{onBack && (
<Button variant="outline" onClick={onBack}>
Back
</Button>
)}
<Button
className="ml-auto"
onClick={handleSubmit}
disabled={isSubmitting || unassignedImages.length > 0}
>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{unassignedImages.length > 0 ? 'Assign all images first' : 'Submit'}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,30 @@
import { useDroppable } from '@dnd-kit/core';
interface DroppableContainerProps {
id: string;
children: React.ReactNode;
isEmpty: boolean;
}
export const DroppableContainer = ({ id, children, isEmpty }: DroppableContainerProps) => {
const { setNodeRef } = useDroppable({
id,
data: {
type: 'container',
isEmpty
}
});
return (
<div
ref={setNodeRef}
id={id}
data-droppable="true"
data-empty={isEmpty ? "true" : "false"}
className="w-full h-full flex flex-row flex-wrap gap-2"
style={{ minHeight: '100px' }} // Ensure minimum height for empty containers
>
{children}
</div>
);
};

View File

@@ -0,0 +1,73 @@
import { Button } from "@/components/ui/button";
import { Loader2, Upload } from "lucide-react";
import { useDropzone } from "react-dropzone";
import { cn } from "@/lib/utils";
interface GenericDropzoneProps {
processingBulk: boolean;
unassignedImages: { previewUrl: string; file: File }[];
showUnassigned: boolean;
onDrop: (files: File[]) => void;
onShowUnassigned: () => void;
}
export const GenericDropzone = ({
processingBulk,
unassignedImages,
showUnassigned,
onDrop,
onShowUnassigned
}: GenericDropzoneProps) => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp']
},
onDrop,
multiple: true
});
return (
<div
{...getRootProps()}
className={cn(
"border-2 border-dashed border-secondary-foreground/30 bg-muted/90 rounded-md w-full py-6 flex flex-col items-center justify-center cursor-pointer hover:bg-muted/70 transition-colors",
isDragActive && "border-primary bg-muted"
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center justify-center h-32 py-2">
{processingBulk ? (
<>
<Loader2 className="h-8 w-8 animate-spin text-primary mb-2" />
<p className="text-base text-muted-foreground">Processing images...</p>
</>
) : isDragActive ? (
<>
<Upload className="h-8 w-8 mb-2 text-primary" />
<p className="text-base text-muted-foreground mb-2">Drop images here</p>
<p className="text-sm text-muted-foreground">&nbsp;</p>
</>
) : (
<>
<Upload className="h-8 w-8 mb-2 text-muted-foreground" />
<p className="text-base text-muted-foreground mb-2">Drop images here or click to select</p>
<p className="text-sm text-muted-foreground">Images dropped here will be automatically assigned to products based on filename</p>
{unassignedImages.length > 0 && !showUnassigned && (
<Button
variant="link"
size="sm"
onClick={(e) => {
e.stopPropagation();
onShowUnassigned();
}}
className="mt-2 text-amber-600 dark:text-amber-400"
>
Show {unassignedImages.length} unassigned {unassignedImages.length === 1 ? 'image' : 'images'}
</Button>
)}
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,60 @@
import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { toast } from "sonner";
interface CopyButtonProps {
text: string;
itemKey: string;
}
export const CopyButton = ({ text }: CopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const canCopy = text && text !== 'N/A';
const copyToClipboard = () => {
if (!canCopy) return;
navigator.clipboard.writeText(text)
.then(() => {
// Show success state
setIsCopied(true);
// Show toast notification
toast.success(`Copied: ${text}`);
// Reset after 2 seconds
setTimeout(() => {
setIsCopied(false);
}, 2000);
})
.catch(err => {
console.error('Failed to copy:', err);
toast.error('Failed to copy to clipboard');
});
};
return (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
copyToClipboard();
}}
className={`ml-1 inline-flex items-center justify-center rounded-full p-1 transition-colors ${
canCopy
? isCopied
? "bg-green-100 text-green-600 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400"
: "text-muted-foreground hover:bg-gray-100 dark:hover:bg-gray-800"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!canCopy}
title={canCopy ? "Copy to clipboard" : "Nothing to copy"}
>
{isCopied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
);
};

View File

@@ -0,0 +1,39 @@
import { Upload } from "lucide-react";
import { useDropzone } from "react-dropzone";
import { cn } from "@/lib/utils";
interface ImageDropzoneProps {
productIndex: number;
onDrop: (files: File[]) => void;
}
export const ImageDropzone = ({ onDrop }: ImageDropzoneProps) => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp']
},
onDrop: (acceptedFiles) => {
onDrop(acceptedFiles);
},
});
return (
<div
{...getRootProps()}
className={cn(
"border-2 border-dashed border-secondary-foreground/30 bg-muted/90 rounded-md h-24 w-24 flex flex-col items-center justify-center self-center cursor-pointer hover:bg-muted/70 transition-colors shrink-0",
isDragActive && "border-primary bg-muted"
)}
>
<input {...getInputProps()} />
{isDragActive ? (
<div className="text-xs text-center text-muted-foreground p-1">Drop images here</div>
) : (
<>
<Upload className="h-5 w-5 mb-1 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Add Images</span>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,163 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Loader2, Link as LinkIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { ImageDropzone } from "./ImageDropzone";
import { SortableImage } from "./SortableImage";
import { CopyButton } from "./CopyButton";
import { ProductImageSortable, Product } from "../../types";
import { DroppableContainer } from "../DroppableContainer";
import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
interface ProductCardProps {
product: Product;
index: number;
urlInput: string;
processingUrl: boolean;
activeDroppableId: string | null;
activeId: string | null;
productImages: ProductImageSortable[];
fileInputRef: React.RefObject<HTMLInputElement>;
onUrlInputChange: (value: string) => void;
onUrlSubmit: (e: React.FormEvent) => void;
onImageUpload: (files: FileList | File[]) => void;
onDragOver: (e: React.DragEvent) => void;
onRemoveImage: (id: string) => void;
getProductContainerClasses: () => string;
findContainer: (id: string) => string | null;
}
export const ProductCard = ({
product,
index,
urlInput,
processingUrl,
activeDroppableId,
activeId,
productImages,
fileInputRef,
onUrlInputChange,
onUrlSubmit,
onImageUpload,
onDragOver,
onRemoveImage,
getProductContainerClasses,
findContainer
}: ProductCardProps) => {
// Function to get images for this product
const getProductImages = () => {
return productImages.filter(img => img.productIndex === index);
};
// Convert string container to number for internal comparison
const getContainerAsNumber = (id: string): number | null => {
const result = findContainer(id);
return result !== null ? parseInt(result) : null;
};
return (
<Card
className={cn(
"p-3 transition-colors",
activeDroppableId === `product-${index}` && activeId &&
getContainerAsNumber(activeId) !== index &&
"ring-2 ring-primary bg-primary/5"
)}
>
<CardContent className="p-0">
<div className="flex flex-col gap-2">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2">
<div className="flex flex-col lg:flex-row lg:items-baseline gap-0.5 lg:gap-4">
<h3 className="text-base font-medium">{product.name || `Product #${index + 1}`}</h3>
<div className="text-xs lg:text-sm text-muted-foreground">
<span className="font-medium">UPC:</span> {product.upc || 'N/A'}
<CopyButton text={product.upc || ''} itemKey={`upc-${index}`} />
{' | '}
<span className="font-medium">Supplier #:</span> {product.supplier_no || 'N/A'}
<CopyButton text={product.supplier_no || ''} itemKey={`supplier-${index}`} />
</div>
</div>
<div className="flex-shrink-0">
<form
className="flex items-center gap-2"
onSubmit={onUrlSubmit}
>
<Input
placeholder="Add image from URL"
value={urlInput}
onChange={(e) => onUrlInputChange(e.target.value)}
className="!text-xs h-8 w-[180px]"
/>
<Button
type="submit"
size="sm"
className="h-8 whitespace-nowrap flex gap-1 items-center text-xs"
disabled={processingUrl || !urlInput}
>
{processingUrl ?
<Loader2 className="h-3.5 w-3.5 animate-spin" /> :
<LinkIcon className="h-3.5 w-3.5" />}
Add
</Button>
</form>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<div className="flex flex-row gap-2 items-start">
<ImageDropzone
productIndex={index}
onDrop={onImageUpload}
/>
</div>
<div
className={getProductContainerClasses()}
style={{
pointerEvents: 'auto',
touchAction: 'none',
minHeight: '100px',
}}
onDragOver={onDragOver}
>
<DroppableContainer
id={`product-${index}`}
isEmpty={getProductImages().length === 0}
>
{getProductImages().length > 0 ? (
<SortableContext
items={getProductImages().map(img => img.id)}
strategy={horizontalListSortingStrategy}
>
{getProductImages().map((image, imgIndex) => (
<SortableImage
key={image.id}
image={image}
productIndex={index}
imgIndex={imgIndex}
productName={product.name}
removeImage={onRemoveImage}
/>
))}
</SortableContext>
) : (
<div className="w-full h-full" data-empty-placeholder="true"></div>
)}
</DroppableContainer>
</div>
</div>
<Input
ref={fileInputRef}
type="file"
className="hidden"
accept="image/*"
multiple
onChange={(e) => e.target.files && onImageUpload(e.target.files)}
/>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,198 @@
import { useState, useRef } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Loader2, Trash2, Maximize2, GripVertical, X } from "lucide-react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
// Define the ProductImage interface
interface ProductImage {
id: string;
url?: string;
imageUrl?: string;
fileName?: string;
loading?: boolean;
isLoading?: boolean;
// Optional fields from the full ProductImage type
productIndex?: number;
pid?: number;
iid?: number;
type?: number;
order?: number;
width?: number;
height?: number;
hidden?: number;
}
// Define the SortableImageProps interface
interface SortableImageProps {
image: ProductImage;
productIndex: number;
imgIndex: number;
productName?: string; // Make this optional
removeImage: (id: string) => void; // Changed to match ProductCard
}
// Function to ensure URLs are properly formatted with absolute paths
const getFullImageUrl = (url: string): string => {
// If the URL is already absolute (starts with http:// or https://) return it as is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Otherwise, it's a relative URL, prepend the domain
const baseUrl = 'https://inventory.acot.site';
// Make sure url starts with / for path
const path = url.startsWith('/') ? url : `/${url}`;
return `${baseUrl}${path}`;
};
export const SortableImage = ({
image,
productIndex,
imgIndex,
productName,
removeImage
}: SortableImageProps) => {
const [dialogOpen, setDialogOpen] = useState(false);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({
id: image.id,
data: {
productIndex,
image,
type: 'image'
}
});
// Create a new style object with fixed dimensions to prevent distortion
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 999 : 1, // Higher z-index when dragging
touchAction: 'none', // Prevent touch scrolling during drag
userSelect: 'none', // Prevent text selection during drag
cursor: isDragging ? 'grabbing' : 'grab',
width: '96px',
height: '96px',
flexShrink: 0,
flexGrow: 0,
position: 'relative',
};
// Create a ref for the buttons to exclude them from drag listeners
const deleteButtonRef = useRef<HTMLButtonElement>(null);
const zoomButtonRef = useRef<HTMLButtonElement>(null);
const displayName = productName || `Product #${productIndex + 1}`;
return (
<div
ref={setNodeRef}
style={style}
className="relative border rounded-md overflow-hidden flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag group hover:ring-2 hover:ring-primary/30 transition-all"
{...attributes}
{...listeners}
onDragStart={(e) => {
// This ensures the native drag doesn't interfere
e.preventDefault();
e.stopPropagation();
}}
>
{image.loading ? (
<div className="flex flex-col items-center justify-center p-2">
<Loader2 className="h-5 w-5 animate-spin mb-1" />
<span className="text-xs text-center truncate w-full">{image.fileName}</span>
</div>
) : (
<>
<img
src={getFullImageUrl(image.url || image.imageUrl || '')}
alt={`${displayName} - Image ${imgIndex + 1}`}
className="h-full w-full object-cover select-none no-native-drag"
draggable={false}
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-200"></div>
<div className="absolute right-0 top-0 p-1 opacity-0 group-hover:opacity-90 transition-opacity">
<GripVertical className="h-3 w-3 text-white drop-shadow-md" />
</div>
<button
ref={deleteButtonRef}
className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-200 top-1 right-1 bg-black/40 hover:bg-black/70 hover:scale-110 rounded-full p-1 text-white z-10"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // Prevent triggering drag listeners
removeImage(image.id);
}}
onPointerDown={(e) => {
e.stopPropagation(); // Prevent drag from starting
}}
onMouseDown={(e) => {
e.stopPropagation(); // Prevent drag from starting
}}
onTouchStart={(e) => {
e.stopPropagation(); // Prevent drag from starting on touch
}}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<button
ref={zoomButtonRef}
className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-200 bottom-1 left-1 bg-black/40 hover:bg-black/70 hover:scale-110 rounded-full p-1 text-white z-10"
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // Prevent triggering drag listeners
setDialogOpen(true);
}}
onPointerDown={(e) => {
e.stopPropagation(); // Prevent drag from starting
}}
onMouseDown={(e) => {
e.stopPropagation(); // Prevent drag from starting
}}
onTouchStart={(e) => {
e.stopPropagation(); // Prevent drag from starting on touch
}}
>
<Maximize2 className="h-3 w-3" />
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-[650px] max-h-[90vh] p-6 bg-background/95 backdrop-blur-sm border border-border rounded-lg shadow-2xl">
<div className="relative flex flex-col items-center justify-center">
<Button
variant="ghost"
size="icon"
className="absolute -top-2 -right-2 bg-black/60 text-white hover:bg-black/80 rounded-full z-10 h-8 w-8"
onClick={() => setDialogOpen(false)}
>
<X className="h-4 w-4" />
</Button>
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black">
<img
src={getFullImageUrl(image.url || image.imageUrl || '')}
alt={`${displayName} - Image ${imgIndex + 1}`}
className="max-h-[70vh] max-w-full object-contain"
/>
</div>
<div className="mt-2 text-sm text-muted-foreground text-center">
{`${displayName} - Image ${imgIndex + 1}`}
</div>
</div>
</DialogContent>
</Dialog>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,60 @@
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { UnassignedImage, Product } from "../types";
import { UnassignedImageItem } from "./UnassignedImagesSection/UnassignedImageItem";
interface UnassignedImagesSectionProps {
showUnassigned: boolean;
unassignedImages: UnassignedImage[];
data: Product[];
onHide: () => void;
onAssign: (imageIndex: number, productIndex: number) => void;
onRemove: (index: number) => void;
}
export const UnassignedImagesSection = ({
showUnassigned,
unassignedImages,
data,
onHide,
onAssign,
onRemove
}: UnassignedImagesSectionProps) => {
if (!showUnassigned || unassignedImages.length === 0) return null;
return (
<div className="mb-4 px-4">
<div className="bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded-md p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-amber-500" />
<h3 className="text-sm font-medium text-amber-700 dark:text-amber-400">
Unassigned Images ({unassignedImages.length})
</h3>
</div>
<Button
variant="ghost"
size="sm"
onClick={onHide}
className="h-8 text-muted-foreground"
>
Hide
</Button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{unassignedImages.map((image, index) => (
<UnassignedImageItem
key={index}
image={image}
index={index}
data={data}
onAssign={onAssign}
onRemove={onRemove}
/>
))}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,112 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Trash2, Maximize2, X } from "lucide-react";
import {
Dialog,
DialogContent,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { UnassignedImage, Product } from "../../types";
interface UnassignedImageItemProps {
image: UnassignedImage;
index: number;
data: Product[];
onAssign: (imageIndex: number, productIndex: number) => void;
onRemove: (index: number) => void;
}
export const UnassignedImageItem = ({
image,
index,
data,
onAssign,
onRemove
}: UnassignedImageItemProps) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<div className="relative border rounded-md overflow-hidden">
<img
src={image.previewUrl}
alt={`Unassigned image ${index + 1}`}
className="h-full w-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 bg-black/60 p-2">
<p className="text-white text-xs mb-1 truncate">{image.file.name}</p>
<div className="flex gap-2">
<Select onValueChange={(value) => onAssign(index, parseInt(value))}>
<SelectTrigger className="h-7 text-xs bg-white dark:bg-gray-800 border-0 text-black dark:text-white">
<SelectValue placeholder="Assign to..." />
</SelectTrigger>
<SelectContent>
{data.map((product: Product, productIndex: number) => (
<SelectItem key={productIndex} value={productIndex.toString()}>
{product.name || `Product #${productIndex + 1}`}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 bg-white dark:bg-gray-800 text-black dark:text-white"
onClick={(e) => {
e.stopPropagation();
onRemove(index);
}}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* Zoom button for unassigned images */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<button
className="absolute top-1 left-1 bg-black/60 rounded-full p-1 text-white z-10 hover:bg-black/80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDialogOpen(true);
}}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
>
<Maximize2 className="h-3.5 w-3.5" />
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-[650px] max-h-[90vh] p-6 bg-background/95 backdrop-blur-sm border border-border rounded-lg shadow-2xl">
<div className="relative flex flex-col items-center justify-center">
<Button
variant="ghost"
size="icon"
className="absolute -top-2 -right-2 bg-black/60 text-white hover:bg-black/80 rounded-full z-10 h-8 w-8"
onClick={() => setDialogOpen(false)}
>
<X className="h-4 w-4" />
</Button>
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black">
<img
src={image.previewUrl}
alt={`Unassigned image: ${image.file.name}`}
className="max-h-[70vh] max-w-full object-contain"
/>
</div>
<div className="mt-2 text-sm text-muted-foreground text-center">
{`Unassigned image: ${image.file.name}`}
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
};

View File

@@ -0,0 +1,183 @@
import { useState } from "react";
import { toast } from "sonner";
import { UnassignedImage, Product } from "../types";
type HandleImageUploadFn = (files: FileList | File[], productIndex: number) => Promise<void>;
interface UseBulkImageUploadProps {
data: Product[];
handleImageUpload: HandleImageUploadFn;
}
export const useBulkImageUpload = ({ data, handleImageUpload }: UseBulkImageUploadProps) => {
const [unassignedImages, setUnassignedImages] = useState<UnassignedImage[]>([]);
const [processingBulk, setProcessingBulk] = useState(false);
const [showUnassigned, setShowUnassigned] = useState(false);
// Function to extract identifiers from a filename
const extractIdentifiers = (filename: string): string[] => {
// Remove file extension and convert to lowercase
const nameWithoutExt = filename.split('.').slice(0, -1).join('.').toLowerCase();
// Split by common separators
const parts = nameWithoutExt.split(/[-_\s.]+/);
// Add the full name without extension as a possible identifier
const identifiers = [nameWithoutExt];
// Add parts with at least 3 characters
identifiers.push(...parts.filter(part => part.length >= 3));
// Look for potential UPC or product codes (digits only)
const digitOnlyParts = parts.filter(part => /^\d+$/.test(part) && part.length >= 5);
identifiers.push(...digitOnlyParts);
// Look for product codes (mix of letters and digits)
const productCodes = parts.filter(part =>
/^[a-z0-9]+$/.test(part) &&
/\d/.test(part) &&
/[a-z]/.test(part) &&
part.length >= 4
);
identifiers.push(...productCodes);
return [...new Set(identifiers)]; // Remove duplicates
};
// Function to find product index by identifier
const findProductByIdentifier = (identifier: string): number => {
// Try to match against supplier_no, upc, SKU, or name
return data.findIndex((product: Product) => {
// Skip if product is missing all identifiers
if (!product.supplier_no && !product.upc && !product.sku && !product.name) {
return false;
}
const supplierNo = String(product.supplier_no || '').toLowerCase();
const upc = String(product.upc || '').toLowerCase();
const sku = String(product.sku || '').toLowerCase();
const name = String(product.name || '').toLowerCase();
const model = String(product.model || '').toLowerCase();
// For exact matches, prioritize certain fields
if (
(supplierNo && identifier === supplierNo) ||
(upc && identifier === upc) ||
(sku && identifier === sku)
) {
return true;
}
// For partial matches, check if the identifier is contained within the field
// or if the field is contained within the identifier
return (
(supplierNo && (supplierNo.includes(identifier) || identifier.includes(supplierNo))) ||
(upc && (upc.includes(identifier) || identifier.includes(upc))) ||
(sku && (sku.includes(identifier) || identifier.includes(sku))) ||
(model && (model.includes(identifier) || identifier.includes(model))) ||
(name && name.includes(identifier))
);
});
};
// Function to create preview URLs for files
const createPreviewUrl = (file: File): string => {
return URL.createObjectURL(file);
};
// Function to handle bulk image upload
const handleBulkUpload = async (files: File[]) => {
if (!files.length) return;
setProcessingBulk(true);
const unassigned: UnassignedImage[] = [];
for (const file of files) {
// Extract identifiers from filename
const identifiers = extractIdentifiers(file.name);
let assigned = false;
// Try to match each identifier
for (const identifier of identifiers) {
const productIndex = findProductByIdentifier(identifier);
if (productIndex !== -1) {
// Found a match, upload to this product
await handleImageUpload([file], productIndex);
assigned = true;
break;
}
}
// If no match was found, add to unassigned
if (!assigned) {
unassigned.push({
file,
previewUrl: createPreviewUrl(file)
});
}
}
// Update unassigned images
setUnassignedImages(prev => [...prev, ...unassigned]);
setProcessingBulk(false);
// Show summary toast
const assignedCount = files.length - unassigned.length;
if (assignedCount > 0) {
toast.success(`Auto-assigned ${assignedCount} ${assignedCount === 1 ? 'image' : 'images'} to products`);
}
if (unassigned.length > 0) {
toast.warning(`Could not auto-assign ${unassigned.length} ${unassigned.length === 1 ? 'image' : 'images'}`);
setShowUnassigned(true);
}
};
// Function to manually assign an unassigned image
const assignImageToProduct = async (imageIndex: number, productIndex: number) => {
const image = unassignedImages[imageIndex];
if (!image) return;
// Upload the image to the selected product
await handleImageUpload([image.file], productIndex);
// Remove from unassigned list
setUnassignedImages(prev => prev.filter((_, idx) => idx !== imageIndex));
// Revoke the preview URL to free memory
URL.revokeObjectURL(image.previewUrl);
toast.success(`Image assigned to ${data[productIndex].name || `Product #${productIndex + 1}`}`);
};
// Function to remove an unassigned image
const removeUnassignedImage = (index: number) => {
const image = unassignedImages[index];
if (!image) return;
// Revoke the preview URL to free memory
URL.revokeObjectURL(image.previewUrl);
// Remove from state
setUnassignedImages(prev => prev.filter((_, idx) => idx !== index));
};
// Cleanup function for preview URLs
const cleanupPreviewUrls = () => {
unassignedImages.forEach(image => {
URL.revokeObjectURL(image.previewUrl);
});
};
return {
unassignedImages,
setUnassignedImages,
processingBulk,
showUnassigned,
setShowUnassigned,
handleBulkUpload,
assignImageToProduct,
removeUnassignedImage,
cleanupPreviewUrls
};
};

View File

@@ -0,0 +1,340 @@
import { useState, useEffect } from "react";
import {
DragEndEvent,
DragStartEvent,
DragMoveEvent,
CollisionDetection,
pointerWithin,
rectIntersection
} from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import { toast } from "sonner";
import { ProductImageSortable } from "../types";
type UseDragAndDropProps = {
productImages: ProductImageSortable[];
setProductImages: React.Dispatch<React.SetStateAction<ProductImageSortable[]>>;
data: any[];
};
type UseDragAndDropReturn = {
activeId: string | null;
activeImage: ProductImageSortable | null;
activeDroppableId: string | null;
customCollisionDetection: CollisionDetection;
findContainer: (id: string) => string | null;
getProductImages: (productIndex: number) => ProductImageSortable[];
getProductContainerClasses: (index: number) => string;
handleDragStart: (event: DragStartEvent) => void;
handleDragOver: (event: DragMoveEvent) => void;
handleDragEnd: (event: DragEndEvent) => void;
};
export const useDragAndDrop = ({
productImages,
setProductImages,
data
}: UseDragAndDropProps): UseDragAndDropReturn => {
const [activeId, setActiveId] = useState<string | null>(null);
const [activeImage, setActiveImage] = useState<ProductImageSortable | null>(null);
const [activeDroppableId, setActiveDroppableId] = useState<string | null>(null);
// Custom collision detection algorithm that prioritizes product containers
const customCollisionDetection: CollisionDetection = (args) => {
// Use the built-in pointerWithin algorithm first for better performance
const pointerCollisions = pointerWithin(args);
if (pointerCollisions.length > 0) {
return pointerCollisions;
}
// Fall back to rectIntersection if no pointer collisions
return rectIntersection(args);
};
// Function to find container (productIndex) an image belongs to
const findContainer = (id: string) => {
const image = productImages.find(img => img.id === id);
return image ? image.productIndex.toString() : null;
};
// Function to get images for a specific product
const getProductImages = (productIndex: number) => {
return productImages.filter(img => img.productIndex === productIndex);
};
// Handle drag start to set active image and prevent default behavior
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const activeImageItem = productImages.find(img => img.id === active.id);
setActiveId(active.id.toString());
if (activeImageItem) {
setActiveImage(activeImageItem);
}
};
// Handle drag over to track which product container is being hovered
const handleDragOver = (event: DragMoveEvent) => {
const { over } = event;
if (!over) {
setActiveDroppableId(null);
return;
}
let overContainer = null;
// Check if we're over a product container directly
if (typeof over.id === 'string' && over.id.toString().startsWith('product-')) {
overContainer = over.id.toString();
setActiveDroppableId(overContainer);
}
// Otherwise check if we're over another image
else {
const overImage = productImages.find(img => img.id === over.id);
if (overImage) {
overContainer = `product-${overImage.productIndex}`;
setActiveDroppableId(overContainer);
} else {
setActiveDroppableId(null);
}
}
};
// Update handleDragEnd to work with the updated product data structure
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
// Reset active droppable
setActiveDroppableId(null);
if (!over) {
setActiveId(null);
setActiveImage(null);
return;
}
const activeId = active.id;
const overId = over.id;
// Find the containers (product indices) for the active element
const activeContainer = findContainer(activeId.toString());
let overContainer = null;
// Check if overId is a product container directly
if (typeof overId === 'string' && overId.toString().startsWith('product-')) {
overContainer = overId.toString().split('-')[1];
}
// Otherwise check if it's an image, so find its container
else {
overContainer = findContainer(overId.toString());
}
// If we couldn't determine active container, do nothing
if (!activeContainer) {
setActiveId(null);
setActiveImage(null);
return;
}
// If we couldn't determine the over container, do nothing
if (!overContainer) {
setActiveId(null);
setActiveImage(null);
return;
}
// Convert containers to numbers
const sourceProductIndex = parseInt(activeContainer);
const targetProductIndex = parseInt(overContainer);
// Find the active image
const activeImage = productImages.find(img => img.id === activeId);
if (!activeImage) {
setActiveId(null);
setActiveImage(null);
return;
}
// IMPORTANT: If source and target are different products, ALWAYS prioritize moving over reordering
if (sourceProductIndex !== targetProductIndex) {
// Create a copy of the image with the new product index
const newImage: ProductImageSortable = {
...activeImage,
productIndex: targetProductIndex,
// Generate a new ID for the image in its new location
id: `image-${targetProductIndex}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
};
// Remove the image from the source product and add to target product
setProductImages(items => {
// Remove the image from its current product
const filteredItems = items.filter(item => item.id !== activeId);
// Add the image to the target product
filteredItems.push(newImage);
// Show notification
toast.success(`Image moved from ${data[sourceProductIndex].name || `Product #${sourceProductIndex + 1}`} to ${data[targetProductIndex].name || `Product #${targetProductIndex + 1}`}`);
return filteredItems;
});
}
// Source and target are the same product - this is a reordering operation
else {
// Only attempt reordering if we have at least 2 images in this container
const productImages = getProductImages(sourceProductIndex);
if (productImages.length >= 2) {
// Handle reordering regardless of whether we're over a container or another image
setProductImages(items => {
// Filter to get only the images for this product
const productFilteredItems = items.filter(item => item.productIndex === sourceProductIndex);
// If dropping onto the container itself, put at the end
if (overId.toString().startsWith('product-')) {
// Find active index
const activeIndex = productFilteredItems.findIndex(item => item.id === activeId);
if (activeIndex === -1) {
return items; // No change needed
}
// Move active item to end (remove and push to end)
const newFilteredItems = [...productFilteredItems];
const [movedItem] = newFilteredItems.splice(activeIndex, 1);
newFilteredItems.push(movedItem);
// Create a new full list replacing the items for this product with the reordered ones
const newItems = items.filter(item => item.productIndex !== sourceProductIndex);
newItems.push(...newFilteredItems);
return newItems;
}
// Find indices within the filtered list
const activeIndex = productFilteredItems.findIndex(item => item.id === activeId);
const overIndex = productFilteredItems.findIndex(item => item.id === overId);
// If one of the indices is not found or they're the same, do nothing
if (activeIndex === -1 || overIndex === -1 || activeIndex === overIndex) {
return items;
}
// Reorder the filtered items
const newFilteredItems = arrayMove(productFilteredItems, activeIndex, overIndex);
// Create a new full list replacing the items for this product with the reordered ones
const newItems = items.filter(item => item.productIndex !== sourceProductIndex);
newItems.push(...newFilteredItems);
return newItems;
});
}
}
setActiveId(null);
setActiveImage(null);
};
// Monitor drag events to prevent browser behaviors
useEffect(() => {
// Add a global event listener to prevent browser's native drag behavior
const preventDefaultDragImage = (event: DragEvent) => {
if (activeId) {
event.preventDefault();
}
};
document.addEventListener('dragstart', preventDefaultDragImage);
return () => {
document.removeEventListener('dragstart', preventDefaultDragImage);
};
}, [activeId]);
// Add product IDs to the valid droppable elements
useEffect(() => {
// Add data-droppable attributes to make product containers easier to identify
data.forEach((_, index) => {
const container = document.getElementById(`product-${index}`);
if (container) {
container.setAttribute('data-droppable', 'true');
container.setAttribute('aria-dropeffect', 'move');
// Check if the container has images
const hasImages = getProductImages(index).length > 0;
// Set data-empty attribute for tracking purposes
container.setAttribute('data-empty', hasImages ? 'false' : 'true');
// Ensure the container has sufficient size to be a drop target
if (container.offsetHeight < 100) {
container.style.minHeight = '100px';
}
}
});
}, [data, productImages]); // Add productImages as a dependency to re-run when images change
// Effect to register browser-level drag events on product containers
useEffect(() => {
// For each product container
data.forEach((_, index) => {
const container = document.getElementById(`product-${index}`);
if (container) {
// Define handlers for native browser drag events
const handleNativeDragOver = (e: DragEvent) => {
e.preventDefault();
setActiveDroppableId(`product-${index}`);
};
const handleNativeDragLeave = () => {
if (activeDroppableId === `product-${index}`) {
setActiveDroppableId(null);
}
};
// Add these handlers
container.addEventListener('dragover', handleNativeDragOver);
container.addEventListener('dragleave', handleNativeDragLeave);
// Return cleanup function
return () => {
container.removeEventListener('dragover', handleNativeDragOver);
container.removeEventListener('dragleave', handleNativeDragLeave);
};
}
});
}, [data, productImages, activeDroppableId]); // Re-run when data or productImages change
// Function to add more visual indication when dragging
const getProductContainerClasses = (index: number) => {
const isValidDropTarget = activeId && findContainer(activeId) !== index.toString();
const isActiveDropTarget = activeDroppableId === `product-${index}`;
return [
"flex-1 min-h-[6rem] rounded-md p-2 transition-all",
// Only show borders during active drag operations
isValidDropTarget && isActiveDropTarget
? "border-2 border-dashed border-primary bg-primary/10"
: isValidDropTarget
? "border border-dashed border-muted-foreground/30"
: ""
].filter(Boolean).join(" ");
};
return {
activeId,
activeImage,
activeDroppableId,
customCollisionDetection,
findContainer,
getProductImages,
getProductContainerClasses,
handleDragStart,
handleDragOver,
handleDragEnd
};
};

View File

@@ -0,0 +1,195 @@
import { toast } from "sonner";
import config from "@/config";
import { Product, ProductImageSortable } from "../types";
interface UseProductImageOperationsProps {
data: Product[];
productImages: ProductImageSortable[];
setProductImages: React.Dispatch<React.SetStateAction<ProductImageSortable[]>>;
}
export const useProductImageOperations = ({
data,
productImages,
setProductImages,
}: UseProductImageOperationsProps) => {
// Function to remove an image URL from a product
const removeImageFromProduct = (productIndex: number, imageUrl: string) => {
// Create a copy of the data
const newData = [...data];
// Get the current product
const product = newData[productIndex];
// We need to update product_images array directly instead of the image_url field
if (!product.product_images) {
product.product_images = [];
} else if (typeof product.product_images === 'string') {
// Handle case where it might be a comma-separated string
product.product_images = product.product_images.split(',').filter(Boolean);
}
// Filter out the image URL we're removing
if (Array.isArray(product.product_images)) {
product.product_images = product.product_images.filter((url: string) => url && url !== imageUrl);
}
return newData;
};
// Function to add an image URL to a product
const addImageToProduct = (productIndex: number, imageUrl: string) => {
// Create a copy of the data
const newData = [...data];
// Get the current product
const product = newData[productIndex];
// Initialize product_images array if it doesn't exist
if (!product.product_images) {
product.product_images = [];
} else if (typeof product.product_images === 'string') {
// Handle case where it might be a comma-separated string
product.product_images = product.product_images.split(',').filter(Boolean);
}
// Ensure it's an array
if (!Array.isArray(product.product_images)) {
product.product_images = [product.product_images].filter(Boolean);
}
// Only add if the URL doesn't already exist
if (!product.product_images.includes(imageUrl)) {
product.product_images.push(imageUrl);
}
return newData;
};
// Function to handle image upload - update product data
const handleImageUpload = async (files: FileList | File[], productIndex: number) => {
if (!files || files.length === 0) return;
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Add placeholder for this image
const newImage: ProductImageSortable = {
id: `image-${productIndex}-${Date.now()}-${i}`, // Generate a unique ID
productIndex,
imageUrl: '',
loading: true,
fileName: file.name,
// Add required schema fields for ProductImageSortable
pid: data[productIndex].id || 0,
iid: 0,
type: 0,
order: 0,
width: 0,
height: 0,
hidden: 0
};
setProductImages(prev => [...prev, newImage]);
// Create form data for upload
const formData = new FormData();
formData.append('image', file);
formData.append('productIndex', productIndex.toString());
formData.append('upc', data[productIndex].upc || '');
formData.append('supplier_no', data[productIndex].supplier_no || '');
try {
// Upload the image
const response = await fetch(`${config.apiUrl}/import/upload-image`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Failed to upload image');
}
const result = await response.json();
// Update the image URL in our state
setProductImages(prev =>
prev.map(img =>
(img.loading && img.productIndex === productIndex && img.fileName === file.name)
? { ...img, imageUrl: result.imageUrl, loading: false }
: img
)
);
// Update the product data with the new image URL
addImageToProduct(productIndex, result.imageUrl);
toast.success(`Image uploaded for ${data[productIndex].name || `Product #${productIndex + 1}`}`);
} catch (error) {
console.error('Upload error:', error);
// Remove the failed image from our state
setProductImages(prev =>
prev.filter(img =>
!(img.loading && img.productIndex === productIndex && img.fileName === file.name)
)
);
toast.error(`Failed to upload image: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
};
// Function to remove an image - update to work with product_images
const removeImage = async (imageIndex: number) => {
const image = productImages[imageIndex];
if (!image) return;
try {
// Check if this is an external URL-based image or an uploaded image
const isExternalUrl = image.imageUrl.startsWith('http') &&
!image.imageUrl.includes(config.apiUrl.replace(/^https?:\/\//, ''));
// Only call the API to delete the file if it's an uploaded image
if (!isExternalUrl) {
// Extract the filename from the URL
const urlParts = image.imageUrl.split('/');
const filename = urlParts[urlParts.length - 1];
// Call API to delete the image
const response = await fetch(`${config.apiUrl}/import/delete-image`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageUrl: image.imageUrl,
filename
}),
});
if (!response.ok) {
throw new Error('Failed to delete image');
}
}
// Remove the image from our state
setProductImages(prev => prev.filter((_, idx) => idx !== imageIndex));
// Remove the image URL from the product data
removeImageFromProduct(image.productIndex, image.imageUrl);
toast.success('Image removed successfully');
} catch (error) {
console.error('Delete error:', error);
toast.error(`Failed to remove image: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
return {
removeImageFromProduct,
addImageToProduct,
handleImageUpload,
removeImage,
};
};

View File

@@ -0,0 +1,88 @@
import { useState } from "react";
import { ProductImageSortable, Product } from "../types";
export const useProductImagesInit = (data: Product[]) => {
// Initialize product images from data
const [productImages, setProductImages] = useState<ProductImageSortable[]>(() => {
// Convert existing product_images to ProductImageSortable objects
const initialImages: ProductImageSortable[] = [];
data.forEach((product: Product, productIndex: number) => {
if (product.product_images) {
let images: any[] = [];
// Handle different formats of product_images
if (typeof product.product_images === 'string') {
try {
// Try to parse as JSON
images = JSON.parse(product.product_images);
} catch (e) {
// If not JSON, split by comma if it's a string
images = product.product_images.split(',').filter(Boolean).map((url: string) => ({
imageUrl: url.trim(),
pid: product.id || 0,
iid: 0,
type: 0,
order: 255,
width: 0,
height: 0,
hidden: 0
}));
}
} else if (Array.isArray(product.product_images)) {
// Use the array directly
images = product.product_images;
} else if (product.product_images) {
// Handle case where it might be a single value
images = [product.product_images];
}
// Create ProductImageSortable objects for each image
images.forEach((img, i) => {
// Handle both URL strings and structured image objects
const imageUrl = typeof img === 'string' ? img : img.imageUrl;
if (imageUrl && imageUrl.trim()) {
initialImages.push({
id: `image-${productIndex}-initial-${i}`,
productIndex,
imageUrl: imageUrl.trim(),
loading: false,
fileName: `Image ${i + 1}`,
// Add schema fields
pid: product.id || 0,
iid: typeof img === 'object' && img.iid ? img.iid : i,
type: typeof img === 'object' && img.type !== undefined ? img.type : 0,
order: typeof img === 'object' && img.order !== undefined ? img.order : i,
width: typeof img === 'object' && img.width ? img.width : 0,
height: typeof img === 'object' && img.height ? img.height : 0,
hidden: typeof img === 'object' && img.hidden !== undefined ? img.hidden : 0
});
}
});
}
});
return initialImages;
});
// Function to ensure URLs are properly formatted with absolute paths
const getFullImageUrl = (url: string): string => {
// If the URL is already absolute (starts with http:// or https://) return it as is
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Otherwise, it's a relative URL, prepend the domain
const baseUrl = 'https://inventory.acot.site';
// Make sure url starts with / for path
const path = url.startsWith('/') ? url : `/${url}`;
return `${baseUrl}${path}`;
};
return {
productImages,
setProductImages,
getFullImageUrl
};
};

View File

@@ -0,0 +1,98 @@
import { useState } from "react";
import { toast } from "sonner";
import { Product, ProductImageSortable } from "../types";
type AddImageToProductFn = (productIndex: number, imageUrl: string) => Product[];
interface UseUrlImageUploadProps {
data: Product[];
setProductImages: React.Dispatch<React.SetStateAction<ProductImageSortable[]>>;
addImageToProduct: AddImageToProductFn;
}
export const useUrlImageUpload = ({
data,
setProductImages,
addImageToProduct
}: UseUrlImageUploadProps) => {
const [urlInputs, setUrlInputs] = useState<{ [key: number]: string }>({});
const [processingUrls, setProcessingUrls] = useState<{ [key: number]: boolean }>({});
// Handle adding an image from a URL - simplified to skip server
const handleAddImageFromUrl = async (productIndex: number, url: string) => {
if (!url || !url.trim()) {
toast.error("Please enter a valid URL");
return;
}
try {
// Set processing state
setProcessingUrls(prev => ({ ...prev, [productIndex]: true }));
// Validate URL format
let validatedUrl = url.trim();
// Add protocol if missing
if (!validatedUrl.startsWith('http://') && !validatedUrl.startsWith('https://')) {
validatedUrl = `https://${validatedUrl}`;
}
// Basic URL validation
try {
new URL(validatedUrl);
} catch (e) {
toast.error("Invalid URL format. Please enter a valid URL");
setProcessingUrls(prev => ({ ...prev, [productIndex]: false }));
return;
}
// Create a unique ID for this image
const imageId = `image-${productIndex}-url-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
// Create the new image object with the URL
const newImage: ProductImageSortable = {
id: imageId,
productIndex,
imageUrl: validatedUrl,
loading: false, // We're not loading from server, so it's ready immediately
fileName: "From URL",
// Add required schema fields
pid: data[productIndex].id || 0,
iid: 0,
type: 0,
order: 0,
width: 0,
height: 0,
hidden: 0
};
// Add the image directly to the product images list
setProductImages(prev => [...prev, newImage]);
// Update the product data with the new image URL
addImageToProduct(productIndex, validatedUrl);
// Clear the URL input field on success
setUrlInputs(prev => ({ ...prev, [productIndex]: '' }));
toast.success(`Image URL added to ${data[productIndex].name || `Product #${productIndex + 1}`}`);
} catch (error) {
console.error('Add image from URL error:', error);
toast.error(`Failed to add image URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setProcessingUrls(prev => ({ ...prev, [productIndex]: false }));
}
};
// Update the URL input value
const updateUrlInput = (productIndex: number, value: string) => {
setUrlInputs(prev => ({ ...prev, [productIndex]: value }));
};
return {
urlInputs,
processingUrls,
handleAddImageFromUrl,
updateUrlInput
};
};

View File

@@ -0,0 +1,35 @@
export type ProductImage = {
productIndex: number;
imageUrl: string;
loading: boolean;
fileName: string;
// Schema fields
pid: number;
iid: number;
type: number;
order: number;
width: number;
height: number;
hidden: number;
}
export type UnassignedImage = {
file: File;
previewUrl: string;
}
// Product ID type to handle the sortable state
export type ProductImageSortable = ProductImage & {
id: string;
};
// Shared Product interface
export interface Product {
id?: number;
name?: string;
upc?: string;
supplier_no?: string;
sku?: string;
model?: string;
product_images?: string | string[];
}

View File

@@ -0,0 +1,31 @@
import { motion } from "framer-motion"
import { CgCheck } from "react-icons/cg"
const animationConfig = {
transition: {
duration: 0.1,
},
exit: { scale: 0.5, opacity: 0 },
initial: { scale: 0.5, opacity: 0 },
animate: { scale: 1, opacity: 1 },
}
type MatchIconProps = {
isChecked: boolean
}
export const MatchIcon = ({ isChecked }: MatchIconProps) => {
return (
<div
className="flex items-center justify-center rounded-full border-2 border-yellow-500 bg-background text-background transition-colors duration-100 min-w-6 min-h-6 w-6 h-6 ml-3.5 mr-3 data-[highlighted=true]:bg-green-500 data-[highlighted=true]:border-green-500"
data-highlighted={isChecked}
data-testid="column-checkmark"
>
{isChecked && (
<motion.div {...animationConfig}>
<CgCheck size="24px" />
</motion.div>
)}
</div>
)
}

View File

@@ -0,0 +1,133 @@
import { useRsi } from "../../../hooks/useRsi"
import type { Column } from "../MatchColumnsStep"
import { ColumnType } from "../MatchColumnsStep"
import type { Fields } from "../../../types"
import {
Card,
CardContent,
CardHeader,
} from "@/components/ui/card"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Check } from "lucide-react"
type TemplateColumnProps<T extends string> = {
column: Column<T>
onChange: (value: T, columnIndex: number) => void
onSubChange: (value: string, columnIndex: number, entry: string) => void
}
const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: any) => {
const field = fields.find((f) => "value" in column && f.key === column.value)
if (!field) return ""
return `${translations.matchColumnsStep.matchDropdownTitle} ${field.label} (${
"matchedOptions" in column ? column.matchedOptions.filter((option) => !option.value).length : 0
} ${translations.matchColumnsStep.unmatched})`
}
export const TemplateColumn = <T extends string>({ column, onChange, onSubChange }: TemplateColumnProps<T>) => {
const { translations, fields } = useRsi<T>()
const isIgnored = column.type === ColumnType.ignored
const isChecked =
column.type === ColumnType.matched ||
column.type === ColumnType.matchedCheckbox ||
column.type === ColumnType.matchedSelectOptions
const isSelect = "matchedOptions" in column
const selectOptions = fields.map(({ label, key }) => ({ value: key, label }))
const selectValue = column.type === ColumnType.empty ? undefined :
selectOptions.find(({ value }) => "value" in column && column.value === value)?.value
if (isIgnored) {
return null
}
return (
<Card className="h-full">
<CardHeader className="flex flex-row items-center justify-between space-x-2 p-4">
<div className="flex-1">
<Select
key={`select-${column.index}-${("value" in column ? column.value : "empty")}`}
value={selectValue}
onValueChange={(value) => onChange(value as T, column.index)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={translations.matchColumnsStep.selectPlaceholder} />
</SelectTrigger>
<SelectContent
side="bottom"
align="start"
className="z-[1500]"
>
{selectOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{isChecked && (
<div className="flex h-8 w-8 items-center justify-center rounded-full border border-green-700 bg-green-300 dark:bg-green-900/20">
<Check className="h-4 w-4 text-green-700 dark:text-green-500" />
</div>
)}
</CardHeader>
{isSelect && (
<CardContent className="p-4">
<Accordion type="multiple" className="w-full">
<AccordionItem value="options" className="border-none">
<AccordionTrigger className="py-2 text-sm hover:no-underline">
{getAccordionTitle(fields, column, translations)}
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
{column.matchedOptions.map((option) => (
<div key={option.entry} className="space-y-1">
<p className="text-sm text-muted-foreground">
{option.entry}
</p>
<Select
value={option.value}
onValueChange={(value) => onSubChange(value, column.index, option.entry!)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={translations.matchColumnsStep.subSelectPlaceholder} />
</SelectTrigger>
<SelectContent
side="bottom"
align="start"
className="z-[1000]"
>
{(() => {
const field = fields.find((f) => "value" in column && f.key === column.value)
if (!field || !("fieldType" in field) || !("options" in field.fieldType)) return null
return field.fieldType.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))
})()}
</SelectContent>
</Select>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
)}
</Card>
)
}

View File

@@ -0,0 +1,27 @@
import lavenstein from "js-levenshtein"
import type { Fields } from "../../../types"
type AutoMatchAccumulator<T> = {
distance: number
value: T
}
export const findMatch = <T extends string>(
header: string,
fields: Fields<T>,
autoMapDistance: number,
): T | undefined => {
const headerLower = header.toLowerCase()
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
const distance = Math.min(
...[
lavenstein(field.key.toLowerCase(), headerLower),
...(field.alternateMatches?.map((alternate) => lavenstein(alternate.toLowerCase(), headerLower)) || []),
],
)
return distance < acc.distance || acc.distance === undefined
? ({ value: field.key, distance } as AutoMatchAccumulator<T>)
: acc
}, {} as AutoMatchAccumulator<T>)
return smallestValue.distance <= autoMapDistance ? smallestValue.value : undefined
}

View File

@@ -0,0 +1,15 @@
import type { Fields } from "../../../types"
import type { Columns } from "../MatchColumnsStep"
export const findUnmatchedRequiredFields = <T extends string>(fields: Fields<T>, columns: Columns<T>) => {
// Get all required fields
const requiredFields = fields
.filter((field) => field.validations?.some((validation: any) =>
validation.rule === "required" || validation.required === true
))
// Find which required fields are not matched in columns
return requiredFields
.filter((field) => columns.findIndex((column) => "value" in column && column.value === field.key) === -1)
.map((field) => field.key) || []
}

View File

@@ -0,0 +1,6 @@
import type { Fields } from "../../../types"
export const getFieldOptions = <T extends string>(fields: Fields<T>, fieldKey: string) => {
const field = fields.find(({ key }) => fieldKey === key)!
return field.fieldType.type === "select" ? field.fieldType.options : []
}

View File

@@ -0,0 +1,41 @@
import lavenstein from "js-levenshtein"
import { findMatch } from "./findMatch"
import type { Field, Fields } from "../../../types"
import { setColumn } from "./setColumn"
import type { Column, Columns } from "../MatchColumnsStep"
import type { MatchColumnsProps } from "../MatchColumnsStep"
export const getMatchedColumns = <T extends string>(
columns: Columns<T>,
fields: Fields<T>,
data: MatchColumnsProps<T>["data"],
autoMapDistance: number,
autoMapSelectValues?: boolean,
) =>
columns.reduce<Column<T>[]>((arr, column) => {
const autoMatch = findMatch(column.header, fields, autoMapDistance)
if (autoMatch) {
const field = fields.find((field) => field.key === autoMatch) as Field<T>
const duplicateIndex = arr.findIndex((column) => "value" in column && column.value === field.key)
const duplicate = arr[duplicateIndex]
if (duplicate && "value" in duplicate) {
return lavenstein(duplicate.value, duplicate.header) < lavenstein(autoMatch, column.header)
? [
...arr.slice(0, duplicateIndex),
setColumn(arr[duplicateIndex], field, data, autoMapSelectValues),
...arr.slice(duplicateIndex + 1),
setColumn(column),
]
: [
...arr.slice(0, duplicateIndex),
setColumn(arr[duplicateIndex]),
...arr.slice(duplicateIndex + 1),
setColumn(column, field, data, autoMapSelectValues),
]
} else {
return [...arr, setColumn(column, field, data, autoMapSelectValues)]
}
} else {
return [...arr, column]
}
}, [])

View File

@@ -0,0 +1,13 @@
const booleanWhitelist: Record<string, boolean> = {
yes: true,
no: false,
true: true,
false: false,
}
export const normalizeCheckboxValue = (value: string | undefined): boolean => {
if (value && value.toLowerCase() in booleanWhitelist) {
return booleanWhitelist[value.toLowerCase()]
}
return false
}

View File

@@ -0,0 +1,67 @@
import type { Columns } from "../MatchColumnsStep"
import { ColumnType } from "../MatchColumnsStep"
import type { Data, Fields, RawData } from "../../../types"
import { normalizeCheckboxValue } from "./normalizeCheckboxValue"
export const normalizeTableData = <T extends string>(columns: Columns<T>, data: RawData[], fields: Fields<T>) =>
data.map((row) =>
columns.reduce((acc, column, index) => {
const curr = row[index]
switch (column.type) {
case ColumnType.matchedCheckbox: {
const field = fields.find((field) => field.key === column.value)!
if ("booleanMatches" in field.fieldType && Object.keys(field.fieldType).length) {
const booleanMatchKey = Object.keys(field.fieldType.booleanMatches || []).find(
(key) => key.toLowerCase() === curr?.toLowerCase(),
)!
const booleanMatch = field.fieldType.booleanMatches?.[booleanMatchKey]
acc[column.value] = (booleanMatchKey ? booleanMatch : normalizeCheckboxValue(curr)) as Data<T>[T]
} else {
acc[column.value] = normalizeCheckboxValue(curr) as Data<T>[T]
}
return acc
}
case ColumnType.matched: {
acc[column.value] = (curr === "" ? undefined : curr) as Data<T>[T]
return acc
}
case ColumnType.matchedMultiInput: {
const field = fields.find((field) => field.key === column.value)!
if (curr) {
const separator = field.fieldType.type === "multi-input" ? field.fieldType.separator || "," : ","
acc[column.value] = curr.split(separator).map(v => v.trim()).filter(Boolean) as Data<T>[T]
} else {
acc[column.value] = undefined as Data<T>[T]
}
return acc
}
case ColumnType.matchedSelect:
case ColumnType.matchedSelectOptions: {
const matchedOption = column.matchedOptions.find(({ entry }) => entry === curr)
acc[column.value] = (matchedOption?.value || undefined) as Data<T>[T]
return acc
}
case ColumnType.matchedMultiSelect: {
const field = fields.find((field) => field.key === column.value)!
if (curr) {
const separator = field.fieldType.type === "multi-select" ? field.fieldType.separator || "," : ","
const entries = curr.split(separator).map(v => v.trim()).filter(Boolean)
const values = entries.map(entry => {
const matchedOption = column.matchedOptions.find(({ entry: optEntry }) => optEntry === entry)
return matchedOption?.value
}).filter(Boolean) as string[]
acc[column.value] = (values.length ? values : undefined) as Data<T>[T]
} else {
acc[column.value] = undefined as Data<T>[T]
}
return acc
}
case ColumnType.empty:
case ColumnType.ignored: {
return acc
}
default:
return acc
}
}, {} as Data<T>),
)

View File

@@ -0,0 +1,65 @@
import type { Field, MultiSelect } from "../../../types"
import { Column, ColumnType, MatchColumnsProps, MatchedOptions } from "../MatchColumnsStep"
import { uniqueEntries } from "./uniqueEntries"
export const setColumn = <T extends string>(
oldColumn: Column<T>,
field?: Field<T>,
data?: MatchColumnsProps<T>["data"],
autoMapSelectValues?: boolean,
): Column<T> => {
switch (field?.fieldType.type) {
case "select":
const fieldOptions = field.fieldType.options
const uniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions<T>[]
const matchedOptions = autoMapSelectValues
? uniqueData.map((record) => {
const value = fieldOptions.find(
(fieldOption) => fieldOption.value === record.entry || fieldOption.label === record.entry,
)?.value
return value ? ({ ...record, value } as MatchedOptions<T>) : (record as MatchedOptions<T>)
})
: uniqueData
const allMatched = matchedOptions.filter((o) => o.value).length == uniqueData?.length
return {
...oldColumn,
type: allMatched ? ColumnType.matchedSelectOptions : ColumnType.matchedSelect,
value: field.key,
matchedOptions,
}
case "multi-select":
const multiSelectFieldType = field.fieldType as MultiSelect
const multiSelectFieldOptions = multiSelectFieldType.options
const multiSelectUniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions<T>[]
const multiSelectMatchedOptions = autoMapSelectValues
? multiSelectUniqueData.map((record) => {
// Split the entry by the separator (default to comma)
const entries = record.entry.split(multiSelectFieldType.separator || ",").map(e => e.trim())
// Try to match each entry to an option
const values = entries.map(entry => {
const value = multiSelectFieldOptions.find(
(fieldOption) => fieldOption.value === entry || fieldOption.label === entry,
)?.value
return value
}).filter(Boolean) as T[]
return { ...record, value: values.length ? values[0] : undefined } as MatchedOptions<T>
})
: multiSelectUniqueData
return {
...oldColumn,
type: ColumnType.matchedMultiSelect,
value: field.key,
matchedOptions: multiSelectMatchedOptions,
}
case "checkbox":
return { index: oldColumn.index, type: ColumnType.matchedCheckbox, value: field.key, header: oldColumn.header }
case "input":
return { index: oldColumn.index, type: ColumnType.matched, value: field.key, header: oldColumn.header }
case "multi-input":
return { index: oldColumn.index, type: ColumnType.matchedMultiInput, value: field.key, header: oldColumn.header }
default:
return { index: oldColumn.index, header: oldColumn.header, type: ColumnType.empty }
}
}

View File

@@ -0,0 +1,7 @@
import { Column, ColumnType } from "../MatchColumnsStep"
export const setIgnoreColumn = <T extends string>({ header, index }: Column<T>): Column<T> => ({
header,
index,
type: ColumnType.ignored,
})

View File

@@ -0,0 +1,20 @@
import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn, MatchedMultiSelectColumn } from "../MatchColumnsStep"
export const setSubColumn = <T>(
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T>,
entry: string,
value: string,
): MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T> => {
const options = oldColumn.matchedOptions.map((option) => (option.entry === entry ? { ...option, value } : option))
const allMatched = options.every(({ value }) => !!value)
if (oldColumn.type === ColumnType.matchedMultiSelect) {
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[] }
}
if (allMatched) {
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelectOptions }
} else {
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelect }
}
}

View File

@@ -0,0 +1,11 @@
import uniqBy from "lodash/uniqBy"
import type { MatchColumnsProps, MatchedOptions } from "../MatchColumnsStep"
export const uniqueEntries = <T extends string>(
data: MatchColumnsProps<T>["data"],
index: number,
): Partial<MatchedOptions<T>>[] =>
uniqBy(
data.map((row) => ({ entry: row[index] })),
"entry",
).filter(({ entry }) => !!entry)

View File

@@ -0,0 +1,194 @@
import { useCallback, useState } from "react"
import { SelectHeaderTable } from "./components/SelectHeaderTable"
import { useRsi } from "../../hooks/useRsi"
import type { RawData } from "../../types"
import { Button } from "@/components/ui/button"
import { useToast } from "@/hooks/use-toast"
type SelectHeaderProps = {
data: RawData[]
onContinue: (headerValues: RawData, data: RawData[]) => Promise<void>
onBack?: () => void
}
export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps) => {
const { translations } = useRsi()
const { toast } = useToast()
const [selectedRows, setSelectedRows] = useState<ReadonlySet<number>>(new Set([0]))
const [isLoading, setIsLoading] = useState(false)
const [localData, setLocalData] = useState<RawData[]>(data)
const handleContinue = useCallback(async () => {
const [selectedRowIndex] = selectedRows
// We consider data above header to be redundant
const trimmedData = localData.slice(selectedRowIndex + 1)
setIsLoading(true)
await onContinue(localData[selectedRowIndex], trimmedData)
setIsLoading(false)
}, [onContinue, localData, selectedRows])
const discardEmptyAndDuplicateRows = useCallback(() => {
// Helper function to count non-empty values in a row
const countNonEmptyValues = (values: Record<string, any>): number => {
return Object.values(values).filter(val =>
val !== undefined &&
val !== null &&
(typeof val === 'string' ? val.trim() !== '' : true)
).length;
};
// Helper function to normalize row values for case-insensitive comparison
const normalizeRowForComparison = (row: Record<string, any>): Record<string, any> => {
return Object.entries(row).reduce((acc, [key, value]) => {
// Convert string values to lowercase for case-insensitive comparison
if (typeof value === 'string') {
acc[key.toLowerCase()] = value.toLowerCase().trim();
} else {
acc[key.toLowerCase()] = value;
}
return acc;
}, {} as Record<string, any>);
};
// First, analyze all rows to determine if we have rows with multiple values
const rowsWithValues = localData.map(row => {
return countNonEmptyValues(row);
});
// Check if we have any rows with more than one value
const hasMultiValueRows = rowsWithValues.some(count => count > 1);
// Get the selected header row
const [selectedRowIndex] = selectedRows;
const selectedHeaderRow = localData[selectedRowIndex];
// Debug: Log the selected header row
console.log("Selected header row:", selectedHeaderRow);
const normalizedHeaderRow = normalizeRowForComparison(selectedHeaderRow);
// Debug: Log the normalized header row
console.log("Normalized header row:", normalizedHeaderRow);
const selectedHeaderStr = JSON.stringify(Object.entries(normalizedHeaderRow).sort());
// Filter out empty rows, rows with single values (if we have multi-value rows),
// and duplicate rows (including duplicates of the header row)
const seen = new Set<string>();
// Add the selected header row to the seen set first
seen.add(selectedHeaderStr);
// Debug: Track which rows are being removed and why
const removedRows: { index: number; reason: string; row: any }[] = [];
const filteredRows = localData.filter((row, index) => {
// Always keep the selected header row
if (index === selectedRowIndex) {
return true;
}
// Check if it's empty or has only one value
const nonEmptyCount = rowsWithValues[index];
if (nonEmptyCount === 0 || (hasMultiValueRows && nonEmptyCount <= 1)) {
removedRows.push({ index, reason: "Empty or single value", row });
return false;
}
// Check if it's a duplicate (case-insensitive)
const normalizedRow = normalizeRowForComparison(row);
// Debug: If this row might be a duplicate of the header, log it
if (index < 5 || index === selectedRowIndex + 1 || index === selectedRowIndex - 1) {
console.log(`Row ${index} normalized:`, normalizedRow);
}
const rowStr = JSON.stringify(Object.entries(normalizedRow).sort());
if (seen.has(rowStr)) {
removedRows.push({
index,
reason: "Duplicate",
row
});
return false;
}
seen.add(rowStr);
return true;
});
// Debug: Log removed rows
console.log("Removed rows:", removedRows);
// Only update if we actually removed any rows
if (filteredRows.length < localData.length) {
// Adjust the selected row index if needed
const newSelectedIndex = filteredRows.findIndex(row =>
JSON.stringify(Object.entries(normalizeRowForComparison(row)).sort()) === selectedHeaderStr
);
// Debug: Log the new selected index
console.log("New selected index:", newSelectedIndex);
setLocalData(filteredRows);
setSelectedRows(new Set([newSelectedIndex]));
toast({
title: "Rows removed",
description: `Removed ${localData.length - filteredRows.length} empty, single-value, or duplicate rows`,
variant: "default"
});
} else {
toast({
title: "No rows removed",
description: "No empty, single-value, or duplicate rows were found",
variant: "default"
});
}
}, [localData, selectedRows, toast]);
return (
<div className="flex flex-col h-[calc(100vh-9.5rem)]">
<div className="px-8 py-6 bg-background flex justify-between items-end">
<div>
<h2 className="text-2xl font-semibold text-foreground">
{translations.selectHeaderStep.title}
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Select the row that contains your column headers
</p>
</div>
<Button
variant="secondary"
size="sm"
onClick={discardEmptyAndDuplicateRows}
>
Remove Empty/Duplicates
</Button>
</div>
<div className="flex-1 flex flex-col min-h-0">
<div className="px-8 flex-1 overflow-auto">
<SelectHeaderTable
data={localData}
selectedRows={selectedRows}
setSelectedRows={setSelectedRows}
/>
</div>
</div>
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-2">
{onBack && (
<Button variant="outline" onClick={onBack}>
{translations.selectHeaderStep.backButtonTitle}
</Button>
)}
<Button
className="ml-auto"
disabled={isLoading}
onClick={handleContinue}
>
{translations.selectHeaderStep.nextButtonTitle}
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,86 @@
import { useMemo } from "react"
import type { RawData } from "../../../types"
import {
Table,
TableBody,
TableCell,
TableRow,
} from "@/components/ui/table"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
interface Props {
data: RawData[]
selectedRows: ReadonlySet<number>
setSelectedRows: (rows: ReadonlySet<number>) => void
}
export const SelectHeaderTable = ({ data, selectedRows, setSelectedRows }: Props) => {
const columns = useMemo(() => {
const longestRowLength = data.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0)
return Array.from(Array(longestRowLength), (_, index) => ({
key: index.toString(),
name: `Column ${index + 1}`,
}))
}, [data])
if (!data || data.length === 0) {
return (
<div className="p-4">
<p className="text-sm text-muted-foreground">No data available to select headers from.</p>
</div>
)
}
const selectedRowIndex = Array.from(selectedRows)[0]
const gridTemplateColumns = `60px repeat(${columns.length}, minmax(150px, 300px))`
return (
<div className="rounded-md border p-3">
<div className="h-[calc(100vh-23rem)] overflow-auto">
<Table className="relative w-full" style={{ tableLayout: 'fixed' }}>
<TableBody>
<RadioGroup
value={selectedRowIndex?.toString()}
onValueChange={(value) => setSelectedRows(new Set([parseInt(value)]))}
>
{data.map((row, rowIndex) => (
<TableRow
key={rowIndex}
className={cn(
"grid",
selectedRowIndex === rowIndex && "bg-muted font-bold",
"group hover:bg-muted/50"
)}
style={{ gridTemplateColumns }}
>
<TableCell className="overflow-hidden">
<div className="flex items-center">
<RadioGroupItem value={rowIndex.toString()} id={`row-${rowIndex}`} />
<Label htmlFor={`row-${rowIndex}`} className="sr-only">
Select as header row
</Label>
</div>
</TableCell>
{columns.map((column, colIndex) => (
<TableCell
key={`${rowIndex}-${column.key}`}
className="overflow-hidden"
>
<div className="truncate">
{row[colIndex] || ""}
</div>
</TableCell>
))}
</TableRow>
))}
</RadioGroup>
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -0,0 +1,67 @@
import { Column, FormatterProps, useRowSelection } from "react-data-grid"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import type { RawData } from "../../../types"
const SELECT_COLUMN_KEY = "select-row"
function SelectFormatter(props: FormatterProps<unknown>) {
const [isRowSelected, onRowSelectionChange] = useRowSelection()
return (
<div className="flex h-full items-center pl-2">
<RadioGroup defaultValue={isRowSelected ? "selected" : undefined}>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="selected"
id={`row-${props.rowIdx}`}
checked={isRowSelected}
onClick={(event) => {
onRowSelectionChange({
row: props.row,
checked: !isRowSelected,
isShiftClick: (event.nativeEvent as MouseEvent).shiftKey,
})
}}
/>
<Label
htmlFor={`row-${props.rowIdx}`}
className="sr-only"
>
Select as header row
</Label>
</div>
</RadioGroup>
</div>
)
}
export const SelectColumn: Column<any, any> = {
key: SELECT_COLUMN_KEY,
name: "Select Header",
width: 100,
minWidth: 100,
maxWidth: 100,
resizable: false,
sortable: false,
frozen: true,
cellClass: "rdg-radio",
formatter: SelectFormatter,
}
export const generateSelectionColumns = (data: RawData[]) => {
const longestRowLength = data.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0)
return [
SelectColumn,
...Array.from(Array(longestRowLength), (_, index) => ({
key: index.toString(),
name: `Column ${index + 1}`,
width: 150,
formatter: ({ row }: { row: RawData }) => (
<div className="p-2 overflow-hidden text-ellipsis whitespace-nowrap">
{row[index]}
</div>
),
})),
]
}

View File

@@ -0,0 +1,77 @@
import { useCallback, useState } from "react"
import { useRsi } from "../../hooks/useRsi"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { ChevronLeft } from "lucide-react"
type SelectSheetProps = {
sheetNames: string[]
onContinue: (sheetName: string) => Promise<void>
onBack?: () => void
}
export const SelectSheetStep = ({ sheetNames, onContinue, onBack }: SelectSheetProps) => {
const [isLoading, setIsLoading] = useState(false)
const { translations } = useRsi()
const [value, setValue] = useState(sheetNames[0])
const handleOnContinue = useCallback(
async (data: typeof value) => {
setIsLoading(true)
await onContinue(data)
setIsLoading(false)
},
[onContinue],
)
return (
<div className="flex h-[calc(100vh-10rem)] flex-col">
<div className="flex-1 overflow-hidden">
<div className="px-8 py-6">
<div className="mb-8">
<h2 className="text-3xl font-semibold text-foreground">
{translations.uploadStep.selectSheet.title}
</h2>
</div>
<RadioGroup
value={value}
onValueChange={setValue}
className="space-y-4"
>
{sheetNames.map((sheetName) => (
<div key={sheetName} className="flex items-center space-x-2">
<RadioGroupItem value={sheetName} id={sheetName} />
<Label
htmlFor={sheetName}
className="text-base"
>
{sheetName}
</Label>
</div>
))}
</RadioGroup>
</div>
</div>
<div className="flex items-center justify-between border-t px-8 py-4 bg-muted -mb-1">
{onBack && (
<Button
variant="ghost"
onClick={onBack}
className="gap-2"
>
<ChevronLeft className="h-4 w-4" />
{translations.uploadStep.selectSheet.backButtonTitle}
</Button>
)}
<div className="flex-1" />
<Button
onClick={() => handleOnContinue(value)}
disabled={isLoading}
>
{translations.uploadStep.selectSheet.nextButtonTitle}
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,103 @@
import { StepState, StepType, UploadFlow } from "./UploadFlow"
import { useRsi } from "../hooks/useRsi"
import { useRef, useState, useEffect } from "react"
import { steps, stepTypeToStepIndex, stepIndexToStepType } from "../utils/steps"
import { CgCheck } from "react-icons/cg"
const CheckIcon = ({ color }: { color: string }) => <CgCheck size="24" className={color} />
export const Steps = () => {
const { initialStepState, translations, isNavigationEnabled, isOpen } = useRsi()
const initialStep = stepTypeToStepIndex(initialStepState?.type)
const [activeStep, setActiveStep] = useState(initialStep)
const [state, setState] = useState<StepState>(initialStepState || { type: StepType.upload })
const history = useRef<StepState[]>([])
const prevIsOpen = useRef(isOpen)
// Reset state when dialog is reopened
useEffect(() => {
// Check if dialog was closed and is now open again
if (isOpen && !prevIsOpen.current) {
// Reset to initial state
setActiveStep(initialStep)
setState(initialStepState || { type: StepType.upload })
history.current = []
}
// Update previous isOpen value
prevIsOpen.current = isOpen
}, [isOpen, initialStep, initialStepState])
const onClickStep = (stepIndex: number) => {
const type = stepIndexToStepType(stepIndex)
const historyIdx = history.current.findIndex((v) => v.type === type)
if (historyIdx === -1) return
const nextHistory = history.current.slice(0, historyIdx + 1)
history.current = nextHistory
setState(nextHistory[nextHistory.length - 1])
setActiveStep(stepIndex)
}
const onBack = () => {
onClickStep(Math.max(activeStep - 1, 0))
}
const onNext = (v: StepState) => {
history.current.push(state)
setState(v)
if (v.type === StepType.validateData && 'isFromScratch' in v && v.isFromScratch) {
// If starting from scratch, jump directly to the validation step
const validationStepIndex = steps.indexOf('validationStep')
setActiveStep(validationStepIndex)
} else if (v.type !== StepType.selectSheet) {
setActiveStep(activeStep + 1)
}
}
return (
<>
<div className="hidden border-b bg-muted px-4 py-6 md:block">
<nav className="mx-auto flex items-center justify-center gap-4 lg:gap-24" aria-label="Steps">
{steps.map((key, index) => {
const isActive = index === activeStep
const isCompleted = index < activeStep
return (
<div key={key} className="flex items-center">
<button
className={`group flex items-center ${isNavigationEnabled ? 'cursor-pointer' : 'cursor-default'}`}
onClick={isNavigationEnabled ? () => onClickStep(index) : undefined}
disabled={!isNavigationEnabled}
>
<div className={`flex shrink-0 h-10 w-10 items-center justify-center rounded-full border-2 ${
isActive
? 'border-primary bg-primary text-primary-foreground'
: isCompleted
? 'border-primary bg-primary text-primary-foreground'
: 'border-muted-foreground/20 bg-background'
}`}>
{isCompleted ? (
<CheckIcon color="text-primary-foreground" />
) : (
<span className={`text-sm font-medium ${
isActive ? 'text-primary-foreground' : 'text-muted-foreground'
}`}>
{index + 1}
</span>
)}
</div>
<span className={`ml-2 text-sm font-medium ${
isActive ? 'text-foreground' : 'text-muted-foreground'
}`}>
{translations[key].title}
</span>
</button>
</div>
)
})}
</nav>
</div>
<UploadFlow state={state} onNext={onNext} onBack={isNavigationEnabled ? onBack : undefined} />
</>
)
}

View File

@@ -0,0 +1,258 @@
import { useCallback, useState } from "react"
import type XLSX from "xlsx"
import { UploadStep } from "./UploadStep/UploadStep"
import { SelectHeaderStep } from "./SelectHeaderStep/SelectHeaderStep"
import { SelectSheetStep } from "./SelectSheetStep/SelectSheetStep"
import { mapWorkbook } from "../utils/mapWorkbook"
import { ValidationStepNew } from "./ValidationStepNew"
import { ImageUploadStep } from "./ImageUploadStep/ImageUploadStep"
import { MatchColumnsStep, type GlobalSelections } from "./MatchColumnsStep/MatchColumnsStep"
import { exceedsMaxRecords } from "../utils/exceedsMaxRecords"
import { useRsi } from "../hooks/useRsi"
import type { RawData, Data } from "../types"
import { Progress } from "@/components/ui/progress"
import { useToast } from "@/hooks/use-toast"
import { addErrorsAndRunHooks } from "./ValidationStepNew/utils/dataMutations"
export enum StepType {
upload = "upload",
selectSheet = "selectSheet",
selectHeader = "selectHeader",
matchColumns = "matchColumns",
validateData = "validateData",
imageUpload = "imageUpload",
}
export type StepState =
| {
type: StepType.upload
}
| {
type: StepType.selectSheet
workbook: XLSX.WorkBook
}
| {
type: StepType.selectHeader
data: RawData[]
}
| {
type: StepType.matchColumns
data: RawData[]
headerValues: RawData
globalSelections?: GlobalSelections
}
| {
type: StepType.validateData
data: any[]
globalSelections?: GlobalSelections
isFromScratch?: boolean
}
| {
type: StepType.imageUpload
data: any[]
file: File
globalSelections?: GlobalSelections
}
interface Props {
state: StepState
onNext: (v: StepState) => void
onBack?: () => void
}
export const UploadFlow = ({ state, onNext, onBack }: Props) => {
const {
maxRecords,
translations,
uploadStepHook,
selectHeaderStepHook,
matchColumnsStepHook,
fields,
rowHook,
tableHook,
onSubmit } = useRsi()
const [uploadedFile, setUploadedFile] = useState<File | null>(null)
const { toast } = useToast()
const errorToast = useCallback(
(description: string) => {
toast({
variant: "destructive",
title: translations.alerts.toast.error,
description,
})
},
[toast, translations],
)
// Keep track of global selections across steps
const [persistedGlobalSelections, setPersistedGlobalSelections] = useState<GlobalSelections | undefined>(
state.type === StepType.validateData || state.type === StepType.matchColumns
? state.globalSelections
: undefined
)
switch (state.type) {
case StepType.upload:
return (
<UploadStep
onContinue={async (workbook, file) => {
setUploadedFile(file)
const isSingleSheet = workbook.SheetNames.length === 1
if (isSingleSheet) {
if (maxRecords && exceedsMaxRecords(workbook.Sheets[workbook.SheetNames[0]], maxRecords)) {
errorToast(translations.uploadStep.maxRecordsExceeded(maxRecords.toString()))
return
}
try {
const mappedWorkbook = await uploadStepHook(mapWorkbook(workbook))
onNext({
type: StepType.selectHeader,
data: mappedWorkbook,
})
} catch (e) {
errorToast((e as Error).message)
}
} else {
onNext({ type: StepType.selectSheet, workbook })
}
}}
setInitialState={(state) => {
// Ensure the state has the correct type
if (state.type === StepType.validateData) {
onNext({
type: StepType.validateData,
data: state.data,
isFromScratch: state.isFromScratch,
globalSelections: undefined
});
}
}}
/>
)
case StepType.selectSheet:
return (
<SelectSheetStep
sheetNames={state.workbook.SheetNames}
onContinue={async (sheetName) => {
if (maxRecords && exceedsMaxRecords(state.workbook.Sheets[sheetName], maxRecords)) {
errorToast(translations.uploadStep.maxRecordsExceeded(maxRecords.toString()))
return
}
try {
const mappedWorkbook = await uploadStepHook(mapWorkbook(state.workbook, sheetName))
onNext({
type: StepType.selectHeader,
data: mappedWorkbook,
})
} catch (e) {
errorToast((e as Error).message)
}
}}
onBack={onBack}
/>
)
case StepType.selectHeader:
return (
<SelectHeaderStep
data={state.data}
onContinue={async (...args) => {
try {
const { data, headerValues } = await selectHeaderStepHook(...args)
onNext({
type: StepType.matchColumns,
data,
headerValues,
globalSelections: persistedGlobalSelections,
})
} catch (e) {
errorToast((e as Error).message)
}
}}
onBack={onBack}
/>
)
case StepType.matchColumns:
return (
<MatchColumnsStep
data={state.data}
headerValues={state.headerValues}
initialGlobalSelections={persistedGlobalSelections}
onContinue={async (values, rawData, columns, globalSelections) => {
try {
const data = await matchColumnsStepHook(values, rawData, columns)
const dataWithMeta = await addErrorsAndRunHooks(data, fields, rowHook, tableHook)
// Apply global selections to each row of data if they exist
const dataWithGlobalSelections = globalSelections
? dataWithMeta.map((row: Data<string> & { __index?: string }) => {
const newRow = { ...row };
if (globalSelections.supplier) newRow.supplier = globalSelections.supplier;
if (globalSelections.company) newRow.company = globalSelections.company;
return newRow;
})
: dataWithMeta;
setPersistedGlobalSelections(globalSelections)
onNext({
type: StepType.validateData,
data: dataWithGlobalSelections,
globalSelections,
})
} catch (e) {
errorToast((e as Error).message)
}
}}
onBack={onBack}
/>
)
case StepType.validateData:
// Always use the new ValidationStepNew component
return (
<ValidationStepNew
initialData={state.data}
file={uploadedFile || new File([], "empty.xlsx")}
onBack={() => {
// If we started from scratch, we need to go back to the upload step
if (state.isFromScratch) {
onNext({
type: StepType.upload
});
} else if (onBack) {
// Use the provided onBack function
onBack();
}
}}
onNext={(validatedData: any[]) => {
// Go to image upload step with the validated data
onNext({
type: StepType.imageUpload,
data: validatedData,
file: uploadedFile || new File([], "empty.xlsx"),
globalSelections: state.globalSelections
});
}}
isFromScratch={state.isFromScratch}
/>
)
case StepType.imageUpload:
return (
<ImageUploadStep
data={state.data}
file={state.file}
onBack={onBack}
onSubmit={(data, file) => {
// Create a Result object from the array data
const result = {
validData: data as Data<string>[],
invalidData: [] as Data<string>[],
all: data as Data<string>[]
};
onSubmit(result, file);
}}
/>
)
default:
return <Progress value={33} className="w-full" />
}
}

View File

@@ -0,0 +1,61 @@
import type XLSX from "xlsx"
import { useCallback, useState } from "react"
import { useRsi } from "../../hooks/useRsi"
import { DropZone } from "./components/DropZone"
import { StepType } from "../UploadFlow"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
type UploadProps = {
onContinue: (data: XLSX.WorkBook, file: File) => Promise<void>
setInitialState?: (state: { type: StepType; data: any[]; isFromScratch?: boolean }) => void
}
export const UploadStep = ({ onContinue, setInitialState }: UploadProps) => {
const [isLoading, setIsLoading] = useState(false)
const { translations } = useRsi()
const handleOnContinue = useCallback(
async (data: XLSX.WorkBook, file: File) => {
setIsLoading(true)
await onContinue(data, file)
setIsLoading(false)
},
[onContinue],
)
const handleStartFromScratch = useCallback(() => {
if (setInitialState) {
setInitialState({ type: StepType.validateData, data: [{}], isFromScratch: true })
}
}, [setInitialState])
return (
<div className="p-8">
<h2 className="text-3xl font-semibold mb-8 text-left">{translations.uploadStep.title}</h2>
<div className="max-w-xl mx-auto w-full space-y-8">
<div className="rounded-lg p-6 flex flex-col items-center">
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
</div>
<div className="flex items-center justify-center">
<Separator className="w-24" />
<span className="px-3 text-muted-foreground text-sm font-medium">OR</span>
<Separator className="w-24" />
</div>
<div className="flex justify-center">
<Button
onClick={handleStartFromScratch}
variant="outline"
className="min-w-[200px]"
disabled={!setInitialState}
>
Start from scratch
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,85 @@
import { useDropzone } from "react-dropzone"
import * as XLSX from "xlsx"
import { useState } from "react"
import { useRsi } from "../../../hooks/useRsi"
import { readFileAsync } from "../utils/readFilesAsync"
import { Button } from "@/components/ui/button"
import { useToast } from "@/hooks/use-toast"
import { cn } from "@/lib/utils"
type DropZoneProps = {
onContinue: (data: XLSX.WorkBook, file: File) => void
isLoading: boolean
}
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
const { translations, maxFileSize, dateFormat, parseRaw } = useRsi()
const { toast } = useToast()
const [loading, setLoading] = useState(false)
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
noClick: true,
noKeyboard: true,
maxFiles: 1,
maxSize: maxFileSize,
accept: {
"application/vnd.ms-excel": [".xls"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
"text/csv": [".csv"],
},
onDropRejected: (fileRejections) => {
setLoading(false)
fileRejections.forEach((fileRejection) => {
toast({
variant: "destructive",
title: `${fileRejection.file.name} ${translations.uploadStep.dropzone.errorToastDescription}`,
description: fileRejection.errors[0].message,
})
})
},
onDropAccepted: async ([file]) => {
setLoading(true)
const arrayBuffer = await readFileAsync(file)
const workbook = XLSX.read(arrayBuffer, {
cellDates: true,
dateNF: dateFormat,
raw: parseRaw,
dense: true,
type: 'array',
codepage: 65001, // UTF-8
WTF: false // Don't throw on errors
})
setLoading(false)
onContinue(workbook, file)
},
})
return (
<div
{...getRootProps()}
className={cn(
"flex h-full w-full flex-1 flex-col items-center justify-center rounded-lg border-2 border-dashed border-secondary-foreground/30 bg-muted/90 p-12",
isDragActive && "border-primary bg-muted"
)}
>
<input {...getInputProps()} data-testid="rsi-dropzone" />
{isDragActive ? (
<p className="text-lg text-muted-foreground mb-1 py-6">
{translations.uploadStep.dropzone.activeDropzoneTitle}
</p>
) : loading || isLoading ? (
<p className="text-lg text-muted-foreground">
{translations.uploadStep.dropzone.loadingTitle}
</p>
) : (
<>
<p className="mb-4 text-lg text-muted-foreground">
{translations.uploadStep.dropzone.title}
</p>
<Button onClick={open} variant="default">
{translations.uploadStep.dropzone.buttonTitle}
</Button>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,44 @@
import type { Column } from "react-data-grid"
import type { Fields } from "../../../types"
import { CgInfo } from "react-icons/cg"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
export const generateColumns = <T extends string>(fields: Fields<T>) =>
fields.map(
(column): Column<any> => ({
key: column.key,
name: column.label,
minWidth: 150,
headerRenderer: () => (
<div className="flex items-center gap-1 relative">
<div className="flex-1 overflow-hidden text-ellipsis">
{column.label}
</div>
{column.description && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex-none">
<CgInfo className="h-4 w-4" />
</div>
</TooltipTrigger>
<TooltipContent>
{column.description}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
),
formatter: ({ row }) => (
<div className="min-w-full min-h-full overflow-hidden text-ellipsis">
{row[column.key]}
</div>
),
}),
)

View File

@@ -0,0 +1,13 @@
export const readFileAsync = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
resolve(reader.result)
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}

View File

@@ -0,0 +1,58 @@
# ValidationStepNew Component
This is a refactored version of the original ValidationStep component with improved architecture, performance, and maintainability.
## Overview
The ValidationStepNew component is designed to replace the original ValidationStep component in the React Spreadsheet Import library. It provides the same functionality but with a more modular and maintainable codebase.
## Features
- Field-level validation (required, regex, unique)
- Row-level validation (supplier, company fields)
- UPC validation with API integration
- Template management (saving, loading, applying)
- Filtering and sorting capabilities
- Error display and management
- Special field handling (input, multi-input, select, checkbox)
## Usage
To use the new ValidationStepNew component, select the "Use new validation component" checkbox in the MatchColumnsStep. This will route you to the new implementation instead of the original one.
## Architecture
The component is structured as follows:
- **ValidationContainer**: Main container component that coordinates all subcomponents
- **ValidationTable**: Handles data display, filtering, and column configuration
- **ValidationCell**: Cell-level component with specialized rendering based on field type
- **ValidationToolbar**: Top toolbar with actions and statistics
- **ValidationSidebar**: Contains filters, actions, and other UI controls
## State Management
State is managed through custom hooks:
- **useValidationState**: Main state management hook
- **useValidation**: Validation logic
- **useTemplates**: Template management
- **useFilters**: Filtering logic
- **useUpcValidation**: UPC validation
## Development
This component is still under development. The goal is to eventually replace the original ValidationStep component completely once all functionality is implemented and tested.
## Known Issues
- Some TypeScript errors in the UploadFlow component when integrating with the new component
- Not all features from the original component are fully implemented yet
## Roadmap
1. Complete implementation of all features from the original component
2. Add comprehensive tests
3. Improve performance with virtualization for large datasets
4. Add more customization options
5. Replace the original component completely

View File

@@ -0,0 +1,248 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { Loader2, CheckIcon } from 'lucide-react';
import { Code } from '@/components/ui/code';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { AiValidationDetails, AiValidationProgress, CurrentPrompt } from '../hooks/useAiValidation';
interface AiValidationDialogsProps {
aiValidationProgress: AiValidationProgress;
aiValidationDetails: AiValidationDetails;
currentPrompt: CurrentPrompt;
setAiValidationProgress: React.Dispatch<React.SetStateAction<AiValidationProgress>>;
setAiValidationDetails: React.Dispatch<React.SetStateAction<AiValidationDetails>>;
setCurrentPrompt: React.Dispatch<React.SetStateAction<CurrentPrompt>>;
revertAiChange: (productIndex: number, fieldKey: string) => void;
isChangeReverted: (productIndex: number, fieldKey: string) => boolean;
getFieldDisplayValueWithHighlight: (fieldKey: string, originalValue: any, correctedValue: any) => { originalHtml: string, correctedHtml: string };
fields: readonly any[];
}
export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
aiValidationProgress,
aiValidationDetails,
currentPrompt,
setAiValidationProgress,
setAiValidationDetails,
setCurrentPrompt,
revertAiChange,
isChangeReverted,
getFieldDisplayValueWithHighlight,
fields
}) => {
return (
<>
{/* Current Prompt Dialog */}
<Dialog
open={currentPrompt.isOpen}
onOpenChange={(open) => setCurrentPrompt(prev => ({ ...prev, isOpen: open }))}
>
<DialogContent className="max-w-4xl h-[80vh]">
<DialogHeader>
<DialogTitle>Current AI Prompt</DialogTitle>
<DialogDescription>
This is the exact prompt that would be sent to the AI for validation
</DialogDescription>
</DialogHeader>
<ScrollArea className="flex-1">
{currentPrompt.isLoading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
) : (
<Code className="whitespace-pre-wrap p-4">{currentPrompt.prompt}</Code>
)}
</ScrollArea>
</DialogContent>
</Dialog>
{/* AI Validation Progress Dialog */}
<Dialog
open={aiValidationProgress.isOpen}
onOpenChange={(open) => {
// Only allow closing if validation failed
if (!open && aiValidationProgress.step === -1) {
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
}
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>AI Validation Progress</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="flex items-center gap-4">
<div className="flex-1">
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-500"
style={{
width: `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`,
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined
}}
/>
</div>
</div>
<div className="text-sm text-muted-foreground w-12 text-right">
{aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`}
</div>
</div>
<p className="text-center text-sm text-muted-foreground">
{aiValidationProgress.status}
</p>
{(() => {
// Only show time remaining if we have an estimate and are in progress
return aiValidationProgress.estimatedSeconds &&
aiValidationProgress.elapsedSeconds !== undefined &&
aiValidationProgress.step > 0 &&
aiValidationProgress.step < 5 && (
<div className="text-center text-sm">
{(() => {
// Calculate time remaining using the elapsed seconds
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds);
// Format time remaining
if (remainingSeconds < 60) {
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`;
} else {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = Math.round(remainingSeconds % 60);
return `Approximately ${minutes}m ${seconds}s remaining`;
}
})()}
{aiValidationProgress.promptLength && (
<p className="mt-1 text-xs text-muted-foreground">
Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters
</p>
)}
</div>
);
})()}
</div>
</DialogContent>
</Dialog>
{/* AI Validation Results Dialog */}
<Dialog
open={aiValidationDetails.isOpen}
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>AI Validation Results</DialogTitle>
<DialogDescription>
Review the changes and warnings suggested by the AI
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[60vh]">
{aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? (
<div className="mb-6 space-y-6">
<h3 className="font-semibold text-lg">Detailed Changes:</h3>
{aiValidationDetails.changeDetails.map((product, i) => {
// Find the title change if it exists
const titleChange = product.changes.find(c => c.field === 'title');
const titleValue = titleChange ? titleChange.corrected : product.title;
return (
<div key={`product-${i}`} className="border rounded-md p-4">
<h4 className="font-medium text-base mb-3">
{titleValue || `Product ${product.productIndex + 1}`}
</h4>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px]">Field</TableHead>
<TableHead>Original Value</TableHead>
<TableHead>Corrected Value</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{product.changes.map((change, j) => {
const field = fields.find(f => f.key === change.field);
const fieldLabel = field ? field.label : change.field;
const isReverted = isChangeReverted(product.productIndex, change.field);
// Get highlighted differences
const { originalHtml, correctedHtml } = getFieldDisplayValueWithHighlight(
change.field,
change.original,
change.corrected
);
return (
<TableRow key={`change-${j}`}>
<TableCell className="font-medium">{fieldLabel}</TableCell>
<TableCell>
<div
dangerouslySetInnerHTML={{ __html: originalHtml }}
className={isReverted ? "font-medium" : ""}
/>
</TableCell>
<TableCell>
<div
dangerouslySetInnerHTML={{ __html: correctedHtml }}
className={!isReverted ? "font-medium" : ""}
/>
</TableCell>
<TableCell className="text-right">
<div className="mt-2">
{isReverted ? (
<Button
variant="ghost"
size="sm"
className="text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700"
disabled
>
<CheckIcon className="w-4 h-4 mr-1" />
Reverted
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
// Call the revert function directly
revertAiChange(product.productIndex, change.field);
}}
>
Revert Change
</Button>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
})}
</div>
) : (
<div className="py-8 text-center text-muted-foreground">
{aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? (
<div>
<p className="mb-4">No changes were made, but the AI provided some warnings:</p>
<ul className="list-disc pl-8 text-left">
{aiValidationDetails.warnings.map((warning, i) => (
<li key={`warning-${i}`} className="mb-2">{warning}</li>
))}
</ul>
</div>
) : (
<p>No changes or warnings were suggested by the AI.</p>
)}
</div>
)}
</ScrollArea>
</DialogContent>
</Dialog>
</>
);
};

View File

@@ -0,0 +1,42 @@
// Define MultiSelectCell component to fix the import issue
type MultiSelectCellProps = {
field: string;
value: any;
onChange: (value: any) => void;
options: any[];
hasErrors: boolean;
className?: string;
};
// Using _ to indicate intentionally unused parameters
const MultiSelectCell = (_: MultiSelectCellProps) => {
// This is a placeholder implementation
return null;
};
const BaseCellContent = ({ fieldType, field, value, onChange, options, hasErrors, className }: {
fieldType: string;
field: string;
value: any;
onChange: (value: any) => void;
options: any[];
hasErrors: boolean;
className?: string;
}) => {
if (fieldType === 'multi-select' || fieldType === 'multi-input') {
return (
<MultiSelectCell
field={field}
value={value}
onChange={onChange}
options={options}
hasErrors={hasErrors}
className={className}
/>
);
}
return null;
};
export default BaseCellContent;

View File

@@ -0,0 +1,328 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { Template } from '../hooks/validationTypes'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
import { Check, ChevronsUpDown } from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
interface SearchableTemplateSelectProps {
templates: Template[] | undefined;
value: string;
onValueChange: (value: string) => void;
getTemplateDisplayText: (templateId: string | null) => string;
placeholder?: string;
className?: string;
triggerClassName?: string;
defaultBrand?: string;
}
const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
templates = [],
value,
onValueChange,
getTemplateDisplayText,
placeholder = "Select template",
className,
triggerClassName,
defaultBrand,
}) => {
const [searchTerm, setSearchTerm] = useState("");
const [selectedBrand, setSelectedBrand] = useState<string | null>(null);
const [open, setOpen] = useState(false);
// Set default brand when component mounts or defaultBrand changes
useEffect(() => {
if (defaultBrand) {
setSelectedBrand(defaultBrand);
}
}, [defaultBrand]);
// Force a re-render when templates change from empty to non-empty
useEffect(() => {
if (templates && templates.length > 0) {
// Force a re-render by updating state
setSearchTerm("");
}
}, [templates]);
// Handle wheel events for scrolling
const handleWheel = (e: React.WheelEvent) => {
const scrollArea = e.currentTarget;
scrollArea.scrollTop += e.deltaY;
};
// Extract unique brands from templates
const brands = useMemo(() => {
try {
if (!Array.isArray(templates) || templates.length === 0) {
return [];
}
const brandSet = new Set<string>();
const brandNames: {id: string, name: string}[] = [];
templates.forEach(template => {
if (!template?.company) return;
const companyId = template.company;
if (!brandSet.has(companyId)) {
brandSet.add(companyId);
// Try to get the company name from the template display text
try {
const displayText = getTemplateDisplayText(template.id.toString());
const companyName = displayText.split(' - ')[0];
brandNames.push({ id: companyId, name: companyName || companyId });
} catch (err) {
brandNames.push({ id: companyId, name: companyId });
}
}
});
return brandNames.sort((a, b) => a.name.localeCompare(b.name));
} catch (err) {
return [];
}
}, [templates, getTemplateDisplayText]);
// Group templates by company for better organization
const groupedTemplates = useMemo(() => {
try {
if (!Array.isArray(templates) || templates.length === 0) return {};
const groups: Record<string, Template[]> = {};
templates.forEach(template => {
if (!template?.company) return;
const companyId = template.company;
if (!groups[companyId]) {
groups[companyId] = [];
}
groups[companyId].push(template);
});
return groups;
} catch (err) {
return {};
}
}, [templates]);
// Filter templates based on selected brand and search term
const filteredTemplates = useMemo(() => {
try {
if (!Array.isArray(templates) || templates.length === 0) return [];
// First filter by brand if selected
let brandFiltered = templates;
if (selectedBrand) {
// Check if the selected brand has any templates
const brandTemplates = templates.filter(t => t?.company === selectedBrand);
// If the selected brand has templates, use them; otherwise, show all templates
brandFiltered = brandTemplates.length > 0 ? brandTemplates : templates;
}
// Then filter by search term if provided
if (!searchTerm.trim()) return brandFiltered;
const lowerSearchTerm = searchTerm.toLowerCase();
return brandFiltered.filter(template => {
if (!template?.id) return false;
try {
const displayText = getTemplateDisplayText(template.id.toString());
const productType = template.product_type?.toLowerCase() || '';
return displayText.toLowerCase().includes(lowerSearchTerm) ||
productType.includes(lowerSearchTerm);
} catch (error) {
return false;
}
});
} catch (err) {
return [];
}
}, [templates, selectedBrand, searchTerm, getTemplateDisplayText]);
// Handle errors gracefully
const getDisplayText = useCallback(() => {
try {
if (!value) return placeholder;
const template = templates.find(t => t.id.toString() === value);
if (!template) return placeholder;
// Get the original display text
const originalText = getTemplateDisplayText(value);
// Check if it has the expected format "Brand - Product Type"
if (originalText.includes(' - ')) {
const [brand, productType] = originalText.split(' - ', 2);
// Reverse the order to "Product Type - Brand"
return `${productType} - ${brand}`;
}
// If it doesn't match the expected format, return the original text
return originalText;
} catch (err) {
console.error('Error getting display text:', err);
return placeholder;
}
}, [getTemplateDisplayText, placeholder, value, templates]);
// Safe render function for CommandItem
const renderCommandItem = useCallback((template: Template) => {
if (!template?.id) return null;
try {
const displayText = getTemplateDisplayText(template.id.toString());
return (
<CommandItem
key={template.id}
value={template.id.toString()}
onSelect={(currentValue) => {
try {
onValueChange(currentValue);
setOpen(false);
setSearchTerm("");
} catch (err) {
console.error('Error selecting template:', err);
}
}}
className="flex items-center justify-between"
>
<span>{displayText}</span>
{value === template.id.toString() && <Check className="h-4 w-4 ml-2" />}
</CommandItem>
);
} catch (err) {
console.error('Error rendering template item:', err);
return null;
}
}, [onValueChange, value, getTemplateDisplayText]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between overflow-hidden", triggerClassName)}
>
<span className="truncate overflow-hidden mr-1">{getDisplayText()}</span>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 flex-none" />
</Button>
</PopoverTrigger>
<PopoverContent className={cn("w-[300px] p-0", className)}>
<Command>
<div className="flex flex-col p-2 gap-2">
{brands.length > 0 && (
<div className="flex items-center gap-2">
<Select
value={selectedBrand || "all"}
onValueChange={(value) => {
setSelectedBrand(value === "all" ? null : value);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="All Brands" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Brands</SelectItem>
{brands.map(brand => (
<SelectItem key={brand.id} value={brand.id}>
{brand.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<CommandSeparator />
<div className="flex items-center gap-2">
<CommandInput
placeholder="Search by product type..."
value={searchTerm}
onValueChange={setSearchTerm}
className="h-8 flex-1"
/>
</div>
</div>
<CommandEmpty>
<div className="py-6 text-center">
<p className="text-sm text-muted-foreground">No templates found.</p>
</div>
</CommandEmpty>
<CommandList>
<ScrollArea className="max-h-[200px] overflow-y-auto" onWheel={handleWheel}>
{!searchTerm ? (
selectedBrand ? (
groupedTemplates[selectedBrand]?.length > 0 ? (
<CommandGroup heading={brands.find(b => b.id === selectedBrand)?.name || selectedBrand}>
{groupedTemplates[selectedBrand]?.map(template => renderCommandItem(template))}
</CommandGroup>
) : (
// If selected brand has no templates, show all brands
Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => {
const brand = brands.find(b => b.id === companyId);
const companyName = brand ? brand.name : companyId;
return (
<CommandGroup key={companyId} heading={companyName}>
{companyTemplates.map(template => renderCommandItem(template))}
</CommandGroup>
);
})
)
) : (
Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => {
const brand = brands.find(b => b.id === companyId);
const companyName = brand ? brand.name : companyId;
return (
<CommandGroup key={companyId} heading={companyName}>
{companyTemplates.map(template => renderCommandItem(template))}
</CommandGroup>
);
})
)
) : (
<CommandGroup>
{filteredTemplates.map(template => renderCommandItem(template))}
</CommandGroup>
)}
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export default SearchableTemplateSelect;

Some files were not shown because too many files have changed in this diff Show More