17 Commits

Author SHA1 Message Date
387e7e5e73 Clean up 2025-03-22 21:05:24 -04:00
a51a48ce89 Fix item number not getting updated when applying template 2025-03-22 20:55:34 -04:00
aacb3a2fd0 Fix validating required cells when applying template 2025-03-22 17:21:27 -04:00
35d2f0df7c Refactor validation hooks into smaller files 2025-03-21 00:33:06 -04:00
7d46ebd6ba Add skeleton loading state to template field, remove duplicated or unused code in validate step hooks 2025-03-19 14:30:39 -04:00
1496aa57b1 Remove remaining chakra-ui dependencies, clean up files, clean up build errors, move react-spreadsheet-import directory into main component structure 2025-03-19 12:56:56 -04:00
fc9ef2f0d7 Style tweaks, fix image uploads, refactor image upload step into smaller files 2025-03-19 11:27:26 -04:00
af067f7360 Fix line and subline showing as inputs instead of selects 2025-03-18 15:27:55 -04:00
949b543d1f Fix issues with validation errors showing and problems with concurrent editing, improve scroll position saving 2025-03-18 12:38:23 -04:00
8fdb68fb19 Move UPC validation table adapter out of ValidationContainer 2025-03-17 16:36:26 -04:00
136f767309 Move product line fetching out of ValidationContainer, clean up some unused files 2025-03-17 16:24:47 -04:00
aa9664c459 Move UPC validation out of ValidationContainer, add code lines tracking 2025-03-17 16:03:21 -04:00
f60f0b1b5c Merge separate itemNumberCell in to ValidationCell 2025-03-17 14:18:49 -04:00
676cd44d9d Clean up linter errors and add sequential thinking 2025-03-17 14:13:22 -04:00
1d081bb218 Optimize error processing and re-rendering in ValidationCell component. Implemented a centralized processErrors function, improved memoization, and enhanced batch updates to reduce redundancy and improve performance. 2025-03-16 15:25:23 -04:00
52ae7e10aa Refactor validation error handling to use a single source of truth (validationErrors Map), speed up item number generation and loading lines/sublines 2025-03-16 14:09:58 -04:00
153bbecc44 Add standardized error handling with new enums and interfaces for validation errors 2025-03-15 22:11:36 -04:00
148 changed files with 8895 additions and 33539 deletions

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

4
.gitignore vendored
View File

@@ -64,3 +64,7 @@ csv/**/*
!csv/.gitkeep
inventory/tsconfig.tsbuildinfo
inventory-server/scripts/.fuse_hidden00000fa20000000a
.VSCodeCounter/
.VSCodeCounter/*
.VSCodeCounter/**/*

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,22 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@chakra-ui/button": "^2.1.0",
"@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/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
@@ -60,8 +45,6 @@
"@types/js-levenshtein": "^1.1.3",
"@types/uuid": "^10.0.0",
"axios": "^1.8.1",
"chakra-react-select": "^4.7.5",
"chakra-ui-steps": "^2.0.4",
"chart.js": "^4.4.7",
"chartjs-adapter-date-fns": "^3.0.0",
"class-variance-authority": "^0.7.1",

View File

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

View File

