Compare commits
7 Commits
af067f7360
...
add-produc
| Author | SHA1 | Date | |
|---|---|---|---|
| 387e7e5e73 | |||
| a51a48ce89 | |||
| aacb3a2fd0 | |||
| 35d2f0df7c | |||
| 7d46ebd6ba | |||
| 1496aa57b1 | |||
| fc9ef2f0d7 |
@@ -1,43 +0,0 @@
|
|||||||
# Details
|
|
||||||
|
|
||||||
Date : 2025-03-17 14:20:03
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 28 files, 6565 codes, 1027 comments, 1053 blanks, all 8645 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/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/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/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/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 | 1,101 | 234 | 213 | 1,548 |
|
|
||||||
| [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/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/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 | 93 | 13 | 18 | 124 |
|
|
||||||
| [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)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Diff Details
|
|
||||||
|
|
||||||
Date : 2025-03-17 14:20:03
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
|
||||||
|
|
||||||
## Files
|
|
||||||
| filename | language | code | comment | blank | total |
|
|
||||||
| :--- | :--- | ---: | ---: | ---: | ---: |
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Diff Summary
|
|
||||||
|
|
||||||
Date : 2025-03-17 14:20:03
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
|
||||||
|
|
||||||
## Languages
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
|
|
||||||
## Directories
|
|
||||||
| path | files | code | comment | blank | total |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
Date : 2025-03-17 14:20:03
|
|
||||||
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
|
||||||
|
|
||||||
Languages
|
|
||||||
+----------+------------+------------+------------+------------+------------+
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
+----------+------------+------------+------------+------------+------------+
|
|
||||||
+----------+------------+------------+------------+------------+------------+
|
|
||||||
|
|
||||||
Directories
|
|
||||||
+------+------------+------------+------------+------------+------------+
|
|
||||||
| path | files | code | comment | blank | total |
|
|
||||||
+------+------------+------------+------------+------------+------------+
|
|
||||||
+------+------------+------------+------------+------------+------------+
|
|
||||||
|
|
||||||
Files
|
|
||||||
+----------+----------+------------+------------+------------+------------+
|
|
||||||
| filename | language | code | comment | blank | total |
|
|
||||||
+----------+----------+------------+------------+------------+------------+
|
|
||||||
| Total | | 0 | 0 | 0 | 0 |
|
|
||||||
+----------+----------+------------+------------+------------+------------+
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,31 +0,0 @@
|
|||||||
# Summary
|
|
||||||
|
|
||||||
Date : 2025-03-17 14:20:03
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 28 files, 6565 codes, 1027 comments, 1053 blanks, all 8645 lines
|
|
||||||
|
|
||||||
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
|
||||||
|
|
||||||
## Languages
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| TypeScript JSX | 20 | 6,189 | 914 | 970 | 8,073 |
|
|
||||||
| 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 |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| . | 28 | 6,565 | 1,027 | 1,053 | 8,645 |
|
|
||||||
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
|
||||||
| components | 13 | 4,004 | 515 | 536 | 5,055 |
|
|
||||||
| components (Files) | 8 | 2,771 | 357 | 378 | 3,506 |
|
|
||||||
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
|
|
||||||
| hooks | 6 | 2,165 | 393 | 432 | 2,990 |
|
|
||||||
| 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)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
Date : 2025-03-17 14:20:03
|
|
||||||
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
Total : 28 files, 6565 codes, 1027 comments, 1053 blanks, all 8645 lines
|
|
||||||
|
|
||||||
Languages
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
| TypeScript JSX | 20 | 6,189 | 914 | 970 | 8,073 |
|
|
||||||
| 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 |
|
|
||||||
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
|
||||||
| . | 28 | 6,565 | 1,027 | 1,053 | 8,645 |
|
|
||||||
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
|
||||||
| components | 13 | 4,004 | 515 | 536 | 5,055 |
|
|
||||||
| components (Files) | 8 | 2,771 | 357 | 378 | 3,506 |
|
|
||||||
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
|
|
||||||
| hooks | 6 | 2,165 | 393 | 432 | 2,990 |
|
|
||||||
| 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/SaveTemplateDialog.tsx | TypeScript JSX | 83 | 0 | 4 | 87 |
|
|
||||||
| /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/TemplateManager.tsx | TypeScript JSX | 193 | 4 | 15 | 212 |
|
|
||||||
| /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 | 1,101 | 234 | 213 | 1,548 |
|
|
||||||
| /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/useFilters.tsx | TypeScript JSX | 89 | 12 | 16 | 117 |
|
|
||||||
| /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 | 93 | 13 | 18 | 124 |
|
|
||||||
| /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,565 | 1,027 | 1,053 | 8,645 |
|
|
||||||
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Details
|
|
||||||
|
|
||||||
Date : 2025-03-17 16:02:14
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 28 files, 6551 codes, 1023 comments, 1050 blanks, all 8624 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/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/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/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/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 | 971 | 194 | 178 | 1,343 |
|
|
||||||
| [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/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/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)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Diff Details
|
|
||||||
|
|
||||||
Date : 2025-03-17 16:02:14
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 2 files, -14 codes, -4 comments, -3 blanks, all -21 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/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | -130 | -40 | -35 | -205 |
|
|
||||||
| [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 | 116 | 36 | 32 | 184 |
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Diff Summary
|
|
||||||
|
|
||||||
Date : 2025-03-17 16:02:14
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 2 files, -14 codes, -4 comments, -3 blanks, all -21 lines
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
|
||||||
|
|
||||||
## Languages
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| TypeScript JSX | 2 | -14 | -4 | -3 | -21 |
|
|
||||||
|
|
||||||
## Directories
|
|
||||||
| path | files | code | comment | blank | total |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| . | 2 | -14 | -4 | -3 | -21 |
|
|
||||||
| components | 1 | -130 | -40 | -35 | -205 |
|
|
||||||
| hooks | 1 | 116 | 36 | 32 | 184 |
|
|
||||||
|
|
||||||
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
Date : 2025-03-17 16:02:14
|
|
||||||
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
Total : 2 files, -14 codes, -4 comments, -3 blanks, all -21 lines
|
|
||||||
|
|
||||||
Languages
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
| TypeScript JSX | 2 | -14 | -4 | -3 | -21 |
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
|
|
||||||
Directories
|
|
||||||
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
|
||||||
| path | files | code | comment | blank | total |
|
|
||||||
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
|
||||||
| . | 2 | -14 | -4 | -3 | -21 |
|
|
||||||
| components | 1 | -130 | -40 | -35 | -205 |
|
|
||||||
| hooks | 1 | 116 | 36 | 32 | 184 |
|
|
||||||
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
|
||||||
|
|
||||||
Files
|
|
||||||
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
|
||||||
| filename | language | code | comment | blank | total |
|
|
||||||
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
|
||||||
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | -130 | -40 | -35 | -205 |
|
|
||||||
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 116 | 36 | 32 | 184 |
|
|
||||||
| Total | | -14 | -4 | -3 | -21 |
|
|
||||||
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,31 +0,0 @@
|
|||||||
# Summary
|
|
||||||
|
|
||||||
Date : 2025-03-17 16:02:14
|
|
||||||
|
|
||||||
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
|
|
||||||
Total : 28 files, 6551 codes, 1023 comments, 1050 blanks, all 8624 lines
|
|
||||||
|
|
||||||
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
|
||||||
|
|
||||||
## Languages
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| TypeScript JSX | 20 | 6,175 | 910 | 967 | 8,052 |
|
|
||||||
| 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 |
|
|
||||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| . | 28 | 6,551 | 1,023 | 1,050 | 8,624 |
|
|
||||||
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
|
||||||
| components | 13 | 3,874 | 475 | 501 | 4,850 |
|
|
||||||
| components (Files) | 8 | 2,641 | 317 | 343 | 3,301 |
|
|
||||||
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
|
|
||||||
| hooks | 6 | 2,281 | 429 | 464 | 3,174 |
|
|
||||||
| 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)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
Date : 2025-03-17 16:02:14
|
|
||||||
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
|
||||||
Total : 28 files, 6551 codes, 1023 comments, 1050 blanks, all 8624 lines
|
|
||||||
|
|
||||||
Languages
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
| language | files | code | comment | blank | total |
|
|
||||||
+----------------+------------+------------+------------+------------+------------+
|
|
||||||
| TypeScript JSX | 20 | 6,175 | 910 | 967 | 8,052 |
|
|
||||||
| 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 |
|
|
||||||
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
|
||||||
| . | 28 | 6,551 | 1,023 | 1,050 | 8,624 |
|
|
||||||
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
|
||||||
| components | 13 | 3,874 | 475 | 501 | 4,850 |
|
|
||||||
| components (Files) | 8 | 2,641 | 317 | 343 | 3,301 |
|
|
||||||
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
|
|
||||||
| hooks | 6 | 2,281 | 429 | 464 | 3,174 |
|
|
||||||
| 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/SaveTemplateDialog.tsx | TypeScript JSX | 83 | 0 | 4 | 87 |
|
|
||||||
| /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/TemplateManager.tsx | TypeScript JSX | 193 | 4 | 15 | 212 |
|
|
||||||
| /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 | 971 | 194 | 178 | 1,343 |
|
|
||||||
| /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/useFilters.tsx | TypeScript JSX | 89 | 12 | 16 | 117 |
|
|
||||||
| /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,551 | 1,023 | 1,050 | 8,624 |
|
|
||||||
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -64,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/**/*
|
||||||
131
docs/validation-hook-refactor.md
Normal file
131
docs/validation-hook-refactor.md
Normal 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.
|
||||||
2249
inventory/package-lock.json
generated
2249
inventory/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,22 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/button": "^2.1.0",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@chakra-ui/checkbox": "^2.3.2",
|
|
||||||
"@chakra-ui/form-control": "^2.2.0",
|
|
||||||
"@chakra-ui/hooks": "^2.4.3",
|
|
||||||
"@chakra-ui/icons": "^2.2.4",
|
|
||||||
"@chakra-ui/input": "^2.1.2",
|
|
||||||
"@chakra-ui/layout": "^2.3.1",
|
|
||||||
"@chakra-ui/modal": "^2.3.1",
|
|
||||||
"@chakra-ui/popper": "^3.1.0",
|
|
||||||
"@chakra-ui/react": "^2.8.1",
|
|
||||||
"@chakra-ui/select": "^2.1.2",
|
|
||||||
"@chakra-ui/system": "^2.6.2",
|
|
||||||
"@chakra-ui/theme": "^3.4.7",
|
|
||||||
"@chakra-ui/theme-tools": "^2.2.7",
|
|
||||||
"@chakra-ui/utils": "^2.2.3",
|
|
||||||
"@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/react": "^11.14.0",
|
||||||
@@ -60,8 +45,6 @@
|
|||||||
"@types/js-levenshtein": "^1.1.3",
|
"@types/js-levenshtein": "^1.1.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"chakra-react-select": "^4.7.5",
|
|
||||||
"chakra-ui-steps": "^2.0.4",
|
|
||||||
"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",
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -16,7 +16,6 @@ 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 { Import } from '@/pages/Import';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
|
||||||
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -53,30 +52,28 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ChakraProvider>
|
<Toaster richColors position="top-center" />
|
||||||
<Toaster richColors position="top-center" />
|
<Routes>
|
||||||
<Routes>
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route element={
|
||||||
<Route element={
|
<RequireAuth>
|
||||||
<RequireAuth>
|
<MainLayout />
|
||||||
<MainLayout />
|
</RequireAuth>
|
||||||
</RequireAuth>
|
}>
|
||||||
}>
|
<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="/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 />} />
|
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
||||||
<Route path="/purchase-orders" element={<PurchaseOrders />} />
|
<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="/ai-validation/debug" element={<AiValidationDebug />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
</Route>
|
||||||
</Route>
|
</Routes>
|
||||||
</Routes>
|
|
||||||
</ChakraProvider>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import merge from "lodash/merge"
|
import merge from "lodash/merge"
|
||||||
|
|
||||||
import { Steps } from "./steps/Steps"
|
import { Steps } from "./steps/Steps"
|
||||||
import { rtlThemeSupport, themeOverrides } from "./theme"
|
|
||||||
import { Providers } from "./components/Providers"
|
import { Providers } from "./components/Providers"
|
||||||
import type { RsiProps } from "./types"
|
import type { RsiProps } from "./types"
|
||||||
import { ModalWrapper } from "./components/ModalWrapper"
|
import { ModalWrapper } from "./components/ModalWrapper"
|
||||||
import { translations } from "./translationsRSIProps"
|
import { translations } from "./translationsRSIProps"
|
||||||
|
|
||||||
export const defaultTheme = themeOverrides
|
// Simple empty theme placeholder
|
||||||
|
export const defaultTheme = {}
|
||||||
|
|
||||||
export const defaultRSIProps: Partial<RsiProps<any>> = {
|
export const defaultRSIProps: Partial<RsiProps<any>> = {
|
||||||
autoMapHeaders: true,
|
autoMapHeaders: true,
|
||||||
@@ -27,12 +27,9 @@ export const ReactSpreadsheetImport = <T extends string>(propsWithoutDefaults: R
|
|||||||
const props = merge({}, defaultRSIProps, propsWithoutDefaults)
|
const props = merge({}, defaultRSIProps, propsWithoutDefaults)
|
||||||
const mergedTranslations =
|
const mergedTranslations =
|
||||||
props.translations !== translations ? merge(translations, props.translations) : translations
|
props.translations !== translations ? merge(translations, props.translations) : translations
|
||||||
const mergedThemes = props.rtl
|
|
||||||
? merge(defaultTheme, rtlThemeSupport, props.customTheme)
|
|
||||||
: merge(defaultTheme, props.customTheme)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Providers theme={mergedThemes} rsiValues={{ ...props, translations: mergedTranslations }}>
|
<Providers rsiValues={{ ...props, translations: mergedTranslations }}>
|
||||||
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
|
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
|
||||||
<Steps />
|
<Steps />
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { StepType } from "./steps/UploadFlow"
|
export { StepType } from "./steps/UploadFlow"
|
||||||
export { ReactSpreadsheetImport } from "./ReactSpreadsheetImport"
|
export { ReactSpreadsheetImport } from "./ReactSpreadsheetImport"
|
||||||
|
export * from "./types"
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"> </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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||||
@@ -43,23 +41,7 @@ export const SelectHeaderTable = ({ data, selectedRows, setSelectedRows }: Props
|
|||||||
|
|
||||||
<div className="h-[calc(100vh-23rem)] overflow-auto">
|
<div className="h-[calc(100vh-23rem)] overflow-auto">
|
||||||
<Table className="relative w-full" style={{ tableLayout: 'fixed' }}>
|
<Table className="relative w-full" style={{ tableLayout: 'fixed' }}>
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="grid" style={{ gridTemplateColumns }}>
|
|
||||||
<TableHead className="sticky top-0 z-20 bg-background overflow-hidden">
|
|
||||||
|
|
||||||
</TableHead>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<TableHead
|
|
||||||
key={column.key}
|
|
||||||
className="sticky top-0 z-20 bg-background overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="truncate">
|
|
||||||
{column.name}
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={selectedRowIndex?.toString()}
|
value={selectedRowIndex?.toString()}
|
||||||
@@ -223,7 +223,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
onBack();
|
onBack();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onNext={(validatedData) => {
|
onNext={(validatedData: any[]) => {
|
||||||
// Go to image upload step with the validated data
|
// Go to image upload step with the validated data
|
||||||
onNext({
|
onNext({
|
||||||
type: StepType.imageUpload,
|
type: StepType.imageUpload,
|
||||||
@@ -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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { Template } from '../hooks/useValidationState'
|
import { Template } from '../hooks/validationTypes'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -50,7 +50,6 @@ const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [selectedBrand, setSelectedBrand] = useState<string | null>(null);
|
const [selectedBrand, setSelectedBrand] = useState<string | null>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Set default brand when component mounts or defaultBrand changes
|
// Set default brand when component mounts or defaultBrand changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -232,7 +231,7 @@ const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn("w-full justify-between overflow-hidden", triggerClassName)}
|
className={cn("w-full justify-between overflow-hidden", triggerClassName)}
|
||||||
>
|
>
|
||||||
<span className="truncate overflow-hidden mr-2">{getDisplayText()}</span>
|
<span className="truncate overflow-hidden mr-1">{getDisplayText()}</span>
|
||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 flex-none" />
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 flex-none" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -2,18 +2,19 @@ import React, { useMemo } from 'react'
|
|||||||
import ValidationTable from './ValidationTable'
|
import ValidationTable from './ValidationTable'
|
||||||
import { RowSelectionState } from '@tanstack/react-table'
|
import { RowSelectionState } from '@tanstack/react-table'
|
||||||
import { Fields } from '../../../types'
|
import { Fields } from '../../../types'
|
||||||
|
import { Template } from '../hooks/validationTypes'
|
||||||
|
|
||||||
interface UpcValidationTableAdapterProps<T extends string> {
|
interface UpcValidationTableAdapterProps<T extends string> {
|
||||||
data: any[]
|
data: any[]
|
||||||
fields: Fields<string>
|
fields: Fields<string>
|
||||||
validationErrors: Map<number, Record<string, any[]>>
|
validationErrors: Map<number, Record<string, any[]>>
|
||||||
rowSelection: RowSelectionState
|
rowSelection: RowSelectionState
|
||||||
setRowSelection: (value: RowSelectionState) => void
|
setRowSelection: React.Dispatch<React.SetStateAction<RowSelectionState>>
|
||||||
updateRow: (rowIndex: number, key: T, value: any) => void
|
updateRow: (rowIndex: number, key: T, value: any) => void
|
||||||
filters: any
|
filters: any
|
||||||
templates: any[]
|
templates: Template[]
|
||||||
applyTemplate: (templateId: string, rowIndexes: number[]) => void
|
applyTemplate: (templateId: string, rowIndexes: number[]) => void
|
||||||
getTemplateDisplayText: (templateId: string) => string
|
getTemplateDisplayText: (templateId: string | null) => string
|
||||||
isValidatingUpc: (rowIndex: number) => boolean
|
isValidatingUpc: (rowIndex: number) => boolean
|
||||||
validatingUpcRows: number[]
|
validatingUpcRows: number[]
|
||||||
copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void
|
copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void
|
||||||
@@ -27,6 +28,7 @@ interface UpcValidationTableAdapterProps<T extends string> {
|
|||||||
validatingRows: Set<number>
|
validatingRows: Set<number>
|
||||||
getItemNumber: (rowIndex: number) => string | undefined
|
getItemNumber: (rowIndex: number) => string | undefined
|
||||||
}
|
}
|
||||||
|
itemNumbers?: Map<number, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,63 +57,79 @@ function UpcValidationTableAdapter<T extends string>({
|
|||||||
rowSublines,
|
rowSublines,
|
||||||
isLoadingLines,
|
isLoadingLines,
|
||||||
isLoadingSublines,
|
isLoadingSublines,
|
||||||
upcValidation
|
upcValidation,
|
||||||
|
itemNumbers
|
||||||
}: UpcValidationTableAdapterProps<T>) {
|
}: UpcValidationTableAdapterProps<T>) {
|
||||||
// Prepare the validation table with UPC data
|
// Prepare the validation table with UPC data
|
||||||
const AdaptedTable = useMemo(() => React.memo((props: React.ComponentProps<typeof ValidationTable>) => {
|
|
||||||
// Create validatingCells set from validating rows, but only for item_number fields
|
// Create combined validatingCells set from validating rows and external cells
|
||||||
// This ensures only the item_number column shows loading state during UPC validation
|
const combinedValidatingCells = useMemo(() => {
|
||||||
const combinedValidatingCells = new Set<string>();
|
const combined = new Set<string>();
|
||||||
|
|
||||||
// Add UPC validation cells
|
// Add UPC validation cells
|
||||||
upcValidation.validatingRows.forEach(rowIndex => {
|
upcValidation.validatingRows.forEach(rowIndex => {
|
||||||
// Only mark the item_number cells as validating, NOT the UPC or supplier
|
// Only mark the item_number cells as validating, NOT the UPC or supplier
|
||||||
combinedValidatingCells.add(`${rowIndex}-item_number`);
|
combined.add(`${rowIndex}-item_number`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add any other validating cells from state
|
// Add any other validating cells from state
|
||||||
externalValidatingCells.forEach(cellKey => {
|
externalValidatingCells.forEach(cellKey => {
|
||||||
combinedValidatingCells.add(cellKey);
|
combined.add(cellKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert the Map to the expected format for the ValidationTable
|
return combined;
|
||||||
// Create a new Map from the item numbers to ensure proper typing
|
}, [upcValidation.validatingRows, externalValidatingCells]);
|
||||||
const itemNumbersMap = new Map<number, string>();
|
|
||||||
|
|
||||||
// Merge the item numbers with the data for display purposes only
|
// Create a consolidated item numbers map from all sources
|
||||||
const enhancedData = props.data.map((row: any, index: number) => {
|
const consolidatedItemNumbers = useMemo(() => {
|
||||||
|
const result = new Map<number, string>();
|
||||||
|
|
||||||
|
// First add from itemNumbers directly - this is the source of truth for template applications
|
||||||
|
if (itemNumbers) {
|
||||||
|
// Log all numbers for debugging
|
||||||
|
console.log(`[ADAPTER-DEBUG] Received itemNumbers map with ${itemNumbers.size} entries`);
|
||||||
|
|
||||||
|
itemNumbers.forEach((itemNumber, rowIndex) => {
|
||||||
|
console.log(`[ADAPTER-DEBUG] Adding item number for row ${rowIndex} from itemNumbers: ${itemNumber}`);
|
||||||
|
result.set(rowIndex, itemNumber);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each row, ensure we have the most up-to-date item number
|
||||||
|
data.forEach((_, index) => {
|
||||||
|
// Check if upcValidation has an item number for this row
|
||||||
const itemNumber = upcValidation.getItemNumber(index);
|
const itemNumber = upcValidation.getItemNumber(index);
|
||||||
if (itemNumber) {
|
if (itemNumber) {
|
||||||
// Add to our map for proper prop passing
|
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from upcValidation: ${itemNumber}`);
|
||||||
itemNumbersMap.set(index, itemNumber);
|
result.set(index, itemNumber);
|
||||||
|
}
|
||||||
return {
|
|
||||||
...row,
|
// Also check if it's directly in the data
|
||||||
item_number: itemNumber
|
const dataItemNumber = data[index].item_number;
|
||||||
};
|
if (dataItemNumber && !result.has(index)) {
|
||||||
|
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from data: ${dataItemNumber}`);
|
||||||
|
result.set(index, dataItemNumber);
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return result;
|
||||||
<ValidationTable
|
}, [data, itemNumbers, upcValidation]);
|
||||||
{...props}
|
|
||||||
data={enhancedData}
|
// Create upcValidationResults map using the consolidated item numbers
|
||||||
validatingCells={combinedValidatingCells}
|
const upcValidationResults = useMemo(() => {
|
||||||
itemNumbers={itemNumbersMap}
|
const results = new Map<number, { itemNumber: string }>();
|
||||||
isLoadingTemplates={isLoadingTemplates}
|
|
||||||
copyDown={copyDown}
|
// Populate with our consolidated item numbers
|
||||||
rowProductLines={rowProductLines}
|
consolidatedItemNumbers.forEach((itemNumber, rowIndex) => {
|
||||||
rowSublines={rowSublines}
|
results.set(rowIndex, { itemNumber });
|
||||||
isLoadingLines={isLoadingLines}
|
});
|
||||||
isLoadingSublines={isLoadingSublines}
|
|
||||||
/>
|
return results;
|
||||||
);
|
}, [consolidatedItemNumbers]);
|
||||||
}), [upcValidation.validatingRows, upcValidation.getItemNumber, isLoadingTemplates, copyDown, externalValidatingCells, rowProductLines, rowSublines, isLoadingLines, isLoadingSublines]);
|
|
||||||
|
|
||||||
// Render the validation table with the provided props and UPC data
|
// Render the validation table with the provided props and UPC data
|
||||||
return (
|
return (
|
||||||
<AdaptedTable
|
<ValidationTable
|
||||||
data={data}
|
data={data}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
@@ -124,11 +142,11 @@ function UpcValidationTableAdapter<T extends string>({
|
|||||||
templates={templates}
|
templates={templates}
|
||||||
applyTemplate={applyTemplate}
|
applyTemplate={applyTemplate}
|
||||||
getTemplateDisplayText={getTemplateDisplayText}
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
validatingCells={new Set()}
|
validatingCells={combinedValidatingCells}
|
||||||
itemNumbers={new Map()}
|
itemNumbers={consolidatedItemNumbers}
|
||||||
isLoadingTemplates={isLoadingTemplates}
|
isLoadingTemplates={isLoadingTemplates}
|
||||||
copyDown={copyDown}
|
copyDown={copyDown}
|
||||||
upcValidationResults={new Map()}
|
upcValidationResults={upcValidationResults}
|
||||||
rowProductLines={rowProductLines}
|
rowProductLines={rowProductLines}
|
||||||
rowSublines={rowSublines}
|
rowSublines={rowSublines}
|
||||||
isLoadingLines={isLoadingLines}
|
isLoadingLines={isLoadingLines}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Field, ErrorType } from '../../../types'
|
import { Field, ErrorType } from '../../../types'
|
||||||
import { Loader2, AlertCircle, ArrowDown, X } from 'lucide-react'
|
import { AlertCircle, ArrowDown, X } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -11,6 +11,7 @@ import InputCell from './cells/InputCell'
|
|||||||
import SelectCell from './cells/SelectCell'
|
import SelectCell from './cells/SelectCell'
|
||||||
import MultiSelectCell from './cells/MultiSelectCell'
|
import MultiSelectCell from './cells/MultiSelectCell'
|
||||||
import { TableCell } from '@/components/ui/table'
|
import { TableCell } from '@/components/ui/table'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
// Context for copy down selection mode
|
// Context for copy down selection mode
|
||||||
export const CopyDownContext = React.createContext<{
|
export const CopyDownContext = React.createContext<{
|
||||||
@@ -292,8 +293,18 @@ const ValidationCell = React.memo(({
|
|||||||
// Use the CopyDown context
|
// Use the CopyDown context
|
||||||
const copyDownContext = React.useContext(CopyDownContext);
|
const copyDownContext = React.useContext(CopyDownContext);
|
||||||
|
|
||||||
// Display value prioritizes itemNumber if available (for item_number fields)
|
// CRITICAL FIX: For item_number fields, always prioritize the itemNumber prop over the value
|
||||||
const displayValue = fieldKey === 'item_number' && itemNumber ? itemNumber : value;
|
// This ensures that when the itemNumber changes, the display value changes
|
||||||
|
let displayValue;
|
||||||
|
if (fieldKey === 'item_number' && itemNumber) {
|
||||||
|
// Always log when an item_number field is rendered to help debug
|
||||||
|
console.log(`[VC-DEBUG] ValidationCell rendering item_number for row=${rowIndex} with itemNumber=${itemNumber}, value=${value}`);
|
||||||
|
|
||||||
|
// Prioritize itemNumber prop for item_number fields
|
||||||
|
displayValue = itemNumber;
|
||||||
|
} else {
|
||||||
|
displayValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the optimized processErrors function to avoid redundant filtering
|
// Use the optimized processErrors function to avoid redundant filtering
|
||||||
const {
|
const {
|
||||||
@@ -351,12 +362,8 @@ const ValidationCell = React.memo(({
|
|||||||
minWidth: `${width}px`,
|
minWidth: `${width}px`,
|
||||||
maxWidth: `${width}px`,
|
maxWidth: `${width}px`,
|
||||||
boxSizing: 'border-box' as const,
|
boxSizing: 'border-box' as const,
|
||||||
cursor: isInTargetRow ? 'pointer' : undefined,
|
cursor: isInTargetRow ? 'pointer' : undefined
|
||||||
...(isSourceCell ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, boxShadow: '0 0 0 2px #3b82f6' } :
|
}), [width, isInTargetRow]);
|
||||||
isSelectedTarget ? { backgroundColor: '#bfdbfe', borderRadius: '0.375rem', padding: 0 } :
|
|
||||||
isInTargetRow && isTargetRowHovered ? { backgroundColor: '#dbeafe', borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } :
|
|
||||||
isInTargetRow ? { borderRadius: '0.375rem', padding: 0, cursor: 'pointer' } : {})
|
|
||||||
}), [width, isInTargetRow, isSourceCell, isSelectedTarget, isTargetRowHovered]);
|
|
||||||
|
|
||||||
// Memoize the cell class name to prevent re-calculating on every render
|
// Memoize the cell class name to prevent re-calculating on every render
|
||||||
const cellClassName = React.useMemo(() => {
|
const cellClassName = React.useMemo(() => {
|
||||||
@@ -431,12 +438,21 @@ const ValidationCell = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className={`flex items-center justify-center gap-2 border ${hasError || isRequiredButEmpty ? 'border-red-500' : 'border-input'} rounded-sm px-2 py-1.5`}>
|
<div className={`flex items-center justify-center gap-2 border ${hasError || isRequiredButEmpty ? 'border-red-500' : 'border-input'} rounded-md px-2 py-2`}>
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
|
<Skeleton className="w-full h-4" />
|
||||||
<span>Loading...</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`truncate overflow-hidden ${isCopyDownHovered && !copyDownContext.isInCopyDownMode ? 'bg-blue-50/50' : ''}`}>
|
<div
|
||||||
|
className={`truncate overflow-hidden ${isCopyDownHovered && !copyDownContext.isInCopyDownMode ? 'bg-blue-50/50' : ''}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isSourceCell ? '#dbeafe' :
|
||||||
|
isSelectedTarget ? '#bfdbfe' :
|
||||||
|
isInTargetRow && isTargetRowHovered ? '#dbeafe' :
|
||||||
|
undefined,
|
||||||
|
borderRadius: (isSourceCell || isSelectedTarget || isInTargetRow) ? '0.375rem' : undefined,
|
||||||
|
boxShadow: isSourceCell ? '0 0 0 2px #3b82f6' : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
<BaseCellContent
|
<BaseCellContent
|
||||||
field={field}
|
field={field}
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react'
|
||||||
import { useValidationState, Props } from '../hooks/useValidationState'
|
import { useValidationState } from '../hooks/useValidationState'
|
||||||
|
import { Props } from '../hooks/validationTypes'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Loader2, X, Plus, Edit3, Sparkles, FileText } from 'lucide-react'
|
import { Loader2, X, Plus, Edit3, Sparkles, FileText } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -9,7 +10,6 @@ import SearchableTemplateSelect from './SearchableTemplateSelect'
|
|||||||
import { useAiValidation } from '../hooks/useAiValidation'
|
import { useAiValidation } from '../hooks/useAiValidation'
|
||||||
import { AiValidationDialogs } from './AiValidationDialogs'
|
import { AiValidationDialogs } from './AiValidationDialogs'
|
||||||
import { Fields } from '../../../types'
|
import { Fields } from '../../../types'
|
||||||
import { ErrorType, ValidationError, ErrorSources } from '../../../types'
|
|
||||||
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
|
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
|
||||||
import { TemplateForm } from '@/components/templates/TemplateForm'
|
import { TemplateForm } from '@/components/templates/TemplateForm'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
@@ -17,8 +17,7 @@ import { RowSelectionState } from '@tanstack/react-table'
|
|||||||
import { useUpcValidation } from '../hooks/useUpcValidation'
|
import { useUpcValidation } from '../hooks/useUpcValidation'
|
||||||
import { useProductLinesFetching } from '../hooks/useProductLinesFetching'
|
import { useProductLinesFetching } from '../hooks/useProductLinesFetching'
|
||||||
import UpcValidationTableAdapter from './UpcValidationTableAdapter'
|
import UpcValidationTableAdapter from './UpcValidationTableAdapter'
|
||||||
import { clearAllUniquenessCaches } from '../hooks/useValidation'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ValidationContainer component - the main wrapper for the validation step
|
* ValidationContainer component - the main wrapper for the validation step
|
||||||
*
|
*
|
||||||
@@ -49,7 +48,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
validationErrors,
|
validationErrors,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
updateRow,
|
|
||||||
templates,
|
templates,
|
||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
applyTemplate,
|
applyTemplate,
|
||||||
@@ -60,7 +58,10 @@ const ValidationContainer = <T extends string>({
|
|||||||
loadTemplates,
|
loadTemplates,
|
||||||
setData,
|
setData,
|
||||||
fields,
|
fields,
|
||||||
isLoadingTemplates } = validationState
|
isLoadingTemplates,
|
||||||
|
validatingCells,
|
||||||
|
setValidatingCells
|
||||||
|
} = validationState
|
||||||
|
|
||||||
// Use product lines fetching hook
|
// Use product lines fetching hook
|
||||||
const {
|
const {
|
||||||
@@ -72,9 +73,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
fetchSublines
|
fetchSublines
|
||||||
} = useProductLinesFetching(data);
|
} = useProductLinesFetching(data);
|
||||||
|
|
||||||
// Add state for tracking cells in loading state
|
|
||||||
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
// Use UPC validation hook
|
// Use UPC validation hook
|
||||||
const upcValidation = useUpcValidation(data, setData);
|
const upcValidation = useUpcValidation(data, setData);
|
||||||
|
|
||||||
@@ -144,7 +142,6 @@ const ValidationContainer = <T extends string>({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Add a ref to track the last validation time
|
// Add a ref to track the last validation time
|
||||||
const lastValidationTime = useRef(0);
|
|
||||||
|
|
||||||
// Trigger revalidation only for specifically marked fields
|
// Trigger revalidation only for specifically marked fields
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -301,82 +298,8 @@ const ValidationContainer = <T extends string>({
|
|||||||
}, [prepareRowDataForTemplateForm, fetchFieldOptions]);
|
}, [prepareRowDataForTemplateForm, fetchFieldOptions]);
|
||||||
|
|
||||||
// Create a function to validate uniqueness if validateUniqueItemNumbers is not available
|
// Create a function to validate uniqueness if validateUniqueItemNumbers is not available
|
||||||
const validateUniqueValues = useCallback(() => {
|
|
||||||
// Check if validateUniqueItemNumbers exists on validationState using safer method
|
|
||||||
if ('validateUniqueItemNumbers' in validationState &&
|
|
||||||
typeof (validationState as any).validateUniqueItemNumbers === 'function') {
|
|
||||||
(validationState as any).validateUniqueItemNumbers();
|
|
||||||
} else {
|
|
||||||
// Otherwise fall back to revalidating all rows
|
|
||||||
validationState.revalidateRows(Array.from(Array(data.length).keys()));
|
|
||||||
}
|
|
||||||
}, [validationState, data.length]);
|
|
||||||
|
|
||||||
// Apply item numbers to data and trigger revalidation for uniqueness
|
// Apply item numbers to data and trigger revalidation for uniqueness
|
||||||
const applyItemNumbersAndValidate = useCallback(() => {
|
|
||||||
// Clear uniqueness validation caches to ensure fresh validation
|
|
||||||
clearAllUniquenessCaches();
|
|
||||||
|
|
||||||
upcValidation.applyItemNumbersToData((updatedRowIds) => {
|
|
||||||
console.log(`Revalidating item numbers for ${updatedRowIds.length} rows`);
|
|
||||||
|
|
||||||
// Force clearing all uniqueness errors for item_number and upc fields first
|
|
||||||
const newValidationErrors = new Map(validationErrors);
|
|
||||||
|
|
||||||
// Clear uniqueness errors for all rows that had their item numbers updated
|
|
||||||
updatedRowIds.forEach(rowIndex => {
|
|
||||||
const rowErrors = newValidationErrors.get(rowIndex);
|
|
||||||
if (rowErrors) {
|
|
||||||
// Create a copy of row errors without uniqueness errors for item_number/upc
|
|
||||||
const filteredErrors: Record<string, ValidationError[]> = { ...rowErrors };
|
|
||||||
let hasChanges = false;
|
|
||||||
|
|
||||||
// Clear item_number errors if they exist and are uniqueness errors
|
|
||||||
if (filteredErrors.item_number &&
|
|
||||||
filteredErrors.item_number.some(e => e.type === ErrorType.Unique)) {
|
|
||||||
delete filteredErrors.item_number;
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also clear upc/barcode errors if they exist and are uniqueness errors
|
|
||||||
if (filteredErrors.upc &&
|
|
||||||
filteredErrors.upc.some(e => e.type === ErrorType.Unique)) {
|
|
||||||
delete filteredErrors.upc;
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filteredErrors.barcode &&
|
|
||||||
filteredErrors.barcode.some(e => e.type === ErrorType.Unique)) {
|
|
||||||
delete filteredErrors.barcode;
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the map or remove the row entry if no errors remain
|
|
||||||
if (hasChanges) {
|
|
||||||
if (Object.keys(filteredErrors).length > 0) {
|
|
||||||
newValidationErrors.set(rowIndex, filteredErrors);
|
|
||||||
} else {
|
|
||||||
newValidationErrors.delete(rowIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call the revalidateRows function directly with affected rows
|
|
||||||
validationState.revalidateRows(updatedRowIds);
|
|
||||||
|
|
||||||
// Immediately run full uniqueness validation across all rows if available
|
|
||||||
// This is crucial to properly identify new uniqueness issues
|
|
||||||
setTimeout(() => {
|
|
||||||
validateUniqueValues();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Mark all updated rows for revalidation
|
|
||||||
updatedRowIds.forEach(rowIndex => {
|
|
||||||
markRowForRevalidation(rowIndex, 'item_number');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [upcValidation.applyItemNumbersToData, markRowForRevalidation, clearAllUniquenessCaches, validationErrors, validationState.revalidateRows, validateUniqueValues]);
|
|
||||||
|
|
||||||
// Handle next button click - memoized
|
// Handle next button click - memoized
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
@@ -472,28 +395,22 @@ const ValidationContainer = <T extends string>({
|
|||||||
// This function is defined for potential future use but not currently used
|
// This function is defined for potential future use but not currently used
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|
||||||
const handleRowSelectionChange = useCallback((newSelection: RowSelectionState) => {
|
const handleRowSelectionChange = useCallback(
|
||||||
setRowSelection(newSelection);
|
(value: React.SetStateAction<RowSelectionState>) => {
|
||||||
}, [setRowSelection]);
|
setRowSelection(value);
|
||||||
|
},
|
||||||
|
[setRowSelection]
|
||||||
|
);
|
||||||
|
|
||||||
// Add scroll container ref at the container level
|
// Add scroll container ref at the container level
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const lastScrollPosition = useRef({ left: 0, top: 0 });
|
const lastScrollPosition = useRef({ left: 0, top: 0 });
|
||||||
const isScrolling = useRef(false);
|
|
||||||
|
|
||||||
// Track if we're currently validating a UPC
|
// Track if we're currently validating a UPC
|
||||||
const isValidatingUpcRef = useRef(false);
|
|
||||||
|
|
||||||
// Track last UPC update to prevent conflicting changes
|
// Track last UPC update to prevent conflicting changes
|
||||||
const lastUpcUpdate = useRef({
|
|
||||||
rowIndex: -1,
|
|
||||||
supplier: "",
|
|
||||||
upc: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add these ref declarations here, at component level
|
// Add these ref declarations here, at component level
|
||||||
const lastCompanyFetchTime = useRef<Record<string, number>>({});
|
|
||||||
const lastLineFetchTime = useRef<Record<string, number>>({});
|
|
||||||
|
|
||||||
// Memoize scroll handlers - simplified to avoid performance issues
|
// Memoize scroll handlers - simplified to avoid performance issues
|
||||||
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement> | Event) => {
|
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement> | Event) => {
|
||||||
@@ -1042,6 +959,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
isLoadingLines={isLoadingLines}
|
isLoadingLines={isLoadingLines}
|
||||||
isLoadingSublines={isLoadingSublines}
|
isLoadingSublines={isLoadingSublines}
|
||||||
upcValidation={upcValidation}
|
upcValidation={upcValidation}
|
||||||
|
itemNumbers={upcValidation.itemNumbers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
@@ -1150,9 +1068,9 @@ const ValidationContainer = <T extends string>({
|
|||||||
{/* Selection Action Bar - only shown when items are selected */}
|
{/* Selection Action Bar - only shown when items are selected */}
|
||||||
{Object.keys(rowSelection).length > 0 && (
|
{Object.keys(rowSelection).length > 0 && (
|
||||||
<div className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-5 duration-300">
|
<div className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-5 duration-300">
|
||||||
<div className="bg-card shadow-xl rounded-lg border border-muted px-4 py-3 flex items-center gap-3">
|
<div className="bg-card shadow-xl rounded-2xl border border-gray-200 px-4 py-3 flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="mr-2 bg-muted items-center flex text-primary pl-2 pr-7 h-[32px] flex-shrink-0 rounded-md text-xs font-medium border border-primary">
|
<div className="mr-3 bg-muted shadow-xs items-center flex text-primary pl-2 pr-7 h-8 flex-shrink-0 rounded-md text-xs font-medium border border-muted">
|
||||||
{Object.keys(rowSelection).length} selected
|
{Object.keys(rowSelection).length} selected
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1167,11 +1085,10 @@ const ValidationContainer = <T extends string>({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center ml-2 mr-1 shadow-xs">
|
||||||
{isLoadingTemplates ? (
|
{isLoadingTemplates ? (
|
||||||
<Button variant="outline" className="w-[220px] justify-between" disabled>
|
<Button variant="outline" className="w-[250px] justify-between h-8" disabled>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Skeleton className="h-4 w-full" />
|
||||||
Loading templates...
|
|
||||||
</Button>
|
</Button>
|
||||||
) : templates && templates.length > 0 ? (
|
) : templates && templates.length > 0 ? (
|
||||||
<SearchableTemplateSelect
|
<SearchableTemplateSelect
|
||||||
@@ -1183,11 +1100,11 @@ const ValidationContainer = <T extends string>({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
getTemplateDisplayText={getTemplateDisplayText}
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
placeholder="Apply template to selected"
|
placeholder="Apply template to selected rows"
|
||||||
triggerClassName="w-[220px]"
|
triggerClassName="w-[250px] text-xs h-8"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" className="w-full justify-between" disabled>
|
<Button variant="outline" className="w-full justify-between text-xs" disabled>
|
||||||
No templates available
|
No templates available
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -1198,14 +1115,16 @@ const ValidationContainer = <T extends string>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={openTemplateForm}
|
onClick={openTemplateForm}
|
||||||
|
className="h-8 mr-1 shadow-xs"
|
||||||
>
|
>
|
||||||
Save as Template
|
Save as template
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={isFromScratch ? "destructive" : "outline"}
|
variant={"destructive"}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="h-8 shadow-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('Delete/Discard button clicked');
|
console.log('Delete/Discard button clicked');
|
||||||
console.log('Row selection state:', rowSelection);
|
console.log('Row selection state:', rowSelection);
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ColumnDef
|
ColumnDef
|
||||||
} from '@tanstack/react-table'
|
} from '@tanstack/react-table'
|
||||||
import { Fields, Field } from '../../../types'
|
import { Fields, Field } from '../../../types'
|
||||||
import { RowData, Template } from '../hooks/useValidationState'
|
import { RowData, Template } from '../hooks/validationTypes'
|
||||||
import ValidationCell, { CopyDownContext } from './ValidationCell'
|
import ValidationCell, { CopyDownContext } from './ValidationCell'
|
||||||
import { useRsi } from '../../../hooks/useRsi'
|
import { useRsi } from '../../../hooks/useRsi'
|
||||||
import SearchableTemplateSelect from './SearchableTemplateSelect'
|
import SearchableTemplateSelect from './SearchableTemplateSelect'
|
||||||
@@ -15,7 +15,7 @@ import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table'
|
|||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
// Define a simple Error type locally to avoid import issues
|
// Define a simple Error type locally to avoid import issues
|
||||||
type ErrorType = {
|
type ErrorType = {
|
||||||
@@ -67,10 +67,9 @@ const MemoizedTemplateSelect = React.memo(({
|
|||||||
}) => {
|
}) => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" className="w-full justify-between overflow-hidden" disabled>
|
<div className="flex items-center justify-center gap-2 border border-input rounded-md px-2 py-2">
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin flex-none" />
|
<Skeleton className="h-4 w-full" />
|
||||||
<span className="truncate overflow-hidden">Loading...</span>
|
</div>
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +138,15 @@ const MemoizedCell = React.memo(({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, (prev, next) => {
|
}, (prev, next) => {
|
||||||
|
// CRITICAL FIX: Never memoize item_number cells - always re-render them
|
||||||
|
if (prev.fieldKey === 'item_number') {
|
||||||
|
return false; // Never skip re-renders for item_number cells
|
||||||
|
}
|
||||||
|
|
||||||
// Optimize the memo comparison function for better performance
|
// Optimize the memo comparison function for better performance
|
||||||
// Only re-render if these essential props change
|
// Only re-render if these essential props change
|
||||||
const valueEqual = prev.value === next.value;
|
const valueEqual = prev.value === next.value;
|
||||||
const isValidatingEqual = prev.isValidating === next.isValidating;
|
const isValidatingEqual = prev.isValidating === next.isValidating;
|
||||||
const itemNumberEqual = prev.itemNumber === next.itemNumber;
|
|
||||||
|
|
||||||
// Shallow equality check for errors array
|
// Shallow equality check for errors array
|
||||||
const errorsEqual = prev.errors === next.errors || (
|
const errorsEqual = prev.errors === next.errors || (
|
||||||
@@ -162,7 +165,7 @@ const MemoizedCell = React.memo(({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Skip checking for props that rarely change
|
// Skip checking for props that rarely change
|
||||||
return valueEqual && isValidatingEqual && errorsEqual && optionsEqual && itemNumberEqual;
|
return valueEqual && isValidatingEqual && errorsEqual && optionsEqual;
|
||||||
});
|
});
|
||||||
|
|
||||||
MemoizedCell.displayName = 'MemoizedCell';
|
MemoizedCell.displayName = 'MemoizedCell';
|
||||||
@@ -185,7 +188,10 @@ const ValidationTable = <T extends string>({
|
|||||||
rowProductLines = {},
|
rowProductLines = {},
|
||||||
rowSublines = {},
|
rowSublines = {},
|
||||||
isLoadingLines = {},
|
isLoadingLines = {},
|
||||||
isLoadingSublines = {}
|
isLoadingSublines = {},
|
||||||
|
isValidatingUpc,
|
||||||
|
validatingUpcRows = [],
|
||||||
|
upcValidationResults
|
||||||
}: ValidationTableProps<T>) => {
|
}: ValidationTableProps<T>) => {
|
||||||
const { translations } = useRsi<T>();
|
const { translations } = useRsi<T>();
|
||||||
|
|
||||||
@@ -254,7 +260,7 @@ const ValidationTable = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex h-[40px] items-center justify-center">
|
<div className="flex items-center justify-center py-9">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={row.getIsSelected()}
|
checked={row.getIsSelected()}
|
||||||
onCheckedChange={(value) => handleRowSelect(!!value, row)}
|
onCheckedChange={(value) => handleRowSelect(!!value, row)}
|
||||||
@@ -328,6 +334,34 @@ const ValidationTable = <T extends string>({
|
|||||||
copyDown(rowIndex, fieldKey, endRowIndex);
|
copyDown(rowIndex, fieldKey, endRowIndex);
|
||||||
}, [copyDown]);
|
}, [copyDown]);
|
||||||
|
|
||||||
|
// Use validatingUpcRows for calculation
|
||||||
|
const isRowValidatingUpc = useCallback((rowIndex: number) => {
|
||||||
|
return isValidatingUpc?.(rowIndex) || validatingUpcRows.includes(rowIndex);
|
||||||
|
}, [isValidatingUpc, validatingUpcRows]);
|
||||||
|
|
||||||
|
// Use upcValidationResults for display, prioritizing the most recent values
|
||||||
|
const getRowUpcResult = useCallback((rowIndex: number) => {
|
||||||
|
// ALWAYS get from the data array directly - most authoritative source
|
||||||
|
const rowData = data[rowIndex];
|
||||||
|
if (rowData && rowData.item_number) {
|
||||||
|
return rowData.item_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps are only backup sources when data doesn't have a value
|
||||||
|
const itemNumberFromMap = itemNumbers.get(rowIndex);
|
||||||
|
if (itemNumberFromMap) {
|
||||||
|
return itemNumberFromMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort - upcValidationResults
|
||||||
|
const upcResult = upcValidationResults?.get(rowIndex)?.itemNumber;
|
||||||
|
if (upcResult) {
|
||||||
|
return upcResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [data, itemNumbers, upcValidationResults]);
|
||||||
|
|
||||||
// Memoize field columns with stable handlers
|
// Memoize field columns with stable handlers
|
||||||
const fieldColumns = useMemo(() => fields.map((field): ColumnDef<RowData<T>, any> | null => {
|
const fieldColumns = useMemo(() => fields.map((field): ColumnDef<RowData<T>, any> | null => {
|
||||||
// Don't filter out disabled fields, just pass the disabled state to the cell component
|
// Don't filter out disabled fields, just pass the disabled state to the cell component
|
||||||
@@ -368,6 +402,10 @@ const ValidationTable = <T extends string>({
|
|||||||
if (validatingCells.has(cellLoadingKey)) {
|
if (validatingCells.has(cellLoadingKey)) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
}
|
}
|
||||||
|
// Check if UPC is validating for this row and field is item_number
|
||||||
|
else if (fieldKey === 'item_number' && isRowValidatingUpc(row.index)) {
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
// Add loading state for line/subline fields
|
// Add loading state for line/subline fields
|
||||||
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -395,26 +433,40 @@ const ValidationTable = <T extends string>({
|
|||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
console.log(`Field ${fieldKey} in ValidationTable (after deep clone):`, {
|
|
||||||
originalField: field,
|
|
||||||
modifiedField: fieldWithType,
|
|
||||||
options,
|
|
||||||
hasOptions: options && options.length > 0,
|
|
||||||
disabled: fieldWithType.disabled
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL CHANGE: Get item number directly from row data first for item_number fields
|
||||||
|
let itemNumber;
|
||||||
|
if (fieldKey === 'item_number') {
|
||||||
|
// Check directly in row data first - this is the most accurate source
|
||||||
|
const directValue = row.original[fieldKey];
|
||||||
|
if (directValue) {
|
||||||
|
itemNumber = directValue;
|
||||||
|
} else {
|
||||||
|
// Fall back to centralized getter that checks all sources
|
||||||
|
itemNumber = getRowUpcResult(row.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: For item_number fields, create a unique key that includes the itemNumber value
|
||||||
|
// This forces a complete re-render when the itemNumber changes
|
||||||
|
const cellKey = fieldKey === 'item_number'
|
||||||
|
? `cell-${row.index}-${fieldKey}-${itemNumber || 'empty'}-${Date.now()}` // Force re-render on every render cycle for item_number
|
||||||
|
: `cell-${row.index}-${fieldKey}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemoizedCell
|
<MemoizedCell
|
||||||
|
key={cellKey} // CRITICAL: Add key to force re-render when itemNumber changes
|
||||||
field={fieldWithType as Field<string>}
|
field={fieldWithType as Field<string>}
|
||||||
value={row.original[field.key as keyof typeof row.original]}
|
value={fieldKey === 'item_number' && row.original[field.key]
|
||||||
|
? row.original[field.key] // Use direct value from row data
|
||||||
|
: row.original[field.key as keyof typeof row.original]}
|
||||||
onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)}
|
onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)}
|
||||||
errors={cellErrors}
|
errors={cellErrors}
|
||||||
isValidating={isLoading}
|
isValidating={isLoading}
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
options={options}
|
options={options}
|
||||||
itemNumber={itemNumbers.get(row.index)}
|
itemNumber={itemNumber}
|
||||||
width={fieldWidth}
|
width={fieldWidth}
|
||||||
rowIndex={row.index}
|
rowIndex={row.index}
|
||||||
copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)}
|
copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)}
|
||||||
@@ -424,7 +476,9 @@ const ValidationTable = <T extends string>({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}).filter((col): col is ColumnDef<RowData<T>, any> => col !== null),
|
}).filter((col): col is ColumnDef<RowData<T>, any> => col !== null),
|
||||||
[fields, validationErrors, validatingCells, itemNumbers, handleFieldUpdate, handleCopyDown, optionsCache, data.length, rowProductLines, rowSublines, isLoadingLines, isLoadingSublines]);
|
[fields, validationErrors, validatingCells, itemNumbers, handleFieldUpdate, handleCopyDown, optionsCache,
|
||||||
|
data.length, rowProductLines, rowSublines, isLoadingLines, isLoadingSublines,
|
||||||
|
isRowValidatingUpc, getRowUpcResult]);
|
||||||
|
|
||||||
// Combine columns
|
// Combine columns
|
||||||
const columns = useMemo(() => [selectionColumn, templateColumn, ...fieldColumns], [selectionColumn, templateColumn, fieldColumns]);
|
const columns = useMemo(() => [selectionColumn, templateColumn, ...fieldColumns], [selectionColumn, templateColumn, fieldColumns]);
|
||||||
@@ -590,7 +644,7 @@ const ValidationTable = <T extends string>({
|
|||||||
key={row.id}
|
key={row.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted/50",
|
"hover:bg-muted/50",
|
||||||
row.getIsSelected() ? "bg-muted/50" : "",
|
row.getIsSelected() ? "!bg-blue-50/50" : "",
|
||||||
hasErrors ? "bg-red-50/40" : "",
|
hasErrors ? "bg-red-50/40" : "",
|
||||||
isCopyDownTarget ? "cursor-pointer copy-down-target-row" : ""
|
isCopyDownTarget ? "cursor-pointer copy-down-target-row" : ""
|
||||||
)}
|
)}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback, useDeferredValue, useTransition, useRef, useEffect, useMemo } from 'react'
|
import React, { useState, useCallback, useTransition, useRef, useEffect, useMemo } from 'react'
|
||||||
import { Field } from '../../../../types'
|
import { Field } from '../../../../types'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -46,7 +46,6 @@ const InputCell = <T extends string>({
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editValue, setEditValue] = useState('');
|
const [editValue, setEditValue] = useState('');
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const deferredEditValue = useDeferredValue(editValue);
|
|
||||||
|
|
||||||
// Use a ref to track if we need to process the value
|
// Use a ref to track if we need to process the value
|
||||||
const needsProcessingRef = useRef(false);
|
const needsProcessingRef = useRef(false);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getApiUrl, RowData } from './useValidationState';
|
import { getApiUrl, RowData } from './validationTypes';
|
||||||
import { Fields, InfoWithSource, ErrorSources, ErrorType } from '../../../types';
|
import { Fields } from '../../../types';
|
||||||
import { Meta } from '../types';
|
import { Meta } from '../types';
|
||||||
import { addErrorsAndRunHooks } from '../utils/dataMutations';
|
import { addErrorsAndRunHooks } from '../utils/dataMutations';
|
||||||
import * as Diff from 'diff';
|
import * as Diff from 'diff';
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { Field, Fields, RowHook } from '../../../types';
|
||||||
|
import type { Meta } from '../types';
|
||||||
|
import { ErrorType, ValidationError } from '../../../types';
|
||||||
|
import { RowData, isEmpty } from './validationTypes';
|
||||||
|
|
||||||
|
// Create a cache for validation results to avoid repeated validation of the same data
|
||||||
|
const validationResultCache = new Map();
|
||||||
|
|
||||||
|
// Add a function to clear cache for a specific field value
|
||||||
|
export const clearValidationCacheForField = (fieldKey: string) => {
|
||||||
|
// Look for entries that match this field key
|
||||||
|
validationResultCache.forEach((_, key) => {
|
||||||
|
if (key.startsWith(`${fieldKey}-`)) {
|
||||||
|
validationResultCache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a special function to clear all uniqueness validation caches
|
||||||
|
export const clearAllUniquenessCaches = () => {
|
||||||
|
// Clear cache for common unique fields
|
||||||
|
['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'].forEach(fieldKey => {
|
||||||
|
clearValidationCacheForField(fieldKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also clear any cache entries that might involve uniqueness validation
|
||||||
|
validationResultCache.forEach((_, key) => {
|
||||||
|
if (key.includes('unique')) {
|
||||||
|
validationResultCache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFieldValidation = <T extends string>(
|
||||||
|
fields: Fields<T>,
|
||||||
|
rowHook?: RowHook<T>
|
||||||
|
) => {
|
||||||
|
// Validate a single field
|
||||||
|
const validateField = useCallback((
|
||||||
|
value: any,
|
||||||
|
field: Field<T>
|
||||||
|
): ValidationError[] => {
|
||||||
|
const errors: ValidationError[] = [];
|
||||||
|
|
||||||
|
if (!field.validations) return errors;
|
||||||
|
|
||||||
|
// Create a cache key using field key, value, and validation rules
|
||||||
|
const cacheKey = `${field.key}-${String(value)}-${JSON.stringify(field.validations)}`;
|
||||||
|
|
||||||
|
// Check cache first to avoid redundant validation
|
||||||
|
if (validationResultCache.has(cacheKey)) {
|
||||||
|
return validationResultCache.get(cacheKey) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
field.validations.forEach(validation => {
|
||||||
|
switch (validation.rule) {
|
||||||
|
case 'required':
|
||||||
|
// Use the shared isEmpty function
|
||||||
|
if (isEmpty(value)) {
|
||||||
|
errors.push({
|
||||||
|
message: validation.errorMessage || 'This field is required',
|
||||||
|
level: validation.level || 'error',
|
||||||
|
type: ErrorType.Required
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'unique':
|
||||||
|
// Unique validation happens at table level, not here
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'regex':
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(validation.value, validation.flags);
|
||||||
|
if (!regex.test(String(value))) {
|
||||||
|
errors.push({
|
||||||
|
message: validation.errorMessage,
|
||||||
|
level: validation.level || 'error',
|
||||||
|
type: ErrorType.Regex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Invalid regex in validation:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store results in cache to speed up future validations
|
||||||
|
validationResultCache.set(cacheKey, errors);
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Validate a single row
|
||||||
|
const validateRow = useCallback(async (
|
||||||
|
row: RowData<T>,
|
||||||
|
rowIndex: number,
|
||||||
|
allRows: RowData<T>[]
|
||||||
|
): Promise<Meta> => {
|
||||||
|
// Run field-level validations
|
||||||
|
const fieldErrors: Record<string, ValidationError[]> = {};
|
||||||
|
|
||||||
|
fields.forEach(field => {
|
||||||
|
const value = row[String(field.key) as keyof typeof row];
|
||||||
|
const errors = validateField(value, field as Field<T>);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
fieldErrors[String(field.key)] = errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Special validation for supplier and company fields - only apply if the field exists in fields
|
||||||
|
if (fields.some(field => String(field.key) === 'supplier') && isEmpty(row.supplier)) {
|
||||||
|
fieldErrors['supplier'] = [{
|
||||||
|
message: 'Supplier is required',
|
||||||
|
level: 'error',
|
||||||
|
type: ErrorType.Required
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.some(field => String(field.key) === 'company') && isEmpty(row.company)) {
|
||||||
|
fieldErrors['company'] = [{
|
||||||
|
message: 'Company is required',
|
||||||
|
level: 'error',
|
||||||
|
type: ErrorType.Required
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run row hook if provided
|
||||||
|
let rowHookResult: Meta = {
|
||||||
|
__index: row.__index || String(rowIndex)
|
||||||
|
};
|
||||||
|
if (rowHook) {
|
||||||
|
try {
|
||||||
|
// Call the row hook and extract only the __index property
|
||||||
|
const result = await rowHook(row, rowIndex, allRows);
|
||||||
|
rowHookResult.__index = result.__index || rowHookResult.__index;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in row hook:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer need to merge errors since we're not storing them in the row data
|
||||||
|
// The calling code should handle storing errors in the validationErrors Map
|
||||||
|
|
||||||
|
return {
|
||||||
|
__index: row.__index || String(rowIndex)
|
||||||
|
};
|
||||||
|
}, [fields, validateField, rowHook]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateField,
|
||||||
|
validateRow,
|
||||||
|
clearValidationCacheForField,
|
||||||
|
clearAllUniquenessCaches
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
|
import { FilterState, RowData } from './validationTypes';
|
||||||
|
import type { Fields } from '../../../types';
|
||||||
|
import { ValidationError } from '../../../types';
|
||||||
|
|
||||||
|
export const useFilterManagement = <T extends string>(
|
||||||
|
data: RowData<T>[],
|
||||||
|
fields: Fields<T>,
|
||||||
|
validationErrors: Map<number, Record<string, ValidationError[]>>
|
||||||
|
) => {
|
||||||
|
// Filter state
|
||||||
|
const [filters, setFilters] = useState<FilterState>({
|
||||||
|
searchText: "",
|
||||||
|
showErrorsOnly: false,
|
||||||
|
filterField: null,
|
||||||
|
filterValue: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter data based on current filter state
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
return data.filter((row, index) => {
|
||||||
|
// Filter by search text
|
||||||
|
if (filters.searchText) {
|
||||||
|
const searchLower = filters.searchText.toLowerCase();
|
||||||
|
const matchesSearch = fields.some((field) => {
|
||||||
|
const value = row[field.key as keyof typeof row];
|
||||||
|
if (value === undefined || value === null) return false;
|
||||||
|
return String(value).toLowerCase().includes(searchLower);
|
||||||
|
});
|
||||||
|
if (!matchesSearch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by errors
|
||||||
|
if (filters.showErrorsOnly) {
|
||||||
|
const hasErrors =
|
||||||
|
validationErrors.has(index) &&
|
||||||
|
Object.keys(validationErrors.get(index) || {}).length > 0;
|
||||||
|
if (!hasErrors) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by field value
|
||||||
|
if (filters.filterField && filters.filterValue) {
|
||||||
|
const fieldValue = row[filters.filterField as keyof typeof row];
|
||||||
|
if (fieldValue === undefined) return false;
|
||||||
|
|
||||||
|
const valueStr = String(fieldValue).toLowerCase();
|
||||||
|
const filterStr = filters.filterValue.toLowerCase();
|
||||||
|
|
||||||
|
if (!valueStr.includes(filterStr)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [data, fields, filters, validationErrors]);
|
||||||
|
|
||||||
|
// Get filter fields
|
||||||
|
const filterFields = useMemo(() => {
|
||||||
|
return fields.map((field) => ({
|
||||||
|
key: String(field.key),
|
||||||
|
label: field.label,
|
||||||
|
}));
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
// Get filter values for the selected field
|
||||||
|
const filterValues = useMemo(() => {
|
||||||
|
if (!filters.filterField) return [];
|
||||||
|
|
||||||
|
// Get unique values for the selected field
|
||||||
|
const uniqueValues = new Set<string>();
|
||||||
|
|
||||||
|
data.forEach((row) => {
|
||||||
|
const value = row[filters.filterField as keyof typeof row];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
uniqueValues.add(String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(uniqueValues).map((value) => ({
|
||||||
|
value,
|
||||||
|
label: value,
|
||||||
|
}));
|
||||||
|
}, [data, filters.filterField]);
|
||||||
|
|
||||||
|
// Update filters
|
||||||
|
const updateFilters = useCallback((newFilters: Partial<FilterState>) => {
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...newFilters,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset filters
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
setFilters({
|
||||||
|
searchText: "",
|
||||||
|
showErrorsOnly: false,
|
||||||
|
filterField: null,
|
||||||
|
filterValue: null,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
filteredData,
|
||||||
|
filterFields,
|
||||||
|
filterValues,
|
||||||
|
updateFilters,
|
||||||
|
resetFilters
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { RowData } from './validationTypes';
|
||||||
|
import type { Field, Fields } from '../../../types';
|
||||||
|
import { ErrorType, ValidationError } from '../../../types';
|
||||||
|
|
||||||
|
export const useRowOperations = <T extends string>(
|
||||||
|
data: RowData<T>[],
|
||||||
|
fields: Fields<T>,
|
||||||
|
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
|
||||||
|
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>,
|
||||||
|
validateFieldFromHook: (value: any, field: Field<T>) => ValidationError[]
|
||||||
|
) => {
|
||||||
|
// Helper function to validate a field value
|
||||||
|
const fieldValidationHelper = useCallback(
|
||||||
|
(rowIndex: number, specificField?: string) => {
|
||||||
|
// Skip validation if row doesn't exist
|
||||||
|
if (rowIndex < 0 || rowIndex >= data.length) return;
|
||||||
|
|
||||||
|
// Get the row data
|
||||||
|
const row = data[rowIndex];
|
||||||
|
|
||||||
|
// If validating a specific field, only check that field
|
||||||
|
if (specificField) {
|
||||||
|
const field = fields.find((f) => String(f.key) === specificField);
|
||||||
|
if (field) {
|
||||||
|
const value = row[specificField as keyof typeof row];
|
||||||
|
|
||||||
|
// Use state setter instead of direct mutation
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newErrors = new Map(prev);
|
||||||
|
const existingErrors = { ...(newErrors.get(rowIndex) || {}) };
|
||||||
|
|
||||||
|
// Quick check for required fields - this prevents flashing errors
|
||||||
|
const isRequired = field.validations?.some(
|
||||||
|
(v) => v.rule === "required"
|
||||||
|
);
|
||||||
|
const isEmpty =
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === "" ||
|
||||||
|
(Array.isArray(value) && value.length === 0) ||
|
||||||
|
(typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
Object.keys(value).length === 0);
|
||||||
|
|
||||||
|
// For non-empty values, remove required errors immediately
|
||||||
|
if (isRequired && !isEmpty && existingErrors[specificField]) {
|
||||||
|
const nonRequiredErrors = existingErrors[specificField].filter(
|
||||||
|
(e) => e.type !== ErrorType.Required
|
||||||
|
);
|
||||||
|
if (nonRequiredErrors.length === 0) {
|
||||||
|
// If no other errors, remove the field entirely from errors
|
||||||
|
delete existingErrors[specificField];
|
||||||
|
} else {
|
||||||
|
existingErrors[specificField] = nonRequiredErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run full validation for the field
|
||||||
|
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||||
|
|
||||||
|
// Update validation errors for this field
|
||||||
|
if (errors.length > 0) {
|
||||||
|
existingErrors[specificField] = errors;
|
||||||
|
} else {
|
||||||
|
delete existingErrors[specificField];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update validation errors map
|
||||||
|
if (Object.keys(existingErrors).length > 0) {
|
||||||
|
newErrors.set(rowIndex, existingErrors);
|
||||||
|
} else {
|
||||||
|
newErrors.delete(rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validate all fields in the row
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newErrors = new Map(prev);
|
||||||
|
const rowErrors: Record<string, ValidationError[]> = {};
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const fieldKey = String(field.key);
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
rowErrors[fieldKey] = errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update validation errors map
|
||||||
|
if (Object.keys(rowErrors).length > 0) {
|
||||||
|
newErrors.set(rowIndex, rowErrors);
|
||||||
|
} else {
|
||||||
|
newErrors.delete(rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, fields, validateFieldFromHook, setValidationErrors]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use validateRow as an alias for fieldValidationHelper for compatibility
|
||||||
|
const validateRow = fieldValidationHelper;
|
||||||
|
|
||||||
|
// Modified updateRow function that properly handles field-specific validation
|
||||||
|
const updateRow = useCallback(
|
||||||
|
(rowIndex: number, key: T, value: any) => {
|
||||||
|
// Process value before updating data
|
||||||
|
let processedValue = value;
|
||||||
|
|
||||||
|
// Strip dollar signs from price fields
|
||||||
|
if (
|
||||||
|
(key === "msrp" || key === "cost_each") &&
|
||||||
|
typeof value === "string"
|
||||||
|
) {
|
||||||
|
processedValue = value.replace(/[$,]/g, "");
|
||||||
|
|
||||||
|
// Also ensure it's a valid number
|
||||||
|
const numValue = parseFloat(processedValue);
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
processedValue = numValue.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the row data first
|
||||||
|
const rowData = data[rowIndex];
|
||||||
|
if (!rowData) {
|
||||||
|
console.error(`No row data found for index ${rowIndex}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of the row to avoid mutation
|
||||||
|
const updatedRow = { ...rowData, [key]: processedValue };
|
||||||
|
|
||||||
|
// Update the data immediately - this sets the value
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||||
|
newData[rowIndex] = updatedRow;
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the field definition
|
||||||
|
const field = fields.find((f) => String(f.key) === key);
|
||||||
|
if (!field) return;
|
||||||
|
|
||||||
|
// CRITICAL FIX: Combine both validation operations into a single state update
|
||||||
|
// to prevent intermediate rendering that causes error icon flashing
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
const existingErrors = newMap.get(rowIndex) || {};
|
||||||
|
const newRowErrors = { ...existingErrors };
|
||||||
|
|
||||||
|
// Check for required field first
|
||||||
|
const isRequired = field.validations?.some(
|
||||||
|
(v) => v.rule === "required"
|
||||||
|
);
|
||||||
|
const isEmpty =
|
||||||
|
processedValue === undefined ||
|
||||||
|
processedValue === null ||
|
||||||
|
processedValue === "" ||
|
||||||
|
(Array.isArray(processedValue) && processedValue.length === 0) ||
|
||||||
|
(typeof processedValue === "object" &&
|
||||||
|
processedValue !== null &&
|
||||||
|
Object.keys(processedValue).length === 0);
|
||||||
|
|
||||||
|
// For required fields with values, remove required errors
|
||||||
|
if (isRequired && !isEmpty && newRowErrors[key as string]) {
|
||||||
|
const hasRequiredError = newRowErrors[key as string].some(
|
||||||
|
(e) => e.type === ErrorType.Required
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasRequiredError) {
|
||||||
|
// Remove required errors but keep other types of errors
|
||||||
|
const nonRequiredErrors = newRowErrors[key as string].filter(
|
||||||
|
(e) => e.type !== ErrorType.Required
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nonRequiredErrors.length === 0) {
|
||||||
|
// If no other errors, delete the field's errors entirely
|
||||||
|
delete newRowErrors[key as string];
|
||||||
|
} else {
|
||||||
|
// Otherwise keep non-required errors
|
||||||
|
newRowErrors[key as string] = nonRequiredErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now run full validation for the field (except for required which we already handled)
|
||||||
|
const errors = validateFieldFromHook(
|
||||||
|
processedValue,
|
||||||
|
field as unknown as Field<T>
|
||||||
|
).filter((e) => e.type !== ErrorType.Required || isEmpty);
|
||||||
|
|
||||||
|
// Update with new validation results
|
||||||
|
if (errors.length > 0) {
|
||||||
|
newRowErrors[key as string] = errors;
|
||||||
|
} else if (!newRowErrors[key as string]) {
|
||||||
|
// If no errors found and no existing errors, ensure field is removed from errors
|
||||||
|
delete newRowErrors[key as string];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the map
|
||||||
|
if (Object.keys(newRowErrors).length > 0) {
|
||||||
|
newMap.set(rowIndex, newRowErrors);
|
||||||
|
} else {
|
||||||
|
newMap.delete(rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle simple secondary effects here
|
||||||
|
setTimeout(() => {
|
||||||
|
// Use __index to find the actual row in the full data array
|
||||||
|
const rowId = rowData.__index;
|
||||||
|
|
||||||
|
// Handle company change - clear line/subline
|
||||||
|
if (key === "company" && processedValue) {
|
||||||
|
// Clear any existing line/subline values
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
const idx = newData.findIndex((item) => item.__index === rowId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
newData[idx] = {
|
||||||
|
...newData[idx],
|
||||||
|
line: undefined,
|
||||||
|
subline: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle line change - clear subline
|
||||||
|
if (key === "line" && processedValue) {
|
||||||
|
// Clear any existing subline value
|
||||||
|
setData((prevData) => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
const idx = newData.findIndex((item) => item.__index === rowId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
newData[idx] = {
|
||||||
|
...newData[idx],
|
||||||
|
subline: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
[data, fields, validateFieldFromHook, setData, setValidationErrors]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Improved revalidateRows function
|
||||||
|
const revalidateRows = useCallback(
|
||||||
|
async (
|
||||||
|
rowIndexes: number[],
|
||||||
|
updatedFields?: { [rowIndex: number]: string[] }
|
||||||
|
) => {
|
||||||
|
// Process all specified rows using a single state update to avoid race conditions
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newErrors = new Map(prev);
|
||||||
|
|
||||||
|
// Process each row
|
||||||
|
for (const rowIndex of rowIndexes) {
|
||||||
|
if (rowIndex < 0 || rowIndex >= data.length) continue;
|
||||||
|
|
||||||
|
const row = data[rowIndex];
|
||||||
|
if (!row) continue;
|
||||||
|
|
||||||
|
// If we have specific fields to update for this row
|
||||||
|
const fieldsToValidate = updatedFields?.[rowIndex] || [];
|
||||||
|
|
||||||
|
if (fieldsToValidate.length > 0) {
|
||||||
|
// Get existing errors for this row
|
||||||
|
const existingRowErrors = { ...(newErrors.get(rowIndex) || {}) };
|
||||||
|
|
||||||
|
// Validate each specified field
|
||||||
|
for (const fieldKey of fieldsToValidate) {
|
||||||
|
const field = fields.find((f) => String(f.key) === fieldKey);
|
||||||
|
if (!field) continue;
|
||||||
|
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
|
||||||
|
// Run validation for this field
|
||||||
|
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||||
|
|
||||||
|
// Update errors for this field
|
||||||
|
if (errors.length > 0) {
|
||||||
|
existingRowErrors[fieldKey] = errors;
|
||||||
|
} else {
|
||||||
|
delete existingRowErrors[fieldKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the row's errors
|
||||||
|
if (Object.keys(existingRowErrors).length > 0) {
|
||||||
|
newErrors.set(rowIndex, existingRowErrors);
|
||||||
|
} else {
|
||||||
|
newErrors.delete(rowIndex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No specific fields provided - validate the entire row
|
||||||
|
const rowErrors: Record<string, ValidationError[]> = {};
|
||||||
|
|
||||||
|
// Validate all fields in the row
|
||||||
|
for (const field of fields) {
|
||||||
|
const fieldKey = String(field.key);
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
|
||||||
|
// Run validation for this field
|
||||||
|
const errors = validateFieldFromHook(value, field as unknown as Field<T>);
|
||||||
|
|
||||||
|
// Update errors for this field
|
||||||
|
if (errors.length > 0) {
|
||||||
|
rowErrors[fieldKey] = errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the row's errors
|
||||||
|
if (Object.keys(rowErrors).length > 0) {
|
||||||
|
newErrors.set(rowIndex, rowErrors);
|
||||||
|
} else {
|
||||||
|
newErrors.delete(rowIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[data, fields, validateFieldFromHook]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy a cell value to all cells below it in the same column
|
||||||
|
const copyDown = useCallback(
|
||||||
|
(rowIndex: number, key: T) => {
|
||||||
|
// Get the source value to copy
|
||||||
|
const sourceValue = data[rowIndex][key];
|
||||||
|
|
||||||
|
// Update all rows below with the same value using the existing updateRow function
|
||||||
|
// This ensures all validation logic runs consistently
|
||||||
|
for (let i = rowIndex + 1; i < data.length; i++) {
|
||||||
|
// Just use updateRow which will handle validation with proper timing
|
||||||
|
updateRow(i, key, sourceValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, updateRow]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateRow,
|
||||||
|
updateRow,
|
||||||
|
revalidateRows,
|
||||||
|
copyDown
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Template, RowData, TemplateState, getApiUrl } from './validationTypes';
|
||||||
|
import { RowSelectionState } from '@tanstack/react-table';
|
||||||
|
import { ValidationError } from '../../../types';
|
||||||
|
|
||||||
|
export const useTemplateManagement = <T extends string>(
|
||||||
|
data: RowData<T>[],
|
||||||
|
setData: React.Dispatch<React.SetStateAction<RowData<T>[]>>,
|
||||||
|
rowSelection: RowSelectionState,
|
||||||
|
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>,
|
||||||
|
setRowValidationStatus: React.Dispatch<React.SetStateAction<Map<number, "pending" | "validating" | "validated" | "error">>>,
|
||||||
|
validateRow: (rowIndex: number, specificField?: string) => void,
|
||||||
|
isApplyingTemplateRef: React.MutableRefObject<boolean>,
|
||||||
|
upcValidation: {
|
||||||
|
validateUpc: (rowIndex: number, supplierId: string, upcValue: string) => Promise<{success: boolean, itemNumber?: string}>,
|
||||||
|
applyItemNumbersToData: (onApplied?: (updatedRowIds: number[]) => void) => void
|
||||||
|
},
|
||||||
|
setValidatingCells?: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||||
|
) => {
|
||||||
|
// Template state
|
||||||
|
const [templates, setTemplates] = useState<Template[]>([]);
|
||||||
|
const [isLoadingTemplates, setIsLoadingTemplates] = useState(true);
|
||||||
|
const [templateState, setTemplateState] = useState<TemplateState>({
|
||||||
|
selectedTemplateId: null,
|
||||||
|
showSaveTemplateDialog: false,
|
||||||
|
newTemplateName: "",
|
||||||
|
newTemplateType: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load templates
|
||||||
|
const loadTemplates = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingTemplates(true);
|
||||||
|
console.log("Fetching templates from:", `${getApiUrl()}/templates`);
|
||||||
|
const response = await fetch(`${getApiUrl()}/templates`);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch templates");
|
||||||
|
const templateData = await response.json();
|
||||||
|
const validTemplates = templateData.filter(
|
||||||
|
(t: any) =>
|
||||||
|
t && typeof t === "object" && t.id && t.company && t.product_type
|
||||||
|
);
|
||||||
|
setTemplates(validTemplates);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching templates:", error);
|
||||||
|
toast.error("Failed to load templates");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingTemplates(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Refresh templates
|
||||||
|
const refreshTemplates = useCallback(() => {
|
||||||
|
loadTemplates();
|
||||||
|
}, [loadTemplates]);
|
||||||
|
|
||||||
|
// Save a new template
|
||||||
|
const saveTemplate = useCallback(
|
||||||
|
async (name: string, type: string) => {
|
||||||
|
try {
|
||||||
|
// Get selected rows
|
||||||
|
const selectedRowIndex = Number(Object.keys(rowSelection)[0]);
|
||||||
|
const selectedRow = data[selectedRowIndex];
|
||||||
|
|
||||||
|
if (!selectedRow) {
|
||||||
|
toast.error("Please select a row to create a template");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract data for template, removing metadata fields
|
||||||
|
const {
|
||||||
|
__index,
|
||||||
|
__template,
|
||||||
|
__original,
|
||||||
|
__corrected,
|
||||||
|
__changes,
|
||||||
|
...templateData
|
||||||
|
} = selectedRow as any;
|
||||||
|
|
||||||
|
// Clean numeric values (remove $ from price fields)
|
||||||
|
const cleanedData: Record<string, any> = {};
|
||||||
|
|
||||||
|
// Process each key-value pair
|
||||||
|
Object.entries(templateData).forEach(([key, value]) => {
|
||||||
|
// Handle numeric values with dollar signs
|
||||||
|
if (typeof value === "string" && value.includes("$")) {
|
||||||
|
cleanedData[key] = value.replace(/[$,\s]/g, "").trim();
|
||||||
|
}
|
||||||
|
// Handle array values (like categories or ship_restrictions)
|
||||||
|
else if (Array.isArray(value)) {
|
||||||
|
cleanedData[key] = value;
|
||||||
|
}
|
||||||
|
// Handle other values
|
||||||
|
else {
|
||||||
|
cleanedData[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the template to the API
|
||||||
|
const response = await fetch(`${getApiUrl()}/templates`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...cleanedData,
|
||||||
|
company: name,
|
||||||
|
product_type: type,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(
|
||||||
|
errorData.error || errorData.details || "Failed to save template"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the new template from the response
|
||||||
|
const newTemplate = await response.json();
|
||||||
|
|
||||||
|
// Update the templates list with the new template
|
||||||
|
setTemplates((prev) => [...prev, newTemplate]);
|
||||||
|
|
||||||
|
// Update the row to show it's using this template
|
||||||
|
setData((prev) => {
|
||||||
|
const newData = [...prev];
|
||||||
|
if (newData[selectedRowIndex]) {
|
||||||
|
newData[selectedRowIndex] = {
|
||||||
|
...newData[selectedRowIndex],
|
||||||
|
__template: newTemplate.id.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`Template "${name}" saved successfully`);
|
||||||
|
|
||||||
|
// Reset dialog state
|
||||||
|
setTemplateState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
showSaveTemplateDialog: false,
|
||||||
|
newTemplateName: "",
|
||||||
|
newTemplateType: "",
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving template:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Failed to save template"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, rowSelection, setData]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply template to rows - optimized version
|
||||||
|
const applyTemplate = useCallback(
|
||||||
|
(templateId: string, rowIndexes: number[]) => {
|
||||||
|
const template = templates.find((t) => t.id.toString() === templateId);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
toast.error("Template not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Applying template ${templateId} to rows:`, rowIndexes);
|
||||||
|
|
||||||
|
// Validate row indexes
|
||||||
|
const validRowIndexes = rowIndexes.filter(
|
||||||
|
(index) => index >= 0 && index < data.length && Number.isInteger(index)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validRowIndexes.length === 0) {
|
||||||
|
toast.error("No valid rows to update");
|
||||||
|
console.error("Invalid row indexes:", rowIndexes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the template application flag
|
||||||
|
isApplyingTemplateRef.current = true;
|
||||||
|
|
||||||
|
// Save scroll position
|
||||||
|
const scrollPosition = {
|
||||||
|
left: window.scrollX,
|
||||||
|
top: window.scrollY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a copy of data and process all rows at once to minimize state updates
|
||||||
|
const newData = [...data];
|
||||||
|
const batchErrors = new Map<number, Record<string, ValidationError[]>>();
|
||||||
|
const batchStatuses = new Map<
|
||||||
|
number,
|
||||||
|
"pending" | "validating" | "validated" | "error"
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Extract template fields once outside the loop
|
||||||
|
const templateFields = Object.entries(template).filter(
|
||||||
|
([key]) =>
|
||||||
|
![
|
||||||
|
"id",
|
||||||
|
"__meta",
|
||||||
|
"__template",
|
||||||
|
"__original",
|
||||||
|
"__corrected",
|
||||||
|
"__changes",
|
||||||
|
].includes(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply template to each valid row
|
||||||
|
validRowIndexes.forEach((index) => {
|
||||||
|
// Create a new row with template values
|
||||||
|
const originalRow = newData[index];
|
||||||
|
const updatedRow = { ...originalRow } as Record<string, any>;
|
||||||
|
|
||||||
|
// Apply template fields (excluding metadata fields)
|
||||||
|
for (const [key, value] of templateFields) {
|
||||||
|
updatedRow[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the row as using this template
|
||||||
|
updatedRow.__template = templateId;
|
||||||
|
|
||||||
|
// Update the row in the data array
|
||||||
|
newData[index] = updatedRow as RowData<T>;
|
||||||
|
|
||||||
|
// Clear validation errors and mark as validated
|
||||||
|
batchErrors.set(index, {});
|
||||||
|
batchStatuses.set(index, "validated");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check which rows need UPC validation
|
||||||
|
const upcValidationRows = validRowIndexes.filter((rowIndex) => {
|
||||||
|
const row = newData[rowIndex];
|
||||||
|
return row && row.upc && row.supplier;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform a single update for all rows
|
||||||
|
setData(newData);
|
||||||
|
|
||||||
|
// Update all validation errors and statuses at once
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newErrors = new Map(prev);
|
||||||
|
for (const [rowIndex, errors] of batchErrors.entries()) {
|
||||||
|
newErrors.set(rowIndex, errors);
|
||||||
|
}
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
|
||||||
|
setRowValidationStatus((prev) => {
|
||||||
|
const newStatus = new Map(prev);
|
||||||
|
for (const [rowIndex, status] of batchStatuses.entries()) {
|
||||||
|
newStatus.set(rowIndex, status);
|
||||||
|
}
|
||||||
|
return newStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore scroll position
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo(scrollPosition.left, scrollPosition.top);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
if (validRowIndexes.length === 1) {
|
||||||
|
toast.success("Template applied");
|
||||||
|
} else if (validRowIndexes.length > 1) {
|
||||||
|
toast.success(`Template applied to ${validRowIndexes.length} rows`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset template application flag to allow validation
|
||||||
|
isApplyingTemplateRef.current = false;
|
||||||
|
|
||||||
|
// If there are rows with both UPC and supplier, validate them
|
||||||
|
if (upcValidationRows.length > 0) {
|
||||||
|
console.log(`Validating UPCs for ${upcValidationRows.length} rows after template application`);
|
||||||
|
|
||||||
|
// Process each row sequentially - this mimics the exact manual edit behavior
|
||||||
|
const processNextValidation = (index = 0) => {
|
||||||
|
if (index >= upcValidationRows.length) {
|
||||||
|
return; // All rows processed
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowIndex = upcValidationRows[index];
|
||||||
|
const row = newData[rowIndex];
|
||||||
|
|
||||||
|
if (row && row.supplier && row.upc) {
|
||||||
|
// The EXACT implementation from handleUpdateRow when supplier is edited manually:
|
||||||
|
|
||||||
|
// 1. Mark the item_number cell as being validated - THIS IS CRITICAL FOR LOADING STATE
|
||||||
|
const cellKey = `${rowIndex}-item_number`;
|
||||||
|
|
||||||
|
// Clear validation errors for this field
|
||||||
|
setValidationErrors(prev => {
|
||||||
|
const newErrors = new Map(prev);
|
||||||
|
if (newErrors.has(rowIndex)) {
|
||||||
|
const rowErrors = { ...newErrors.get(rowIndex) };
|
||||||
|
if (rowErrors.item_number) {
|
||||||
|
delete rowErrors.item_number;
|
||||||
|
}
|
||||||
|
newErrors.set(rowIndex, rowErrors);
|
||||||
|
}
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set loading state - using setValidatingCells from props
|
||||||
|
if (setValidatingCells) {
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.add(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate UPC for this row
|
||||||
|
upcValidation.validateUpc(rowIndex, row.supplier.toString(), row.upc.toString())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success && result.itemNumber) {
|
||||||
|
// CRITICAL FIX: Directly update data with the item number to ensure immediate UI update
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
|
||||||
|
// Update this specific row with the item number
|
||||||
|
if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||||
|
newData[rowIndex] = {
|
||||||
|
...newData[rowIndex],
|
||||||
|
item_number: result.itemNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also trigger other relevant updates
|
||||||
|
upcValidation.applyItemNumbersToData();
|
||||||
|
|
||||||
|
// Mark for revalidation after item numbers are updated
|
||||||
|
setTimeout(() => {
|
||||||
|
// Validate the row EXACTLY like in manual edit
|
||||||
|
validateRow(rowIndex, 'item_number');
|
||||||
|
|
||||||
|
// CRITICAL FIX: Make one final check to ensure data is correct
|
||||||
|
setTimeout(() => {
|
||||||
|
// Get the current item number from the data
|
||||||
|
const currentItemNumber = (() => {
|
||||||
|
try {
|
||||||
|
const dataAtThisPointInTime = data[rowIndex];
|
||||||
|
return dataAtThisPointInTime?.item_number;
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// If the data is wrong at this point, fix it directly
|
||||||
|
if (currentItemNumber !== result.itemNumber) {
|
||||||
|
// Directly update the data to fix the issue
|
||||||
|
setData(dataRightNow => {
|
||||||
|
const fixedData = [...dataRightNow];
|
||||||
|
if (rowIndex >= 0 && rowIndex < fixedData.length) {
|
||||||
|
fixedData[rowIndex] = {
|
||||||
|
...fixedData[rowIndex],
|
||||||
|
item_number: result.itemNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return fixedData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then do a force update after a brief delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setData(currentData => {
|
||||||
|
// Critical fix: ensure the item number is correct
|
||||||
|
if (currentData[rowIndex] && currentData[rowIndex].item_number !== result.itemNumber) {
|
||||||
|
// Create a completely new array with the correct item number
|
||||||
|
const fixedData = [...currentData];
|
||||||
|
fixedData[rowIndex] = {
|
||||||
|
...fixedData[rowIndex],
|
||||||
|
item_number: result.itemNumber
|
||||||
|
};
|
||||||
|
return fixedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a completely new array
|
||||||
|
return [...currentData];
|
||||||
|
});
|
||||||
|
}, 20);
|
||||||
|
} else {
|
||||||
|
// Item number is already correct, just do the force update
|
||||||
|
setData(currentData => {
|
||||||
|
// Create a completely new array
|
||||||
|
return [...currentData];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// Clear loading state
|
||||||
|
if (setValidatingCells) {
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue to next row after validation is complete
|
||||||
|
setTimeout(() => processNextValidation(index + 1), 100);
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
// Clear loading state on failure
|
||||||
|
if (setValidatingCells) {
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue to next row if validation fails
|
||||||
|
setTimeout(() => processNextValidation(index + 1), 100);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Error validating UPC for row ${rowIndex}:`, err);
|
||||||
|
|
||||||
|
// Clear loading state on error
|
||||||
|
if (setValidatingCells) {
|
||||||
|
setValidatingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue to next row despite error
|
||||||
|
setTimeout(() => processNextValidation(index + 1), 100);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Skip this row and continue to the next
|
||||||
|
processNextValidation(index + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start processing validations
|
||||||
|
processNextValidation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
data,
|
||||||
|
templates,
|
||||||
|
setData,
|
||||||
|
setValidationErrors,
|
||||||
|
setRowValidationStatus,
|
||||||
|
validateRow,
|
||||||
|
upcValidation,
|
||||||
|
setValidatingCells
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply template to selected rows
|
||||||
|
const applyTemplateToSelected = useCallback(
|
||||||
|
(templateId: string) => {
|
||||||
|
if (!templateId) return;
|
||||||
|
|
||||||
|
// Update the selected template ID
|
||||||
|
setTemplateState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedTemplateId: templateId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get selected row keys (which may be UUIDs)
|
||||||
|
const selectedKeys = Object.entries(rowSelection)
|
||||||
|
.filter(([_, selected]) => selected === true)
|
||||||
|
.map(([key, _]) => key);
|
||||||
|
|
||||||
|
console.log("Selected row keys:", selectedKeys);
|
||||||
|
|
||||||
|
if (selectedKeys.length === 0) {
|
||||||
|
toast.error("No rows selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map UUID keys to array indices
|
||||||
|
const selectedIndexes = selectedKeys
|
||||||
|
.map((key) => {
|
||||||
|
// Find the matching row index in the data array
|
||||||
|
const index = data.findIndex(
|
||||||
|
(row) =>
|
||||||
|
(row.__index && row.__index === key) || // Match by __index
|
||||||
|
String(data.indexOf(row)) === key // Or by numeric index
|
||||||
|
);
|
||||||
|
return index;
|
||||||
|
})
|
||||||
|
.filter((index) => index !== -1); // Filter out any not found
|
||||||
|
|
||||||
|
console.log("Mapped row indices:", selectedIndexes);
|
||||||
|
|
||||||
|
if (selectedIndexes.length === 0) {
|
||||||
|
toast.error("Could not find selected rows");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply template to selected rows
|
||||||
|
applyTemplate(templateId, selectedIndexes);
|
||||||
|
},
|
||||||
|
[rowSelection, applyTemplate, setTemplateState, data]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
templates,
|
||||||
|
isLoadingTemplates,
|
||||||
|
templateState,
|
||||||
|
setTemplateState,
|
||||||
|
loadTemplates,
|
||||||
|
refreshTemplates,
|
||||||
|
saveTemplate,
|
||||||
|
applyTemplate,
|
||||||
|
applyTemplateToSelected
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { RowData } from './validationTypes';
|
||||||
|
import type { Fields } from '../../../types';
|
||||||
|
import { ErrorSources, ErrorType, ValidationError } from '../../../types';
|
||||||
|
|
||||||
|
export const useUniqueItemNumbersValidation = <T extends string>(
|
||||||
|
data: RowData<T>[],
|
||||||
|
fields: Fields<T>,
|
||||||
|
setValidationErrors: React.Dispatch<React.SetStateAction<Map<number, Record<string, ValidationError[]>>>>
|
||||||
|
) => {
|
||||||
|
// Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode
|
||||||
|
const validateUniqueItemNumbers = useCallback(async () => {
|
||||||
|
console.log("Validating unique fields");
|
||||||
|
|
||||||
|
// Skip if no data
|
||||||
|
if (!data.length) return;
|
||||||
|
|
||||||
|
// Track unique identifiers in maps
|
||||||
|
const uniqueFieldsMap = new Map<string, Map<string, number[]>>();
|
||||||
|
|
||||||
|
// Find fields that need uniqueness validation
|
||||||
|
const uniqueFields = fields
|
||||||
|
.filter((field) => field.validations?.some((v) => v.rule === "unique"))
|
||||||
|
.map((field) => String(field.key));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${uniqueFields.length} fields requiring uniqueness validation:`,
|
||||||
|
uniqueFields
|
||||||
|
);
|
||||||
|
|
||||||
|
// Always check item_number uniqueness even if not explicitly defined
|
||||||
|
if (!uniqueFields.includes("item_number")) {
|
||||||
|
uniqueFields.push("item_number");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize maps for each unique field
|
||||||
|
uniqueFields.forEach((fieldKey) => {
|
||||||
|
uniqueFieldsMap.set(fieldKey, new Map<string, number[]>());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize batch updates
|
||||||
|
const errors = new Map<number, Record<string, ValidationError[]>>();
|
||||||
|
|
||||||
|
// Single pass through data to identify all unique values
|
||||||
|
data.forEach((row, index) => {
|
||||||
|
uniqueFields.forEach((fieldKey) => {
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
|
||||||
|
// Skip empty values
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueStr = String(value);
|
||||||
|
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (fieldMap) {
|
||||||
|
// Get or initialize the array of indices for this value
|
||||||
|
const indices = fieldMap.get(valueStr) || [];
|
||||||
|
indices.push(index);
|
||||||
|
fieldMap.set(valueStr, indices);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process duplicates
|
||||||
|
uniqueFields.forEach((fieldKey) => {
|
||||||
|
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
||||||
|
if (!fieldMap) return;
|
||||||
|
|
||||||
|
fieldMap.forEach((indices, value) => {
|
||||||
|
// Only process if there are duplicates
|
||||||
|
if (indices.length > 1) {
|
||||||
|
// Get the validation rule for this field
|
||||||
|
const field = fields.find((f) => String(f.key) === fieldKey);
|
||||||
|
const validationRule = field?.validations?.find(
|
||||||
|
(v) => v.rule === "unique"
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorObj = {
|
||||||
|
message:
|
||||||
|
validationRule?.errorMessage || `Duplicate ${fieldKey}: ${value}`,
|
||||||
|
level: validationRule?.level || ("error" as "error"),
|
||||||
|
source: ErrorSources.Table,
|
||||||
|
type: ErrorType.Unique,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add error to each row with this value
|
||||||
|
indices.forEach((rowIndex) => {
|
||||||
|
const rowErrors = errors.get(rowIndex) || {};
|
||||||
|
rowErrors[fieldKey] = [errorObj];
|
||||||
|
errors.set(rowIndex, rowErrors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply batch updates only if we have errors to report
|
||||||
|
if (errors.size > 0) {
|
||||||
|
// OPTIMIZATION: Check if we actually have new errors before updating state
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
// We'll update errors with a single batch operation
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
|
||||||
|
// Check each row for changes
|
||||||
|
errors.forEach((rowErrors, rowIndex) => {
|
||||||
|
const existingErrors = newMap.get(rowIndex) || {};
|
||||||
|
const updatedErrors = { ...existingErrors };
|
||||||
|
let rowHasChanges = false;
|
||||||
|
|
||||||
|
// Check each field for changes
|
||||||
|
Object.entries(rowErrors).forEach(([fieldKey, fieldErrors]) => {
|
||||||
|
// Compare with existing errors
|
||||||
|
const existingFieldErrors = existingErrors[fieldKey];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!existingFieldErrors ||
|
||||||
|
existingFieldErrors.length !== fieldErrors.length ||
|
||||||
|
!existingFieldErrors.every(
|
||||||
|
(err, idx) =>
|
||||||
|
err.message === fieldErrors[idx].message &&
|
||||||
|
err.type === fieldErrors[idx].type
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// We have a change
|
||||||
|
updatedErrors[fieldKey] = fieldErrors;
|
||||||
|
rowHasChanges = true;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only update if we have changes
|
||||||
|
if (rowHasChanges) {
|
||||||
|
newMap.set(rowIndex, updatedErrors);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only return a new map if we have changes
|
||||||
|
return hasChanges ? newMap : prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Uniqueness validation complete");
|
||||||
|
}, [data, fields, setValidationErrors]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateUniqueItemNumbers
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { Fields } from '../../../types';
|
||||||
|
import { ErrorSources, ErrorType } from '../../../types';
|
||||||
|
import { RowData, InfoWithSource, isEmpty } from './validationTypes';
|
||||||
|
|
||||||
|
export const useUniqueValidation = <T extends string>(
|
||||||
|
fields: Fields<T>
|
||||||
|
) => {
|
||||||
|
// Additional function to explicitly validate uniqueness for specified fields
|
||||||
|
const validateUniqueField = useCallback((data: RowData<T>[], fieldKey: string) => {
|
||||||
|
// Field keys that need special handling for uniqueness
|
||||||
|
const uniquenessFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||||
|
|
||||||
|
// If the field doesn't need uniqueness validation, return empty errors
|
||||||
|
if (!uniquenessFields.includes(fieldKey)) {
|
||||||
|
const field = fields.find(f => String(f.key) === fieldKey);
|
||||||
|
if (!field || !field.validations?.some(v => v.rule === 'unique')) {
|
||||||
|
return new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create map to track errors
|
||||||
|
const uniqueErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
|
||||||
|
// Find the field definition
|
||||||
|
const field = fields.find(f => String(f.key) === fieldKey);
|
||||||
|
if (!field) return uniqueErrors;
|
||||||
|
|
||||||
|
// Get validation properties
|
||||||
|
const validation = field.validations?.find(v => v.rule === 'unique');
|
||||||
|
const allowEmpty = validation?.allowEmpty ?? false;
|
||||||
|
const errorMessage = validation?.errorMessage || `${field.label} must be unique`;
|
||||||
|
const level = validation?.level || 'error';
|
||||||
|
|
||||||
|
// Track values for uniqueness check
|
||||||
|
const valueMap = new Map<string, number[]>();
|
||||||
|
|
||||||
|
// Build value map
|
||||||
|
data.forEach((row, rowIndex) => {
|
||||||
|
const value = String(row[fieldKey as keyof typeof row] || '');
|
||||||
|
|
||||||
|
// Skip empty values if allowed
|
||||||
|
if (allowEmpty && isEmpty(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valueMap.has(value)) {
|
||||||
|
valueMap.set(value, [rowIndex]);
|
||||||
|
} else {
|
||||||
|
valueMap.get(value)?.push(rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add errors for duplicate values
|
||||||
|
valueMap.forEach((rowIndexes, value) => {
|
||||||
|
if (rowIndexes.length > 1) {
|
||||||
|
// Skip empty values
|
||||||
|
if (!value || value.trim() === '') return;
|
||||||
|
|
||||||
|
// Add error to all duplicate rows
|
||||||
|
rowIndexes.forEach(rowIndex => {
|
||||||
|
// Create errors object if needed
|
||||||
|
if (!uniqueErrors.has(rowIndex)) {
|
||||||
|
uniqueErrors.set(rowIndex, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add error for this field
|
||||||
|
uniqueErrors.get(rowIndex)![fieldKey] = {
|
||||||
|
message: errorMessage,
|
||||||
|
level: level as 'info' | 'warning' | 'error',
|
||||||
|
source: ErrorSources.Table,
|
||||||
|
type: ErrorType.Unique
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueErrors;
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
// Validate uniqueness for multiple fields
|
||||||
|
const validateUniqueFields = useCallback((data: RowData<T>[], fieldKeys: string[]) => {
|
||||||
|
// Process each field and merge results
|
||||||
|
const allErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
|
||||||
|
fieldKeys.forEach(fieldKey => {
|
||||||
|
const fieldErrors = validateUniqueField(data, fieldKey);
|
||||||
|
|
||||||
|
// Merge errors
|
||||||
|
fieldErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!allErrors.has(rowIdx)) {
|
||||||
|
allErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(allErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return allErrors;
|
||||||
|
}, [validateUniqueField]);
|
||||||
|
|
||||||
|
// Run complete validation for uniqueness
|
||||||
|
const validateAllUniqueFields = useCallback((data: RowData<T>[]) => {
|
||||||
|
// Get fields requiring uniqueness validation
|
||||||
|
const uniqueFields = fields
|
||||||
|
.filter(field => field.validations?.some(v => v.rule === 'unique'))
|
||||||
|
.map(field => String(field.key));
|
||||||
|
|
||||||
|
// Also add standard unique fields that might not be explicitly marked as unique
|
||||||
|
const standardUniqueFields = ['item_number', 'upc', 'barcode', 'supplier_no', 'notions_no'];
|
||||||
|
|
||||||
|
// Combine all fields that need uniqueness validation
|
||||||
|
const allUniqueFieldKeys = [...new Set([
|
||||||
|
...uniqueFields,
|
||||||
|
...standardUniqueFields
|
||||||
|
])];
|
||||||
|
|
||||||
|
// Filter to only fields that exist in the data
|
||||||
|
const existingFields = allUniqueFieldKeys.filter(fieldKey =>
|
||||||
|
data.some(row => fieldKey in row)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate all fields at once
|
||||||
|
return validateUniqueFields(data, existingFields);
|
||||||
|
}, [fields, validateUniqueFields]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateUniqueField,
|
||||||
|
validateUniqueFields,
|
||||||
|
validateAllUniqueFields
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -30,13 +30,6 @@ export const useUpcValidation = (
|
|||||||
const processedUpcMapRef = useRef(new Map<string, string>());
|
const processedUpcMapRef = useRef(new Map<string, string>());
|
||||||
const initialUpcValidationDoneRef = useRef(false);
|
const initialUpcValidationDoneRef = useRef(false);
|
||||||
|
|
||||||
// For batch validation
|
|
||||||
const validationQueueRef = useRef<Array<{rowIndex: number, supplierId: string, upcValue: string}>>([]);
|
|
||||||
const isProcessingBatchRef = useRef(false);
|
|
||||||
|
|
||||||
// For validation results
|
|
||||||
const [upcValidationResults] = useState<Map<number, { itemNumber: string }>>(new Map());
|
|
||||||
|
|
||||||
// Helper to create cell key
|
// Helper to create cell key
|
||||||
const getCellKey = (rowIndex: number, fieldKey: string) => `${rowIndex}-${fieldKey}`;
|
const getCellKey = (rowIndex: number, fieldKey: string) => `${rowIndex}-${fieldKey}`;
|
||||||
|
|
||||||
@@ -56,17 +49,40 @@ export const useUpcValidation = (
|
|||||||
|
|
||||||
// Update item number
|
// Update item number
|
||||||
const updateItemNumber = useCallback((rowIndex: number, itemNumber: string) => {
|
const updateItemNumber = useCallback((rowIndex: number, itemNumber: string) => {
|
||||||
console.log(`Setting item number for row ${rowIndex} to ${itemNumber}`);
|
// CRITICAL: Update BOTH the data state and the ref
|
||||||
validationStateRef.current.itemNumbers.set(rowIndex, itemNumber);
|
// First, update the data directly to ensure UI consistency
|
||||||
setItemNumberUpdates(new Map(validationStateRef.current.itemNumbers));
|
setData(prevData => {
|
||||||
}, []);
|
// Create a new copy of the data
|
||||||
|
const newData = [...prevData];
|
||||||
|
|
||||||
// Mark a row as being validated
|
// Only update if the row exists
|
||||||
const startValidatingRow = useCallback((rowIndex: number) => {
|
if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||||
validationStateRef.current.validatingRows.add(rowIndex);
|
// First, we need a new object reference for the row to force a re-render
|
||||||
setValidatingRows(new Set(validationStateRef.current.validatingRows));
|
newData[rowIndex] = {
|
||||||
setIsValidatingUpc(true);
|
...newData[rowIndex],
|
||||||
}, []);
|
item_number: itemNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update the itemNumbers map AFTER the data is updated
|
||||||
|
// This ensures the map reflects the current state of the data
|
||||||
|
setTimeout(() => {
|
||||||
|
// Update the ref with the same value
|
||||||
|
validationStateRef.current.itemNumbers.set(rowIndex, itemNumber);
|
||||||
|
|
||||||
|
// CRITICAL: Force a React state update to ensure all components re-render
|
||||||
|
// Created a brand new Map object to ensure React detects the change
|
||||||
|
const newItemNumbersMap = new Map(validationStateRef.current.itemNumbers);
|
||||||
|
setItemNumberUpdates(newItemNumbersMap);
|
||||||
|
|
||||||
|
// Force an immediate React render cycle by triggering state updates
|
||||||
|
setValidatingCellKeys(new Set(validationStateRef.current.validatingCells));
|
||||||
|
setValidatingRows(new Set(validationStateRef.current.validatingRows));
|
||||||
|
}, 0);
|
||||||
|
}, [setData]);
|
||||||
|
|
||||||
// Mark a row as no longer being validated
|
// Mark a row as no longer being validated
|
||||||
const stopValidatingRow = useCallback((rowIndex: number) => {
|
const stopValidatingRow = useCallback((rowIndex: number) => {
|
||||||
@@ -139,11 +155,22 @@ export const useUpcValidation = (
|
|||||||
);
|
);
|
||||||
previousKeys.forEach(key => validationStateRef.current.activeValidations.delete(key));
|
previousKeys.forEach(key => validationStateRef.current.activeValidations.delete(key));
|
||||||
|
|
||||||
// Start validation - track this with the ref to avoid race conditions
|
// Log validation start to help debug template issues
|
||||||
startValidatingRow(rowIndex);
|
console.log(`[UPC-DEBUG] Starting UPC validation for row ${rowIndex} with supplier ${supplierId}, upc ${upcValue}`);
|
||||||
startValidatingCell(rowIndex, 'item_number');
|
|
||||||
|
|
||||||
console.log(`Validating UPC: rowIndex=${rowIndex}, supplierId=${supplierId}, upc=${upcValue}`);
|
// IMPORTANT: Set validation state using setState to FORCE UI updates
|
||||||
|
validationStateRef.current.validatingRows.add(rowIndex);
|
||||||
|
setValidatingRows(new Set(validationStateRef.current.validatingRows));
|
||||||
|
setIsValidatingUpc(true);
|
||||||
|
|
||||||
|
// Start cell validation and explicitly update UI via setState
|
||||||
|
const cellKey = getCellKey(rowIndex, 'item_number');
|
||||||
|
validationStateRef.current.validatingCells.add(cellKey);
|
||||||
|
setValidatingCellKeys(new Set(validationStateRef.current.validatingCells));
|
||||||
|
|
||||||
|
console.log(`[UPC-DEBUG] Set loading state for row ${rowIndex}, cell key ${cellKey}`);
|
||||||
|
console.log(`[UPC-DEBUG] Current validating rows: ${Array.from(validationStateRef.current.validatingRows).join(', ')}`);
|
||||||
|
console.log(`[UPC-DEBUG] Current validating cells: ${Array.from(validationStateRef.current.validatingCells).join(', ')}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a unique key for this validation to track it
|
// Create a unique key for this validation to track it
|
||||||
@@ -164,18 +191,43 @@ export const useUpcValidation = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch the product by UPC
|
// Fetch the product by UPC
|
||||||
|
console.log(`[UPC-DEBUG] Fetching product data for UPC ${upcValue} with supplier ${supplierId}`);
|
||||||
const product = await fetchProductByUpc(supplierId, upcValue);
|
const product = await fetchProductByUpc(supplierId, upcValue);
|
||||||
|
console.log(`[UPC-DEBUG] Fetch complete for row ${rowIndex}, success: ${!product.error}`);
|
||||||
|
|
||||||
// Check if this validation is still relevant (hasn't been superseded by another)
|
// Check if this validation is still relevant (hasn't been superseded by another)
|
||||||
if (!validationStateRef.current.activeValidations.has(validationKey)) {
|
if (!validationStateRef.current.activeValidations.has(validationKey)) {
|
||||||
console.log(`Validation ${validationKey} was cancelled`);
|
console.log(`[UPC-DEBUG] Validation ${validationKey} was cancelled`);
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the item number from the API response - check for !error since API returns { error: boolean, data: any }
|
// Extract the item number from the API response - check for !error since API returns { error: boolean, data: any }
|
||||||
if (product && !product.error && product.data?.itemNumber) {
|
if (product && !product.error && product.data?.itemNumber) {
|
||||||
// Store this validation result
|
console.log(`[UPC-DEBUG] Got item number for row ${rowIndex}: ${product.data.itemNumber}`);
|
||||||
updateItemNumber(rowIndex, product.data.itemNumber);
|
|
||||||
|
// CRITICAL FIX: Directly update the data with the new item number first
|
||||||
|
setData(prevData => {
|
||||||
|
const newData = [...prevData];
|
||||||
|
if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||||
|
// This should happen before updating the map
|
||||||
|
newData[rowIndex] = {
|
||||||
|
...newData[rowIndex],
|
||||||
|
item_number: product.data.itemNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then, update the map to match what's now in the data
|
||||||
|
validationStateRef.current.itemNumbers.set(rowIndex, product.data.itemNumber);
|
||||||
|
|
||||||
|
// CRITICAL: Force a React state update to ensure all components re-render
|
||||||
|
// Created a brand new Map object to ensure React detects the change
|
||||||
|
const newItemNumbersMap = new Map(validationStateRef.current.itemNumbers);
|
||||||
|
setItemNumberUpdates(newItemNumbersMap);
|
||||||
|
|
||||||
|
// Force a shallow copy of the itemNumbers map to trigger useEffect dependencies
|
||||||
|
validationStateRef.current.itemNumbers = new Map(validationStateRef.current.itemNumbers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -183,7 +235,7 @@ export const useUpcValidation = (
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// No item number found but validation was still attempted
|
// No item number found but validation was still attempted
|
||||||
console.log(`No item number found for UPC ${upcValue}`);
|
console.log(`[UPC-DEBUG] No item number found for UPC ${upcValue}`);
|
||||||
|
|
||||||
// Clear any existing item number to show validation was attempted and failed
|
// Clear any existing item number to show validation was attempted and failed
|
||||||
if (validationStateRef.current.itemNumbers.has(rowIndex)) {
|
if (validationStateRef.current.itemNumbers.has(rowIndex)) {
|
||||||
@@ -194,157 +246,74 @@ export const useUpcValidation = (
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error validating UPC:', error);
|
console.error('[UPC-DEBUG] Error validating UPC:', error);
|
||||||
return { success: false };
|
return { success: false };
|
||||||
} finally {
|
} finally {
|
||||||
// End validation
|
// End validation - FORCE UI update by using setState directly
|
||||||
stopValidatingRow(rowIndex);
|
console.log(`[UPC-DEBUG] Ending validation for row ${rowIndex}`);
|
||||||
stopValidatingCell(rowIndex, 'item_number');
|
|
||||||
|
validationStateRef.current.validatingRows.delete(rowIndex);
|
||||||
|
setValidatingRows(new Set(validationStateRef.current.validatingRows));
|
||||||
|
|
||||||
|
if (validationStateRef.current.validatingRows.size === 0) {
|
||||||
|
setIsValidatingUpc(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
validationStateRef.current.validatingCells.delete(cellKey);
|
||||||
|
setValidatingCellKeys(new Set(validationStateRef.current.validatingCells));
|
||||||
|
|
||||||
|
console.log(`[UPC-DEBUG] Cleared loading state for row ${rowIndex}`);
|
||||||
|
console.log(`[UPC-DEBUG] Updated validating rows: ${Array.from(validationStateRef.current.validatingRows).join(', ')}`);
|
||||||
|
console.log(`[UPC-DEBUG] Updated validating cells: ${Array.from(validationStateRef.current.validatingCells).join(', ')}`);
|
||||||
}
|
}
|
||||||
}, [fetchProductByUpc, updateItemNumber, startValidatingCell, stopValidatingCell, startValidatingRow, stopValidatingRow, setData]);
|
}, [fetchProductByUpc, updateItemNumber, setData]);
|
||||||
|
|
||||||
// Apply item numbers to data
|
// Apply all pending item numbers to the data state
|
||||||
const applyItemNumbersToData = useCallback((onApplied?: (updatedRowIds: number[]) => void) => {
|
const applyItemNumbersToData = useCallback((callback?: (updatedRows: number[]) => void) => {
|
||||||
// Create a copy of the current item numbers map to avoid race conditions
|
// Skip if we have nothing to apply
|
||||||
const currentItemNumbers = new Map(validationStateRef.current.itemNumbers);
|
if (validationStateRef.current.itemNumbers.size === 0) {
|
||||||
|
if (callback) callback([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only apply if we have any item numbers
|
// Gather all row IDs that will be updated
|
||||||
if (currentItemNumbers.size === 0) return;
|
const rowIds: number[] = [];
|
||||||
|
|
||||||
// Track updated row indices to pass to callback
|
|
||||||
const updatedRowIndices: number[] = [];
|
|
||||||
|
|
||||||
// Log for debugging
|
|
||||||
console.log(`Applying ${currentItemNumbers.size} item numbers to data`);
|
|
||||||
|
|
||||||
|
// Update the data state with all item numbers
|
||||||
setData(prevData => {
|
setData(prevData => {
|
||||||
// Create a new copy of the data
|
|
||||||
const newData = [...prevData];
|
const newData = [...prevData];
|
||||||
|
|
||||||
// Update each row with its item number without affecting other fields
|
// Apply each item number to the data
|
||||||
currentItemNumbers.forEach((itemNumber, rowIndex) => {
|
validationStateRef.current.itemNumbers.forEach((itemNumber, rowIndex) => {
|
||||||
if (rowIndex < newData.length) {
|
// Ensure row exists and value has actually changed
|
||||||
console.log(`Setting item_number for row ${rowIndex} to ${itemNumber}`);
|
if (rowIndex >= 0 && rowIndex < newData.length &&
|
||||||
|
newData[rowIndex]?.item_number !== itemNumber) {
|
||||||
|
|
||||||
// Only update the item_number field, leaving other fields unchanged
|
// Create a new row object to force re-rendering
|
||||||
newData[rowIndex] = {
|
newData[rowIndex] = {
|
||||||
...newData[rowIndex],
|
...newData[rowIndex],
|
||||||
item_number: itemNumber
|
item_number: itemNumber
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track which rows were updated
|
// Track which row was updated for the callback
|
||||||
updatedRowIndices.push(rowIndex);
|
rowIds.push(rowIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call the callback if provided, after state updates are processed
|
// Force a re-render by updating React state
|
||||||
if (onApplied && updatedRowIndices.length > 0) {
|
setTimeout(() => {
|
||||||
// Use setTimeout to ensure this happens after the state update
|
setItemNumberUpdates(new Map(validationStateRef.current.itemNumbers));
|
||||||
setTimeout(() => {
|
}, 0);
|
||||||
onApplied(updatedRowIndices);
|
|
||||||
}, 100); // Use 100ms to ensure the data update is fully processed
|
// Call the callback with the updated row IDs
|
||||||
|
if (callback) {
|
||||||
|
callback(rowIds);
|
||||||
}
|
}
|
||||||
}, [setData]);
|
}, [setData]);
|
||||||
|
|
||||||
// Process validation queue in batches - faster processing with smaller batches
|
|
||||||
const processBatchValidation = useCallback(async () => {
|
|
||||||
if (isProcessingBatchRef.current) return;
|
|
||||||
if (validationQueueRef.current.length === 0) return;
|
|
||||||
|
|
||||||
console.log(`Processing validation batch with ${validationQueueRef.current.length} items`);
|
|
||||||
isProcessingBatchRef.current = true;
|
|
||||||
|
|
||||||
// Process in smaller batches for better UI responsiveness
|
|
||||||
const BATCH_SIZE = 5;
|
|
||||||
const queue = [...validationQueueRef.current];
|
|
||||||
validationQueueRef.current = [];
|
|
||||||
|
|
||||||
// Track if any updates were made
|
|
||||||
let updatesApplied = false;
|
|
||||||
|
|
||||||
// Track updated row indices
|
|
||||||
const updatedRows: number[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Process in small batches
|
|
||||||
for (let i = 0; i < queue.length; i += BATCH_SIZE) {
|
|
||||||
const batch = queue.slice(i, i + BATCH_SIZE);
|
|
||||||
|
|
||||||
// Process batch in parallel
|
|
||||||
const results = await Promise.all(batch.map(async ({ rowIndex, supplierId, upcValue }) => {
|
|
||||||
try {
|
|
||||||
// Skip if already validated
|
|
||||||
const cacheKey = `${supplierId}-${upcValue}`;
|
|
||||||
if (processedUpcMapRef.current.has(cacheKey)) {
|
|
||||||
const cachedItemNumber = processedUpcMapRef.current.get(cacheKey);
|
|
||||||
if (cachedItemNumber) {
|
|
||||||
console.log(`Using cached item number for row ${rowIndex}: ${cachedItemNumber}`);
|
|
||||||
updateItemNumber(rowIndex, cachedItemNumber);
|
|
||||||
updatesApplied = true;
|
|
||||||
updatedRows.push(rowIndex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch from API
|
|
||||||
const result = await fetchProductByUpc(supplierId, upcValue);
|
|
||||||
|
|
||||||
if (!result.error && result.data?.itemNumber) {
|
|
||||||
const itemNumber = result.data.itemNumber;
|
|
||||||
|
|
||||||
// Store in cache
|
|
||||||
processedUpcMapRef.current.set(cacheKey, itemNumber);
|
|
||||||
|
|
||||||
// Update item number
|
|
||||||
updateItemNumber(rowIndex, itemNumber);
|
|
||||||
updatesApplied = true;
|
|
||||||
updatedRows.push(rowIndex);
|
|
||||||
|
|
||||||
console.log(`Set item number for row ${rowIndex} to ${itemNumber}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error processing row ${rowIndex}:`, error);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
// Clear validation state
|
|
||||||
stopValidatingRow(rowIndex);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// If any updates were applied in this batch, update the data
|
|
||||||
if (results.some(Boolean) && updatesApplied) {
|
|
||||||
applyItemNumbersToData(updatedRowIds => {
|
|
||||||
console.log(`Processed batch UPC validation for rows: ${updatedRowIds.join(', ')}`);
|
|
||||||
});
|
|
||||||
updatesApplied = false;
|
|
||||||
updatedRows.length = 0; // Clear the array
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between batches to allow UI to update
|
|
||||||
if (i + BATCH_SIZE < queue.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in batch processing:', error);
|
|
||||||
} finally {
|
|
||||||
isProcessingBatchRef.current = false;
|
|
||||||
|
|
||||||
// Process any new items
|
|
||||||
if (validationQueueRef.current.length > 0) {
|
|
||||||
setTimeout(processBatchValidation, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [fetchProductByUpc, updateItemNumber, stopValidatingRow, applyItemNumbersToData]);
|
|
||||||
|
|
||||||
// For immediate processing
|
|
||||||
|
|
||||||
// Batch validate all UPCs in the data
|
// Batch validate all UPCs in the data
|
||||||
const validateAllUPCs = useCallback(async () => {
|
const validateAllUPCs = useCallback(async () => {
|
||||||
// Skip if we've already done the initial validation
|
// Skip if we've already done the initial validation
|
||||||
@@ -508,8 +477,8 @@ export const useUpcValidation = (
|
|||||||
getItemNumber,
|
getItemNumber,
|
||||||
applyItemNumbersToData,
|
applyItemNumbersToData,
|
||||||
|
|
||||||
// Results
|
// CRITICAL: Expose the itemNumbers map directly
|
||||||
upcValidationResults,
|
itemNumbers: validationStateRef.current.itemNumbers,
|
||||||
|
|
||||||
// Initialization state
|
// Initialization state
|
||||||
initialValidationDone: initialUpcValidationDoneRef.current
|
initialValidationDone: initialUpcValidationDoneRef.current
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import type { Field, Fields, RowHook } from '../../../types'
|
||||||
|
import { ErrorSources } from '../../../types'
|
||||||
|
import { RowData, InfoWithSource } from './validationTypes'
|
||||||
|
import { useFieldValidation, clearValidationCacheForField, clearAllUniquenessCaches } from './useFieldValidation'
|
||||||
|
import { useUniqueValidation } from './useUniqueValidation'
|
||||||
|
|
||||||
|
// Main validation hook that brings together field and uniqueness validation
|
||||||
|
export const useValidation = <T extends string>(
|
||||||
|
fields: Fields<T>,
|
||||||
|
rowHook?: RowHook<T>
|
||||||
|
) => {
|
||||||
|
// Use the field validation hook
|
||||||
|
const { validateField, validateRow } = useFieldValidation(fields, rowHook);
|
||||||
|
|
||||||
|
// Use the uniqueness validation hook
|
||||||
|
const {
|
||||||
|
validateUniqueField,
|
||||||
|
validateAllUniqueFields
|
||||||
|
} = useUniqueValidation(fields);
|
||||||
|
|
||||||
|
// Run complete validation
|
||||||
|
const validateData = useCallback(async (data: RowData<T>[], fieldToUpdate?: { rowIndex: number, fieldKey: string }) => {
|
||||||
|
const validationErrors = new Map<number, Record<string, InfoWithSource>>();
|
||||||
|
|
||||||
|
// If we're updating a specific field, only validate that field for that row
|
||||||
|
if (fieldToUpdate) {
|
||||||
|
const { rowIndex, fieldKey } = fieldToUpdate;
|
||||||
|
|
||||||
|
// Special handling for fields that often update item_number
|
||||||
|
const triggersItemNumberValidation = fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'supplier';
|
||||||
|
|
||||||
|
// If updating a uniqueness field or field that affects item_number, clear ALL related validation caches
|
||||||
|
const isUniqueField = fieldKey === 'upc' || fieldKey === 'item_number' ||
|
||||||
|
fieldKey === 'supplier_no' || fieldKey === 'notions_no' ||
|
||||||
|
fieldKey === 'name' || triggersItemNumberValidation;
|
||||||
|
|
||||||
|
// Force cache clearing for uniqueness-validated fields to ensure fresh validation
|
||||||
|
if (isUniqueField) {
|
||||||
|
console.log(`Clearing validation cache for uniqueness field: ${fieldKey}`);
|
||||||
|
clearValidationCacheForField(fieldKey);
|
||||||
|
|
||||||
|
// If a field that might affect item_number, also clear item_number cache
|
||||||
|
if (triggersItemNumberValidation) {
|
||||||
|
console.log('Also clearing item_number validation cache');
|
||||||
|
clearValidationCacheForField('item_number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowIndex >= 0 && rowIndex < data.length) {
|
||||||
|
const row = data[rowIndex];
|
||||||
|
|
||||||
|
// Find the field definition
|
||||||
|
const field = fields.find(f => String(f.key) === fieldKey);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
// Validate just this field for this row
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
const errors = validateField(value, field as Field<T>);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
// Store the validation error
|
||||||
|
validationErrors.set(rowIndex, {
|
||||||
|
[fieldKey]: {
|
||||||
|
message: errors[0].message,
|
||||||
|
level: errors[0].level as 'info' | 'warning' | 'error',
|
||||||
|
source: ErrorSources.Row,
|
||||||
|
type: errors[0].type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field requires uniqueness validation or if it's item_number after UPC/Supplier change
|
||||||
|
const needsUniquenessCheck = isUniqueField ||
|
||||||
|
field.validations?.some(v => v.rule === 'unique');
|
||||||
|
|
||||||
|
if (needsUniquenessCheck) {
|
||||||
|
console.log(`Running immediate uniqueness validation for field ${fieldKey}`);
|
||||||
|
|
||||||
|
// For item_number updated via UPC validation, or direct UPC update, check both fields
|
||||||
|
if (fieldKey === 'item_number' || fieldKey === 'upc' || fieldKey === 'barcode') {
|
||||||
|
// Validate both item_number and UPC/barcode fields for uniqueness
|
||||||
|
const itemNumberUniqueErrors = validateUniqueField(data, 'item_number');
|
||||||
|
const upcUniqueErrors = validateUniqueField(data, fieldKey === 'item_number' ? 'upc' : fieldKey);
|
||||||
|
|
||||||
|
// Combine the errors
|
||||||
|
itemNumberUniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
|
||||||
|
upcUniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Normal uniqueness validation for other fields
|
||||||
|
const uniqueErrors = validateUniqueField(data, fieldKey);
|
||||||
|
|
||||||
|
// Add unique errors to validation errors
|
||||||
|
uniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full validation - all fields for all rows
|
||||||
|
console.log('Running full validation for all fields and rows');
|
||||||
|
|
||||||
|
// Process each row for field-level validations
|
||||||
|
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||||
|
const row = data[rowIndex];
|
||||||
|
let rowErrors: Record<string, InfoWithSource> = {};
|
||||||
|
|
||||||
|
// Validate all fields for this row
|
||||||
|
fields.forEach(field => {
|
||||||
|
const fieldKey = String(field.key);
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
const errors = validateField(value, field as Field<T>);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
rowErrors[fieldKey] = {
|
||||||
|
message: errors[0].message,
|
||||||
|
level: errors[0].level as 'info' | 'warning' | 'error',
|
||||||
|
source: ErrorSources.Row,
|
||||||
|
type: errors[0].type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add row to validationErrors if it has any errors
|
||||||
|
if (Object.keys(rowErrors).length > 0) {
|
||||||
|
validationErrors.set(rowIndex, rowErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all unique fields
|
||||||
|
const uniqueErrors = validateAllUniqueFields(data);
|
||||||
|
|
||||||
|
// Merge in unique errors
|
||||||
|
uniqueErrors.forEach((errors, rowIdx) => {
|
||||||
|
if (!validationErrors.has(rowIdx)) {
|
||||||
|
validationErrors.set(rowIdx, {});
|
||||||
|
}
|
||||||
|
Object.assign(validationErrors.get(rowIdx)!, errors);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Uniqueness validation complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
validationErrors
|
||||||
|
};
|
||||||
|
}, [fields, validateField, validateUniqueField, validateAllUniqueFields]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateData,
|
||||||
|
validateField,
|
||||||
|
validateRow,
|
||||||
|
validateUniqueField,
|
||||||
|
clearValidationCacheForField,
|
||||||
|
clearAllUniquenessCaches
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||||
|
import { useRsi } from "../../../hooks/useRsi";
|
||||||
|
import { ErrorType } from "../../../types";
|
||||||
|
import { RowSelectionState } from "@tanstack/react-table";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import config from "@/config";
|
||||||
|
import { useValidation } from "./useValidation";
|
||||||
|
import { useRowOperations } from "./useRowOperations";
|
||||||
|
import { useTemplateManagement } from "./useTemplateManagement";
|
||||||
|
import { useFilterManagement } from "./useFilterManagement";
|
||||||
|
import { useUniqueItemNumbersValidation } from "./useUniqueItemNumbersValidation";
|
||||||
|
import { useUpcValidation } from "./useUpcValidation";
|
||||||
|
import { Props, RowData } from "./validationTypes";
|
||||||
|
|
||||||
|
export const useValidationState = <T extends string>({
|
||||||
|
initialData,
|
||||||
|
onBack,
|
||||||
|
onNext,
|
||||||
|
}: Props<T>) => {
|
||||||
|
const { fields, rowHook, tableHook } = useRsi<T>();
|
||||||
|
|
||||||
|
// Import validateField from useValidation
|
||||||
|
const { validateField: validateFieldFromHook } = useValidation<T>(
|
||||||
|
fields,
|
||||||
|
rowHook
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add ref to track template application state
|
||||||
|
const isApplyingTemplateRef = useRef(false);
|
||||||
|
|
||||||
|
// Core data state
|
||||||
|
const [data, setData] = useState<RowData<T>[]>(() => {
|
||||||
|
// Clean price fields in initial data before setting state
|
||||||
|
return initialData.map((row) => {
|
||||||
|
const updatedRow = { ...row } as Record<string, any>;
|
||||||
|
|
||||||
|
// Clean MSRP
|
||||||
|
if (typeof updatedRow.msrp === "string") {
|
||||||
|
updatedRow.msrp = updatedRow.msrp.replace(/[$,]/g, "");
|
||||||
|
const numValue = parseFloat(updatedRow.msrp);
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
updatedRow.msrp = numValue.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean cost_each
|
||||||
|
if (typeof updatedRow.cost_each === "string") {
|
||||||
|
updatedRow.cost_each = updatedRow.cost_each.replace(/[$,]/g, "");
|
||||||
|
const numValue = parseFloat(updatedRow.cost_each);
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
updatedRow.cost_each = numValue.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default tax category if not already set
|
||||||
|
if (
|
||||||
|
updatedRow.tax_cat === undefined ||
|
||||||
|
updatedRow.tax_cat === null ||
|
||||||
|
updatedRow.tax_cat === ""
|
||||||
|
) {
|
||||||
|
updatedRow.tax_cat = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default shipping restrictions if not already set
|
||||||
|
if (
|
||||||
|
updatedRow.ship_restrictions === undefined ||
|
||||||
|
updatedRow.ship_restrictions === null ||
|
||||||
|
updatedRow.ship_restrictions === ""
|
||||||
|
) {
|
||||||
|
updatedRow.ship_restrictions = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedRow as RowData<T>;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Row selection state
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
|
||||||
|
// Validation state
|
||||||
|
const [isValidating] = useState(false);
|
||||||
|
const [validationErrors, setValidationErrors] = useState<
|
||||||
|
Map<number, Record<string, any[]>>
|
||||||
|
>(new Map());
|
||||||
|
const [rowValidationStatus, setRowValidationStatus] = useState<
|
||||||
|
Map<number, "pending" | "validating" | "validated" | "error">
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
// Add state for tracking cells in loading state
|
||||||
|
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const initialValidationDoneRef = useRef(false);
|
||||||
|
const isValidatingRef = useRef(false);
|
||||||
|
|
||||||
|
// Use row operations hook
|
||||||
|
const { validateRow, updateRow, revalidateRows, copyDown } = useRowOperations<T>(
|
||||||
|
data,
|
||||||
|
fields,
|
||||||
|
setData,
|
||||||
|
setValidationErrors,
|
||||||
|
validateFieldFromHook
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use UPC validation hook - MUST be initialized before template management
|
||||||
|
const upcValidation = useUpcValidation(data, setData);
|
||||||
|
|
||||||
|
// Use unique item numbers validation hook
|
||||||
|
const { validateUniqueItemNumbers } = useUniqueItemNumbersValidation<T>(
|
||||||
|
data,
|
||||||
|
fields,
|
||||||
|
setValidationErrors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use template management hook
|
||||||
|
const templateManagement = useTemplateManagement<T>(
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
rowSelection,
|
||||||
|
setValidationErrors,
|
||||||
|
setRowValidationStatus,
|
||||||
|
validateRow,
|
||||||
|
isApplyingTemplateRef,
|
||||||
|
upcValidation,
|
||||||
|
setValidatingCells
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use filter management hook
|
||||||
|
const filterManagement = useFilterManagement<T>(data, fields, validationErrors);
|
||||||
|
|
||||||
|
// Run validation when data changes - FIXED to prevent recursive validation
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip initial load - we have a separate initialization process
|
||||||
|
if (!initialValidationDoneRef.current) return;
|
||||||
|
|
||||||
|
// Don't run validation during template application
|
||||||
|
if (isApplyingTemplateRef.current) return;
|
||||||
|
|
||||||
|
// CRITICAL FIX: Skip validation if we're already validating to prevent infinite loops
|
||||||
|
if (isValidatingRef.current) return;
|
||||||
|
|
||||||
|
console.log("Running validation on data change");
|
||||||
|
isValidatingRef.current = true;
|
||||||
|
|
||||||
|
// For faster validation, run synchronously instead of in an async function
|
||||||
|
const validateFields = () => {
|
||||||
|
try {
|
||||||
|
// Run regex validations on all rows
|
||||||
|
const regexFields = fields.filter((field) =>
|
||||||
|
field.validations?.some((v) => v.rule === "regex")
|
||||||
|
);
|
||||||
|
if (regexFields.length > 0) {
|
||||||
|
// Create a map to collect validation errors
|
||||||
|
const regexErrors = new Map<
|
||||||
|
number,
|
||||||
|
Record<string, any[]>
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Check each row for regex errors
|
||||||
|
data.forEach((row, rowIndex) => {
|
||||||
|
const rowErrors: Record<string, any[]> = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
// Check each regex field
|
||||||
|
regexFields.forEach((field) => {
|
||||||
|
const key = String(field.key);
|
||||||
|
const value = row[key as keyof typeof row];
|
||||||
|
|
||||||
|
// Skip empty values
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find regex validation
|
||||||
|
const regexValidation = field.validations?.find(
|
||||||
|
(v) => v.rule === "regex"
|
||||||
|
);
|
||||||
|
if (regexValidation) {
|
||||||
|
try {
|
||||||
|
// Check if value matches regex
|
||||||
|
const regex = new RegExp(
|
||||||
|
regexValidation.value,
|
||||||
|
regexValidation.flags
|
||||||
|
);
|
||||||
|
if (!regex.test(String(value))) {
|
||||||
|
// Add regex validation error
|
||||||
|
rowErrors[key] = [
|
||||||
|
{
|
||||||
|
message: regexValidation.errorMessage,
|
||||||
|
level: regexValidation.level || "error",
|
||||||
|
source: "row",
|
||||||
|
type: "regex",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid regex in validation:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add errors if any found
|
||||||
|
if (hasErrors) {
|
||||||
|
regexErrors.set(rowIndex, rowErrors);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update validation errors
|
||||||
|
if (regexErrors.size > 0) {
|
||||||
|
setValidationErrors((prev) => {
|
||||||
|
const newErrors = new Map(prev);
|
||||||
|
// Merge in regex errors
|
||||||
|
for (const [rowIndex, errors] of regexErrors.entries()) {
|
||||||
|
const existingErrors = newErrors.get(rowIndex) || {};
|
||||||
|
newErrors.set(rowIndex, { ...existingErrors, ...errors });
|
||||||
|
}
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run uniqueness validations immediately
|
||||||
|
validateUniqueItemNumbers();
|
||||||
|
} finally {
|
||||||
|
// Always ensure the ref is reset, even if an error occurs
|
||||||
|
setTimeout(() => {
|
||||||
|
isValidatingRef.current = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run validation immediately
|
||||||
|
validateFields();
|
||||||
|
}, [data, fields, validateUniqueItemNumbers]);
|
||||||
|
|
||||||
|
// Add field options query
|
||||||
|
const { data: fieldOptionsData } = useQuery({
|
||||||
|
queryKey: ["import-field-options"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`${config.apiUrl}/import/field-options`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch field options");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
|
||||||
|
gcTime: 30 * 60 * 1000, // Keep in cache for 30 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get display text for a template
|
||||||
|
const getTemplateDisplayText = useCallback(
|
||||||
|
(templateId: string | null) => {
|
||||||
|
if (!templateId) return "Select a template";
|
||||||
|
|
||||||
|
const template = templateManagement.templates.find((t) => t.id.toString() === templateId);
|
||||||
|
if (!template) return "Unknown template";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const companyId = template.company || "";
|
||||||
|
const productType = template.product_type || "Unknown Type";
|
||||||
|
|
||||||
|
// Find company name from field options
|
||||||
|
const companyName =
|
||||||
|
fieldOptionsData?.companies?.find(
|
||||||
|
(c: { value: string; label: string }) => c.value === companyId
|
||||||
|
)?.label || companyId;
|
||||||
|
|
||||||
|
return `${companyName} - ${productType}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error formatting template display text:",
|
||||||
|
error,
|
||||||
|
template
|
||||||
|
);
|
||||||
|
return "Error displaying template";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[templateManagement.templates, fieldOptionsData]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if there are any errors
|
||||||
|
const hasErrors = useMemo(() => {
|
||||||
|
for (const [_, status] of rowValidationStatus.entries()) {
|
||||||
|
if (status === "error") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [rowValidationStatus]);
|
||||||
|
|
||||||
|
// Create a function to handle button clicks (continue or back)
|
||||||
|
const handleButtonClick = useCallback(
|
||||||
|
async (direction: "next" | "back") => {
|
||||||
|
if (direction === "back" && onBack) {
|
||||||
|
// If a specific action is defined for back, use it
|
||||||
|
onBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === "next") {
|
||||||
|
// When proceeding to the next screen, check for unvalidated rows first
|
||||||
|
const hasErrors = [...validationErrors.entries()].some(
|
||||||
|
([_, errors]) => {
|
||||||
|
return Object.values(errors).some((errorSet) =>
|
||||||
|
errorSet.some((error) => error.type !== ErrorType.Required)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
// We have validation errors - ask the user to fix them first or continue anyway
|
||||||
|
const shouldContinue = window.confirm(
|
||||||
|
"There are validation errors in your data. Do you want to continue anyway?"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
// User chose to fix errors
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the data for the next step
|
||||||
|
try {
|
||||||
|
// No toast here - unnecessary and distracting
|
||||||
|
|
||||||
|
// Call onNext with the cleaned data
|
||||||
|
if (onNext) {
|
||||||
|
// Remove metadata fields before passing to onNext
|
||||||
|
const cleanedData = data.map((row) => {
|
||||||
|
const {
|
||||||
|
__index,
|
||||||
|
__template,
|
||||||
|
__original,
|
||||||
|
__corrected,
|
||||||
|
__changes,
|
||||||
|
...cleanRow
|
||||||
|
} = row;
|
||||||
|
return cleanRow as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
onNext(cleanedData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error proceeding to next step:", error);
|
||||||
|
toast.error("Error saving data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, onBack, onNext, validationErrors]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize validation on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialValidationDoneRef.current) return;
|
||||||
|
|
||||||
|
console.log("Running initial validation");
|
||||||
|
|
||||||
|
const runCompleteValidation = async () => {
|
||||||
|
if (!data || data.length === 0) return;
|
||||||
|
|
||||||
|
console.log("Running complete validation...");
|
||||||
|
|
||||||
|
// Get required fields
|
||||||
|
const requiredFields = fields.filter((field) =>
|
||||||
|
field.validations?.some((v) => v.rule === "required")
|
||||||
|
);
|
||||||
|
console.log(`Found ${requiredFields.length} required fields`);
|
||||||
|
|
||||||
|
// Get fields that have regex validation
|
||||||
|
const regexFields = fields.filter((field) =>
|
||||||
|
field.validations?.some((v) => v.rule === "regex")
|
||||||
|
);
|
||||||
|
console.log(`Found ${regexFields.length} fields with regex validation`);
|
||||||
|
|
||||||
|
// Get fields that need uniqueness validation
|
||||||
|
const uniqueFields = fields.filter((field) =>
|
||||||
|
field.validations?.some((v) => v.rule === "unique")
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Found ${uniqueFields.length} fields requiring uniqueness validation`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limit batch size to avoid UI freezing
|
||||||
|
const BATCH_SIZE = 100;
|
||||||
|
const totalRows = data.length;
|
||||||
|
|
||||||
|
// Initialize new data for any modifications
|
||||||
|
const newData = [...data];
|
||||||
|
|
||||||
|
// Create a temporary Map to collect all validation errors
|
||||||
|
const validationErrorsTemp = new Map<
|
||||||
|
number,
|
||||||
|
Record<string, any[]>
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Variables for batching
|
||||||
|
let currentBatch = 0;
|
||||||
|
const totalBatches = Math.ceil(totalRows / BATCH_SIZE);
|
||||||
|
|
||||||
|
const processBatch = async () => {
|
||||||
|
// Calculate batch range
|
||||||
|
const startIdx = currentBatch * BATCH_SIZE;
|
||||||
|
const endIdx = Math.min(startIdx + BATCH_SIZE, totalRows);
|
||||||
|
console.log(
|
||||||
|
`Processing batch ${
|
||||||
|
currentBatch + 1
|
||||||
|
}/${totalBatches} (rows ${startIdx} to ${endIdx - 1})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process rows in this batch
|
||||||
|
const batchPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (let rowIndex = startIdx; rowIndex < endIdx; rowIndex++) {
|
||||||
|
batchPromises.push(
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
const row = data[rowIndex];
|
||||||
|
|
||||||
|
// Skip if row is empty or undefined
|
||||||
|
if (!row) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store field errors for this row
|
||||||
|
const fieldErrors: Record<string, any[]> = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
// Check if price fields need formatting
|
||||||
|
const rowAsRecord = row as Record<string, any>;
|
||||||
|
let mSrpNeedsProcessing = false;
|
||||||
|
let costEachNeedsProcessing = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
rowAsRecord.msrp &&
|
||||||
|
typeof rowAsRecord.msrp === "string" &&
|
||||||
|
(rowAsRecord.msrp.includes("$") ||
|
||||||
|
rowAsRecord.msrp.includes(","))
|
||||||
|
) {
|
||||||
|
mSrpNeedsProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
rowAsRecord.cost_each &&
|
||||||
|
typeof rowAsRecord.cost_each === "string" &&
|
||||||
|
(rowAsRecord.cost_each.includes("$") ||
|
||||||
|
rowAsRecord.cost_each.includes(","))
|
||||||
|
) {
|
||||||
|
costEachNeedsProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process price fields if needed
|
||||||
|
if (mSrpNeedsProcessing || costEachNeedsProcessing) {
|
||||||
|
// Create a clean copy only if needed
|
||||||
|
const cleanedRow = { ...row } as Record<string, any>;
|
||||||
|
|
||||||
|
if (mSrpNeedsProcessing) {
|
||||||
|
const msrpValue = rowAsRecord.msrp.replace(/[$,]/g, "");
|
||||||
|
const numValue = parseFloat(msrpValue);
|
||||||
|
cleanedRow.msrp = !isNaN(numValue)
|
||||||
|
? numValue.toFixed(2)
|
||||||
|
: msrpValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (costEachNeedsProcessing) {
|
||||||
|
const costValue = rowAsRecord.cost_each.replace(/[$,]/g, "");
|
||||||
|
const numValue = parseFloat(costValue);
|
||||||
|
cleanedRow.cost_each = !isNaN(numValue)
|
||||||
|
? numValue.toFixed(2)
|
||||||
|
: costValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newData[rowIndex] = cleanedRow as RowData<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
const key = String(field.key);
|
||||||
|
const value = row[key as keyof typeof row];
|
||||||
|
|
||||||
|
// Skip non-required empty fields
|
||||||
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === "" ||
|
||||||
|
(Array.isArray(value) && value.length === 0) ||
|
||||||
|
(typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
Object.keys(value).length === 0)
|
||||||
|
) {
|
||||||
|
// Add error for empty required fields
|
||||||
|
fieldErrors[key] = [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
field.validations?.find((v) => v.rule === "required")
|
||||||
|
?.errorMessage || "This field is required",
|
||||||
|
level: "error",
|
||||||
|
source: "row",
|
||||||
|
type: "required",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate regex fields - even if they have data
|
||||||
|
for (const field of regexFields) {
|
||||||
|
const key = String(field.key);
|
||||||
|
const value = row[key as keyof typeof row];
|
||||||
|
|
||||||
|
// Skip empty values as they're handled by required validation
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find regex validation
|
||||||
|
const regexValidation = field.validations?.find(
|
||||||
|
(v) => v.rule === "regex"
|
||||||
|
);
|
||||||
|
if (regexValidation) {
|
||||||
|
try {
|
||||||
|
// Check if value matches regex
|
||||||
|
const regex = new RegExp(
|
||||||
|
regexValidation.value,
|
||||||
|
regexValidation.flags
|
||||||
|
);
|
||||||
|
if (!regex.test(String(value))) {
|
||||||
|
// Add regex validation error
|
||||||
|
fieldErrors[key] = [
|
||||||
|
{
|
||||||
|
message: regexValidation.errorMessage,
|
||||||
|
level: regexValidation.level || "error",
|
||||||
|
source: "row",
|
||||||
|
type: "regex",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid regex in validation:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update validation errors for this row
|
||||||
|
if (hasErrors) {
|
||||||
|
validationErrorsTemp.set(rowIndex, fieldErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all row validations to complete
|
||||||
|
await Promise.all(batchPromises);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processAllBatches = async () => {
|
||||||
|
for (let batch = 0; batch < totalBatches; batch++) {
|
||||||
|
currentBatch = batch;
|
||||||
|
await processBatch();
|
||||||
|
|
||||||
|
// Yield to UI thread periodically
|
||||||
|
if (batch % 2 === 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All batches complete
|
||||||
|
console.log("All initial validation batches complete");
|
||||||
|
|
||||||
|
// Apply collected validation errors all at once
|
||||||
|
setValidationErrors(validationErrorsTemp);
|
||||||
|
|
||||||
|
// Apply any data changes (like price formatting)
|
||||||
|
if (JSON.stringify(data) !== JSON.stringify(newData)) {
|
||||||
|
setData(newData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run uniqueness validation after the basic validation
|
||||||
|
validateUniqueItemNumbers();
|
||||||
|
|
||||||
|
// Mark that initial validation is done
|
||||||
|
initialValidationDoneRef.current = true;
|
||||||
|
|
||||||
|
console.log("Initial validation complete");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the validation process
|
||||||
|
processAllBatches();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the complete validation
|
||||||
|
runCompleteValidation();
|
||||||
|
}, [data, fields, setData, setValidationErrors, validateUniqueItemNumbers]);
|
||||||
|
|
||||||
|
// Update fields with latest options
|
||||||
|
const fieldsWithOptions = useMemo(() => {
|
||||||
|
if (!fieldOptionsData) return fields;
|
||||||
|
|
||||||
|
return fields.map((field) => {
|
||||||
|
// Skip fields that aren't select or multi-select
|
||||||
|
if (
|
||||||
|
typeof field.fieldType !== "object" ||
|
||||||
|
(field.fieldType.type !== "select" &&
|
||||||
|
field.fieldType.type !== "multi-select")
|
||||||
|
) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the correct options based on field key
|
||||||
|
let options = [];
|
||||||
|
switch (field.key) {
|
||||||
|
case "company":
|
||||||
|
options = [...(fieldOptionsData.companies || [])];
|
||||||
|
break;
|
||||||
|
case "supplier":
|
||||||
|
options = [...(fieldOptionsData.suppliers || [])];
|
||||||
|
break;
|
||||||
|
case "categories":
|
||||||
|
options = [...(fieldOptionsData.categories || [])];
|
||||||
|
break;
|
||||||
|
case "themes":
|
||||||
|
options = [...(fieldOptionsData.themes || [])];
|
||||||
|
break;
|
||||||
|
case "colors":
|
||||||
|
options = [...(fieldOptionsData.colors || [])];
|
||||||
|
break;
|
||||||
|
case "tax_cat":
|
||||||
|
options = [...(fieldOptionsData.taxCategories || [])];
|
||||||
|
// Ensure tax_cat is always a select, not multi-select
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
fieldType: {
|
||||||
|
type: "select",
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case "ship_restrictions":
|
||||||
|
options = [...(fieldOptionsData.shippingRestrictions || [])];
|
||||||
|
break;
|
||||||
|
case "artist":
|
||||||
|
options = [...(fieldOptionsData.artists || [])];
|
||||||
|
break;
|
||||||
|
case "size_cat":
|
||||||
|
options = [...(fieldOptionsData.sizes || [])];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
options = [...(field.fieldType.options || [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
fieldType: {
|
||||||
|
...field.fieldType,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [fields, fieldOptionsData]);
|
||||||
|
|
||||||
|
// Load templates on mount
|
||||||
|
useEffect(() => {
|
||||||
|
templateManagement.loadTemplates();
|
||||||
|
}, [templateManagement.loadTemplates]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
filteredData: filterManagement.filteredData,
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
isValidating,
|
||||||
|
validationErrors,
|
||||||
|
rowValidationStatus,
|
||||||
|
validateRow,
|
||||||
|
hasErrors,
|
||||||
|
|
||||||
|
// CRITICAL: Export validatingCells to make it available to ValidationContainer
|
||||||
|
validatingCells,
|
||||||
|
setValidatingCells,
|
||||||
|
|
||||||
|
// Row selection
|
||||||
|
rowSelection,
|
||||||
|
setRowSelection,
|
||||||
|
|
||||||
|
// Row manipulation
|
||||||
|
updateRow,
|
||||||
|
copyDown,
|
||||||
|
|
||||||
|
// Templates
|
||||||
|
templates: templateManagement.templates,
|
||||||
|
isLoadingTemplates: templateManagement.isLoadingTemplates,
|
||||||
|
selectedTemplateId: templateManagement.templateState.selectedTemplateId,
|
||||||
|
showSaveTemplateDialog: templateManagement.templateState.showSaveTemplateDialog,
|
||||||
|
newTemplateName: templateManagement.templateState.newTemplateName,
|
||||||
|
newTemplateType: templateManagement.templateState.newTemplateType,
|
||||||
|
setTemplateState: templateManagement.setTemplateState,
|
||||||
|
templateState: templateManagement.templateState,
|
||||||
|
loadTemplates: templateManagement.loadTemplates,
|
||||||
|
saveTemplate: templateManagement.saveTemplate,
|
||||||
|
applyTemplate: templateManagement.applyTemplate,
|
||||||
|
applyTemplateToSelected: templateManagement.applyTemplateToSelected,
|
||||||
|
getTemplateDisplayText,
|
||||||
|
refreshTemplates: templateManagement.refreshTemplates,
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
filters: filterManagement.filters,
|
||||||
|
filterFields: filterManagement.filterFields,
|
||||||
|
filterValues: filterManagement.filterValues,
|
||||||
|
updateFilters: filterManagement.updateFilters,
|
||||||
|
resetFilters: filterManagement.resetFilters,
|
||||||
|
|
||||||
|
// Fields reference
|
||||||
|
fields: fieldsWithOptions, // Return updated fields with options
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
rowHook,
|
||||||
|
tableHook,
|
||||||
|
|
||||||
|
// Button handling
|
||||||
|
handleButtonClick,
|
||||||
|
revalidateRows,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import type { Data } from "../../../types";
|
||||||
|
import { ErrorSources, ErrorType } from "../../../types";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
|
// Define the Props interface for ValidationStepNew
|
||||||
|
export interface Props<T extends string> {
|
||||||
|
initialData: RowData<T>[];
|
||||||
|
file?: File;
|
||||||
|
onBack?: () => void;
|
||||||
|
onNext?: (data: RowData<T>[]) => void;
|
||||||
|
isFromScratch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended Data type with meta information
|
||||||
|
export type RowData<T extends string> = Data<T> & {
|
||||||
|
__index?: string;
|
||||||
|
__template?: string;
|
||||||
|
__original?: Record<string, any>;
|
||||||
|
__corrected?: Record<string, any>;
|
||||||
|
__changes?: Record<string, boolean>;
|
||||||
|
upc?: string;
|
||||||
|
barcode?: string;
|
||||||
|
supplier?: string;
|
||||||
|
company?: string;
|
||||||
|
item_number?: string;
|
||||||
|
[key: string]: any; // Allow any string key for dynamic fields
|
||||||
|
};
|
||||||
|
|
||||||
|
// Template interface
|
||||||
|
export interface Template {
|
||||||
|
id: number;
|
||||||
|
company: string;
|
||||||
|
product_type: string;
|
||||||
|
[key: string]: string | number | boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props for the useValidationState hook
|
||||||
|
export interface ValidationStateProps<T extends string> extends Props<T> {}
|
||||||
|
|
||||||
|
// Interface for validation results
|
||||||
|
export interface ValidationResult {
|
||||||
|
error?: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
type?: ErrorType;
|
||||||
|
source?: ErrorSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter state interface
|
||||||
|
export interface FilterState {
|
||||||
|
searchText: string;
|
||||||
|
showErrorsOnly: boolean;
|
||||||
|
filterField: string | null;
|
||||||
|
filterValue: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI validation state interface for useUpcValidation
|
||||||
|
export interface ValidationState {
|
||||||
|
validatingCells: Set<string>; // Using rowIndex-fieldKey as identifier
|
||||||
|
itemNumbers: Map<number, string>; // Using rowIndex as key
|
||||||
|
validatingRows: Set<number>; // Rows currently being validated
|
||||||
|
activeValidations: Set<string>; // Active validations
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoWithSource interface for validation errors
|
||||||
|
export interface InfoWithSource {
|
||||||
|
message: string;
|
||||||
|
level: 'info' | 'warning' | 'error';
|
||||||
|
source: ErrorSources;
|
||||||
|
type: ErrorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template state interface
|
||||||
|
export interface TemplateState {
|
||||||
|
selectedTemplateId: string | null;
|
||||||
|
showSaveTemplateDialog: boolean;
|
||||||
|
newTemplateName: string;
|
||||||
|
newTemplateType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add config at the top of the file
|
||||||
|
// Import the config or access it through window
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
config?: {
|
||||||
|
apiUrl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a helper to get API URL consistently
|
||||||
|
export const getApiUrl = () => config.apiUrl;
|
||||||
|
|
||||||
|
// Shared utility function for checking empty values
|
||||||
|
export const isEmpty = (value: any): boolean =>
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === '' ||
|
||||||
|
(Array.isArray(value) && value.length === 0) ||
|
||||||
|
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import ValidationContainer from './components/ValidationContainer'
|
import ValidationContainer from './components/ValidationContainer'
|
||||||
import { Props } from './hooks/useValidationState'
|
import { Props } from './hooks/validationTypes'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ValidationStepNew component - modern implementation of the validation step
|
* ValidationStepNew component - modern implementation of the validation step
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user