Compare commits
17 Commits
cb46970808
...
add-produc
| Author | SHA1 | Date | |
|---|---|---|---|
| 387e7e5e73 | |||
| a51a48ce89 | |||
| aacb3a2fd0 | |||
| 35d2f0df7c | |||
| 7d46ebd6ba | |||
| 1496aa57b1 | |||
| fc9ef2f0d7 | |||
| af067f7360 | |||
| 949b543d1f | |||
| 8fdb68fb19 | |||
| 136f767309 | |||
| aa9664c459 | |||
| f60f0b1b5c | |||
| 676cd44d9d | |||
| 1d081bb218 | |||
| 52ae7e10aa | |||
| 153bbecc44 |
41
.VSCodeCounter/2025-03-17_16-24-17/details.md
Normal file
41
.VSCodeCounter/2025-03-17_16-24-17/details.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Details
|
||||||
|
|
||||||
|
Date : 2025-03-17 16:24:17
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 26 files, 6193 codes, 1008 comments, 1017 blanks, all 8218 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md) | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx) | TypeScript JSX | 230 | 10 | 8 | 248 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx) | TypeScript JSX | 18 | 0 | 3 | 21 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx) | TypeScript JSX | 273 | 19 | 37 | 329 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 374 | 42 | 44 | 460 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 730 | 126 | 106 | 962 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 499 | 48 | 54 | 601 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx) | TypeScript JSX | 112 | 12 | 21 | 145 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 232 | 31 | 32 | 295 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 407 | 56 | 52 | 515 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx) | TypeScript JSX | 193 | 23 | 22 | 238 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | 289 | 36 | 31 | 356 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx) | TypeScript JSX | 500 | 75 | 89 | 664 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 248 | 69 | 74 | 391 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx) | TypeScript JSX | 204 | 26 | 33 | 263 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 209 | 49 | 50 | 308 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 219 | 39 | 47 | 305 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 1,060 | 228 | 229 | 1,517 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx) | TypeScript JSX | 20 | 6 | 2 | 28 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts) | TypeScript | 4 | 0 | 1 | 5 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts) | TypeScript | 16 | 4 | 4 | 24 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts) | TypeScript | 124 | 4 | 14 | 142 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts) | TypeScript | 21 | 15 | 5 | 41 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts) | TypeScript | 43 | 24 | 7 | 74 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js) | JavaScript | 28 | 7 | 9 | 44 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts) | TypeScript | 101 | 59 | 24 | 184 |
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
20
.VSCodeCounter/2025-03-17_16-24-17/diff-details.md
Normal file
20
.VSCodeCounter/2025-03-17_16-24-17/diff-details.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Diff Details
|
||||||
|
|
||||||
|
Date : 2025-03-17 16:24:17
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 5 files, -358 codes, -15 comments, -33 blanks, all -406 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx) | TypeScript JSX | -83 | 0 | -4 | -87 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx) | TypeScript JSX | -193 | -4 | -15 | -212 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | -241 | -68 | -72 | -381 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx) | TypeScript JSX | -89 | -12 | -16 | -117 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 248 | 69 | 74 | 391 |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
23
.VSCodeCounter/2025-03-17_16-24-17/diff.md
Normal file
23
.VSCodeCounter/2025-03-17_16-24-17/diff.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Diff Summary
|
||||||
|
|
||||||
|
Date : 2025-03-17 16:24:17
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 5 files, -358 codes, -15 comments, -33 blanks, all -406 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| TypeScript JSX | 5 | -358 | -15 | -33 | -406 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 5 | -358 | -15 | -33 | -406 |
|
||||||
|
| components | 3 | -517 | -72 | -91 | -680 |
|
||||||
|
| hooks | 2 | 159 | 57 | 58 | 274 |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
31
.VSCodeCounter/2025-03-17_16-24-17/diff.txt
Normal file
31
.VSCodeCounter/2025-03-17_16-24-17/diff.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Date : 2025-03-17 16:24:17
|
||||||
|
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
Total : 5 files, -358 codes, -15 comments, -33 blanks, all -406 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| TypeScript JSX | 5 | -358 | -15 | -33 | -406 |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 5 | -358 | -15 | -33 | -406 |
|
||||||
|
| components | 3 | -517 | -72 | -91 | -680 |
|
||||||
|
| hooks | 2 | 159 | 57 | 58 | 274 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx | TypeScript JSX | -83 | 0 | -4 | -87 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx | TypeScript JSX | -193 | -4 | -15 | -212 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | -241 | -68 | -72 | -381 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx | TypeScript JSX | -89 | -12 | -16 | -117 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 248 | 69 | 74 | 391 |
|
||||||
|
| Total | | -358 | -15 | -33 | -406 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
1
.VSCodeCounter/2025-03-17_16-24-17/results.json
Normal file
1
.VSCodeCounter/2025-03-17_16-24-17/results.json
Normal file
File diff suppressed because one or more lines are too long
31
.VSCodeCounter/2025-03-17_16-24-17/results.md
Normal file
31
.VSCodeCounter/2025-03-17_16-24-17/results.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
Date : 2025-03-17 16:24:17
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 26 files, 6193 codes, 1008 comments, 1017 blanks, all 8218 lines
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| TypeScript JSX | 18 | 5,817 | 895 | 934 | 7,646 |
|
||||||
|
| TypeScript | 6 | 309 | 106 | 55 | 470 |
|
||||||
|
| Markdown | 1 | 39 | 0 | 19 | 58 |
|
||||||
|
| JavaScript | 1 | 28 | 7 | 9 | 44 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 26 | 6,193 | 1,008 | 1,017 | 8,218 |
|
||||||
|
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
||||||
|
| components | 11 | 3,357 | 403 | 410 | 4,170 |
|
||||||
|
| components (Files) | 6 | 2,124 | 245 | 252 | 2,621 |
|
||||||
|
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
|
||||||
|
| hooks | 6 | 2,440 | 486 | 522 | 3,448 |
|
||||||
|
| types | 1 | 16 | 4 | 4 | 24 |
|
||||||
|
| utils | 5 | 317 | 109 | 59 | 485 |
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
60
.VSCodeCounter/2025-03-17_16-24-17/results.txt
Normal file
60
.VSCodeCounter/2025-03-17_16-24-17/results.txt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
Date : 2025-03-17 16:24:17
|
||||||
|
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
Total : 26 files, 6193 codes, 1008 comments, 1017 blanks, all 8218 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| TypeScript JSX | 18 | 5,817 | 895 | 934 | 7,646 |
|
||||||
|
| TypeScript | 6 | 309 | 106 | 55 | 470 |
|
||||||
|
| Markdown | 1 | 39 | 0 | 19 | 58 |
|
||||||
|
| JavaScript | 1 | 28 | 7 | 9 | 44 |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 26 | 6,193 | 1,008 | 1,017 | 8,218 |
|
||||||
|
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
||||||
|
| components | 11 | 3,357 | 403 | 410 | 4,170 |
|
||||||
|
| components (Files) | 6 | 2,124 | 245 | 252 | 2,621 |
|
||||||
|
| components/cells | 5 | 1,233 | 158 | 158 | 1,549 |
|
||||||
|
| hooks | 6 | 2,440 | 486 | 522 | 3,448 |
|
||||||
|
| types | 1 | 16 | 4 | 4 | 24 |
|
||||||
|
| utils | 5 | 317 | 109 | 59 | 485 |
|
||||||
|
+------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx | TypeScript JSX | 230 | 10 | 8 | 248 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx | TypeScript JSX | 18 | 0 | 3 | 21 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx | TypeScript JSX | 273 | 19 | 37 | 329 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 374 | 42 | 44 | 460 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 730 | 126 | 106 | 962 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 499 | 48 | 54 | 601 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx | TypeScript JSX | 112 | 12 | 21 | 145 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 232 | 31 | 32 | 295 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 407 | 56 | 52 | 515 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx | TypeScript JSX | 193 | 23 | 22 | 238 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | 289 | 36 | 31 | 356 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx | TypeScript JSX | 500 | 75 | 89 | 664 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 248 | 69 | 74 | 391 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx | TypeScript JSX | 204 | 26 | 33 | 263 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 209 | 49 | 50 | 308 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 219 | 39 | 47 | 305 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 1,060 | 228 | 229 | 1,517 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx | TypeScript JSX | 20 | 6 | 2 | 28 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts | TypeScript | 4 | 0 | 1 | 5 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts | TypeScript | 16 | 4 | 4 | 24 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts | TypeScript | 124 | 4 | 14 | 142 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts | TypeScript | 21 | 15 | 5 | 41 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts | TypeScript | 43 | 24 | 7 | 74 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js | JavaScript | 28 | 7 | 9 | 44 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts | TypeScript | 101 | 59 | 24 | 184 |
|
||||||
|
| Total | | 6,193 | 1,008 | 1,017 | 8,218 |
|
||||||
|
+------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
42
.VSCodeCounter/2025-03-18_12-39-04/details.md
Normal file
42
.VSCodeCounter/2025-03-18_12-39-04/details.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Details
|
||||||
|
|
||||||
|
Date : 2025-03-18 12:39:04
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 27 files, 6925 codes, 1247 comments, 1248 blanks, all 9420 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md) | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx) | TypeScript JSX | 230 | 10 | 8 | 248 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx) | TypeScript JSX | 18 | 0 | 3 | 21 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx) | TypeScript JSX | 273 | 19 | 37 | 329 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx) | TypeScript JSX | 113 | 17 | 10 | 140 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 377 | 49 | 54 | 480 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 969 | 182 | 158 | 1,309 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 509 | 50 | 57 | 616 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx) | TypeScript JSX | 112 | 12 | 21 | 145 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 233 | 34 | 33 | 300 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 420 | 66 | 59 | 545 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx) | TypeScript JSX | 193 | 23 | 22 | 238 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | 227 | 36 | 32 | 295 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx) | TypeScript JSX | 500 | 75 | 89 | 664 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 264 | 75 | 81 | 420 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx) | TypeScript JSX | 204 | 26 | 33 | 263 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 337 | 88 | 92 | 517 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 360 | 78 | 85 | 523 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx) | TypeScript JSX | 20 | 6 | 2 | 28 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts) | TypeScript | 4 | 0 | 1 | 5 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts) | TypeScript | 16 | 4 | 4 | 24 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts) | TypeScript | 124 | 4 | 14 | 142 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts) | TypeScript | 21 | 15 | 5 | 41 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts) | TypeScript | 43 | 24 | 7 | 74 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js) | JavaScript | 28 | 7 | 9 | 44 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts) | TypeScript | 101 | 59 | 24 | 184 |
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
26
.VSCodeCounter/2025-03-18_12-39-04/diff-details.md
Normal file
26
.VSCodeCounter/2025-03-18_12-39-04/diff-details.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Diff Details
|
||||||
|
|
||||||
|
Date : 2025-03-18 12:39:04
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 11 files, 732 codes, 239 comments, 231 blanks, all 1202 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx) | TypeScript JSX | 113 | 17 | 10 | 140 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 3 | 7 | 10 | 20 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 239 | 56 | 52 | 347 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 10 | 2 | 3 | 15 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 1 | 3 | 1 | 5 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 13 | 10 | 7 | 30 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | -62 | 0 | 1 | -61 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 16 | 6 | 7 | 29 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 128 | 39 | 42 | 209 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 141 | 39 | 38 | 218 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 130 | 60 | 60 | 250 |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
25
.VSCodeCounter/2025-03-18_12-39-04/diff.md
Normal file
25
.VSCodeCounter/2025-03-18_12-39-04/diff.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Diff Summary
|
||||||
|
|
||||||
|
Date : 2025-03-18 12:39:04
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 11 files, 732 codes, 239 comments, 231 blanks, all 1202 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| TypeScript JSX | 11 | 732 | 239 | 231 | 1,202 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 11 | 732 | 239 | 231 | 1,202 |
|
||||||
|
| components | 7 | 317 | 95 | 84 | 496 |
|
||||||
|
| components (Files) | 4 | 365 | 82 | 75 | 522 |
|
||||||
|
| components/cells | 3 | -48 | 13 | 9 | -26 |
|
||||||
|
| hooks | 4 | 415 | 144 | 147 | 706 |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
39
.VSCodeCounter/2025-03-18_12-39-04/diff.txt
Normal file
39
.VSCodeCounter/2025-03-18_12-39-04/diff.txt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
Date : 2025-03-18 12:39:04
|
||||||
|
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
Total : 11 files, 732 codes, 239 comments, 231 blanks, all 1202 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| TypeScript JSX | 11 | 732 | 239 | 231 | 1,202 |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 11 | 732 | 239 | 231 | 1,202 |
|
||||||
|
| components | 7 | 317 | 95 | 84 | 496 |
|
||||||
|
| components (Files) | 4 | 365 | 82 | 75 | 522 |
|
||||||
|
| components/cells | 3 | -48 | 13 | 9 | -26 |
|
||||||
|
| hooks | 4 | 415 | 144 | 147 | 706 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx | TypeScript JSX | 113 | 17 | 10 | 140 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 3 | 7 | 10 | 20 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 239 | 56 | 52 | 347 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 10 | 2 | 3 | 15 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 1 | 3 | 1 | 5 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 13 | 10 | 7 | 30 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | -62 | 0 | 1 | -61 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 16 | 6 | 7 | 29 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 128 | 39 | 42 | 209 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 141 | 39 | 38 | 218 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 130 | 60 | 60 | 250 |
|
||||||
|
| Total | | 732 | 239 | 231 | 1,202 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
1
.VSCodeCounter/2025-03-18_12-39-04/results.json
Normal file
1
.VSCodeCounter/2025-03-18_12-39-04/results.json
Normal file
File diff suppressed because one or more lines are too long
31
.VSCodeCounter/2025-03-18_12-39-04/results.md
Normal file
31
.VSCodeCounter/2025-03-18_12-39-04/results.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
Date : 2025-03-18 12:39:04
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 27 files, 6925 codes, 1247 comments, 1248 blanks, all 9420 lines
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| TypeScript JSX | 19 | 6,549 | 1,134 | 1,165 | 8,848 |
|
||||||
|
| TypeScript | 6 | 309 | 106 | 55 | 470 |
|
||||||
|
| Markdown | 1 | 39 | 0 | 19 | 58 |
|
||||||
|
| JavaScript | 1 | 28 | 7 | 9 | 44 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 27 | 6,925 | 1,247 | 1,248 | 9,420 |
|
||||||
|
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
||||||
|
| components | 12 | 3,674 | 498 | 494 | 4,666 |
|
||||||
|
| components (Files) | 7 | 2,489 | 327 | 327 | 3,143 |
|
||||||
|
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
|
||||||
|
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
|
||||||
|
| types | 1 | 16 | 4 | 4 | 24 |
|
||||||
|
| utils | 5 | 317 | 109 | 59 | 485 |
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
61
.VSCodeCounter/2025-03-18_12-39-04/results.txt
Normal file
61
.VSCodeCounter/2025-03-18_12-39-04/results.txt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
Date : 2025-03-18 12:39:04
|
||||||
|
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
Total : 27 files, 6925 codes, 1247 comments, 1248 blanks, all 9420 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| TypeScript JSX | 19 | 6,549 | 1,134 | 1,165 | 8,848 |
|
||||||
|
| TypeScript | 6 | 309 | 106 | 55 | 470 |
|
||||||
|
| Markdown | 1 | 39 | 0 | 19 | 58 |
|
||||||
|
| JavaScript | 1 | 28 | 7 | 9 | 44 |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 27 | 6,925 | 1,247 | 1,248 | 9,420 |
|
||||||
|
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
||||||
|
| components | 12 | 3,674 | 498 | 494 | 4,666 |
|
||||||
|
| components (Files) | 7 | 2,489 | 327 | 327 | 3,143 |
|
||||||
|
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
|
||||||
|
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
|
||||||
|
| types | 1 | 16 | 4 | 4 | 24 |
|
||||||
|
| utils | 5 | 317 | 109 | 59 | 485 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx | TypeScript JSX | 230 | 10 | 8 | 248 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx | TypeScript JSX | 18 | 0 | 3 | 21 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx | TypeScript JSX | 273 | 19 | 37 | 329 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx | TypeScript JSX | 113 | 17 | 10 | 140 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 377 | 49 | 54 | 480 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 969 | 182 | 158 | 1,309 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 509 | 50 | 57 | 616 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx | TypeScript JSX | 112 | 12 | 21 | 145 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 233 | 34 | 33 | 300 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 420 | 66 | 59 | 545 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx | TypeScript JSX | 193 | 23 | 22 | 238 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | 227 | 36 | 32 | 295 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx | TypeScript JSX | 500 | 75 | 89 | 664 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 264 | 75 | 81 | 420 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx | TypeScript JSX | 204 | 26 | 33 | 263 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 337 | 88 | 92 | 517 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 360 | 78 | 85 | 523 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx | TypeScript JSX | 20 | 6 | 2 | 28 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts | TypeScript | 4 | 0 | 1 | 5 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts | TypeScript | 16 | 4 | 4 | 24 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts | TypeScript | 124 | 4 | 14 | 142 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts | TypeScript | 21 | 15 | 5 | 41 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts | TypeScript | 43 | 24 | 7 | 74 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js | JavaScript | 28 | 7 | 9 | 44 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts | TypeScript | 101 | 59 | 24 | 184 |
|
||||||
|
| Total | | 6,925 | 1,247 | 1,248 | 9,420 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
42
.VSCodeCounter/2025-03-18_13-49-23/details.md
Normal file
42
.VSCodeCounter/2025-03-18_13-49-23/details.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Details
|
||||||
|
|
||||||
|
Date : 2025-03-18 13:49:23
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 27 files, 6961 codes, 1254 comments, 1252 blanks, all 9467 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md) | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx) | TypeScript JSX | 230 | 10 | 8 | 248 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx) | TypeScript JSX | 18 | 0 | 3 | 21 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx) | TypeScript JSX | 273 | 19 | 37 | 329 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx) | TypeScript JSX | 113 | 17 | 10 | 140 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 395 | 51 | 55 | 501 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx) | TypeScript JSX | 969 | 182 | 158 | 1,309 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 527 | 55 | 60 | 642 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx) | TypeScript JSX | 112 | 12 | 21 | 145 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx) | TypeScript JSX | 233 | 34 | 33 | 300 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx) | TypeScript JSX | 420 | 66 | 59 | 545 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx) | TypeScript JSX | 193 | 23 | 22 | 238 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx) | TypeScript JSX | 227 | 36 | 32 | 295 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx) | TypeScript JSX | 500 | 75 | 89 | 664 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx) | TypeScript JSX | 264 | 75 | 81 | 420 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx) | TypeScript JSX | 204 | 26 | 33 | 263 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx) | TypeScript JSX | 337 | 88 | 92 | 517 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx) | TypeScript JSX | 360 | 78 | 85 | 523 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx) | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx) | TypeScript JSX | 20 | 6 | 2 | 28 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts) | TypeScript | 4 | 0 | 1 | 5 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts) | TypeScript | 16 | 4 | 4 | 24 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts) | TypeScript | 124 | 4 | 14 | 142 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts) | TypeScript | 21 | 15 | 5 | 41 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts) | TypeScript | 43 | 24 | 7 | 74 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js) | JavaScript | 28 | 7 | 9 | 44 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts) | TypeScript | 101 | 59 | 24 | 184 |
|
||||||
|
|
||||||
|
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
17
.VSCodeCounter/2025-03-18_13-49-23/diff-details.md
Normal file
17
.VSCodeCounter/2025-03-18_13-49-23/diff-details.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Diff Details
|
||||||
|
|
||||||
|
Date : 2025-03-18 13:49:23
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 2 files, 36 codes, 7 comments, 4 blanks, all 47 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx) | TypeScript JSX | 18 | 2 | 1 | 21 |
|
||||||
|
| [inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx](/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx) | TypeScript JSX | 18 | 5 | 3 | 26 |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||||
22
.VSCodeCounter/2025-03-18_13-49-23/diff.md
Normal file
22
.VSCodeCounter/2025-03-18_13-49-23/diff.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Diff Summary
|
||||||
|
|
||||||
|
Date : 2025-03-18 13:49:23
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 2 files, 36 codes, 7 comments, 4 blanks, all 47 lines
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| TypeScript JSX | 2 | 36 | 7 | 4 | 47 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 2 | 36 | 7 | 4 | 47 |
|
||||||
|
| components | 2 | 36 | 7 | 4 | 47 |
|
||||||
|
|
||||||
|
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||||
27
.VSCodeCounter/2025-03-18_13-49-23/diff.txt
Normal file
27
.VSCodeCounter/2025-03-18_13-49-23/diff.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Date : 2025-03-18 13:49:23
|
||||||
|
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
Total : 2 files, 36 codes, 7 comments, 4 blanks, all 47 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| TypeScript JSX | 2 | 36 | 7 | 4 | 47 |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 2 | 36 | 7 | 4 | 47 |
|
||||||
|
| components | 2 | 36 | 7 | 4 | 47 |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 18 | 2 | 1 | 21 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 18 | 5 | 3 | 26 |
|
||||||
|
| Total | | 36 | 7 | 4 | 47 |
|
||||||
|
+---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
1
.VSCodeCounter/2025-03-18_13-49-23/results.json
Normal file
1
.VSCodeCounter/2025-03-18_13-49-23/results.json
Normal file
File diff suppressed because one or more lines are too long
31
.VSCodeCounter/2025-03-18_13-49-23/results.md
Normal file
31
.VSCodeCounter/2025-03-18_13-49-23/results.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
Date : 2025-03-18 13:49:23
|
||||||
|
|
||||||
|
Directory /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
|
||||||
|
Total : 27 files, 6961 codes, 1254 comments, 1252 blanks, all 9467 lines
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| TypeScript JSX | 19 | 6,585 | 1,141 | 1,169 | 8,895 |
|
||||||
|
| TypeScript | 6 | 309 | 106 | 55 | 470 |
|
||||||
|
| Markdown | 1 | 39 | 0 | 19 | 58 |
|
||||||
|
| JavaScript | 1 | 28 | 7 | 9 | 44 |
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| . | 27 | 6,961 | 1,254 | 1,252 | 9,467 |
|
||||||
|
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
||||||
|
| components | 12 | 3,710 | 505 | 498 | 4,713 |
|
||||||
|
| components (Files) | 7 | 2,525 | 334 | 331 | 3,190 |
|
||||||
|
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
|
||||||
|
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
|
||||||
|
| types | 1 | 16 | 4 | 4 | 24 |
|
||||||
|
| utils | 5 | 317 | 109 | 59 | 485 |
|
||||||
|
|
||||||
|
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||||
61
.VSCodeCounter/2025-03-18_13-49-23/results.txt
Normal file
61
.VSCodeCounter/2025-03-18_13-49-23/results.txt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
Date : 2025-03-18 13:49:23
|
||||||
|
Directory : /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew
|
||||||
|
Total : 27 files, 6961 codes, 1254 comments, 1252 blanks, all 9467 lines
|
||||||
|
|
||||||
|
Languages
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| language | files | code | comment | blank | total |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
| TypeScript JSX | 19 | 6,585 | 1,141 | 1,169 | 8,895 |
|
||||||
|
| TypeScript | 6 | 309 | 106 | 55 | 470 |
|
||||||
|
| Markdown | 1 | 39 | 0 | 19 | 58 |
|
||||||
|
| JavaScript | 1 | 28 | 7 | 9 | 44 |
|
||||||
|
+----------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Directories
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| path | files | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
| . | 27 | 6,961 | 1,254 | 1,252 | 9,467 |
|
||||||
|
| . (Files) | 3 | 63 | 6 | 22 | 91 |
|
||||||
|
| components | 12 | 3,710 | 505 | 498 | 4,713 |
|
||||||
|
| components (Files) | 7 | 2,525 | 334 | 331 | 3,190 |
|
||||||
|
| components/cells | 5 | 1,185 | 171 | 167 | 1,523 |
|
||||||
|
| hooks | 6 | 2,855 | 630 | 669 | 4,154 |
|
||||||
|
| types | 1 | 16 | 4 | 4 | 24 |
|
||||||
|
| utils | 5 | 317 | 109 | 59 | 485 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||||
|
|
||||||
|
Files
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| filename | language | code | comment | blank | total |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md | Markdown | 39 | 0 | 19 | 58 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx | TypeScript JSX | 230 | 10 | 8 | 248 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx | TypeScript JSX | 18 | 0 | 3 | 21 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx | TypeScript JSX | 273 | 19 | 37 | 329 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx | TypeScript JSX | 113 | 17 | 10 | 140 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx | TypeScript JSX | 395 | 51 | 55 | 501 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx | TypeScript JSX | 969 | 182 | 158 | 1,309 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx | TypeScript JSX | 527 | 55 | 60 | 642 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx | TypeScript JSX | 112 | 12 | 21 | 145 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx | TypeScript JSX | 233 | 34 | 33 | 300 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx | TypeScript JSX | 420 | 66 | 59 | 545 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx | TypeScript JSX | 193 | 23 | 22 | 238 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx | TypeScript JSX | 227 | 36 | 32 | 295 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx | TypeScript JSX | 500 | 75 | 89 | 664 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx | TypeScript JSX | 264 | 75 | 81 | 420 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx | TypeScript JSX | 204 | 26 | 33 | 263 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx | TypeScript JSX | 337 | 88 | 92 | 517 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx | TypeScript JSX | 360 | 78 | 85 | 523 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx | TypeScript JSX | 1,190 | 288 | 289 | 1,767 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx | TypeScript JSX | 20 | 6 | 2 | 28 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts | TypeScript | 4 | 0 | 1 | 5 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts | TypeScript | 16 | 4 | 4 | 24 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts | TypeScript | 124 | 4 | 14 | 142 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts | TypeScript | 21 | 15 | 5 | 41 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts | TypeScript | 43 | 24 | 7 | 74 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js | JavaScript | 28 | 7 | 9 | 44 |
|
||||||
|
| /Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts | TypeScript | 101 | 59 | 24 | 184 |
|
||||||
|
| Total | | 6,961 | 1,254 | 1,252 | 9,467 |
|
||||||
|
+-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+
|
||||||
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.
|
||||||
354
docs/validation-process-issues.md
Normal file
354
docs/validation-process-issues.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
## 1. ✅ Error Filtering Logic Inconsistency (RESOLVED)
|
||||||
|
|
||||||
|
> **Note: This issue has been resolved by implementing a type-based error system.**
|
||||||
|
|
||||||
|
The filtering logic in `ValidationCell.tsx` previously relied on string matching, which was fragile:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Old implementation (string-based matching)
|
||||||
|
const filteredErrors = React.useMemo(() => {
|
||||||
|
return !isEmpty(value)
|
||||||
|
? errors.filter(error => !error.message?.toLowerCase().includes('required'))
|
||||||
|
: errors;
|
||||||
|
}, [value, errors]);
|
||||||
|
|
||||||
|
// New implementation (type-based filtering)
|
||||||
|
const filteredErrors = React.useMemo(() => {
|
||||||
|
return !isEmpty(value)
|
||||||
|
? errors.filter(error => error.type !== ErrorType.Required)
|
||||||
|
: errors;
|
||||||
|
}, [value, errors]);
|
||||||
|
```
|
||||||
|
|
||||||
|
The solution implemented:
|
||||||
|
- Added an `ErrorType` enum in `types.ts` to standardize error categorization
|
||||||
|
- Updated all error creation to include the appropriate error type
|
||||||
|
- Modified error filtering to use the type property instead of string matching
|
||||||
|
- Ensured consistent error handling across the application
|
||||||
|
|
||||||
|
**Guidelines for future development:**
|
||||||
|
- Always use the `ErrorType` enum when creating errors
|
||||||
|
- Never rely on string matching for error filtering
|
||||||
|
- Ensure all error objects include the `type` property
|
||||||
|
- Use the appropriate error type for each validation rule:
|
||||||
|
- `ErrorType.Required` for required field validations
|
||||||
|
- `ErrorType.Regex` for regex validations
|
||||||
|
- `ErrorType.Unique` for uniqueness validations
|
||||||
|
- `ErrorType.Custom` for custom validations
|
||||||
|
- `ErrorType.Api` for API-based validations
|
||||||
|
|
||||||
|
## 2. ⚠️ Redundant Error Processing (PARTIALLY RESOLVED)
|
||||||
|
|
||||||
|
> **Note: This issue has been partially resolved by the re-rendering optimizations.**
|
||||||
|
|
||||||
|
The system still processes errors in multiple places:
|
||||||
|
- In `ValidationCell.tsx`, errors are filtered by the optimized `processErrors` function
|
||||||
|
- In `useValidation.tsx`, errors are generated at the field level
|
||||||
|
- In `ValidationContainer.tsx`, errors are manipulated at the container level
|
||||||
|
|
||||||
|
While the error processing has been optimized to be more efficient, there is still some redundancy in how errors are handled across components. However, the current implementation has mitigated the performance impact.
|
||||||
|
|
||||||
|
**Improvements made:**
|
||||||
|
- Created a central `processErrors` function in ValidationCell that efficiently handles error filtering
|
||||||
|
- Implemented a batched update system to reduce redundant error processing
|
||||||
|
- Added better memoization to avoid reprocessing errors when not needed
|
||||||
|
|
||||||
|
**Future improvement opportunities:**
|
||||||
|
- Further consolidate error processing logic into a single location
|
||||||
|
- Create a dedicated error handling service or hook
|
||||||
|
- Implement a more declarative approach to error handling
|
||||||
|
|
||||||
|
## 3. Race Conditions in Async Validation
|
||||||
|
|
||||||
|
async validations could create race conditions:
|
||||||
|
- If a user types quickly, multiple validation requests might be in flight
|
||||||
|
- Later responses could overwrite more recent ones if they complete out of order
|
||||||
|
- The debouncing helps but doesn't fully solve this issue
|
||||||
|
|
||||||
|
## 4. Memory Leaks in Timeout Management
|
||||||
|
|
||||||
|
The validation timeouts are stored in refs:
|
||||||
|
```typescript
|
||||||
|
const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({});
|
||||||
|
```
|
||||||
|
|
||||||
|
While there is cleanup on unmount, if rows are added/removed dynamically, timeouts for deleted rows might not be properly cleared.
|
||||||
|
|
||||||
|
## 5. ✅ Inefficient Error Storage (RESOLVED)
|
||||||
|
|
||||||
|
**Status: RESOLVED**
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Previously, validation errors were stored in multiple locations:
|
||||||
|
- In the `validationErrors` Map in `useValidationState`
|
||||||
|
- In the row data itself as `__errors`
|
||||||
|
|
||||||
|
This redundancy caused several issues:
|
||||||
|
- Inconsistent error states between the two storage locations
|
||||||
|
- Increased memory usage by storing the same information twice
|
||||||
|
- Complex state management to keep both sources in sync
|
||||||
|
- Difficulty reasoning about where errors should be accessed from
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
We've implemented a unified error storage approach by:
|
||||||
|
- Making the `validationErrors` Map in `useValidationState` the single source of truth for all validation errors
|
||||||
|
- Removed the `__errors` property from row data
|
||||||
|
- Updated all validation functions to interact with the central error store instead of modifying row data
|
||||||
|
- Modified UPC validation to use the central error store
|
||||||
|
- Updated all components to read errors from the `validationErrors` Map instead of row data
|
||||||
|
|
||||||
|
### Key Changes
|
||||||
|
|
||||||
|
1. Modified `dataMutations.ts` to stop storing errors in row data
|
||||||
|
2. Updated the `Meta` type to remove the `__errors` property
|
||||||
|
3. Modified the `RowData` type to remove the `__errors` property
|
||||||
|
4. Updated the `useValidation` hook to return errors separately from row data
|
||||||
|
5. Modified the `useAiValidation` hook to work with the central error store
|
||||||
|
6. Updated the `useFilters` hook to check for errors in the `validationErrors` Map
|
||||||
|
7. Modified the `ValidationTable` and `ValidationCell` components to read errors from the `validationErrors` Map
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
- **Single Source of Truth**: All validation errors are now stored in one place
|
||||||
|
- **Reduced Memory Usage**: No duplicate storage of error information
|
||||||
|
- **Simplified State Management**: Only one state to update when errors change
|
||||||
|
- **Cleaner Data Structure**: Row data no longer contains validation metadata
|
||||||
|
- **More Maintainable Code**: Clearer separation of concerns between data and validation
|
||||||
|
|
||||||
|
### Future Improvements
|
||||||
|
|
||||||
|
While this refactoring addresses the core issue of inefficient error storage, there are still opportunities for further optimization:
|
||||||
|
|
||||||
|
1. ✅ **Redundant Error Processing**: ~~The validation process still performs some redundant calculations that could be optimized.~~ This has been largely addressed by the re-rendering optimizations.
|
||||||
|
2. **Race Conditions**: Async validation can lead to race conditions when multiple validations are triggered in quick succession.
|
||||||
|
3. **Memory Leaks**: The timeout management for validation could be improved to prevent potential memory leaks.
|
||||||
|
4. **Tight Coupling**: Components are still tightly coupled to the validation state structure.
|
||||||
|
5. **Error Prioritization**: The system doesn't prioritize errors well, showing all errors at once rather than focusing on the most critical ones first.
|
||||||
|
|
||||||
|
### Validation Flow
|
||||||
|
|
||||||
|
The validation process now works as follows:
|
||||||
|
|
||||||
|
1. **Error Generation**:
|
||||||
|
- Field-level validations generate errors based on validation rules
|
||||||
|
- Row-level hooks add custom validation errors
|
||||||
|
- Table-level validations (like uniqueness checks) add errors across rows
|
||||||
|
|
||||||
|
2. **Error Storage**:
|
||||||
|
- All errors are stored in the `validationErrors` Map in `useValidationState`
|
||||||
|
- The Map uses row indices as keys and objects of field errors as values
|
||||||
|
|
||||||
|
3. **Error Display**:
|
||||||
|
- The `ValidationTable` component checks the `validationErrors` Map to highlight rows with errors
|
||||||
|
- The `ValidationCell` component receives errors for specific fields from the `validationErrors` Map
|
||||||
|
- Errors are filtered in the UI to avoid showing "required" errors for fields with values
|
||||||
|
|
||||||
|
This focused refactoring approach has successfully addressed a critical issue while keeping changes manageable and targeted.
|
||||||
|
|
||||||
|
## 6. ✅ Excessive Re-rendering (RESOLVED)
|
||||||
|
|
||||||
|
**Status: RESOLVED**
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
The validation system was suffering from excessive re-renders due to several key issues:
|
||||||
|
|
||||||
|
- **Inefficient Error Filtering**: The ValidationCell component was filtering errors on every render
|
||||||
|
- **Redundant Error Processing**: The same validation work was repeated in multiple components
|
||||||
|
- **Poor Memoization**: Components were inadequately memoized, causing unnecessary re-renders
|
||||||
|
- **Inefficient Batch Updates**: The state update system wasn't optimally batching changes
|
||||||
|
|
||||||
|
These issues led to performance problems, especially with large datasets, and affected the user experience.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
We've implemented a comprehensive optimization approach:
|
||||||
|
|
||||||
|
- **Optimized Error Processing**: Created an efficient `processErrors` function in ValidationCell that calculates all derived state in one pass
|
||||||
|
- **Enhanced Memoization**: Improved memo comparison functions to avoid unnecessary rerenders
|
||||||
|
- **Improved Batch Updates**: Redesigned the batching system to aggregate multiple changes before state updates
|
||||||
|
- **Single Update Pattern**: Implemented a queue-based update mechanism that applies multiple state changes at once
|
||||||
|
|
||||||
|
### Key Changes
|
||||||
|
|
||||||
|
1. Added a more efficient error processing function in ValidationCell
|
||||||
|
2. Created an enhanced error comparison function to properly compare error arrays
|
||||||
|
3. Improved the memo comparison function in ValidationCell
|
||||||
|
4. Added a batch update system in useValidationState
|
||||||
|
5. Implemented a queue-based update mechanism for row modifications
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
- **Improved Performance**: Reduced render cycles = faster UI response
|
||||||
|
- **Better User Experience**: Less lag when editing large datasets
|
||||||
|
- **Reduced Memory Usage**: Fewer component instantiations and temporary objects
|
||||||
|
- **Increased Scalability**: The application can now handle larger datasets without slowdown
|
||||||
|
- **Maintainable Code**: More predictable update flow that's easier to debug and extend
|
||||||
|
|
||||||
|
### Guidelines for future development
|
||||||
|
|
||||||
|
- Use the `processErrors` function for error filtering and processing
|
||||||
|
- Ensure React.memo components have proper comparison functions
|
||||||
|
- Use the batched update system for state changes
|
||||||
|
- Maintain stable references to objects and functions
|
||||||
|
- Use appropriate React hooks (useMemo, useCallback) with correct dependencies
|
||||||
|
- Avoid unnecessary recreations of arrays, objects, and functions
|
||||||
|
|
||||||
|
## 7. Complex Error Merging Logic
|
||||||
|
|
||||||
|
When merging errors from different sources, the logic is complex and potentially error-prone:
|
||||||
|
```typescript
|
||||||
|
// Merge field errors and row hook errors
|
||||||
|
const mergedErrors: Record<string, InfoWithSource> = {}
|
||||||
|
|
||||||
|
// Convert field errors to InfoWithSource
|
||||||
|
Object.entries(fieldErrors).forEach(([key, errors]) => {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
mergedErrors[key] = {
|
||||||
|
message: errors[0].message,
|
||||||
|
level: errors[0].level,
|
||||||
|
source: ErrorSources.Row,
|
||||||
|
type: errors[0].type || ErrorType.Custom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This only takes the first error for each field, potentially hiding important validation issues.
|
||||||
|
|
||||||
|
## 8. ✅ Inconsistent Error Handling for Empty Values (PARTIALLY RESOLVED)
|
||||||
|
|
||||||
|
> **Note: This issue has been partially resolved by standardizing the isEmpty function and error type system.**
|
||||||
|
|
||||||
|
The system previously had different approaches to handling empty values:
|
||||||
|
- Some validations skipped empty values unless they're required
|
||||||
|
- Others processed empty values differently
|
||||||
|
- The `isEmpty` function was defined multiple times with slight variations
|
||||||
|
|
||||||
|
The solution implemented:
|
||||||
|
- Standardized the `isEmpty` function implementation
|
||||||
|
- Ensured consistent error type usage for required field validations
|
||||||
|
- Made error filtering consistent across the application
|
||||||
|
|
||||||
|
**Guidelines for future development:**
|
||||||
|
- Always use the shared `isEmpty` function for checking empty values
|
||||||
|
- Ensure consistent handling of empty values across all validation rules
|
||||||
|
- Use the `ErrorType.Required` type for all required field validations
|
||||||
|
|
||||||
|
## 9. Tight Coupling Between Components
|
||||||
|
|
||||||
|
The validation system is tightly coupled across components:
|
||||||
|
- `ValidationCell` needs to understand the structure of errors
|
||||||
|
- `ValidationTable` needs to extract and pass the right errors
|
||||||
|
- `ValidationContainer` directly manipulates the error structure
|
||||||
|
|
||||||
|
This makes it harder to refactor or reuse components independently.
|
||||||
|
|
||||||
|
## 10. Limited Error Prioritization
|
||||||
|
|
||||||
|
There's no clear prioritization of errors:
|
||||||
|
- When multiple errors exist for a field, which one should be shown first?
|
||||||
|
- Are some errors more important than others?
|
||||||
|
- The current system mostly shows the first error it finds
|
||||||
|
|
||||||
|
A more robust approach would be to have a consistent error source identification system and a clear prioritization strategy for displaying errors.
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
Let me explain how these hooks fit together to create the validation errors that eventually get filtered in the `ValidationCell` component:
|
||||||
|
|
||||||
|
## The Validation Flow
|
||||||
|
|
||||||
|
1. **useValidationState Hook**:
|
||||||
|
This is the main state management hook used by the `ValidationContainer` component. It:
|
||||||
|
- Manages the core data state (`data`)
|
||||||
|
- Tracks validation errors in a Map (`validationErrors`)
|
||||||
|
- Provides functions to update and validate rows
|
||||||
|
|
||||||
|
2. **useValidation Hook**:
|
||||||
|
This is a utility hook that provides the core validation logic:
|
||||||
|
- `validateField`: Validates a single field against its validation rules
|
||||||
|
- `validateRow`: Validates an entire row, field by field
|
||||||
|
- `validateTable`: Runs table-level validations
|
||||||
|
- `validateUnique`: Checks for uniqueness constraints
|
||||||
|
- `validateData`: Orchestrates the complete validation process
|
||||||
|
|
||||||
|
## How Errors Are Generated
|
||||||
|
|
||||||
|
Validation errors come from multiple sources:
|
||||||
|
|
||||||
|
1. **Field-Level Validations**:
|
||||||
|
In `useValidation.tsx`, the `validateField` function checks individual fields against rules like:
|
||||||
|
- `required`: Field must have a value
|
||||||
|
- `regex`: Value must match a pattern
|
||||||
|
- `min`/`max`: Numeric constraints
|
||||||
|
|
||||||
|
2. **Row-Level Validations**:
|
||||||
|
The `validateRow` function in `useValidation.tsx` runs:
|
||||||
|
- Field validations for each field in the row
|
||||||
|
- Special validations for required fields like supplier and company
|
||||||
|
- Custom row hooks provided by the application
|
||||||
|
|
||||||
|
3. **Table-Level Validations**:
|
||||||
|
- `validateUnique` checks for duplicate values in fields marked as unique
|
||||||
|
- `validateTable` runs custom table hooks for cross-row validations
|
||||||
|
|
||||||
|
4. **API-Based Validations**:
|
||||||
|
In `useValidationState.tsx` and `ValidationContainer.tsx`:
|
||||||
|
- UPC validation via API calls
|
||||||
|
- Item number uniqueness checks
|
||||||
|
|
||||||
|
## The Error Flow
|
||||||
|
|
||||||
|
1. Errors are collected in the `validationErrors` Map in `useValidationState`
|
||||||
|
2. This Map is passed to `ValidationTable` as a prop
|
||||||
|
3. `ValidationTable` extracts the relevant errors for each cell and passes them to `ValidationCell`
|
||||||
|
4. In `ValidationCell`, the errors are filtered based on whether the cell has a value:
|
||||||
|
```typescript
|
||||||
|
// Updated implementation using type-based filtering
|
||||||
|
const filteredErrors = React.useMemo(() => {
|
||||||
|
return !isEmpty(value)
|
||||||
|
? errors.filter(error => error.type !== ErrorType.Required)
|
||||||
|
: errors;
|
||||||
|
}, [value, errors]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Insights
|
||||||
|
|
||||||
|
1. **Error Structure**:
|
||||||
|
Errors now have a consistent structure with type information:
|
||||||
|
```typescript
|
||||||
|
type ErrorObject = {
|
||||||
|
message: string;
|
||||||
|
level: string; // 'error', 'warning', etc.
|
||||||
|
source?: ErrorSources; // Where the error came from
|
||||||
|
type: ErrorType; // The type of error (Required, Regex, Unique, etc.)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Error Sources**:
|
||||||
|
Errors can come from:
|
||||||
|
- Field validations (required, regex, etc.)
|
||||||
|
- Row validations (custom business logic)
|
||||||
|
- Table validations (uniqueness checks)
|
||||||
|
- API validations (UPC checks)
|
||||||
|
|
||||||
|
3. **Error Types**:
|
||||||
|
Errors are now categorized by type:
|
||||||
|
- `ErrorType.Required`: Field is required but empty
|
||||||
|
- `ErrorType.Regex`: Value doesn't match the regex pattern
|
||||||
|
- `ErrorType.Unique`: Value must be unique across rows
|
||||||
|
- `ErrorType.Custom`: Custom validation errors
|
||||||
|
- `ErrorType.Api`: Errors from API calls
|
||||||
|
|
||||||
|
4. **Error Filtering**:
|
||||||
|
The filtering in `ValidationCell` is now more robust:
|
||||||
|
- When a field has a value, errors of type `ErrorType.Required` are filtered out
|
||||||
|
- When a field is empty, all errors are shown
|
||||||
|
|
||||||
|
5. **Performance Optimizations**:
|
||||||
|
- Batch processing of validations
|
||||||
|
- Debounced updates to avoid excessive re-renders
|
||||||
|
- Memoization of computed values
|
||||||
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[];
|
||||||
|
}
|
||||||
@@ -1188,7 +1188,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
})) as unknown as Fields<T>
|
})) as unknown as Fields<T>
|
||||||
|
|
||||||
const unmatched = findUnmatchedRequiredFields(typedFields, columns);
|
const unmatched = findUnmatchedRequiredFields(typedFields, columns);
|
||||||
console.log("Unmatched required fields:", unmatched);
|
|
||||||
return unmatched;
|
return unmatched;
|
||||||
}, [fields, columns])
|
}, [fields, columns])
|
||||||
|
|
||||||
@@ -1200,7 +1199,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
|
|||||||
// Type assertion to handle the DeepReadonly<T> vs string type mismatch
|
// Type assertion to handle the DeepReadonly<T> vs string type mismatch
|
||||||
return !unmatchedRequiredFields.includes(key as any);
|
return !unmatchedRequiredFields.includes(key as any);
|
||||||
});
|
});
|
||||||
console.log("Matched required fields:", matched);
|
|
||||||
return matched;
|
return matched;
|
||||||
}, [requiredFields, unmatchedRequiredFields]);
|
}, [requiredFields, unmatchedRequiredFields]);
|
||||||
|
|
||||||
@@ -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()}
|
||||||
@@ -185,12 +185,10 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
|
|
||||||
// Apply global selections to each row of data if they exist
|
// Apply global selections to each row of data if they exist
|
||||||
const dataWithGlobalSelections = globalSelections
|
const dataWithGlobalSelections = globalSelections
|
||||||
? dataWithMeta.map((row: Data<string> & { __errors?: any; __index?: string }) => {
|
? dataWithMeta.map((row: Data<string> & { __index?: string }) => {
|
||||||
const newRow = { ...row };
|
const newRow = { ...row };
|
||||||
if (globalSelections.supplier) newRow.supplier = globalSelections.supplier;
|
if (globalSelections.supplier) newRow.supplier = globalSelections.supplier;
|
||||||
if (globalSelections.company) newRow.company = globalSelections.company;
|
if (globalSelections.company) newRow.company = globalSelections.company;
|
||||||
if (globalSelections.line) newRow.line = globalSelections.line;
|
|
||||||
if (globalSelections.subline) newRow.subline = globalSelections.subline;
|
|
||||||
return newRow;
|
return newRow;
|
||||||
})
|
})
|
||||||
: dataWithMeta;
|
: dataWithMeta;
|
||||||
@@ -225,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>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import ValidationTable from './ValidationTable'
|
||||||
|
import { RowSelectionState } from '@tanstack/react-table'
|
||||||
|
import { Fields } from '../../../types'
|
||||||
|
import { Template } from '../hooks/validationTypes'
|
||||||
|
|
||||||
|
interface UpcValidationTableAdapterProps<T extends string> {
|
||||||
|
data: any[]
|
||||||
|
fields: Fields<string>
|
||||||
|
validationErrors: Map<number, Record<string, any[]>>
|
||||||
|
rowSelection: RowSelectionState
|
||||||
|
setRowSelection: React.Dispatch<React.SetStateAction<RowSelectionState>>
|
||||||
|
updateRow: (rowIndex: number, key: T, value: any) => void
|
||||||
|
filters: any
|
||||||
|
templates: Template[]
|
||||||
|
applyTemplate: (templateId: string, rowIndexes: number[]) => void
|
||||||
|
getTemplateDisplayText: (templateId: string | null) => string
|
||||||
|
isValidatingUpc: (rowIndex: number) => boolean
|
||||||
|
validatingUpcRows: number[]
|
||||||
|
copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void
|
||||||
|
validatingCells: Set<string>
|
||||||
|
isLoadingTemplates: boolean
|
||||||
|
rowProductLines: Record<string, any[]>
|
||||||
|
rowSublines: Record<string, any[]>
|
||||||
|
isLoadingLines: Record<string, boolean>
|
||||||
|
isLoadingSublines: Record<string, boolean>
|
||||||
|
upcValidation: {
|
||||||
|
validatingRows: Set<number>
|
||||||
|
getItemNumber: (rowIndex: number) => string | undefined
|
||||||
|
}
|
||||||
|
itemNumbers?: Map<number, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpcValidationTableAdapter component - connects UPC validation data to ValidationTable
|
||||||
|
*
|
||||||
|
* This component adapts UPC validation data and functionality to work with the core ValidationTable,
|
||||||
|
* transforming item numbers and validation states into a format the table component can render.
|
||||||
|
*/
|
||||||
|
function UpcValidationTableAdapter<T extends string>({
|
||||||
|
data,
|
||||||
|
fields,
|
||||||
|
validationErrors,
|
||||||
|
rowSelection,
|
||||||
|
setRowSelection,
|
||||||
|
updateRow,
|
||||||
|
filters,
|
||||||
|
templates,
|
||||||
|
applyTemplate,
|
||||||
|
getTemplateDisplayText,
|
||||||
|
isValidatingUpc,
|
||||||
|
validatingUpcRows,
|
||||||
|
copyDown,
|
||||||
|
validatingCells: externalValidatingCells,
|
||||||
|
isLoadingTemplates,
|
||||||
|
rowProductLines,
|
||||||
|
rowSublines,
|
||||||
|
isLoadingLines,
|
||||||
|
isLoadingSublines,
|
||||||
|
upcValidation,
|
||||||
|
itemNumbers
|
||||||
|
}: UpcValidationTableAdapterProps<T>) {
|
||||||
|
// Prepare the validation table with UPC data
|
||||||
|
|
||||||
|
// Create combined validatingCells set from validating rows and external cells
|
||||||
|
const combinedValidatingCells = useMemo(() => {
|
||||||
|
const combined = new Set<string>();
|
||||||
|
|
||||||
|
// Add UPC validation cells
|
||||||
|
upcValidation.validatingRows.forEach(rowIndex => {
|
||||||
|
// Only mark the item_number cells as validating, NOT the UPC or supplier
|
||||||
|
combined.add(`${rowIndex}-item_number`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any other validating cells from state
|
||||||
|
externalValidatingCells.forEach(cellKey => {
|
||||||
|
combined.add(cellKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
return combined;
|
||||||
|
}, [upcValidation.validatingRows, externalValidatingCells]);
|
||||||
|
|
||||||
|
// Create a consolidated item numbers map from all sources
|
||||||
|
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);
|
||||||
|
if (itemNumber) {
|
||||||
|
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from upcValidation: ${itemNumber}`);
|
||||||
|
result.set(index, itemNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if it's directly in the data
|
||||||
|
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 result;
|
||||||
|
}, [data, itemNumbers, upcValidation]);
|
||||||
|
|
||||||
|
// Create upcValidationResults map using the consolidated item numbers
|
||||||
|
const upcValidationResults = useMemo(() => {
|
||||||
|
const results = new Map<number, { itemNumber: string }>();
|
||||||
|
|
||||||
|
// Populate with our consolidated item numbers
|
||||||
|
consolidatedItemNumbers.forEach((itemNumber, rowIndex) => {
|
||||||
|
results.set(rowIndex, { itemNumber });
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}, [consolidatedItemNumbers]);
|
||||||
|
|
||||||
|
// Render the validation table with the provided props and UPC data
|
||||||
|
return (
|
||||||
|
<ValidationTable
|
||||||
|
data={data}
|
||||||
|
fields={fields}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
updateRow={updateRow as unknown as (rowIndex: number, key: string, value: any) => void}
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
isValidatingUpc={isValidatingUpc}
|
||||||
|
validatingUpcRows={validatingUpcRows}
|
||||||
|
filters={filters}
|
||||||
|
templates={templates}
|
||||||
|
applyTemplate={applyTemplate}
|
||||||
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
|
validatingCells={combinedValidatingCells}
|
||||||
|
itemNumbers={consolidatedItemNumbers}
|
||||||
|
isLoadingTemplates={isLoadingTemplates}
|
||||||
|
copyDown={copyDown}
|
||||||
|
upcValidationResults={upcValidationResults}
|
||||||
|
rowProductLines={rowProductLines}
|
||||||
|
rowSublines={rowSublines}
|
||||||
|
isLoadingLines={isLoadingLines}
|
||||||
|
isLoadingSublines={isLoadingSublines}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpcValidationTableAdapter
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Field, ErrorType } from '../../../types'
|
||||||
|
import { AlertCircle, ArrowDown, X } from 'lucide-react'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
import InputCell from './cells/InputCell'
|
||||||
|
import SelectCell from './cells/SelectCell'
|
||||||
|
import MultiSelectCell from './cells/MultiSelectCell'
|
||||||
|
import { TableCell } from '@/components/ui/table'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
|
// Context for copy down selection mode
|
||||||
|
export const CopyDownContext = React.createContext<{
|
||||||
|
isInCopyDownMode: boolean;
|
||||||
|
sourceRowIndex: number | null;
|
||||||
|
sourceFieldKey: string | null;
|
||||||
|
targetRowIndex: number | null;
|
||||||
|
setIsInCopyDownMode: (value: boolean) => void;
|
||||||
|
setSourceRowIndex: (value: number | null) => void;
|
||||||
|
setSourceFieldKey: (value: string | null) => void;
|
||||||
|
setTargetRowIndex: (value: number | null) => void;
|
||||||
|
handleCopyDownComplete: (sourceRowIndex: number, fieldKey: string, targetRowIndex: number) => void;
|
||||||
|
}>({
|
||||||
|
isInCopyDownMode: false,
|
||||||
|
sourceRowIndex: null,
|
||||||
|
sourceFieldKey: null,
|
||||||
|
targetRowIndex: null,
|
||||||
|
setIsInCopyDownMode: () => {},
|
||||||
|
setSourceRowIndex: () => {},
|
||||||
|
setSourceFieldKey: () => {},
|
||||||
|
setTargetRowIndex: () => {},
|
||||||
|
handleCopyDownComplete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define error object type
|
||||||
|
type ErrorObject = {
|
||||||
|
message: string;
|
||||||
|
level: string;
|
||||||
|
source?: string;
|
||||||
|
type?: ErrorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a value is empty - utility function shared by all components
|
||||||
|
const isEmpty = (val: any): boolean =>
|
||||||
|
val === undefined ||
|
||||||
|
val === null ||
|
||||||
|
val === '' ||
|
||||||
|
(Array.isArray(val) && val.length === 0) ||
|
||||||
|
(typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0);
|
||||||
|
|
||||||
|
// Memoized validation icon component
|
||||||
|
const ValidationIcon = React.memo(({ error }: { error: ErrorObject }) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={200}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="cursor-help">
|
||||||
|
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[300px] text-wrap break-words">
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
));
|
||||||
|
|
||||||
|
ValidationIcon.displayName = 'ValidationIcon';
|
||||||
|
|
||||||
|
// Memoized base cell content component
|
||||||
|
const BaseCellContent = React.memo(({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
hasErrors,
|
||||||
|
options = [],
|
||||||
|
className = '',
|
||||||
|
fieldKey = ''
|
||||||
|
}: {
|
||||||
|
field: Field<string>;
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
hasErrors: boolean;
|
||||||
|
options?: readonly any[];
|
||||||
|
className?: string;
|
||||||
|
fieldKey?: string;
|
||||||
|
}) => {
|
||||||
|
// Get field type information
|
||||||
|
const fieldType = fieldKey === 'line' || fieldKey === 'subline'
|
||||||
|
? 'select'
|
||||||
|
: typeof field.fieldType === 'string'
|
||||||
|
? field.fieldType
|
||||||
|
: field.fieldType?.type || 'input';
|
||||||
|
|
||||||
|
// Check for multiline input
|
||||||
|
const isMultiline = typeof field.fieldType === 'object' &&
|
||||||
|
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
|
||||||
|
field.fieldType.multiline === true;
|
||||||
|
|
||||||
|
// Check for price field
|
||||||
|
const isPrice = typeof field.fieldType === 'object' &&
|
||||||
|
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
|
||||||
|
field.fieldType.price === true;
|
||||||
|
|
||||||
|
// Special case for line and subline - check this first, before any other field type checks
|
||||||
|
if (fieldKey === 'line' || fieldKey === 'subline') {
|
||||||
|
// Force these fields to always use SelectCell regardless of fieldType
|
||||||
|
return (
|
||||||
|
<SelectCell
|
||||||
|
field={{...field, fieldType: { type: 'select', options }}}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
options={options}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
className={className}
|
||||||
|
disabled={field.disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldType === 'select') {
|
||||||
|
return (
|
||||||
|
<SelectCell
|
||||||
|
field={field}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
options={options}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
className={className}
|
||||||
|
disabled={field.disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldType === 'multi-select' || fieldType === 'multi-input') {
|
||||||
|
return (
|
||||||
|
<MultiSelectCell
|
||||||
|
field={field}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
options={options}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
className={className}
|
||||||
|
disabled={field.disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputCell
|
||||||
|
field={field}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
isMultiline={isMultiline}
|
||||||
|
isPrice={isPrice}
|
||||||
|
disabled={field.disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, (prev, next) => {
|
||||||
|
// Shallow array comparison for options if arrays
|
||||||
|
const optionsEqual = prev.options === next.options ||
|
||||||
|
(Array.isArray(prev.options) && Array.isArray(next.options) &&
|
||||||
|
prev.options.length === next.options.length &&
|
||||||
|
prev.options.every((opt, idx) => opt === (next.options as any[])[idx]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
prev.value === next.value &&
|
||||||
|
prev.hasErrors === next.hasErrors &&
|
||||||
|
prev.field === next.field &&
|
||||||
|
prev.className === next.className &&
|
||||||
|
optionsEqual
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
BaseCellContent.displayName = 'BaseCellContent';
|
||||||
|
|
||||||
|
export interface ValidationCellProps {
|
||||||
|
field: Field<string>
|
||||||
|
value: any
|
||||||
|
onChange: (value: any) => void
|
||||||
|
errors: ErrorObject[]
|
||||||
|
isValidating?: boolean
|
||||||
|
fieldKey: string
|
||||||
|
options?: readonly any[]
|
||||||
|
itemNumber?: string
|
||||||
|
width: number
|
||||||
|
rowIndex: number
|
||||||
|
copyDown?: (endRowIndex?: number) => void
|
||||||
|
totalRows?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add efficient error message extraction function
|
||||||
|
|
||||||
|
// Highly optimized error processing function with fast paths for common cases
|
||||||
|
function processErrors(value: any, errors: ErrorObject[]): {
|
||||||
|
hasError: boolean;
|
||||||
|
isRequiredButEmpty: boolean;
|
||||||
|
shouldShowErrorIcon: boolean;
|
||||||
|
errorMessages: string;
|
||||||
|
} {
|
||||||
|
// Fast path - if no errors or empty error array, return immediately
|
||||||
|
if (!errors || errors.length === 0) {
|
||||||
|
return {
|
||||||
|
hasError: false,
|
||||||
|
isRequiredButEmpty: false,
|
||||||
|
shouldShowErrorIcon: false,
|
||||||
|
errorMessages: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the shared isEmpty function for value checking
|
||||||
|
const valueIsEmpty = isEmpty(value);
|
||||||
|
|
||||||
|
// Fast path for the most common case - required field with empty value
|
||||||
|
if (valueIsEmpty && errors.length === 1 && errors[0].type === ErrorType.Required) {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
isRequiredButEmpty: true,
|
||||||
|
shouldShowErrorIcon: false,
|
||||||
|
errorMessages: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-empty values with errors, we need to show error icons
|
||||||
|
const hasError = errors.some(error => error.level === 'error' || error.level === 'warning');
|
||||||
|
|
||||||
|
// For empty values with required errors, show only a border
|
||||||
|
const isRequiredButEmpty = valueIsEmpty && errors.some(error => error.type === ErrorType.Required);
|
||||||
|
|
||||||
|
// Show error icons for non-empty fields with errors, or for empty fields with non-required errors
|
||||||
|
const shouldShowErrorIcon = hasError && (!valueIsEmpty || !errors.every(error => error.type === ErrorType.Required));
|
||||||
|
|
||||||
|
// Only compute error messages if we're going to show an icon
|
||||||
|
const errorMessages = shouldShowErrorIcon
|
||||||
|
? errors
|
||||||
|
.filter(e => e.level === 'error' || e.level === 'warning')
|
||||||
|
.map(e => e.message)
|
||||||
|
.join('\n')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasError,
|
||||||
|
isRequiredButEmpty,
|
||||||
|
shouldShowErrorIcon,
|
||||||
|
errorMessages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to compare error arrays efficiently with a hash-based approach
|
||||||
|
function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]): boolean {
|
||||||
|
// Fast path for referential equality
|
||||||
|
if (prevErrors === nextErrors) return true;
|
||||||
|
|
||||||
|
// Fast path for length check
|
||||||
|
if (!prevErrors || !nextErrors) return prevErrors === nextErrors;
|
||||||
|
if (prevErrors.length !== nextErrors.length) return false;
|
||||||
|
|
||||||
|
// Generate simple hash from error properties
|
||||||
|
const getErrorHash = (error: ErrorObject): string => {
|
||||||
|
return `${error.message}|${error.level}|${error.type || ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compare using hashes
|
||||||
|
const prevHashes = prevErrors.map(getErrorHash);
|
||||||
|
const nextHashes = nextErrors.map(getErrorHash);
|
||||||
|
|
||||||
|
// Sort hashes to ensure consistent order
|
||||||
|
prevHashes.sort();
|
||||||
|
nextHashes.sort();
|
||||||
|
|
||||||
|
// Compare sorted hash arrays
|
||||||
|
return prevHashes.join(',') === nextHashes.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValidationCell = React.memo(({
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
errors,
|
||||||
|
isValidating,
|
||||||
|
fieldKey,
|
||||||
|
options = [],
|
||||||
|
itemNumber,
|
||||||
|
width,
|
||||||
|
copyDown,
|
||||||
|
rowIndex,
|
||||||
|
totalRows = 0
|
||||||
|
}: ValidationCellProps) => {
|
||||||
|
// Use the CopyDown context
|
||||||
|
const copyDownContext = React.useContext(CopyDownContext);
|
||||||
|
|
||||||
|
// CRITICAL FIX: For item_number fields, always prioritize the itemNumber prop over the 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
|
||||||
|
const {
|
||||||
|
hasError,
|
||||||
|
isRequiredButEmpty,
|
||||||
|
shouldShowErrorIcon,
|
||||||
|
errorMessages
|
||||||
|
} = React.useMemo(() => processErrors(displayValue, errors), [displayValue, errors]);
|
||||||
|
|
||||||
|
// Track whether this cell is the source of a copy-down operation
|
||||||
|
const isSourceCell = copyDownContext.isInCopyDownMode &&
|
||||||
|
rowIndex === copyDownContext.sourceRowIndex &&
|
||||||
|
fieldKey === copyDownContext.sourceFieldKey;
|
||||||
|
|
||||||
|
// Add state for hover on copy down button
|
||||||
|
const [isCopyDownHovered, setIsCopyDownHovered] = React.useState(false);
|
||||||
|
// Add state for hover on target row
|
||||||
|
const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false);
|
||||||
|
|
||||||
|
// Force isValidating to be a boolean
|
||||||
|
const isLoading = isValidating === true;
|
||||||
|
|
||||||
|
// Handle copy down button click
|
||||||
|
const handleCopyDownClick = React.useCallback(() => {
|
||||||
|
if (copyDown && totalRows > rowIndex + 1) {
|
||||||
|
// Enter copy down mode
|
||||||
|
copyDownContext.setIsInCopyDownMode(true);
|
||||||
|
copyDownContext.setSourceRowIndex(rowIndex);
|
||||||
|
copyDownContext.setSourceFieldKey(fieldKey);
|
||||||
|
}
|
||||||
|
}, [copyDown, copyDownContext, fieldKey, rowIndex, totalRows]);
|
||||||
|
|
||||||
|
// Check if this cell is in a row that can be a target for copy down
|
||||||
|
const isInTargetRow = copyDownContext.isInCopyDownMode &&
|
||||||
|
copyDownContext.sourceFieldKey === fieldKey &&
|
||||||
|
rowIndex > (copyDownContext.sourceRowIndex || 0);
|
||||||
|
|
||||||
|
// Check if this row is the currently selected target row
|
||||||
|
const isSelectedTarget = isInTargetRow && rowIndex <= (copyDownContext.targetRowIndex || 0);
|
||||||
|
|
||||||
|
// Handle click on a potential target cell
|
||||||
|
const handleTargetCellClick = React.useCallback(() => {
|
||||||
|
if (isInTargetRow && copyDownContext.sourceRowIndex !== null && copyDownContext.sourceFieldKey !== null) {
|
||||||
|
copyDownContext.handleCopyDownComplete(
|
||||||
|
copyDownContext.sourceRowIndex,
|
||||||
|
copyDownContext.sourceFieldKey,
|
||||||
|
rowIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [copyDownContext, isInTargetRow, rowIndex]);
|
||||||
|
|
||||||
|
// Memoize the cell style objects to avoid recreating them on every render
|
||||||
|
const cellStyle = React.useMemo(() => ({
|
||||||
|
width: `${width}px`,
|
||||||
|
minWidth: `${width}px`,
|
||||||
|
maxWidth: `${width}px`,
|
||||||
|
boxSizing: 'border-box' as const,
|
||||||
|
cursor: isInTargetRow ? 'pointer' : undefined
|
||||||
|
}), [width, isInTargetRow]);
|
||||||
|
|
||||||
|
// Memoize the cell class name to prevent re-calculating on every render
|
||||||
|
const cellClassName = React.useMemo(() => {
|
||||||
|
if (isSourceCell || isSelectedTarget || isInTargetRow) {
|
||||||
|
return isSourceCell ? '!bg-blue-100 !border-blue-500 !rounded-md' :
|
||||||
|
isSelectedTarget ? '!bg-blue-200 !border-blue-200 !rounded-md' :
|
||||||
|
isInTargetRow ? 'hover:!bg-blue-100 !border-blue-200 !rounded-md' : '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [isSourceCell, isSelectedTarget, isInTargetRow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
className="p-1 group relative"
|
||||||
|
style={cellStyle}
|
||||||
|
onClick={isInTargetRow ? handleTargetCellClick : undefined}
|
||||||
|
onMouseEnter={isInTargetRow ? () => setIsTargetRowHovered(true) : undefined}
|
||||||
|
onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined}
|
||||||
|
>
|
||||||
|
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
|
||||||
|
{shouldShowErrorIcon && !isInTargetRow && (
|
||||||
|
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20">
|
||||||
|
<ValidationIcon error={{
|
||||||
|
message: errorMessages,
|
||||||
|
level: 'error',
|
||||||
|
type: ErrorType.Custom
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!shouldShowErrorIcon && copyDown && !isEmpty(displayValue) && !copyDownContext.isInCopyDownMode && (
|
||||||
|
<div className="absolute right-0.5 top-1/2 -translate-y-1/2 z-20 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={handleCopyDownClick}
|
||||||
|
onMouseEnter={() => setIsCopyDownHovered(true)}
|
||||||
|
onMouseLeave={() => setIsCopyDownHovered(false)}
|
||||||
|
className="p-1 rounded-full hover:bg-blue-100 text-blue-500/70 hover:text-blue-600 transition-colors"
|
||||||
|
aria-label="Copy value to rows below"
|
||||||
|
>
|
||||||
|
<ArrowDown className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="font-medium">Copy value to rows below</p>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isSourceCell && (
|
||||||
|
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 z-20">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() => copyDownContext.setIsInCopyDownMode(false)}
|
||||||
|
className="p-1 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 transition-colors"
|
||||||
|
aria-label="Cancel copy down"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Cancel copy down</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={`flex items-center justify-center gap-2 border ${hasError || isRequiredButEmpty ? 'border-red-500' : 'border-input'} rounded-md px-2 py-2`}>
|
||||||
|
<Skeleton className="w-full h-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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
|
||||||
|
field={field}
|
||||||
|
value={displayValue}
|
||||||
|
onChange={onChange}
|
||||||
|
hasErrors={hasError || isRequiredButEmpty}
|
||||||
|
options={options}
|
||||||
|
className={cellClassName}
|
||||||
|
fieldKey={fieldKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
}, (prevProps, nextProps) => {
|
||||||
|
// Fast path: if all props are the same object
|
||||||
|
if (prevProps === nextProps) return true;
|
||||||
|
|
||||||
|
// Optimize the memo comparison function, checking most impactful props first
|
||||||
|
// Check isValidating first as it's most likely to change frequently
|
||||||
|
if (prevProps.isValidating !== nextProps.isValidating) return false;
|
||||||
|
|
||||||
|
// Then check value changes
|
||||||
|
if (prevProps.value !== nextProps.value) return false;
|
||||||
|
|
||||||
|
// Item number is related to validation state
|
||||||
|
if (prevProps.itemNumber !== nextProps.itemNumber) return false;
|
||||||
|
|
||||||
|
// Check errors with our optimized comparison function
|
||||||
|
if (!compareErrorArrays(prevProps.errors, nextProps.errors)) return false;
|
||||||
|
|
||||||
|
// Check field identity
|
||||||
|
if (prevProps.field !== nextProps.field) return false;
|
||||||
|
|
||||||
|
// Shallow options comparison - only if field type is select or multi-select
|
||||||
|
if (prevProps.field.fieldType?.type === 'select' || prevProps.field.fieldType?.type === 'multi-select') {
|
||||||
|
const optionsEqual = prevProps.options === nextProps.options ||
|
||||||
|
(Array.isArray(prevProps.options) &&
|
||||||
|
Array.isArray(nextProps.options) &&
|
||||||
|
prevProps.options.length === nextProps.options.length &&
|
||||||
|
prevProps.options.every((opt, idx) => {
|
||||||
|
const nextOptions = nextProps.options || [];
|
||||||
|
return opt === nextOptions[idx];
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!optionsEqual) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check copy down context changes
|
||||||
|
const copyDownContextChanged =
|
||||||
|
prevProps.rowIndex !== nextProps.rowIndex ||
|
||||||
|
prevProps.fieldKey !== nextProps.fieldKey;
|
||||||
|
|
||||||
|
if (copyDownContextChanged) return false;
|
||||||
|
|
||||||
|
// All essential props are the same - we can skip re-rendering
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ValidationCell.displayName = 'ValidationCell';
|
||||||
|
|
||||||
|
export default ValidationCell;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,15 +7,15 @@ 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'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
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,15 +138,34 @@ 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
|
||||||
// Only re-render if these essential props change
|
// Only re-render if these essential props change
|
||||||
return (
|
const valueEqual = prev.value === next.value;
|
||||||
prev.value === next.value &&
|
const isValidatingEqual = prev.isValidating === next.isValidating;
|
||||||
prev.isValidating === next.isValidating &&
|
|
||||||
prev.itemNumber === next.itemNumber &&
|
// Shallow equality check for errors array
|
||||||
// Deep compare errors
|
const errorsEqual = prev.errors === next.errors || (
|
||||||
prev.errors === next.errors &&
|
Array.isArray(prev.errors) &&
|
||||||
prev.options === next.options
|
Array.isArray(next.errors) &&
|
||||||
|
prev.errors.length === next.errors.length &&
|
||||||
|
prev.errors.every((err, idx) => err === next.errors[idx])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Shallow equality check for options array
|
||||||
|
const optionsEqual = prev.options === next.options || (
|
||||||
|
Array.isArray(prev.options) &&
|
||||||
|
Array.isArray(next.options) &&
|
||||||
|
prev.options.length === next.options.length &&
|
||||||
|
prev.options.every((opt, idx) => opt === next.options?.[idx])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip checking for props that rarely change
|
||||||
|
return valueEqual && isValidatingEqual && errorsEqual && optionsEqual;
|
||||||
});
|
});
|
||||||
|
|
||||||
MemoizedCell.displayName = 'MemoizedCell';
|
MemoizedCell.displayName = 'MemoizedCell';
|
||||||
@@ -170,14 +188,13 @@ 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>();
|
||||||
|
|
||||||
// Debug logs
|
|
||||||
console.log('ValidationTable rowProductLines:', rowProductLines);
|
|
||||||
console.log('ValidationTable rowSublines:', rowSublines);
|
|
||||||
|
|
||||||
// Add state for copy down selection mode
|
// Add state for copy down selection mode
|
||||||
const [isInCopyDownMode, setIsInCopyDownMode] = useState(false);
|
const [isInCopyDownMode, setIsInCopyDownMode] = useState(false);
|
||||||
const [sourceRowIndex, setSourceRowIndex] = useState<number | null>(null);
|
const [sourceRowIndex, setSourceRowIndex] = useState<number | null>(null);
|
||||||
@@ -243,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)}
|
||||||
@@ -293,10 +310,13 @@ const ValidationTable = <T extends string>({
|
|||||||
const cache = new Map<string, readonly any[]>();
|
const cache = new Map<string, readonly any[]>();
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
// Don't skip disabled fields
|
// Get the field key
|
||||||
|
const fieldKey = String(field.key);
|
||||||
|
|
||||||
if (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') {
|
// Handle all select and multi-select fields the same way
|
||||||
const fieldKey = String(field.key);
|
if (field.fieldType &&
|
||||||
|
(typeof field.fieldType === 'object') &&
|
||||||
|
(field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')) {
|
||||||
cache.set(fieldKey, (field.fieldType as any).options || []);
|
cache.set(fieldKey, (field.fieldType as any).options || []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -314,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
|
||||||
@@ -342,34 +390,83 @@ const ValidationTable = <T extends string>({
|
|||||||
|
|
||||||
if (fieldKey === 'line' && rowId && rowProductLines[rowId]) {
|
if (fieldKey === 'line' && rowId && rowProductLines[rowId]) {
|
||||||
options = rowProductLines[rowId];
|
options = rowProductLines[rowId];
|
||||||
console.log(`Setting line options for row ${rowId}:`, options);
|
|
||||||
} else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) {
|
} else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) {
|
||||||
options = rowSublines[rowId];
|
options = rowSublines[rowId];
|
||||||
console.log(`Setting subline options for row ${rowId}:`, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this cell is in loading state
|
// Determine if this cell is in loading state - use a clear consistent approach
|
||||||
let isLoading = validatingCells.has(`${row.index}-${field.key}`);
|
let isLoading = false;
|
||||||
|
|
||||||
|
// Check the validatingCells Set first (for item_number and other fields)
|
||||||
|
const cellLoadingKey = `${row.index}-${fieldKey}`;
|
||||||
|
if (validatingCells.has(cellLoadingKey)) {
|
||||||
|
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
|
||||||
if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
console.log(`Line field for row ${rowId} is loading`);
|
|
||||||
} else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
|
||||||
isLoading = true;
|
|
||||||
console.log(`Subline field for row ${rowId} is loading`);
|
|
||||||
}
|
}
|
||||||
|
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get validation errors for this cell
|
||||||
|
const cellErrors = validationErrors.get(row.index)?.[fieldKey] || [];
|
||||||
|
|
||||||
|
// Create a copy of the field with guaranteed field type for line and subline fields
|
||||||
|
let fieldWithType = field;
|
||||||
|
|
||||||
|
// Ensure line and subline fields always have the correct fieldType
|
||||||
|
if (fieldKey === 'line' || fieldKey === 'subline') {
|
||||||
|
// Create a deep clone of the field to prevent any reference issues
|
||||||
|
fieldWithType = {
|
||||||
|
...JSON.parse(JSON.stringify(field)), // Ensure deep clone
|
||||||
|
fieldType: {
|
||||||
|
type: 'select',
|
||||||
|
options: options
|
||||||
|
},
|
||||||
|
// Explicitly mark as not disabled to ensure dropdown works
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
field={field as Field<string>}
|
key={cellKey} // CRITICAL: Add key to force re-render when itemNumber changes
|
||||||
value={row.original[field.key as keyof typeof row.original]}
|
field={fieldWithType as Field<string>}
|
||||||
|
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={validationErrors.get(row.index)?.[fieldKey] || []}
|
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)}
|
||||||
@@ -379,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]);
|
||||||
@@ -387,15 +486,13 @@ const ValidationTable = <T extends string>({
|
|||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
state: {
|
||||||
state: { rowSelection },
|
rowSelection,
|
||||||
|
},
|
||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getRowId: (row) => {
|
getCoreRowModel: getCoreRowModel(),
|
||||||
if (row.__index) return row.__index;
|
getRowId: useCallback((_row: RowData<T>, index: number) => String(index), []),
|
||||||
const index = data.indexOf(row);
|
|
||||||
return String(index);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate total table width for stable horizontal scrolling
|
// Calculate total table width for stable horizontal scrolling
|
||||||
@@ -481,13 +578,16 @@ const ValidationTable = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Custom Table Header - Always Visible */}
|
{/* Custom Table Header - Always Visible with GPU acceleration */}
|
||||||
<div
|
<div
|
||||||
className={`sticky top-0 z-20 bg-muted border-b shadow-sm`}
|
className="sticky top-0 z-20 bg-muted border-b shadow-sm will-change-transform"
|
||||||
style={{ width: `${totalWidth}px` }}
|
style={{
|
||||||
|
width: `${totalWidth}px`,
|
||||||
|
transform: 'translateZ(0)', // Force GPU acceleration
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{table.getFlatHeaders().map((header, index) => {
|
{table.getFlatHeaders().map((header) => {
|
||||||
const width = header.getSize();
|
const width = header.getSize();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -508,49 +608,57 @@ const ValidationTable = <T extends string>({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Body */}
|
{/* Table Body - With optimized rendering */}
|
||||||
<Table style={{ width: `${totalWidth}px`, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, marginTop: '-1px' }}>
|
<Table style={{
|
||||||
|
width: `${totalWidth}px`,
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
borderCollapse: 'separate',
|
||||||
|
borderSpacing: 0,
|
||||||
|
marginTop: '-1px',
|
||||||
|
willChange: 'transform', // Help browser optimize
|
||||||
|
contain: 'content', // Contain paint operations
|
||||||
|
transform: 'translateZ(0)' // Force GPU acceleration
|
||||||
|
}}>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => {
|
||||||
<TableRow
|
// Precompute validation error status for this row
|
||||||
key={row.id}
|
const hasErrors = validationErrors.has(parseInt(row.id)) &&
|
||||||
className={cn(
|
Object.keys(validationErrors.get(parseInt(row.id)) || {}).length > 0;
|
||||||
"hover:bg-muted/50",
|
|
||||||
row.getIsSelected() ? "bg-muted/50" : "",
|
// Precompute copy down target status
|
||||||
validationErrors.get(data.indexOf(row.original)) &&
|
const isCopyDownTarget = isInCopyDownMode &&
|
||||||
Object.keys(validationErrors.get(data.indexOf(row.original)) || {}).length > 0 ? "bg-red-50/40" : "",
|
sourceRowIndex !== null &&
|
||||||
// Add cursor-pointer class when in copy down mode for target rows
|
parseInt(row.id) > sourceRowIndex;
|
||||||
isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "cursor-pointer copy-down-target-row" : ""
|
|
||||||
)}
|
// Using CSS variables for better performance on hover/state changes
|
||||||
style={{
|
const rowStyle = {
|
||||||
// Force cursor pointer on all target rows
|
cursor: isCopyDownTarget ? 'pointer' : undefined,
|
||||||
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined,
|
position: 'relative' as const,
|
||||||
position: 'relative' // Ensure we can position the overlay
|
willChange: isInCopyDownMode ? 'background-color' : 'auto',
|
||||||
}}
|
contain: 'layout',
|
||||||
onMouseEnter={() => handleRowMouseEnter(row.index)}
|
transition: 'background-color 100ms ease-in-out'
|
||||||
>
|
};
|
||||||
{row.getVisibleCells().map((cell, cellIndex) => {
|
|
||||||
const width = cell.column.getSize();
|
return (
|
||||||
return (
|
<TableRow
|
||||||
<TableCell
|
key={row.id}
|
||||||
key={cell.id}
|
className={cn(
|
||||||
style={{
|
"hover:bg-muted/50",
|
||||||
width: `${width}px`,
|
row.getIsSelected() ? "!bg-blue-50/50" : "",
|
||||||
minWidth: `${width}px`,
|
hasErrors ? "bg-red-50/40" : "",
|
||||||
maxWidth: `${width}px`,
|
isCopyDownTarget ? "cursor-pointer copy-down-target-row" : ""
|
||||||
boxSizing: 'border-box',
|
)}
|
||||||
padding: '0',
|
style={rowStyle}
|
||||||
// Force cursor pointer on all cells in target rows
|
onMouseEnter={() => handleRowMouseEnter(parseInt(row.id))}
|
||||||
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined
|
>
|
||||||
}}
|
{row.getVisibleCells().map((cell: any) => (
|
||||||
className={isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "target-row-cell" : ""}
|
<React.Fragment key={cell.id}>
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</React.Fragment>
|
||||||
);
|
))}
|
||||||
})}
|
</TableRow>
|
||||||
</TableRow>
|
);
|
||||||
))}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
@@ -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);
|
||||||
@@ -107,23 +106,28 @@ const InputCell = <T extends string>({
|
|||||||
|
|
||||||
// Handle blur event - use transition for non-critical updates
|
// Handle blur event - use transition for non-critical updates
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
|
// First - lock in the current edit value to prevent it from being lost
|
||||||
|
const finalValue = editValue.trim();
|
||||||
|
|
||||||
|
// Then transition to non-editing state
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
|
||||||
// Format the value for storage (remove formatting like $ for price)
|
// Format the value for storage (remove formatting like $ for price)
|
||||||
let processedValue = deferredEditValue.trim();
|
let processedValue = finalValue;
|
||||||
|
|
||||||
if (isPrice && processedValue) {
|
if (isPrice && processedValue) {
|
||||||
needsProcessingRef.current = true;
|
needsProcessingRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local display value immediately
|
// Update local display value immediately to prevent UI flicker
|
||||||
setLocalDisplayValue(processedValue);
|
setLocalDisplayValue(processedValue);
|
||||||
|
|
||||||
|
// Commit the change to parent component
|
||||||
onChange(processedValue);
|
onChange(processedValue);
|
||||||
onEndEdit?.();
|
onEndEdit?.();
|
||||||
});
|
});
|
||||||
}, [deferredEditValue, onChange, onEndEdit, isPrice]);
|
}, [editValue, onChange, onEndEdit, isPrice]);
|
||||||
|
|
||||||
// Handle direct input change - optimized to be synchronous for typing
|
// Handle direct input change - optimized to be synchronous for typing
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
@@ -167,41 +167,59 @@ const MultiSelectCell = <T extends string>({
|
|||||||
const commandListRef = useRef<HTMLDivElement>(null)
|
const commandListRef = useRef<HTMLDivElement>(null)
|
||||||
// Add state for hover
|
// Add state for hover
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
// Add ref to track if we need to sync internal state with external value
|
||||||
|
const shouldSyncWithExternalValue = useRef(true)
|
||||||
|
|
||||||
// Create a memoized Set for fast lookups of selected values
|
// Create a memoized Set for fast lookups of selected values
|
||||||
const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]);
|
const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]);
|
||||||
|
|
||||||
// Sync internalValue with external value when component mounts or value changes externally
|
// Sync internalValue with external value when component mounts or value changes externally
|
||||||
|
// Modified to prevent infinite loop by checking if values are different before updating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
// Only sync if we should (not during internal edits) and if not open
|
||||||
// Ensure value is always an array
|
if (shouldSyncWithExternalValue.current && !open) {
|
||||||
setInternalValue(Array.isArray(value) ? value : [])
|
const externalValue = Array.isArray(value) ? value : [];
|
||||||
|
|
||||||
|
// Only update if values are actually different to prevent infinite loops
|
||||||
|
if (internalValue.length !== externalValue.length ||
|
||||||
|
!internalValue.every(v => externalValue.includes(v)) ||
|
||||||
|
!externalValue.every(v => internalValue.includes(v))) {
|
||||||
|
setInternalValue(externalValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [value, open])
|
}, [value, open, internalValue]);
|
||||||
|
|
||||||
// Handle open state changes with improved responsiveness
|
// Handle open state changes with improved responsiveness
|
||||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||||
if (open && !newOpen) {
|
if (open && !newOpen) {
|
||||||
|
// Prevent syncing with external value during our internal update
|
||||||
|
shouldSyncWithExternalValue.current = false;
|
||||||
|
|
||||||
// Only update parent state when dropdown closes
|
// Only update parent state when dropdown closes
|
||||||
// Avoid expensive deep comparison if lengths are different
|
// Make a defensive copy to avoid mutations
|
||||||
if (internalValue.length !== value.length ||
|
const valuesToCommit = [...internalValue];
|
||||||
internalValue.some((v, i) => v !== value[i])) {
|
|
||||||
onChange(internalValue);
|
// Immediate UI update
|
||||||
}
|
setOpen(false);
|
||||||
|
|
||||||
|
// Update parent with the value immediately
|
||||||
|
onChange(valuesToCommit);
|
||||||
if (onEndEdit) onEndEdit();
|
if (onEndEdit) onEndEdit();
|
||||||
|
|
||||||
|
// Allow syncing with external value again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
shouldSyncWithExternalValue.current = true;
|
||||||
|
}, 0);
|
||||||
} else if (newOpen && !open) {
|
} else if (newOpen && !open) {
|
||||||
// Sync internal state with external state when opening
|
// When opening the dropdown, sync with external value
|
||||||
setInternalValue(Array.isArray(value) ? value : []);
|
const externalValue = Array.isArray(value) ? value : [];
|
||||||
|
setInternalValue(externalValue);
|
||||||
setSearchQuery(""); // Reset search query on open
|
setSearchQuery(""); // Reset search query on open
|
||||||
|
setOpen(true);
|
||||||
if (onStartEdit) onStartEdit();
|
if (onStartEdit) onStartEdit();
|
||||||
} else if (!newOpen) {
|
} else if (!newOpen) {
|
||||||
// Handle case when dropdown is already closed but handleOpenChange is called
|
// Handle case when dropdown is already closed but handleOpenChange is called
|
||||||
// This ensures values are saved when clicking the chevron to close
|
setOpen(false);
|
||||||
if (internalValue.length !== value.length ||
|
|
||||||
internalValue.some((v, i) => v !== value[i])) {
|
|
||||||
onChange(internalValue);
|
|
||||||
}
|
|
||||||
if (onEndEdit) onEndEdit();
|
|
||||||
}
|
}
|
||||||
}, [open, internalValue, value, onChange, onStartEdit, onEndEdit]);
|
}, [open, internalValue, value, onChange, onStartEdit, onEndEdit]);
|
||||||
|
|
||||||
@@ -302,13 +320,25 @@ const MultiSelectCell = <T extends string>({
|
|||||||
|
|
||||||
// Update the handleSelect to operate on internalValue instead of directly calling onChange
|
// Update the handleSelect to operate on internalValue instead of directly calling onChange
|
||||||
const handleSelect = useCallback((selectedValue: string) => {
|
const handleSelect = useCallback((selectedValue: string) => {
|
||||||
|
// Prevent syncing with external value during our internal update
|
||||||
|
shouldSyncWithExternalValue.current = false;
|
||||||
|
|
||||||
setInternalValue(prev => {
|
setInternalValue(prev => {
|
||||||
|
let newValue;
|
||||||
if (prev.includes(selectedValue)) {
|
if (prev.includes(selectedValue)) {
|
||||||
return prev.filter(v => v !== selectedValue);
|
// Remove the value
|
||||||
|
newValue = prev.filter(v => v !== selectedValue);
|
||||||
} else {
|
} else {
|
||||||
return [...prev, selectedValue];
|
// Add the value - make a new array to avoid mutations
|
||||||
|
newValue = [...prev, selectedValue];
|
||||||
}
|
}
|
||||||
|
return newValue;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allow syncing with external value again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
shouldSyncWithExternalValue.current = true;
|
||||||
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle wheel scroll in dropdown
|
// Handle wheel scroll in dropdown
|
||||||
@@ -51,8 +51,6 @@ const SelectCell = <T extends string>({
|
|||||||
// Add state for hover
|
// Add state for hover
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
console.log(`SelectCell: field.key=${field.key}, disabled=${disabled}, options=`, options);
|
|
||||||
|
|
||||||
// Helper function to check if a class is present in the className string
|
// Helper function to check if a class is present in the className string
|
||||||
const hasClass = (cls: string): boolean => {
|
const hasClass = (cls: string): boolean => {
|
||||||
const classNames = className.split(' ');
|
const classNames = className.split(' ');
|
||||||
@@ -68,7 +66,6 @@ const SelectCell = <T extends string>({
|
|||||||
|
|
||||||
// Memoize options processing to avoid recalculation on every render
|
// Memoize options processing to avoid recalculation on every render
|
||||||
const selectOptions = useMemo(() => {
|
const selectOptions = useMemo(() => {
|
||||||
console.log(`Processing options for ${field.key}:`, options);
|
|
||||||
// Fast path check - if we have raw options, just use those
|
// Fast path check - if we have raw options, just use those
|
||||||
if (options && options.length > 0) {
|
if (options && options.length > 0) {
|
||||||
// Check if options already have the correct structure to avoid mapping
|
// Check if options already have the correct structure to avoid mapping
|
||||||
@@ -126,8 +123,11 @@ const SelectCell = <T extends string>({
|
|||||||
|
|
||||||
// Handle selection - UPDATE INTERNAL VALUE FIRST
|
// Handle selection - UPDATE INTERNAL VALUE FIRST
|
||||||
const handleSelect = useCallback((selectedValue: string) => {
|
const handleSelect = useCallback((selectedValue: string) => {
|
||||||
|
// Store the selected value to prevent it being lost in async operations
|
||||||
|
const valueToCommit = selectedValue;
|
||||||
|
|
||||||
// 1. Update internal value immediately to prevent UI flicker
|
// 1. Update internal value immediately to prevent UI flicker
|
||||||
setInternalValue(selectedValue);
|
setInternalValue(valueToCommit);
|
||||||
|
|
||||||
// 2. Close the dropdown immediately
|
// 2. Close the dropdown immediately
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -139,105 +139,44 @@ const SelectCell = <T extends string>({
|
|||||||
// This prevents the parent component from re-rendering and causing dropdown to reopen
|
// This prevents the parent component from re-rendering and causing dropdown to reopen
|
||||||
if (onEndEdit) onEndEdit();
|
if (onEndEdit) onEndEdit();
|
||||||
|
|
||||||
// 5. Call onChange in the next tick to avoid synchronous re-renders
|
// 5. Call onChange synchronously to avoid race conditions with other cells
|
||||||
|
onChange(valueToCommit);
|
||||||
|
|
||||||
|
// 6. Clear processing state after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onChange(selectedValue);
|
setIsProcessing(false);
|
||||||
}, 0);
|
}, 200);
|
||||||
}, [onChange, onEndEdit]);
|
}, [onChange, onEndEdit]);
|
||||||
|
|
||||||
// If disabled, render a static view
|
// If disabled, render a static view
|
||||||
if (disabled) {
|
if (disabled && field.key !== 'line' && field.key !== 'subline') {
|
||||||
const displayText = displayValue;
|
const displayText = displayValue;
|
||||||
|
|
||||||
// For debugging, let's render the Popover component even if disabled
|
|
||||||
// This will help us determine if the issue is with the disabled state
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<div
|
||||||
open={open}
|
className={cn(
|
||||||
onOpenChange={(isOpen) => {
|
"w-full px-3 py-2 h-10 rounded-md text-sm flex items-center",
|
||||||
setOpen(isOpen);
|
"border",
|
||||||
if (isOpen && onStartEdit) onStartEdit();
|
hasErrors ? "border-destructive" : "border-input",
|
||||||
}}
|
className
|
||||||
>
|
)}
|
||||||
<PopoverTrigger asChild>
|
style={{
|
||||||
<Button
|
backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' :
|
||||||
variant="outline"
|
hasClass('!bg-blue-200') ? '#bfdbfe' :
|
||||||
role="combobox"
|
hasClass('hover:!bg-blue-100') && isHovered ? '#dbeafe' :
|
||||||
aria-expanded={open}
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-between font-normal",
|
|
||||||
"border",
|
|
||||||
!internalValue && "text-muted-foreground",
|
|
||||||
isProcessing && "text-muted-foreground",
|
|
||||||
hasErrors ? "border-destructive" : "",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' :
|
|
||||||
hasClass('!bg-blue-200') ? '#bfdbfe' :
|
|
||||||
hasClass('hover:!bg-blue-100') && isHovered ? '#dbeafe' :
|
|
||||||
undefined,
|
|
||||||
borderColor: hasClass('!border-blue-500') ? '#3b82f6' :
|
|
||||||
hasClass('!border-blue-200') ? '#bfdbfe' :
|
|
||||||
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
|
|
||||||
undefined,
|
undefined,
|
||||||
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
|
borderColor: hasClass('!border-blue-500') ? '#3b82f6' :
|
||||||
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
|
hasClass('!border-blue-200') ? '#bfdbfe' :
|
||||||
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
|
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
|
||||||
}}
|
undefined,
|
||||||
onClick={(e) => {
|
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
|
||||||
e.preventDefault();
|
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
|
||||||
e.stopPropagation();
|
}}
|
||||||
setOpen(!open);
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
if (!open && onStartEdit) onStartEdit();
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
}}
|
>
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
{displayText || ""}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
</div>
|
||||||
>
|
|
||||||
<span className={isProcessing ? "opacity-70" : ""}>
|
|
||||||
{displayValue}
|
|
||||||
</span>
|
|
||||||
<ChevronsUpDown className="mr-1.5 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="p-0 w-[var(--radix-popover-trigger-width)]"
|
|
||||||
align="start"
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<Command shouldFilter={true}>
|
|
||||||
<CommandInput
|
|
||||||
placeholder="Search..."
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
<CommandList
|
|
||||||
ref={commandListRef}
|
|
||||||
onWheel={handleWheel}
|
|
||||||
className="max-h-[200px]"
|
|
||||||
>
|
|
||||||
<CommandEmpty>No options found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{selectOptions.map((option) => (
|
|
||||||
<CommandItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
onSelect={(value) => handleSelect(value)}
|
|
||||||
className="flex w-full"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4 flex-shrink-0",
|
|
||||||
internalValue === option.value ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="truncate w-full">{option.label}</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 } 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';
|
||||||
@@ -88,21 +88,10 @@ export const useAiValidation = <T extends string>(
|
|||||||
// Call the original hook
|
// Call the original hook
|
||||||
const result = await rowHook(row);
|
const result = await rowHook(row);
|
||||||
// Extract Meta-specific properties
|
// Extract Meta-specific properties
|
||||||
const { __index, __errors } = result;
|
const { __index } = result;
|
||||||
// Return a Meta object with properly typed errors
|
// Return a Meta object with only the __index property
|
||||||
return {
|
return {
|
||||||
__index: __index || row.__index || '',
|
__index: __index || row.__index || ''
|
||||||
__errors: __errors ?
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.entries(__errors).map(([key, value]) => {
|
|
||||||
const errorArray = Array.isArray(value) ? value : [value];
|
|
||||||
return [key, {
|
|
||||||
message: errorArray[0].message,
|
|
||||||
level: errorArray[0].level,
|
|
||||||
source: ErrorSources.Row
|
|
||||||
} as InfoWithSource]
|
|
||||||
})
|
|
||||||
) : null
|
|
||||||
};
|
};
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
@@ -112,18 +101,7 @@ export const useAiValidation = <T extends string>(
|
|||||||
const results = await tableHook(rows);
|
const results = await tableHook(rows);
|
||||||
// Extract Meta-specific properties from each result
|
// Extract Meta-specific properties from each result
|
||||||
return results.map((result, index) => ({
|
return results.map((result, index) => ({
|
||||||
__index: result.__index || rows[index].__index || '',
|
__index: result.__index || rows[index].__index || ''
|
||||||
__errors: result.__errors ?
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.entries(result.__errors).map(([key, value]) => {
|
|
||||||
const errorArray = Array.isArray(value) ? value : [value];
|
|
||||||
return [key, {
|
|
||||||
message: errorArray[0].message,
|
|
||||||
level: errorArray[0].level,
|
|
||||||
source: ErrorSources.Table
|
|
||||||
} as InfoWithSource]
|
|
||||||
})
|
|
||||||
) : null
|
|
||||||
}));
|
}));
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
@@ -283,7 +261,7 @@ export const useAiValidation = <T extends string>(
|
|||||||
|
|
||||||
// Clean the data to ensure we only send what's needed
|
// Clean the data to ensure we only send what's needed
|
||||||
const cleanedData = data.map(item => {
|
const cleanedData = data.map(item => {
|
||||||
const { __errors, __index, ...rest } = item;
|
const { __index, ...rest } = item;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -399,8 +377,8 @@ export const useAiValidation = <T extends string>(
|
|||||||
|
|
||||||
// Clean the data to ensure we only send what's needed
|
// Clean the data to ensure we only send what's needed
|
||||||
const cleanedData = data.map(item => {
|
const cleanedData = data.map(item => {
|
||||||
const { __errors, __index, ...cleanProduct } = item;
|
const { __index, ...rest } = item;
|
||||||
return cleanProduct;
|
return rest;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Cleaned data for validation:', cleanedData);
|
console.log('Cleaned data for validation:', cleanedData);
|
||||||
@@ -601,7 +579,7 @@ export const useAiValidation = <T extends string>(
|
|||||||
|
|
||||||
console.log('Data updated after AI validation:', {
|
console.log('Data updated after AI validation:', {
|
||||||
dataLength: validatedData.length,
|
dataLength: validatedData.length,
|
||||||
hasErrors: validatedData.some(row => row.__errors && Object.keys(row.__errors).length > 0)
|
hasErrors: false // We no longer check row.__errors
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show changes and warnings in dialog after data is updated
|
// Show changes and warnings in dialog after data is updated
|
||||||
@@ -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,420 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing product lines and sublines fetching with caching
|
||||||
|
*/
|
||||||
|
export const useProductLinesFetching = (data: Record<string, any>[]) => {
|
||||||
|
// State for tracking product lines and sublines per row
|
||||||
|
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
|
||||||
|
const [rowSublines, setRowSublines] = useState<Record<string, any[]>>({});
|
||||||
|
|
||||||
|
// State for tracking loading states
|
||||||
|
const [isLoadingLines, setIsLoadingLines] = useState<Record<string, boolean>>({});
|
||||||
|
const [isLoadingSublines, setIsLoadingSublines] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// Add caches for product lines and sublines by company/line ID
|
||||||
|
const [companyLinesCache, setCompanyLinesCache] = useState<Record<string, any[]>>({});
|
||||||
|
const [lineSublineCache, setLineSublineCache] = useState<Record<string, any[]>>({});
|
||||||
|
|
||||||
|
// Function to fetch product lines for a specific company - memoized
|
||||||
|
const fetchProductLines = useCallback(async (rowIndex: string | number, companyId: string) => {
|
||||||
|
try {
|
||||||
|
// Only fetch if we have a valid company ID
|
||||||
|
if (!companyId) return;
|
||||||
|
|
||||||
|
console.log(`Fetching product lines for row ${rowIndex}, company ${companyId}`);
|
||||||
|
|
||||||
|
// Check if we already have this company's lines in the cache
|
||||||
|
if (companyLinesCache[companyId]) {
|
||||||
|
console.log(`Using cached product lines for company ${companyId}`);
|
||||||
|
// Use cached data
|
||||||
|
setRowProductLines(prev => ({ ...prev, [rowIndex]: companyLinesCache[companyId] }));
|
||||||
|
return companyLinesCache[companyId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set loading state for this row
|
||||||
|
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: true }));
|
||||||
|
|
||||||
|
// Fetch product lines from API
|
||||||
|
const productLinesUrl = `/api/import/product-lines/${companyId}`;
|
||||||
|
const response = await axios.get(productLinesUrl);
|
||||||
|
|
||||||
|
const lines = response.data;
|
||||||
|
console.log(`Received ${lines.length} product lines for company ${companyId}`);
|
||||||
|
|
||||||
|
// Format the data properly for dropdown display
|
||||||
|
const formattedLines = lines.map((line: any) => ({
|
||||||
|
label: line.name || line.label || String(line.value || line.id),
|
||||||
|
value: String(line.value || line.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Store in company cache
|
||||||
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: formattedLines }));
|
||||||
|
|
||||||
|
// Store for this specific row
|
||||||
|
setRowProductLines(prev => ({ ...prev, [rowIndex]: formattedLines }));
|
||||||
|
|
||||||
|
return formattedLines;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching product lines for company ${companyId}:`, error);
|
||||||
|
toast.error(`Failed to load product lines for company ${companyId}`);
|
||||||
|
|
||||||
|
// Set empty array for this company to prevent repeated failed requests
|
||||||
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
|
||||||
|
|
||||||
|
// Store empty array for this specific row
|
||||||
|
setRowProductLines(prev => ({ ...prev, [rowIndex]: [] }));
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
// Clear loading state
|
||||||
|
setIsLoadingLines(prev => ({ ...prev, [rowIndex]: false }));
|
||||||
|
}
|
||||||
|
}, [companyLinesCache]);
|
||||||
|
|
||||||
|
// Function to fetch sublines for a specific line - memoized
|
||||||
|
const fetchSublines = useCallback(async (rowIndex: string | number, lineId: string) => {
|
||||||
|
try {
|
||||||
|
// Only fetch if we have a valid line ID
|
||||||
|
if (!lineId) return;
|
||||||
|
|
||||||
|
console.log(`Fetching sublines for row ${rowIndex}, line ${lineId}`);
|
||||||
|
|
||||||
|
// Check if we already have this line's sublines in the cache
|
||||||
|
if (lineSublineCache[lineId]) {
|
||||||
|
console.log(`Using cached sublines for line ${lineId}`);
|
||||||
|
// Use cached data
|
||||||
|
setRowSublines(prev => ({ ...prev, [rowIndex]: lineSublineCache[lineId] }));
|
||||||
|
return lineSublineCache[lineId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set loading state for this row
|
||||||
|
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: true }));
|
||||||
|
|
||||||
|
// Fetch sublines from API
|
||||||
|
const sublinesUrl = `/api/import/sublines/${lineId}`;
|
||||||
|
const response = await axios.get(sublinesUrl);
|
||||||
|
|
||||||
|
const sublines = response.data;
|
||||||
|
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
|
||||||
|
|
||||||
|
// Format the data properly for dropdown display
|
||||||
|
const formattedSublines = sublines.map((subline: any) => ({
|
||||||
|
label: subline.name || subline.label || String(subline.value || subline.id),
|
||||||
|
value: String(subline.value || subline.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Store in line cache
|
||||||
|
setLineSublineCache(prev => ({ ...prev, [lineId]: formattedSublines }));
|
||||||
|
|
||||||
|
// Store for this specific row
|
||||||
|
setRowSublines(prev => ({ ...prev, [rowIndex]: formattedSublines }));
|
||||||
|
|
||||||
|
return formattedSublines;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching sublines for line ${lineId}:`, error);
|
||||||
|
|
||||||
|
// Set empty array for this line to prevent repeated failed requests
|
||||||
|
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
|
||||||
|
|
||||||
|
// Store empty array for this specific row
|
||||||
|
setRowSublines(prev => ({ ...prev, [rowIndex]: [] }));
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
// Clear loading state
|
||||||
|
setIsLoadingSublines(prev => ({ ...prev, [rowIndex]: false }));
|
||||||
|
}
|
||||||
|
}, [lineSublineCache]);
|
||||||
|
|
||||||
|
// When data changes, fetch product lines and sublines for rows that have company/line values
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip if there's no data
|
||||||
|
if (!data.length) return;
|
||||||
|
|
||||||
|
// First check if we need to do anything at all
|
||||||
|
let needsFetching = false;
|
||||||
|
|
||||||
|
// Quick check for any rows that would need fetching
|
||||||
|
for (const row of data) {
|
||||||
|
const rowId = row.__index;
|
||||||
|
if (!rowId) continue;
|
||||||
|
|
||||||
|
if ((row.company && !rowProductLines[rowId]) || (row.line && !rowSublines[rowId])) {
|
||||||
|
needsFetching = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing needs fetching, exit early
|
||||||
|
if (!needsFetching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting to fetch product lines and sublines");
|
||||||
|
|
||||||
|
// Group rows by company and line to minimize API calls
|
||||||
|
const companiesNeeded = new Map<string, string[]>(); // company ID -> row IDs
|
||||||
|
const linesNeeded = new Map<string, string[]>(); // line ID -> row IDs
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
const rowId = row.__index;
|
||||||
|
if (!rowId) return; // Skip rows without an index
|
||||||
|
|
||||||
|
// If row has company but no product lines fetched yet
|
||||||
|
if (row.company && !rowProductLines[rowId]) {
|
||||||
|
const companyId = row.company.toString();
|
||||||
|
if (!companiesNeeded.has(companyId)) {
|
||||||
|
companiesNeeded.set(companyId, []);
|
||||||
|
}
|
||||||
|
companiesNeeded.get(companyId)?.push(rowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If row has line but no sublines fetched yet
|
||||||
|
if (row.line && !rowSublines[rowId]) {
|
||||||
|
const lineId = row.line.toString();
|
||||||
|
if (!linesNeeded.has(lineId)) {
|
||||||
|
linesNeeded.set(lineId, []);
|
||||||
|
}
|
||||||
|
linesNeeded.get(lineId)?.push(rowId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Need to fetch product lines for ${companiesNeeded.size} companies and sublines for ${linesNeeded.size} lines`);
|
||||||
|
|
||||||
|
// If nothing to fetch, exit early to prevent unnecessary processing
|
||||||
|
if (companiesNeeded.size === 0 && linesNeeded.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create arrays to hold all fetch promises
|
||||||
|
const fetchPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
// Set initial loading states for all affected rows
|
||||||
|
const lineLoadingUpdates: Record<string, boolean> = {};
|
||||||
|
const sublineLoadingUpdates: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
// Process companies that need product lines
|
||||||
|
companiesNeeded.forEach((rowIds, companyId) => {
|
||||||
|
// Skip if already in cache
|
||||||
|
if (companyLinesCache[companyId]) {
|
||||||
|
console.log(`Using cached product lines for company ${companyId}`);
|
||||||
|
// Use cached data for all rows with this company
|
||||||
|
const lines = companyLinesCache[companyId];
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = lines;
|
||||||
|
});
|
||||||
|
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set loading state for all affected rows
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
lineLoadingUpdates[rowId] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create fetch promise
|
||||||
|
const fetchPromise = (async () => {
|
||||||
|
// Safety timeout to ensure loading state is cleared after 10 seconds
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
console.log(`Safety timeout triggered for company ${companyId}`);
|
||||||
|
const clearLoadingUpdates: Record<string, boolean> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
clearLoadingUpdates[rowId] = false;
|
||||||
|
});
|
||||||
|
setIsLoadingLines(prev => ({ ...prev, ...clearLoadingUpdates }));
|
||||||
|
|
||||||
|
// Set empty cache to prevent repeated requests
|
||||||
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
|
||||||
|
|
||||||
|
// Update rows with empty array
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = [];
|
||||||
|
});
|
||||||
|
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||||
|
|
||||||
|
toast.error(`Timeout loading product lines for company ${companyId}`);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching product lines for company ${companyId} (affecting ${rowIds.length} rows)`);
|
||||||
|
|
||||||
|
// Fetch product lines from API
|
||||||
|
const productLinesUrl = `/api/import/product-lines/${companyId}`;
|
||||||
|
console.log(`Fetching from URL: ${productLinesUrl}`);
|
||||||
|
|
||||||
|
const response = await axios.get(productLinesUrl);
|
||||||
|
console.log(`Product lines API response status for company ${companyId}:`, response.status);
|
||||||
|
|
||||||
|
const productLines = response.data;
|
||||||
|
console.log(`Received ${productLines.length} product lines for company ${companyId}`);
|
||||||
|
|
||||||
|
// Store in company cache
|
||||||
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: productLines }));
|
||||||
|
|
||||||
|
// Update all rows with this company
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = productLines;
|
||||||
|
});
|
||||||
|
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching product lines for company ${companyId}:`, error);
|
||||||
|
|
||||||
|
// Set empty array for this company to prevent repeated failed requests
|
||||||
|
setCompanyLinesCache(prev => ({ ...prev, [companyId]: [] }));
|
||||||
|
|
||||||
|
// Update rows with empty array
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = [];
|
||||||
|
});
|
||||||
|
setRowProductLines(prev => ({ ...prev, ...updates }));
|
||||||
|
|
||||||
|
// Show error toast
|
||||||
|
toast.error(`Failed to load product lines for company ${companyId}`);
|
||||||
|
} finally {
|
||||||
|
// Clear the safety timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Clear loading state for all affected rows
|
||||||
|
const clearLoadingUpdates: Record<string, boolean> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
clearLoadingUpdates[rowId] = false;
|
||||||
|
});
|
||||||
|
setIsLoadingLines(prev => ({ ...prev, ...clearLoadingUpdates }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
fetchPromises.push(fetchPromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process lines that need sublines
|
||||||
|
linesNeeded.forEach((rowIds, lineId) => {
|
||||||
|
// Skip if already in cache
|
||||||
|
if (lineSublineCache[lineId]) {
|
||||||
|
console.log(`Using cached sublines for line ${lineId}`);
|
||||||
|
// Use cached data for all rows with this line
|
||||||
|
const sublines = lineSublineCache[lineId];
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = sublines;
|
||||||
|
});
|
||||||
|
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set loading state for all affected rows
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
sublineLoadingUpdates[rowId] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create fetch promise
|
||||||
|
const fetchPromise = (async () => {
|
||||||
|
// Safety timeout to ensure loading state is cleared after 10 seconds
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
console.log(`Safety timeout triggered for line ${lineId}`);
|
||||||
|
const clearLoadingUpdates: Record<string, boolean> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
clearLoadingUpdates[rowId] = false;
|
||||||
|
});
|
||||||
|
setIsLoadingSublines(prev => ({ ...prev, ...clearLoadingUpdates }));
|
||||||
|
|
||||||
|
// Set empty cache to prevent repeated requests
|
||||||
|
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
|
||||||
|
|
||||||
|
// Update rows with empty array
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = [];
|
||||||
|
});
|
||||||
|
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||||
|
|
||||||
|
toast.error(`Timeout loading sublines for line ${lineId}`);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching sublines for line ${lineId} (affecting ${rowIds.length} rows)`);
|
||||||
|
|
||||||
|
// Fetch sublines from API
|
||||||
|
const sublinesUrl = `/api/import/sublines/${lineId}`;
|
||||||
|
console.log(`Fetching from URL: ${sublinesUrl}`);
|
||||||
|
|
||||||
|
const response = await axios.get(sublinesUrl);
|
||||||
|
console.log(`Sublines API response status for line ${lineId}:`, response.status);
|
||||||
|
|
||||||
|
const sublines = response.data;
|
||||||
|
console.log(`Received ${sublines.length} sublines for line ${lineId}`);
|
||||||
|
|
||||||
|
// Store in line cache
|
||||||
|
setLineSublineCache(prev => ({ ...prev, [lineId]: sublines }));
|
||||||
|
|
||||||
|
// Update all rows with this line
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = sublines;
|
||||||
|
});
|
||||||
|
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching sublines for line ${lineId}:`, error);
|
||||||
|
|
||||||
|
// Set empty array for this line to prevent repeated failed requests
|
||||||
|
setLineSublineCache(prev => ({ ...prev, [lineId]: [] }));
|
||||||
|
|
||||||
|
// Update rows with empty array
|
||||||
|
const updates: Record<string, any[]> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
updates[rowId] = [];
|
||||||
|
});
|
||||||
|
setRowSublines(prev => ({ ...prev, ...updates }));
|
||||||
|
|
||||||
|
// Show error toast
|
||||||
|
toast.error(`Failed to load sublines for line ${lineId}`);
|
||||||
|
} finally {
|
||||||
|
// Clear the safety timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Clear loading state for all affected rows
|
||||||
|
const clearLoadingUpdates: Record<string, boolean> = {};
|
||||||
|
rowIds.forEach(rowId => {
|
||||||
|
clearLoadingUpdates[rowId] = false;
|
||||||
|
});
|
||||||
|
setIsLoadingSublines(prev => ({ ...prev, ...clearLoadingUpdates }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
fetchPromises.push(fetchPromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set initial loading states
|
||||||
|
if (Object.keys(lineLoadingUpdates).length > 0) {
|
||||||
|
console.log(`Setting loading state for ${Object.keys(lineLoadingUpdates).length} rows (product lines)`);
|
||||||
|
setIsLoadingLines(prev => ({ ...prev, ...lineLoadingUpdates }));
|
||||||
|
}
|
||||||
|
if (Object.keys(sublineLoadingUpdates).length > 0) {
|
||||||
|
console.log(`Setting loading state for ${Object.keys(sublineLoadingUpdates).length} rows (sublines)`);
|
||||||
|
setIsLoadingSublines(prev => ({ ...prev, ...sublineLoadingUpdates }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all fetch operations in parallel
|
||||||
|
Promise.all(fetchPromises).then(() => {
|
||||||
|
console.log("All product lines and sublines fetch operations completed");
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error in fetch operations:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
}, [data, rowProductLines, rowSublines, companyLinesCache, lineSublineCache]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowProductLines,
|
||||||
|
rowSublines,
|
||||||
|
isLoadingLines,
|
||||||
|
isLoadingSublines,
|
||||||
|
fetchProductLines,
|
||||||
|
fetchSublines
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user