@@ -16,7 +16,6 @@ import Forecasting from "@/pages/Forecasting";
import { Vendors } from '@/pages/Vendors';
import { Categories } from '@/pages/Categories';
import { Import } from '@/pages/Import';
import { ChakraProvider } from '@chakra-ui/react';
import { AiValidationDebug } from "@/pages/AiValidationDebug"
const queryClient = new QueryClient();
@@ -53,30 +52,28 @@ function App() {
return (
<QueryClientProvider client={queryClient}>
<ChakraProvider>
<Toaster richColors position="top-center" />
<Routes>
<Route path="/login" element={<Login />} />
<Route element={
<RequireAuth>
<MainLayout />
</RequireAuth>
}>
<Route path="/" element={<Dashboard />} />
<Route path="/products" element={<Products />} />
<Route path="/import" element={<Import />} />
<Route path="/categories" element={<Categories />} />
<Route path="/vendors" element={<Vendors />} />
<Route path="/orders" element={<Orders />} />
<Route path="/purchase-orders" element={<PurchaseOrders />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
<Route path="/forecasting" element={<Forecasting />} />
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</ChakraProvider>
<Toaster richColors position="top-center" />
<Routes>
<Route path="/login" element={<Login />} />
<Route element={
<RequireAuth>
<MainLayout />
</RequireAuth>
}>
<Route path="/" element={<Dashboard />} />
<Route path="/products" element={<Products />} />
<Route path="/import" element={<Import />} />
<Route path="/categories" element={<Categories />} />
<Route path="/vendors" element={<Vendors />} />
<Route path="/orders" element={<Orders />} />
<Route path="/purchase-orders" element={<PurchaseOrders />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
<Route path="/forecasting" element={<Forecasting />} />
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</QueryClientProvider>
);
}

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@ import {
TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { AlertCircle, AlertTriangle } from "lucide-react"
import config from "@/config"
import { format } from "date-fns"
interface Product {
pid: number;
@@ -24,6 +24,24 @@ interface Product {
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() {
const { data: products } = useQuery<Product[]>({
queryKey: ["low-stock"],

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import merge from "lodash/merge"
import { Steps } from "./steps/Steps"
import { rtlThemeSupport, themeOverrides } from "./theme"
import { Providers } from "./components/Providers"
import type { RsiProps } from "./types"
import { ModalWrapper } from "./components/ModalWrapper"
import { translations } from "./translationsRSIProps"
export const defaultTheme = themeOverrides
// Simple empty theme placeholder
export const defaultTheme = {}
export const defaultRSIProps: Partial<RsiProps<any>> = {
autoMapHeaders: true,
@@ -27,12 +27,9 @@ export const ReactSpreadsheetImport = <T extends string>(propsWithoutDefaults: R
const props = merge({}, defaultRSIProps, propsWithoutDefaults)
const mergedTranslations =
props.translations !== translations ? merge(translations, props.translations) : translations
const mergedThemes = props.rtl
? merge(defaultTheme, rtlThemeSupport, props.customTheme)
: merge(defaultTheme, props.customTheme)
return (
<Providers theme={mergedThemes} rsiValues={{ ...props, translations: mergedTranslations }}>
<Providers rsiValues={{ ...props, translations: mergedTranslations }}>
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
<Steps />
</ModalWrapper>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1188,7 +1188,6 @@ export const MatchColumnsStep = React.memo(<T extends string>({
})) as unknown as Fields<T>
const unmatched = findUnmatchedRequiredFields(typedFields, columns);
console.log("Unmatched required fields:", unmatched);
return unmatched;
}, [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
return !unmatchedRequiredFields.includes(key as any);
});
console.log("Matched required fields:", matched);
return matched;
}, [requiredFields, unmatchedRequiredFields]);

View File

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

View File

@@ -4,8 +4,6 @@ import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
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">
<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>
<RadioGroup
value={selectedRowIndex?.toString()}

View File

@@ -185,12 +185,10 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
// Apply global selections to each row of data if they exist
const dataWithGlobalSelections = globalSelections
? dataWithMeta.map((row: Data<string> & { __errors?: any; __index?: string }) => {
? dataWithMeta.map((row: Data<string> & { __index?: string }) => {
const newRow = { ...row };
if (globalSelections.supplier) newRow.supplier = globalSelections.supplier;
if (globalSelections.company) newRow.company = globalSelections.company;
if (globalSelections.line) newRow.line = globalSelections.line;
if (globalSelections.subline) newRow.subline = globalSelections.subline;
return newRow;
})
: dataWithMeta;
@@ -225,7 +223,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
onBack();
}
}}
onNext={(validatedData) => {
onNext={(validatedData: any[]) => {
// Go to image upload step with the validated data
onNext({
type: StepType.imageUpload,

View File

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

View File

@@ -1,5 +1,5 @@
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 {
Command,
@@ -50,7 +50,6 @@ const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
const [searchTerm, setSearchTerm] = useState("");
const [selectedBrand, setSelectedBrand] = useState<string | null>(null);
const [open, setOpen] = useState(false);
const [] = useState<string | null>(null);
// Set default brand when component mounts or defaultBrand changes
useEffect(() => {
@@ -232,7 +231,7 @@ const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
aria-expanded={open}
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" />
</Button>
</PopoverTrigger>

View File

@@ -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

View File

@@ -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;

View File

@@ -7,15 +7,15 @@ import {
ColumnDef
} from '@tanstack/react-table'
import { Fields, Field } from '../../../types'
import { RowData, Template } from '../hooks/useValidationState'
import { RowData, Template } from '../hooks/validationTypes'
import ValidationCell, { CopyDownContext } from './ValidationCell'
import { useRsi } from '../../../hooks/useRsi'
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 { cn } from '@/lib/utils'
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
type ErrorType = {
@@ -67,10 +67,9 @@ const MemoizedTemplateSelect = React.memo(({
}) => {
if (isLoading) {
return (
<Button variant="outline" className="w-full justify-between overflow-hidden" disabled>
<Loader2 className="h-4 w-4 mr-2 animate-spin flex-none" />
<span className="truncate overflow-hidden">Loading...</span>
</Button>
<div className="flex items-center justify-center gap-2 border border-input rounded-md px-2 py-2">
<Skeleton className="h-4 w-full" />
</div>
);
}
@@ -139,15 +138,34 @@ const MemoizedCell = React.memo(({
/>
);
}, (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
return (
prev.value === next.value &&
prev.isValidating === next.isValidating &&
prev.itemNumber === next.itemNumber &&
// Deep compare errors
prev.errors === next.errors &&
prev.options === next.options
const valueEqual = prev.value === next.value;
const isValidatingEqual = prev.isValidating === next.isValidating;
// Shallow equality check for errors array
const errorsEqual = prev.errors === next.errors || (
Array.isArray(prev.errors) &&
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';
@@ -170,14 +188,13 @@ const ValidationTable = <T extends string>({
rowProductLines = {},
rowSublines = {},
isLoadingLines = {},
isLoadingSublines = {}
isLoadingSublines = {},
isValidatingUpc,
validatingUpcRows = [],
upcValidationResults
}: ValidationTableProps<T>) => {
const { translations } = useRsi<T>();
// Debug logs
console.log('ValidationTable rowProductLines:', rowProductLines);
console.log('ValidationTable rowSublines:', rowSublines);
// Add state for copy down selection mode
const [isInCopyDownMode, setIsInCopyDownMode] = useState(false);
const [sourceRowIndex, setSourceRowIndex] = useState<number | null>(null);
@@ -243,7 +260,7 @@ const ValidationTable = <T extends string>({
</div>
),
cell: ({ row }) => (
<div className="flex h-[40px] items-center justify-center">
<div className="flex items-center justify-center py-9">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => handleRowSelect(!!value, row)}
@@ -293,10 +310,13 @@ const ValidationTable = <T extends string>({
const cache = new Map<string, readonly any[]>();
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') {
const fieldKey = String(field.key);
// Handle all select and multi-select fields the same way
if (field.fieldType &&
(typeof field.fieldType === 'object') &&
(field.fieldType.type === 'select' || field.fieldType.type === 'multi-select')) {
cache.set(fieldKey, (field.fieldType as any).options || []);
}
});
@@ -314,6 +334,34 @@ const ValidationTable = <T extends string>({
copyDown(rowIndex, fieldKey, endRowIndex);
}, [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
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
@@ -342,34 +390,83 @@ const ValidationTable = <T extends string>({
if (fieldKey === 'line' && rowId && rowProductLines[rowId]) {
options = rowProductLines[rowId];
console.log(`Setting line options for row ${rowId}:`, options);
} else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) {
options = rowSublines[rowId];
console.log(`Setting subline options for row ${rowId}:`, options);
}
// Determine if this cell is in loading state
let isLoading = validatingCells.has(`${row.index}-${field.key}`);
// Determine if this cell is in loading state - use a clear consistent approach
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
if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
isLoading = true;
console.log(`Line field for row ${rowId} is loading`);
} else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
}
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
isLoading = true;
console.log(`Subline field for row ${rowId} is loading`);
}
// 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 (
<MemoizedCell
field={field as Field<string>}
value={row.original[field.key as keyof typeof row.original]}
key={cellKey} // CRITICAL: Add key to force re-render when itemNumber changes
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)}
errors={validationErrors.get(row.index)?.[fieldKey] || []}
errors={cellErrors}
isValidating={isLoading}
fieldKey={fieldKey}
options={options}
itemNumber={itemNumbers.get(row.index)}
itemNumber={itemNumber}
width={fieldWidth}
rowIndex={row.index}
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),
[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
const columns = useMemo(() => [selectionColumn, templateColumn, ...fieldColumns], [selectionColumn, templateColumn, fieldColumns]);
@@ -387,15 +486,13 @@ const ValidationTable = <T extends string>({
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
state: { rowSelection },
state: {
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getRowId: (row) => {
if (row.__index) return row.__index;
const index = data.indexOf(row);
return String(index);
}
getCoreRowModel: getCoreRowModel(),
getRowId: useCallback((_row: RowData<T>, index: number) => String(index), []),
});
// Calculate total table width for stable horizontal scrolling
@@ -481,13 +578,16 @@ const ValidationTable = <T extends string>({
</div>
)}
<div className="relative">
{/* Custom Table Header - Always Visible */}
{/* Custom Table Header - Always Visible with GPU acceleration */}
<div
className={`sticky top-0 z-20 bg-muted border-b shadow-sm`}
style={{ width: `${totalWidth}px` }}
className="sticky top-0 z-20 bg-muted border-b shadow-sm will-change-transform"
style={{
width: `${totalWidth}px`,
transform: 'translateZ(0)', // Force GPU acceleration
}}
>
<div className="flex">
{table.getFlatHeaders().map((header, index) => {
{table.getFlatHeaders().map((header) => {
const width = header.getSize();
return (
<div
@@ -508,49 +608,57 @@ const ValidationTable = <T extends string>({
</div>
</div>
{/* Table Body */}
<Table style={{ width: `${totalWidth}px`, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, marginTop: '-1px' }}>
{/* Table Body - With optimized rendering */}
<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>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className={cn(
"hover:bg-muted/50",
row.getIsSelected() ? "bg-muted/50" : "",
validationErrors.get(data.indexOf(row.original)) &&
Object.keys(validationErrors.get(data.indexOf(row.original)) || {}).length > 0 ? "bg-red-50/40" : "",
// Add cursor-pointer class when in copy down mode for target rows
isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "cursor-pointer copy-down-target-row" : ""
)}
style={{
// Force cursor pointer on all target rows
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined,
position: 'relative' // Ensure we can position the overlay
}}
onMouseEnter={() => handleRowMouseEnter(row.index)}
>
{row.getVisibleCells().map((cell, cellIndex) => {
const width = cell.column.getSize();
return (
<TableCell
key={cell.id}
style={{
width: `${width}px`,
minWidth: `${width}px`,
maxWidth: `${width}px`,
boxSizing: 'border-box',
padding: '0',
// Force cursor pointer on all cells in target rows
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined
}}
className={isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "target-row-cell" : ""}
>
{table.getRowModel().rows.map((row) => {
// Precompute validation error status for this row
const hasErrors = validationErrors.has(parseInt(row.id)) &&
Object.keys(validationErrors.get(parseInt(row.id)) || {}).length > 0;
// Precompute copy down target status
const isCopyDownTarget = isInCopyDownMode &&
sourceRowIndex !== null &&
parseInt(row.id) > sourceRowIndex;
// Using CSS variables for better performance on hover/state changes
const rowStyle = {
cursor: isCopyDownTarget ? 'pointer' : undefined,
position: 'relative' as const,
willChange: isInCopyDownMode ? 'background-color' : 'auto',
contain: 'layout',
transition: 'background-color 100ms ease-in-out'
};
return (
<TableRow
key={row.id}
className={cn(
"hover:bg-muted/50",
row.getIsSelected() ? "!bg-blue-50/50" : "",
hasErrors ? "bg-red-50/40" : "",
isCopyDownTarget ? "cursor-pointer copy-down-target-row" : ""
)}
style={rowStyle}
onMouseEnter={() => handleRowMouseEnter(parseInt(row.id))}
>
{row.getVisibleCells().map((cell: any) => (
<React.Fragment key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))}
</React.Fragment>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</div>

View File

@@ -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 { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
@@ -46,7 +46,6 @@ const InputCell = <T extends string>({
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState('');
const [isPending, startTransition] = useTransition();
const deferredEditValue = useDeferredValue(editValue);
// Use a ref to track if we need to process the value
const needsProcessingRef = useRef(false);
@@ -107,23 +106,28 @@ const InputCell = <T extends string>({
// Handle blur event - use transition for non-critical updates
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(() => {
setIsEditing(false);
// Format the value for storage (remove formatting like $ for price)
let processedValue = deferredEditValue.trim();
let processedValue = finalValue;
if (isPrice && processedValue) {
needsProcessingRef.current = true;
}
// Update local display value immediately
// Update local display value immediately to prevent UI flicker
setLocalDisplayValue(processedValue);
// Commit the change to parent component
onChange(processedValue);
onEndEdit?.();
});
}, [deferredEditValue, onChange, onEndEdit, isPrice]);
}, [editValue, onChange, onEndEdit, isPrice]);
// Handle direct input change - optimized to be synchronous for typing
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {

View File

@@ -167,41 +167,59 @@ const MultiSelectCell = <T extends string>({
const commandListRef = useRef<HTMLDivElement>(null)
// Add state for hover
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
const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]);
// 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(() => {
if (!open) {
// Ensure value is always an array
setInternalValue(Array.isArray(value) ? value : [])
// Only sync if we should (not during internal edits) and if not open
if (shouldSyncWithExternalValue.current && !open) {
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
const handleOpenChange = useCallback((newOpen: boolean) => {
if (open && !newOpen) {
// Prevent syncing with external value during our internal update
shouldSyncWithExternalValue.current = false;
// Only update parent state when dropdown closes
// Avoid expensive deep comparison if lengths are different
if (internalValue.length !== value.length ||
internalValue.some((v, i) => v !== value[i])) {
onChange(internalValue);
}
// Make a defensive copy to avoid mutations
const valuesToCommit = [...internalValue];
// Immediate UI update
setOpen(false);
// Update parent with the value immediately
onChange(valuesToCommit);
if (onEndEdit) onEndEdit();
// Allow syncing with external value again after a short delay
setTimeout(() => {
shouldSyncWithExternalValue.current = true;
}, 0);
} else if (newOpen && !open) {
// Sync internal state with external state when opening
setInternalValue(Array.isArray(value) ? value : []);
// When opening the dropdown, sync with external value
const externalValue = Array.isArray(value) ? value : [];
setInternalValue(externalValue);
setSearchQuery(""); // Reset search query on open
setOpen(true);
if (onStartEdit) onStartEdit();
} else if (!newOpen) {
// Handle case when dropdown is already closed but handleOpenChange is called
// This ensures values are saved when clicking the chevron to close
if (internalValue.length !== value.length ||
internalValue.some((v, i) => v !== value[i])) {
onChange(internalValue);
}
if (onEndEdit) onEndEdit();
setOpen(false);
}
}, [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
const handleSelect = useCallback((selectedValue: string) => {
// Prevent syncing with external value during our internal update
shouldSyncWithExternalValue.current = false;
setInternalValue(prev => {
let newValue;
if (prev.includes(selectedValue)) {
return prev.filter(v => v !== selectedValue);
// Remove the value
newValue = prev.filter(v => v !== selectedValue);
} 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

View File

@@ -51,8 +51,6 @@ const SelectCell = <T extends string>({
// Add state for hover
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
const hasClass = (cls: string): boolean => {
const classNames = className.split(' ');
@@ -68,7 +66,6 @@ const SelectCell = <T extends string>({
// Memoize options processing to avoid recalculation on every render
const selectOptions = useMemo(() => {
console.log(`Processing options for ${field.key}:`, options);
// Fast path check - if we have raw options, just use those
if (options && options.length > 0) {
// 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
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
setInternalValue(selectedValue);
setInternalValue(valueToCommit);
// 2. Close the dropdown immediately
setOpen(false);
@@ -139,105 +139,44 @@ const SelectCell = <T extends string>({
// This prevents the parent component from re-rendering and causing dropdown to reopen
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(() => {
onChange(selectedValue);
}, 0);
setIsProcessing(false);
}, 200);
}, [onChange, onEndEdit]);
// If disabled, render a static view
if (disabled) {
if (disabled && field.key !== 'line' && field.key !== 'subline') {
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 (
<Popover
open={open}
onOpenChange={(isOpen) => {
setOpen(isOpen);
if (isOpen && onStartEdit) onStartEdit();
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
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' :
<div
className={cn(
"w-full px-3 py-2 h-10 rounded-md text-sm flex items-center",
"border",
hasErrors ? "border-destructive" : "border-input",
className
)}
style={{
backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' :
hasClass('!bg-blue-200') ? '#bfdbfe' :
hasClass('hover:!bg-blue-100') && isHovered ? '#dbeafe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
if (!open && onStartEdit) onStartEdit();
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<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>
borderColor: hasClass('!border-blue-500') ? '#3b82f6' :
hasClass('!border-blue-200') ? '#bfdbfe' :
hasClass('!border-blue-200') && isHovered ? '#bfdbfe' :
undefined,
borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined,
cursor: hasClass('hover:!bg-blue-100') ? 'pointer' : undefined
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{displayText || ""}
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { useState, useCallback } from 'react';
import { toast } from 'sonner';
import { getApiUrl, RowData } from './useValidationState';
import { Fields, InfoWithSource, ErrorSources } from '../../../types';
import { getApiUrl, RowData } from './validationTypes';
import { Fields } from '../../../types';
import { Meta } from '../types';
import { addErrorsAndRunHooks } from '../utils/dataMutations';
import * as Diff from 'diff';
@@ -88,21 +88,10 @@ export const useAiValidation = <T extends string>(
// Call the original hook
const result = await rowHook(row);
// Extract Meta-specific properties
const { __index, __errors } = result;
// Return a Meta object with properly typed errors
const { __index } = result;
// Return a Meta object with only the __index property
return {
__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
__index: __index || row.__index || ''
};
} : undefined;
@@ -112,18 +101,7 @@ export const useAiValidation = <T extends string>(
const results = await tableHook(rows);
// Extract Meta-specific properties from each result
return results.map((result, 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
__index: result.__index || rows[index].__index || ''
}));
} : undefined;
@@ -283,7 +261,7 @@ export const useAiValidation = <T extends string>(
// Clean the data to ensure we only send what's needed
const cleanedData = data.map(item => {
const { __errors, __index, ...rest } = item;
const { __index, ...rest } = item;
return rest;
});
@@ -399,8 +377,8 @@ export const useAiValidation = <T extends string>(
// Clean the data to ensure we only send what's needed
const cleanedData = data.map(item => {
const { __errors, __index, ...cleanProduct } = item;
return cleanProduct;
const { __index, ...rest } = item;
return rest;
});
console.log('Cleaned data for validation:', cleanedData);
@@ -601,7 +579,7 @@ export const useAiValidation = <T extends string>(
console.log('Data updated after AI validation:', {
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

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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
};
};

View File

@@ -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