diff --git a/.VSCodeCounter/2025-03-17_16-24-17/details.md b/.VSCodeCounter/2025-03-17_16-24-17/details.md new file mode 100644 index 0000000..06d6b8c --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/details.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-17_16-24-17/diff-details.md b/.VSCodeCounter/2025-03-17_16-24-17/diff-details.md new file mode 100644 index 0000000..e2452d9 --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/diff-details.md @@ -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 \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-17_16-24-17/diff.md b/.VSCodeCounter/2025-03-17_16-24-17/diff.md new file mode 100644 index 0000000..8cdc16b --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/diff.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-17_16-24-17/diff.txt b/.VSCodeCounter/2025-03-17_16-24-17/diff.txt new file mode 100644 index 0000000..b359f38 --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/diff.txt @@ -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 | ++-------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-17_16-24-17/results.json b/.VSCodeCounter/2025-03-17_16-24-17/results.json new file mode 100644 index 0000000..f39e682 --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/results.json @@ -0,0 +1 @@ +{"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx":{"language":"TypeScript JSX","code":219,"comment":39,"blank":47},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx":{"language":"TypeScript JSX","code":204,"comment":26,"blank":33},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx":{"language":"TypeScript JSX","code":500,"comment":75,"blank":89},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx":{"language":"TypeScript JSX","code":209,"comment":49,"blank":50},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx":{"language":"TypeScript JSX","code":1060,"comment":228,"blank":229},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx":{"language":"TypeScript JSX","code":374,"comment":42,"blank":44},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx":{"language":"TypeScript JSX","code":499,"comment":48,"blank":54},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx":{"language":"TypeScript JSX","code":248,"comment":69,"blank":74},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx":{"language":"TypeScript JSX","code":273,"comment":19,"blank":37},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx":{"language":"TypeScript JSX","code":730,"comment":126,"blank":106},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx":{"language":"TypeScript JSX","code":18,"comment":0,"blank":3},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx":{"language":"TypeScript JSX","code":232,"comment":31,"blank":32},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx":{"language":"TypeScript JSX","code":193,"comment":23,"blank":22},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx":{"language":"TypeScript JSX","code":112,"comment":12,"blank":21},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx":{"language":"TypeScript JSX","code":407,"comment":56,"blank":52},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx":{"language":"TypeScript JSX","code":289,"comment":36,"blank":31},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts":{"language":"TypeScript","code":4,"comment":0,"blank":1},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx":{"language":"TypeScript JSX","code":230,"comment":10,"blank":8},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md":{"language":"Markdown","code":39,"comment":0,"blank":19},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts":{"language":"TypeScript","code":43,"comment":24,"blank":7},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js":{"language":"JavaScript","code":28,"comment":7,"blank":9},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts":{"language":"TypeScript","code":101,"comment":59,"blank":24},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts":{"language":"TypeScript","code":124,"comment":4,"blank":14},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts":{"language":"TypeScript","code":21,"comment":15,"blank":5},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx":{"language":"TypeScript JSX","code":20,"comment":6,"blank":2},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts":{"language":"TypeScript","code":16,"comment":4,"blank":4}} \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-17_16-24-17/results.md b/.VSCodeCounter/2025-03-17_16-24-17/results.md new file mode 100644 index 0000000..31b4059 --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/results.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-17_16-24-17/results.txt b/.VSCodeCounter/2025-03-17_16-24-17/results.txt new file mode 100644 index 0000000..b75c52c --- /dev/null +++ b/.VSCodeCounter/2025-03-17_16-24-17/results.txt @@ -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 | ++------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/details.md b/.VSCodeCounter/2025-03-18_12-39-04/details.md new file mode 100644 index 0000000..495b44b --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/details.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/diff-details.md b/.VSCodeCounter/2025-03-18_12-39-04/diff-details.md new file mode 100644 index 0000000..d6950af --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/diff-details.md @@ -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 \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/diff.md b/.VSCodeCounter/2025-03-18_12-39-04/diff.md new file mode 100644 index 0000000..d190636 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/diff.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/diff.txt b/.VSCodeCounter/2025-03-18_12-39-04/diff.txt new file mode 100644 index 0000000..50afb2b --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/diff.txt @@ -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 | ++-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/results.json b/.VSCodeCounter/2025-03-18_12-39-04/results.json new file mode 100644 index 0000000..af88e87 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/results.json @@ -0,0 +1 @@ +{"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts":{"language":"TypeScript","code":43,"comment":24,"blank":7},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js":{"language":"JavaScript","code":28,"comment":7,"blank":9},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts":{"language":"TypeScript","code":101,"comment":59,"blank":24},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts":{"language":"TypeScript","code":124,"comment":4,"blank":14},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx":{"language":"TypeScript JSX","code":500,"comment":75,"blank":89},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx":{"language":"TypeScript JSX","code":360,"comment":78,"blank":85},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts":{"language":"TypeScript","code":21,"comment":15,"blank":5},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx":{"language":"TypeScript JSX","code":264,"comment":75,"blank":81},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx":{"language":"TypeScript JSX","code":204,"comment":26,"blank":33},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts":{"language":"TypeScript","code":4,"comment":0,"blank":1},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx":{"language":"TypeScript JSX","code":1190,"comment":288,"blank":289},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md":{"language":"Markdown","code":39,"comment":0,"blank":19},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx":{"language":"TypeScript JSX","code":377,"comment":49,"blank":54},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx":{"language":"TypeScript JSX","code":969,"comment":182,"blank":158},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx":{"language":"TypeScript JSX","code":20,"comment":6,"blank":2},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx":{"language":"TypeScript JSX","code":509,"comment":50,"blank":57},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx":{"language":"TypeScript JSX","code":337,"comment":88,"blank":92},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx":{"language":"TypeScript JSX","code":273,"comment":19,"blank":37},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx":{"language":"TypeScript JSX","code":18,"comment":0,"blank":3},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx":{"language":"TypeScript JSX","code":113,"comment":17,"blank":10},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx":{"language":"TypeScript JSX","code":230,"comment":10,"blank":8},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts":{"language":"TypeScript","code":16,"comment":4,"blank":4},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx":{"language":"TypeScript JSX","code":420,"comment":66,"blank":59},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx":{"language":"TypeScript JSX","code":193,"comment":23,"blank":22},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx":{"language":"TypeScript JSX","code":233,"comment":34,"blank":33},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx":{"language":"TypeScript JSX","code":227,"comment":36,"blank":32},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx":{"language":"TypeScript JSX","code":112,"comment":12,"blank":21}} \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/results.md b/.VSCodeCounter/2025-03-18_12-39-04/results.md new file mode 100644 index 0000000..80de2a7 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/results.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_12-39-04/results.txt b/.VSCodeCounter/2025-03-18_12-39-04/results.txt new file mode 100644 index 0000000..e10a94b --- /dev/null +++ b/.VSCodeCounter/2025-03-18_12-39-04/results.txt @@ -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 | ++-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/details.md b/.VSCodeCounter/2025-03-18_13-49-23/details.md new file mode 100644 index 0000000..94a919b --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/details.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/diff-details.md b/.VSCodeCounter/2025-03-18_13-49-23/diff-details.md new file mode 100644 index 0000000..aa88664 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/diff-details.md @@ -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 \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/diff.md b/.VSCodeCounter/2025-03-18_13-49-23/diff.md new file mode 100644 index 0000000..287e98c --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/diff.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/diff.txt b/.VSCodeCounter/2025-03-18_13-49-23/diff.txt new file mode 100644 index 0000000..9663c8f --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/diff.txt @@ -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 | ++---------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/results.json b/.VSCodeCounter/2025-03-18_13-49-23/results.json new file mode 100644 index 0000000..0e92663 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/results.json @@ -0,0 +1 @@ +{"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/README.md":{"language":"Markdown","code":39,"comment":0,"blank":19},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts":{"language":"TypeScript","code":4,"comment":0,"blank":1},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/index.tsx":{"language":"TypeScript JSX","code":20,"comment":6,"blank":2},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx":{"language":"TypeScript JSX","code":1190,"comment":288,"blank":289},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx":{"language":"TypeScript JSX","code":204,"comment":26,"blank":33},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx":{"language":"TypeScript JSX","code":360,"comment":78,"blank":85},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx":{"language":"TypeScript JSX","code":500,"comment":75,"blank":89},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types/index.ts":{"language":"TypeScript","code":16,"comment":4,"blank":4},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useProductLinesFetching.tsx":{"language":"TypeScript JSX","code":264,"comment":75,"blank":81},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx":{"language":"TypeScript JSX","code":337,"comment":88,"blank":92},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx":{"language":"TypeScript JSX","code":273,"comment":19,"blank":37},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts":{"language":"TypeScript","code":101,"comment":59,"blank":24},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts":{"language":"TypeScript","code":124,"comment":4,"blank":14},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx":{"language":"TypeScript JSX","code":18,"comment":0,"blank":3},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx":{"language":"TypeScript JSX","code":113,"comment":17,"blank":10},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx":{"language":"TypeScript JSX","code":969,"comment":182,"blank":158},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx":{"language":"TypeScript JSX","code":527,"comment":55,"blank":60},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx":{"language":"TypeScript JSX","code":230,"comment":10,"blank":8},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts":{"language":"TypeScript","code":21,"comment":15,"blank":5},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx":{"language":"TypeScript JSX","code":395,"comment":51,"blank":55},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts":{"language":"TypeScript","code":43,"comment":24,"blank":7},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx":{"language":"TypeScript JSX","code":112,"comment":12,"blank":21},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js":{"language":"JavaScript","code":28,"comment":7,"blank":9},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx":{"language":"TypeScript JSX","code":227,"comment":36,"blank":32},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx":{"language":"TypeScript JSX","code":233,"comment":34,"blank":33},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx":{"language":"TypeScript JSX","code":420,"comment":66,"blank":59},"file:///Users/matt/Dev/inventory/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx":{"language":"TypeScript JSX","code":193,"comment":23,"blank":22}} \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/results.md b/.VSCodeCounter/2025-03-18_13-49-23/results.md new file mode 100644 index 0000000..50a3b47 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/results.md @@ -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) \ No newline at end of file diff --git a/.VSCodeCounter/2025-03-18_13-49-23/results.txt b/.VSCodeCounter/2025-03-18_13-49-23/results.txt new file mode 100644 index 0000000..55ff082 --- /dev/null +++ b/.VSCodeCounter/2025-03-18_13-49-23/results.txt @@ -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 | ++-------------------------------------------------------------------------------------------------------------------------------------------+----------------+------------+------------+------------+------------+ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 22495cf..e7f8454 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,11 @@ dashboard-server/meta-server/._package-lock.json dashboard-server/meta-server/._services *.tsbuildinfo +uploads/* +uploads/**/* +**/uploads/* +**/uploads/**/* + # CSV data files *.csv csv/* @@ -59,3 +64,7 @@ csv/**/* !csv/.gitkeep inventory/tsconfig.tsbuildinfo inventory-server/scripts/.fuse_hidden00000fa20000000a + +.VSCodeCounter/ +.VSCodeCounter/* +.VSCodeCounter/**/* \ No newline at end of file diff --git a/docs/ValidationStep-Refactoring-Plan.md b/docs/ValidationStep-Refactoring-Plan.md new file mode 100644 index 0000000..5b97edc --- /dev/null +++ b/docs/ValidationStep-Refactoring-Plan.md @@ -0,0 +1,396 @@ +# ValidationStep Component Refactoring Plan + +## Overview + +This document outlines a comprehensive plan to refactor the current ValidationStep component (4000+ lines) into a more maintainable, modular structure. The new implementation will be developed alongside the existing component without modifying the original code. Once completed, the previous step in the workflow will offer the option to continue to either the original ValidationStep or the new implementation. + +## Table of Contents + +1. [Current Component Analysis](#current-component-analysis) +2. [New Architecture Design](#new-architecture-design) +3. [Component Structure](#component-structure) +4. [State Management](#state-management) +5. [Key Features Implementation](#key-features-implementation) +6. [Integration Plan](#integration-plan) +7. [Testing Strategy](#testing-strategy) +8. [Project Timeline](#project-timeline) +9. [Design Principles](#design-principles) +10. [Appendix: Function Reference](#appendix-function-reference) + +## Current Component Analysis + +The current ValidationStep component has several issues: + +- **Size**: At over 4000 lines, it's difficult to maintain and understand +- **Multiple responsibilities**: Handles validation, UI rendering, template management, and more +- **Special cases**: Contains numerous special case handlers and exceptions +- **Complex state management**: State is distributed across multiple useState calls +- **Tightly coupled concerns**: UI, validation logic, and business rules are intertwined + +### Key Features to Preserve + +1. **Data Validation** + - Field-level validation (required, regex, unique) + - Row-level validation (supplier, company fields) + - UPC validation with API integration + - AI-assisted validation + +2. **Template Management** + - Saving, loading, and applying templates + - Template-based validation + +3. **UI Components** + - Editable table with specialized cell renderers + - Error display and management + - Filtering and sorting capabilities + - Status indicators and progress tracking + +4. **Special Field Handling** + - Input fields with price formatting + - Multi-input fields with separator configuration + - Select fields with dropdown options + - Checkbox fields with boolean value mapping + - UPC fields with specialized validation + +5. **User Interaction Flows** + - Tab and keyboard navigation + - Bulk operations (select all, apply template) + - Row validation on value change + - Error reporting and display + +## New Architecture Design + +The new architecture will follow these principles: + +1. **Separation of Concerns** + - UI rendering separate from business logic + - Validation logic isolated from state management + - Clear interfaces between components + +2. **Composable Components** + - Small, focused components with single responsibilities + - Reusable pattern for different field types + +3. **Centralized State Management** + - Custom hooks for state management + - Clear data flow patterns + - Reduced prop drilling + +4. **Consistent Error Handling** + - Standardized error structure + - Predictable error propagation + - User-friendly error display + +5. **Performance Optimization** + - Virtualized table rendering + - Memoization of expensive computations + - Deferred validation for better user experience + +## Component Structure + +The new ValidationStepNew folder has the following structure: + +``` +ValidationStepNew/ +├── index.tsx # Main entry point that composes all pieces +├── components/ # UI Components +│ ├── ValidationContainer.tsx # Main wrapper component +│ ├── ValidationTable.tsx # Table implementation +│ ├── ValidationCell.tsx # Cell component +│ ├── ValidationSidebar.tsx # Sidebar with controls +│ ├── ValidationToolbar.tsx # Top toolbar (removed as unnecessary) +│ ├── TemplateManager.tsx # Template management +│ ├── FilterPanel.tsx # Filtering interface (integrated into Container) +│ └── cells/ # Specialized cell renderers +│ ├── InputCell.tsx +│ ├── SelectCell.tsx +│ ├── MultiInputCell.tsx +│ └── CheckboxCell.tsx +├── hooks/ # Custom hooks +│ ├── useValidationState.tsx # Main state management +│ ├── useTemplates.tsx # Template-related logic (integrated into ValidationState) +│ ├── useFilters.tsx # Filtering logic (integrated into ValidationState) +│ └── useUpcValidation.tsx # UPC-specific validation +└── utils/ # Utility functions + ├── validationUtils.ts # Validation helper functions + ├── formatters.ts # Value formatting utilities + └── constants.ts # Constant values and configuration +``` + +### Component Responsibilities + +#### ValidationContainer +- Main container component +- Coordinates between subcomponents +- Manages global state +- Handles navigation events (next, back) +- Contains filter controls + +#### ValidationTable +- Displays the data in tabular form +- Manages selection state +- Handles keyboard navigation +- Integrates with TanStack Table +- Displays properly styled rows and cells + +#### ValidationCell +- Factory component that renders appropriate cell type +- Manages cell-level state +- Handles validation errors display +- Manages edit mode +- Shows consistent error indicators + +#### TemplateManager +- Handles template selection UI +- Provides template save/load functionality +- Manages template application to rows + +#### Cell Components +- **InputCell**: Handles text input with multiline and price support +- **MultiInputCell**: Handles multiple values with separator configuration +- **SelectCell**: Command/popover component for single selection +- **CheckboxCell**: Boolean value selection with mapping support + +## State Management + +### Core State Interface + +```typescript +interface ValidationState { + // Core data + data: RowData[]; + filteredData: RowData[]; + + // Validation state + isValidating: boolean; + validationErrors: Map>; + rowValidationStatus: Map; + + // Selection state + rowSelection: RowSelectionState; + + // Template state + templates: Template[]; + selectedTemplateId: string | null; + + // Filter state + filters: FilterState; + + // Methods + updateRow: (rowIndex: number, key: T, value: any) => void; + validateRow: (rowIndex: number) => Promise; + validateUpc: (rowIndex: number, upcValue: string) => Promise; + applyTemplate: (templateId: string, rowIndexes: number[]) => void; + saveTemplate: (name: string, type: string) => void; + setFilters: (newFilters: Partial) => void; + // Additional methods... +} +``` + +### useValidationState Hook + +The main state management hook handles: + +- Data manipulation (update, sort, filter) +- Selection management +- Validation coordination +- Integration with validation utilities +- Template management +- Filtering and sorting + +## Key Features Implementation + +### 1. Field Type Handling + +Implemented a strategy pattern for different field types: + +```typescript +// In ValidationCell +const renderCellContent = () => { + const fieldType = field.fieldType.type + + switch (fieldType) { + case 'input': + return field={field} value={value} onChange={onChange} ... /> + case 'multi-input': + return field={field} value={value} onChange={onChange} ... /> + case 'select': + return field={field} value={value} onChange={onChange} ... /> + // etc. + } +} +``` + +### 2. Validation Logic + +Validation is broken down into clear steps: + +1. **Field Validation**: Apply field-level validations (required, regex, etc.) +2. **Row Validation**: Apply row-level validations and rowHook +3. **Table Validation**: Apply table-level validations (unique) and tableHook + +Validation now happens automatically without explicit buttons, with immediate feedback on field blur. + +### 3. UI Components + +UI components follow these principles: + +1. **Consistent Styling**: All components use shadcn UI for consistent look and feel +2. **Visual Feedback**: Errors are clearly indicated with icons and border styling +3. **Intuitive Editing**: Fields show outlines even when not in focus, and edit on click +4. **Proper Command Pattern**: Select and multi-select fields use command/popover pattern for better UX +5. **Focus Management**: Fields close when clicking away and perform validation on blur + +## Design Principles + +Based on user preferences and best practices, the following design principles guide this refactoring: + +1. **Automatic Validation** + - Validation should happen automatically without explicit buttons + - All validation should run on initial data load + - Fields should validate on blur (when user clicks away) + +2. **Modern UI Patterns** + - Command/popover components for all selects and multi-selects + - Consistent field outlines and borders even when not in focus + - Badge patterns for multi-select items + - Clear visual indicators for errors + +3. **Reduced Complexity** + - Remove unnecessary UI elements like "validate all" buttons + - Eliminate redundant state and toast notifications + - Simplify component hierarchy where possible + - Find root causes rather than adding special cases + +4. **Consistent Component Behavior** + - Fields should close when clicking away + - All inputs should follow the same editing pattern + - Error handling should be consistent across all field types + - Multi-select fields should allow selecting multiple items with clear visual feedback + +## Integration Plan + +### 1. Creating the New Component Structure + +Folder structure has been created without modifying the existing code: + +```bash +mkdir -p inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/{components,hooks,utils} +mkdir -p inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells +``` + +### 2. Implementing Basic Components + +Core components have been implemented: + +1. Created index.tsx as the main entry point +2. Implemented ValidationContainer with basic state management +3. Created ValidationTable for data display +4. Implemented basic cell rendering with specialized cell types + +### 3. Implementing State Management + +State management has been implemented: + +1. Created useValidationState hook +2. Implemented data transformation utilities +3. Added validation logic + +### 4. Integrating with Previous Step + +The previous step component allows choosing between validation implementations, enabling gradual testing and adoption. + +## Testing Strategy + +1. **Unit Tests** + - Test individual utility functions + - Test hooks in isolation + - Test individual UI components + +2. **Integration Tests** + - Test component interactions + - Test state management flow + - Test validation logic integration + +3. **Comparison Tests** + - Compare output of new component with original + - Verify that all functionality works the same + +4. **Performance Tests** + - Measure render times + - Evaluate memory usage + - Compare against original component + +## Project Timeline + +1. **Phase 1: Initial Structure (Completed)** + - Set up folder structure + - Implement basic components + - Create core state management + +2. **Phase 2: Core Functionality (In Progress)** + - Implement validation logic (completed) + - Create cell renderers (completed) + - Add template management (in progress) + +3. **Phase 3: Special Features (Upcoming)** + - Implement UPC validation + - Add AI validation + - Handle special cases + +4. **Phase 4: UI Refinement (Ongoing)** + - Improve error display (completed) + - Enhance user interactions (completed) + - Optimize performance (in progress) + +5. **Phase 5: Testing and Integration (Upcoming)** + - Write tests + - Fix bugs + - Integrate with previous step + +## Appendix: Function Reference + +This section documents the core functions from the original ValidationStep that need to be preserved in the new implementation. + +### Validation Functions + +1. **validateRegex** - Validates values against regex patterns +2. **getValidationError** - Determines field-level validation errors +3. **validateAndCommit** - Validates and commits new values +4. **validateData** - Validates all data rows +5. **validateUpcAndGenerateItemNumbers** - Validates UPC codes and generates item numbers + +### Formatting Functions + +1. **formatPrice** - Formats price values +2. **getDisplayValue** - Gets formatted display value based on field type +3. **isMultiInputType** - Checks if field is multi-input type +4. **getMultiInputSeparator** - Gets separator for multi-input fields +5. **isPriceField** - Checks if field should be formatted as price + +### Template Functions + +1. **loadTemplates** - Loads templates from storage +2. **saveTemplate** - Saves a new template +3. **applyTemplate** - Applies a template to selected rows +4. **getTemplateDisplayText** - Gets display text for a template + +### AI Validation Functions + +1. **handleAiValidation** - Triggers AI validation +2. **showCurrentPrompt** - Shows current AI prompt +3. **getFieldDisplayValue** - Gets display value for a field +4. **highlightDifferences** - Highlights differences between original and corrected values +5. **getFieldDisplayValueWithHighlight** - Gets display value with highlighted differences +6. **revertAiChange** - Reverts an AI-suggested change +7. **isChangeReverted** - Checks if an AI change has been reverted + +### Event Handlers + +1. **handleUpcValueUpdate** - Handles UPC value updates +2. **handleBlur** - Handles input blur events +3. **handleWheel** - Handles wheel events for navigation +4. **copyValueDown** - Copies a value to cells below +5. **handleSkuGeneration** - Generates SKUs + +By following this refactoring plan, we continue to transform the monolithic ValidationStep component into a modular, maintainable set of components while preserving all existing functionality and aligning with user preferences for design and behavior. \ No newline at end of file diff --git a/docs/ValidationStepNew-Implementation-Status.md b/docs/ValidationStepNew-Implementation-Status.md new file mode 100644 index 0000000..b18e6b2 --- /dev/null +++ b/docs/ValidationStepNew-Implementation-Status.md @@ -0,0 +1,137 @@ +# ValidationStepNew Implementation Status + +## Overview + +This document outlines the current status of the ValidationStepNew implementation, a refactored version of the original ValidationStep component. The goal is to create a more maintainable, modular component that preserves all functionality of the original while eliminating technical debt and implementing modern UI patterns. + +## Design Principles + +Based on the user's preferences, we're following these core design principles: + +1. **Automatic Validation** + - ✅ Validation runs automatically on data load + - ✅ No explicit "validate all" button needed + - ✅ Fields validate on blur when user clicks away + - ✅ Immediate visual feedback for validation errors + +2. **Modern UI Patterns** + - ✅ Command/popover components for selects and multi-selects + - ✅ Consistent field outlines and borders even when not in focus + - ✅ Badge pattern for multi-select field items + - ✅ Visual indicators for errors with appropriate styling + +3. **Reduced Complexity** + - ✅ Removed unnecessary UI elements like "validate all" button + - ✅ Eliminated redundant toast notifications + - ✅ Simplified component hierarchy + - ✅ Fixed root causes rather than adding special cases + +4. **Consistent Behavior** + - ✅ Fields close when clicking away + - ✅ All inputs follow the same editing pattern + - ✅ Error handling is consistent across field types + - ✅ Multi-select fields allow selecting multiple items + +## Completed Components + +### Core Structure +- ✅ Main component structure +- ✅ Directory organization +- ✅ TypeScript interfaces +- ✅ Props definition and passing + +### State Management +- ✅ `useValidationState` hook for centralized state +- ✅ Data validation logic +- ✅ Integration with rowHook and tableHook +- ✅ Error tracking and management +- ✅ Row selection +- ✅ Automatic validation on data load + +### UI Components +- ✅ ValidationContainer with appropriate layout +- ✅ ValidationTable with shadcn UI components +- ✅ ValidationCell factory component +- ✅ Row select/deselect functionality +- ✅ Error display and indicators +- ✅ Selection action bar + +### Cell Components +- ✅ InputCell with price and multiline support +- ✅ MultiInputCell with separator configuration +- ✅ SelectCell using command/popover pattern +- ✅ CheckboxCell with boolean mapping +- ✅ Consistent styling across all field types +- ✅ Proper edit/view state management +- ✅ Outlined borders in both edit and view modes + +### Utility Functions +- ✅ Value formatting for display +- ✅ Field type detection +- ✅ Error creation and management +- ✅ Price formatting + +### UI Improvements +- ✅ Consistent borders and field outlines +- ✅ Fields that properly close when clicking away +- ✅ Multi-select with badge UI pattern +- ✅ Command pattern for searchable select menus +- ✅ Better visual error indication + +## Pending Tasks + +### Enhanced Validation +- ⏳ AI validation system +- ⏳ Custom validation hooks +- ⏳ Enhanced UPC validation with API integration +- ⏳ Validation visualizations + +### Advanced UI Features +- ⏳ Table virtualization for performance +- ⏳ Drag-and-drop reordering +- ⏳ Bulk operations (copy down, fill all, etc.) +- ⏳ Keyboard navigation improvements +- ⏳ Template dialogs and management UI + +### Special Features +- ⏳ Image preview integration +- ⏳ SKU generation system +- ⏳ Item number generation +- ⏳ Dependent dropdown values + +### Testing +- ⏳ Unit tests for utility functions +- ⏳ Component tests +- ⏳ Integration tests +- ⏳ Performance benchmarks + +## Known Issues + +1. TypeScript error for `validationDisabled` property in ValidationCell.tsx +2. Some type casting is needed due to complex generic types +3. Need to address edge cases for multi-select fields validation +4. Proper error handling for API calls needs implementation + +## Next Steps + +1. Fix TypeScript errors in ValidationCell and related components +2. Complete template management functionality +3. Implement UPC validation with API integration +4. Make multi-select field validation more robust +5. Add comprehensive tests + +## Performance Improvements + +We've already implemented several performance optimizations: + +1. ✅ More efficient state updates by removing unnecessary re-renders +2. ✅ Better error handling to prevent cascading validations +3. ✅ Improved component isolation to prevent unnecessary re-renders +4. ✅ Automatic validation that doesn't block the UI + +Additional planned improvements: + +1. Virtualized table rendering for large datasets +2. Memoization of expensive calculations +3. Optimized state updates to minimize re-renders +4. Batched API calls for validation \ No newline at end of file diff --git a/docs/fix-multi-select.md b/docs/fix-multi-select.md new file mode 100644 index 0000000..38159c9 --- /dev/null +++ b/docs/fix-multi-select.md @@ -0,0 +1,72 @@ +# Solution: Keeping Dropdowns Open During Multiple Selections + +## The Problem + +When implementing a multi-select dropdown in React, a common issue occurs: + +1. You select an item in the dropdown +2. The `onChange` handler is called, updating the data +3. This triggers a re-render of the parent component (in this case, the entire table) +4. During the re-render, the dropdown is unmounted and remounted +5. This causes the dropdown to close before you can make multiple selections + +## The Solution: Deferred State Updates + +The key insight is to **separate local state management from parent state updates**: + +```typescript +// Step 1: Add local state to track selections +const [internalValue, setInternalValue] = useState(value) + +// Step 2: Handle popover open state changes +const handleOpenChange = useCallback((newOpen: boolean) => { + if (open && !newOpen) { + // Only update parent state when dropdown closes + if (JSON.stringify(internalValue) !== JSON.stringify(value)) { + onChange(internalValue); + } + } + + setOpen(newOpen); + + if (newOpen) { + // Sync internal state with external state when opening + setInternalValue(value); + } +}, [open, internalValue, value, onChange]); + +// Step 3: Toggle selection only updates internal state +const toggleSelection = useCallback((selectedValue: string) => { + setInternalValue(prev => { + if (prev.includes(selectedValue)) { + return prev.filter(v => v !== selectedValue); + } else { + return [...prev, selectedValue]; + } + }); +}, []); +``` + +## Why This Works + +1. **No parent re-renders during selection**: Since we're only updating local state, the parent component doesn't re-render during selection. +2. **Consistent UI**: The dropdown shows accurate selected states using the internal value. +3. **Data integrity**: The final selections are properly synchronized back to the parent when done. +4. **Resilient to external changes**: Initial state is synchronized when opening the dropdown. + +## Implementation Steps + +1. Create a local state variable to track selections inside the component +2. Only make selections against this local state while the dropdown is open +3. Defer updating the parent until the dropdown is explicitly closed +4. When opening, synchronize the internal state with the external value + +## Benefits + +This pattern: +- Avoids re-render cycles that would unmount the dropdown +- Maintains UI consistency during multi-selection +- Simplifies the component's interaction with parent components +- Works with existing component lifecycles rather than fighting against them + +This solution is much simpler than trying to prevent event propagation or manipulating DOM events, and addresses the root cause of the issue: premature re-rendering. \ No newline at end of file diff --git a/docs/validate-table-changes-implementation-issue4.md b/docs/validate-table-changes-implementation-issue4.md new file mode 100644 index 0000000..0fc814f --- /dev/null +++ b/docs/validate-table-changes-implementation-issue4.md @@ -0,0 +1,239 @@ +# Validation Display Issue Implementation + +## Issue Being Addressed + +**Validation Display Issue**: Validation isn't happening beyond checking if a cell is required or not. All validation rules defined in import.tsx need to be respected. +* Required fields correctly show a red border when empty (✅ ALREADY WORKING) +* Non-empty fields with validation errors (regex, unique, etc.) should show a red border AND an alert circle icon with tooltip explaining the error (❌ NOT WORKING) + +## Implementation Attempts + +!!!!**NOTE** All previous attempts have been reverted and are no longer part of the code, please take this into account when trying a new solution. !!!! + +### Attempt 1: Fix Validation Display Logic + +**Approach**: Modified `processErrors` function to separate required errors from validation errors and show alert icons only for non-empty fields with validation errors. + +**Changes Made**: +```typescript +function processErrors(value: any, errors: ErrorObject[]) { + // ...existing code... + + // Separate required errors from other validation errors + const requiredErrors = errors.filter(error => + error.message?.toLowerCase().includes('required') + ); + const validationErrors = errors.filter(error => + !error.message?.toLowerCase().includes('required') + ); + + const isRequiredButEmpty = valueIsEmpty && requiredErrors.length > 0; + const hasValidationErrors = validationErrors.length > 0; + const shouldShowErrorIcon = hasValidationErrors && !valueIsEmpty; + + // ...more code... +} +``` + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 2: Comprehensive Fix for Validation Display + +**Approach**: Completely rewrote `processErrors` function with consistent empty value detection, clear error separation, and improved error message extraction. + +**Changes Made**: +```typescript +function processErrors(value: any, errors: ErrorObject[]) { + if (!errors || errors.length === 0) { + return { filteredErrors: [], hasError: false, isRequiredButEmpty: false, + shouldShowErrorIcon: false, errorMessages: '' }; + } + + const valueIsEmpty = isEmpty(value); + const requiredErrors = errors.filter(error => + error.message?.toLowerCase().includes('required') + ); + const validationErrors = errors.filter(error => + !error.message?.toLowerCase().includes('required') + ); + + let filteredErrors = valueIsEmpty ? requiredErrors : validationErrors; + + const isRequiredButEmpty = valueIsEmpty && requiredErrors.length > 0; + const hasValidationErrors = validationErrors.length > 0; + const hasError = isRequiredButEmpty || hasValidationErrors; + const shouldShowErrorIcon = hasValidationErrors && !valueIsEmpty; + + let errorMessages = ''; + if (shouldShowErrorIcon) { + errorMessages = validationErrors.map(getErrorMessage).join('\n'); + } + + return { filteredErrors, hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages }; +} +``` + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 3: Simplified Error Processing Logic + +**Approach**: Refactored `processErrors` to use shared `isEmpty` function, simplified error icon logic, and made error message extraction more direct. + +**Changes Made**: +```typescript +function processErrors(value: any, errors: ErrorObject[]) { + if (!errors || errors.length === 0) { + return { filteredErrors: [], hasError: false, isRequiredButEmpty: false, + shouldShowErrorIcon: false, errorMessages: '' }; + } + + const valueIsEmpty = isEmpty(value); + const requiredErrors = errors.filter(error => + error.message?.toLowerCase().includes('required') + ); + const validationErrors = errors.filter(error => + !error.message?.toLowerCase().includes('required') + ); + + let filteredErrors = valueIsEmpty ? requiredErrors : validationErrors; + + const isRequiredButEmpty = valueIsEmpty && requiredErrors.length > 0; + const hasValidationErrors = !valueIsEmpty && validationErrors.length > 0; + const hasError = isRequiredButEmpty || hasValidationErrors; + const shouldShowErrorIcon = hasValidationErrors; + + let errorMessages = ''; + if (shouldShowErrorIcon) { + errorMessages = validationErrors.map(getErrorMessage).join('\n'); + } + + return { filteredErrors, hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages }; +} +``` + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 4: Consistent Error Processing Across Components + +**Approach**: Updated both `processErrors` function and `ValidationCell` component to ensure consistent error handling between components. + +**Changes Made**: +```typescript +// In processErrors function +function processErrors(value: any, errors: ErrorObject[]) { + // Similar to Attempt 3 with consistent error handling +} + +// In ValidationCell component +const ValidationCell = ({ field, value, onChange, errors, /* other props */ }) => { + // ...existing code... + + // Use the processErrors function to handle validation errors + const { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages } = + React.useMemo(() => processErrors(value, errors), [value, errors]); + + // ...rest of the component... +} +``` + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 5: Unified Error Processing with ItemNumberCell + +**Approach**: Replaced custom error processing in `ValidationCell` with the same `processErrors` utility used by `ItemNumberCell`. + +**Changes Made**: +```typescript +const ValidationCell = ({ field, value, onChange, errors, /* other props */ }) => { + // State and context setup... + + // For item_number fields, use the specialized component + if (fieldKey === 'item_number') { + return ; + } + + // Use the same processErrors utility function that ItemNumberCell uses + const { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages } = + React.useMemo(() => processErrors(value, errors), [value, errors]); + + // Rest of component... +} +``` + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 6: Standardize Error Processing Across Cell Types + +**Approach**: Standardized error handling across all cell types using the shared `processErrors` utility function. + +**Changes Made**: Similar to Attempt 5, with focus on standardizing the approach for determining when to show validation error icons. + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 7: Replace Custom Error Processing with Shared Utility + +**Approach**: Ensured consistent error handling between `ItemNumberCell` and regular `ValidationCell` components. + +**Changes Made**: Similar to Attempts 5 and 6, with focus on using the shared utility function consistently. + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +### Attempt 8: Improved Error Normalization and Deep Comparison + +**Approach**: Modified `MemoizedCell` in `ValidationTable.tsx` to use deep comparison for error objects and improved error normalization. + +**Changes Made**: +```typescript +// Create a memoized cell component +const MemoizedCell = React.memo(({ field, value, onChange, errors, /* other props */ }) => { + return ; +}, (prev, next) => { + // Basic prop comparison + if (prev.value !== next.value) return false; + if (prev.isValidating !== next.isValidating) return false; + if (prev.itemNumber !== next.itemNumber) return false; + + // Deep compare errors - critical for validation display + if (!prev.errors && next.errors) return false; + if (prev.errors && !next.errors) return false; + if (prev.errors && next.errors) { + if (prev.errors.length !== next.errors.length) return false; + + // Compare each error object + for (let i = 0; i < prev.errors.length; i++) { + if (prev.errors[i].message !== next.errors[i].message) return false; + if (prev.errors[i].level !== next.errors[i].level) return false; + if (prev.errors[i].source !== next.errors[i].source) return false; + } + } + + // Compare options... + + return true; +}); + +// In the field columns definition: +cell: ({ row }) => { + const rowErrors = validationErrors.get(row.index); + const cellErrors = rowErrors?.[fieldKey] || []; + + // Ensure cellErrors is always an array + const normalizedErrors = Array.isArray(cellErrors) ? cellErrors : [cellErrors]; + + return ; +} +``` + +**Result**: Non-empty fields with validation errors still aren't displaying the alert icon with tooltip. + +## Root Causes (Revised Hypothesis) + +After multiple attempts, the issue appears more complex than initially thought. Possible root causes: + +1. **Error Object Structure**: Error objects might not have the expected structure or properties +2. **Error Propagation**: Errors might be getting filtered out before reaching cell components +3. **Validation Rules Configuration**: Validation rules in import.tsx might be incorrectly configured +4. **Error State Management**: Error state might not be properly updated or might be reset incorrectly +5. **Component Rendering Logic**: Components might not re-render when validation state changes +6. **CSS/Styling Issues**: Validation icons might be rendered but hidden due to styling issues +7. **Validation Timing**: Validation might be happening at the wrong time or getting overridden \ No newline at end of file diff --git a/docs/validate-table-changes-implementation-issue8.md b/docs/validate-table-changes-implementation-issue8.md new file mode 100644 index 0000000..314771b --- /dev/null +++ b/docs/validate-table-changes-implementation-issue8.md @@ -0,0 +1,138 @@ +# Multiple Cell Edit Issue Implementation + +## Issue Being Addressed + +**Multiple Cell Edit Issue**: When you enter values in 2+ cells before validation finishes, contents from all edited cells get erased when validation finishes. + +## Implementation Attempts + +### Attempt 1: Fix Multiple Cell Edit Issue (First Approach) + +**Approach**: +- Added a tracking mechanism using a Set to keep track of cells that are currently being edited +- Modified the `flushPendingUpdates` function to preserve values of cells being edited +- Added cleanup of editing state after validation completes + +**Changes Made**: +```typescript +// Add ref to track cells currently being edited +const currentlyEditingCellsRef = useRef(new Set()); + +// Update a row's field value +const updateRow = useCallback((rowIndex: number, key: T, value: any) => { + // Add this cell to currently editing cells + const cellKey = `${rowIndex}-${key}`; + currentlyEditingCellsRef.current.add(cellKey); + + // ...existing code... + + // After validation completes, remove this cell from currently editing list + setTimeout(() => { + currentlyEditingCellsRef.current.delete(cellKey); + }, 100); +}, []); + +// Modify flushPendingUpdates to respect currently editing cells +const flushPendingUpdates = useCallback(() => { + // ...existing code... + + if (dataUpdates.length > 0) { + setData(prev => { + // ...existing code... + + dataUpdates.forEach((row, index) => { + if (index < newData.length) { + const updatedRow = { ...row }; + + // Check if any fields in this row are currently being edited + // If so, preserve their current values in the previous data + Object.keys(prev[index] || {}).forEach(key => { + const cellKey = `${index}-${key}`; + if (currentlyEditingCellsRef.current.has(cellKey)) { + // Keep the value from the previous state for this field + updatedRow[key] = prev[index][key]; + } + }); + + newData[index] = updatedRow; + } + }); + + return newData; + }); + } +}, []); +``` + +**Result**: +- Slight improvement - the first value entered was saved, but any subsequent values still got erased + +### Attempt 2: Fix Multiple Cell Edit Issue (Second Approach) + +**Approach**: +- Completely revised the cell editing tracking system +- Used a Map with timestamps to track editing cells more accurately +- Added proper Promise-based tracking for cell validation +- Increased timeout from 100ms to 1000ms +- Made cleanup more robust by checking if it's still the same editing session + +**Changes Made**: +```typescript +// Add ref to track cells currently being edited with timestamps +const currentlyEditingCellsRef = useRef(new Map()); + +// Add ref to track validation promises +const validationPromisesRef = useRef>>(new Map()); + +// Update a row's field value +const updateRow = useCallback((rowIndex: number, key: T, value: any) => { + // Mark this cell as being edited with the current timestamp + const cellKey = `${rowIndex}-${key}`; + currentlyEditingCellsRef.current.set(cellKey, Date.now()); + + // ...existing code... + + // Create a validation promise + const validationPromise = new Promise((resolve) => { + setTimeout(() => { + try { + validateRow(rowIndex); + } finally { + resolve(); + } + }, 0); + }); + + validationPromisesRef.current.set(cellKey, validationPromise); + + // When validation is complete, remove from validating cells + validationPromise.then(() => { + // ...existing code... + + // Keep this cell in the editing state for a longer time + setTimeout(() => { + if (currentlyEditingCellsRef.current.has(cellKey)) { + currentlyEditingCellsRef.current.delete(cellKey); + } + }, 1000); // Keep as "editing" for 1 second + }); +}, []); +``` + +**Result**: +- Worse than the first approach - now all values get erased, including the first one + +## Root Causes (Hypothesized) + +- The validation process might be updating the entire data state, causing race conditions with cell edits +- The timing of validation completions might be problematic +- State updates might be happening in a way that overwrites user changes +- The cell state tracking system is not robust enough to prevent overwrites + +## Next Steps + +The issue requires a more fundamental approach than just tweaking the editing logic. We need to: + +1. Implement a more robust state management system for cell edits that can survive validation cycles +2. Consider disabling validation during active editing +3. Implement a proper "dirty state" tracking system for cells \ No newline at end of file diff --git a/docs/validate-table-changes.md b/docs/validate-table-changes.md new file mode 100644 index 0000000..ceca153 --- /dev/null +++ b/docs/validate-table-changes.md @@ -0,0 +1,305 @@ +# Current Issues to Address +4. Validation isn't happening beyond checking if a cell is required or not - needs to respect rules in import.tsx + * Red cell outline if cell is required and it's empty + * Red outline + alert circle icon with tooltip if cell is NOT empty and isn't valid +8. When you enter a value in 2+ cells before validation finishes, contents from all edited cells get erased when validation finishes + +## Do NOT change or edit +* Anything related to AI validation +* Anything about how templates or UPC validation work (only focus on specific issues described above) +* Anything outside of the ValidationStepNew folder + +## Issues already fixed - do not work on these +✅FIXED 1. The red row background should go away when all cells in the row are valid and all required cells are populated +✅FIXED 2. Columns alignment with header is slightly off, gets worse the further right you go +✅FIXED 3. The copy down button is in the way of the validation error icon and the select open trigger - all three need to be in unique locations +✅FIXED 5. Description column needs to have an expanded view of some sort, maybe a popover to allow for easier editing + * Don't distort table to make it happen +✅FIXED 6. Need to ensure all cell's contents don't overflow the input (truncate). COO does this currently, probably more +✅FIXED 7. The template select cell is expanding, needs to be fixed size and truncate +✅FIXED 9. Import dialog state not fully reset when closing? (validate data step appears scrolled to the middle of the table where I left it) +✅FIXED 10. UPC column doesn't need to show loading state when Item Number is being processed, only show on item number column +✅FIXED 11. Copy down needs to show a loading state on the cells that it will copy to +✅FIXED 12. Shipping restrictions/tax category should default to ID 0 if we didn't get it elsewhere +✅FIXED 13. Header row should be sticky (both up/down and left/right) +✅FIXED 14. Need a way to scroll around table if user doesn't have mouse wheel for left/right +✅FIXED 15. Enhance copy down feature by allowing user to choose the last cell to copy to, instead of going all the way to the bottom + +--------- + +# Validation Step Components Overview + +## Core Components + +### ValidationContainer +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx` +- Main wrapper component for the validation step +- Manages global state and coordinates between subcomponents +- Handles navigation events (next, back) +- Manages template application and validation state +- Coordinates UPC validation and product line loading +- Manages row selection and filtering +- Contains cache management for UPC validation results +- Maintains item number references separate from main data + +### ValidationTable +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx` +- Handles data display and column configuration +- Uses TanStack Table for core functionality +- Features: + - Sticky header (both vertical and horizontal) - currently doesn't work properly + - Row selection with checkboxes + - Template selection column + - Dynamic column widths based on field types - specified in import.tsx component + - Copy down functionality for cell values + - Error highlighting for rows and cells + - Loading states for cells being validated + +### ValidationCell +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx` +- Base cell component that renders different cell types based on field configuration +- Handles error display with tooltips +- Manages copy down button visibility +- Supports loading states during validation +- Cell Types: + 1. InputCell: For single-value text input + 2. SelectCell: For dropdown selection + 3. MultiInputCell: For multiple value inputs + 4. Template selection cells with SearchableTemplateSelect component + +### SearchableTemplateSelect +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx` +- Advanced template selection component with search functionality +- Features: + - Real-time search filtering of templates + - Customizable display text for templates + - Support for default brand selection + - Accessible popover interface + - Keyboard navigation support + - Custom styling through className props + - Scroll event handling for nested scrollable areas + +### TemplateManager +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/TemplateManager.tsx` +- Comprehensive template management interface +- Features: + - Template selection with search functionality + - Save template dialog with name and type inputs + - Batch template application to selected rows + - Template count tracking + - Toast notifications for user feedback + - Dialog-based interface for template operations + +### AiValidationDialogs +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/AiValidationDialogs.tsx` +- Manages AI-assisted validation dialogs and interactions + +### SaveTemplateDialog +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/SaveTemplateDialog.tsx` +- Dialog component for saving new templates + +## Cell Components + +### InputCell +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx` +- Handles single value text input +- Features: + - Inline/edit mode switching + - Multiline support + - Price formatting + - Error state display + - Loading state during validation + - Width constraints + - Automated cleanPriceFields processing for "$" formatting + +### SelectCell +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx` +- Handles dropdown selection +- Features: + - Searchable dropdown + - Custom option rendering + - Error state display + - Loading state during validation + - Width constraints + - Disabled state support + - Deferred search query handling for performance + +### MultiInputCell +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx` +- Handles multiple value inputs +- Features: + - Comma-separated input support + - Multi-select dropdown for predefined options + - Custom separators + - Badge display for selected count + - Truncation for long values + - Width constraints + - Price formatting support + - Internal state management to avoid excessive re-renders + +## Validation System + +### useValidation Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidation.tsx` +- Provides core validation logic +- Validates at multiple levels: + 1. Field-level validation (required, regex, unique) + 2. Row-level validation (supplier, company fields) + 3. Table-level validation + 4. Custom validation hooks support +- Error object structure includes message, level, and source properties +- Handles debounced validation updates to avoid UI freezing + +### useAiValidation Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useAiValidation.tsx` +- Manages AI-assisted validation logic and state +- Features: + - Tracks detailed changes per product + - Manages validation progress with estimated completion time + - Handles warnings and change suggestions + - Supports diff generation for changes + - Progress tracking with step indicators + - Prompt management for AI interactions + - Timer management for long-running operations + +### useTemplates Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useTemplates.tsx` +- Comprehensive template management system +- Features: + - Template CRUD operations + - Template application logic + - Default value handling + - Template search and filtering + - Batch template operations + - Template validation + +### useUpcValidation Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useUpcValidation.tsx` +- Dedicated UPC validation management +- Features: + - UPC format validation + - Supplier data validation + - Cache management for validation results + - Batch processing of UPC validations + - Item number generation logic + - Loading state management + +### useFilters Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useFilters.tsx` +- Advanced filtering system for table data +- Features: + - Multiple filter criteria support + - Dynamic filter updates + - Filter persistence + - Filter combination logic + - Performance optimized filtering + +### useValidationState Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx` +- Manages global validation state +- Handles: + - Data updates + - Template management + - Error tracking using Map objects + - Row selection + - Filtering + - UPC validation with caching to prevent duplicate API calls + - Product line loading + - Batch processing of updates + - Default value application for tax_cat and ship_restrictions (defaulting to "0") + - Price field auto-formatting to remove "$" symbols + +### Utility Files +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validationUtils.ts` +- Core validation utility functions +- Includes: + - Field validation logic + - Error message formatting + - Validation rule processing + - Type checking utilities + +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/errorUtils.ts` +- Error handling and formatting utilities +- Includes: + - Error object creation + - Error message formatting + - Error source tracking + - Error level management + +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/dataMutations.ts` +- Data transformation and mutation utilities +- Includes: + - Row data updates + - Batch data processing + - Data structure conversions + - Change tracking + +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/validation-helper.js` +- Helper functions for validation +- Includes: + - Common validation patterns + - Validation state management + - Validation result processing + +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/utils/upcValidation.ts` +- UPC-specific validation utilities +- Includes: + - UPC format checking + - Checksum validation + - Supplier data matching + - Cache management + +### Types +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/types.ts` +- Core type definitions for the validation step + +### Validation Types +1. Required field validation +2. Regex pattern validation +3. Unique value validation +4. Custom field validation +5. Row-level validation +6. Table-level validation + +## State Management + +### useValidationState Hook +`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx` +- Manages global validation state +- Handles: + - Data updates + - Template management + - Error tracking using Map objects + - Row selection + - Filtering + - UPC validation with caching to prevent duplicate API calls + - Product line loading + - Batch processing of updates + - Default value application for tax_cat and ship_restrictions (defaulting to "0") + - Price field auto-formatting to remove "$" symbols + +## UPC Validation System + +### UPC Processing +- Validates UPCs against supplier data +- Cache system for UPC validation results +- Batch processing of UPC validation requests +- Auto-generation of item numbers based on UPC +- Special loading states for UPC/item number fields +- Separate state tracking to avoid unnecessary data structure updates + +## Template System + +### Template Management +- Supports saving and loading templates +- Template application to single/multiple rows +- Default template values +- Template search and filtering + +## Performance Optimizations +1. Memoized components to prevent unnecessary renders +2. Virtualized table for large datasets +3. Deferred value updates for search inputs +4. Efficient error state management +5. Optimized cell update handling + diff --git a/docs/validation-hook-refactor.md b/docs/validation-hook-refactor.md new file mode 100644 index 0000000..6895166 --- /dev/null +++ b/docs/validation-hook-refactor.md @@ -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. diff --git a/docs/validation-process-issues.md b/docs/validation-process-issues.md new file mode 100644 index 0000000..9d9b7b2 --- /dev/null +++ b/docs/validation-process-issues.md @@ -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>({}); +``` + +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 = {} + +// 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 \ No newline at end of file diff --git a/docs/validation-table-scroll-issue.md b/docs/validation-table-scroll-issue.md new file mode 100644 index 0000000..8177a61 --- /dev/null +++ b/docs/validation-table-scroll-issue.md @@ -0,0 +1,538 @@ +# ValidationTable Scroll Position Issue + +## Problem Description + +The `ValidationTable` component in the inventory application suffers from a persistent scroll position issue. When the table content updates or re-renders, the scroll position resets to the top left corner. This creates a poor user experience, especially when users are working with large datasets and need to maintain their position while making edits or filtering data. + +Specific behaviors: +- Scroll position resets to the top left corner during re-renders +- User loses their place in the table when data is updated +- The table does not preserve vertical or horizontal scroll position + +## Relevant Files + +- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx`** + - Main component that renders the validation table + - Handles scroll position management + +- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx`** + - Parent component that wraps ValidationTable + - Creates an EnhancedValidationTable wrapper component + - Manages data and state for the validation table + +- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/hooks/useValidationState.tsx`** + - Provides state management and data manipulation functions + - Contains scroll-related code in the `updateRow` function + +- **`inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx`** + - Renders individual cells in the table + - May influence re-renders that affect scroll position + +## Failed Attempts + +We've tried multiple approaches to fix the scroll position issue, none of which have been successful: + +### 1. Using Refs for Scroll Position + +```typescript +const scrollPosition = useRef({ + left: 0, + top: 0 +}); + +// Capture position on scroll +const handleScroll = useCallback(() => { + if (tableContainerRef.current) { + scrollPosition.current = { + left: tableContainerRef.current.scrollLeft, + top: tableContainerRef.current.scrollTop + }; + } +}, []); + +// Restore in useLayoutEffect +useLayoutEffect(() => { + const container = tableContainerRef.current; + if (container) { + const { left, top } = scrollPosition.current; + if (left || top) { + container.scrollLeft = left; + container.scrollTop = top; + } + } +}); +``` + +Result: Scroll position was still lost during updates. + +### 2. Multiple Restoration Attempts with Timeouts + +```typescript +// Multiple timeouts at different intervals +setTimeout(() => { + if (tableContainerRef.current) { + tableContainerRef.current.scrollTop = savedPosition.top; + tableContainerRef.current.scrollLeft = savedPosition.left; + } +}, 0); + +setTimeout(() => { + if (tableContainerRef.current) { + tableContainerRef.current.scrollTop = savedPosition.top; + tableContainerRef.current.scrollLeft = savedPosition.left; + } +}, 50); + +// Additional timeouts at 100ms, 300ms +``` + +Result: Still not reliable, scroll position would reset between timeouts or after all timeouts completed. + +### 3. Using MutationObserver and ResizeObserver + +```typescript +// Create a mutation observer to detect DOM changes +const mutationObserver = new MutationObserver(() => { + if (shouldPreserveScroll) { + restoreScrollPosition(); + } +}); + +// Start observing the table for DOM changes +mutationObserver.observe(scrollableContainer, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'] +}); + +// Create a resize observer +const resizeObserver = new ResizeObserver(() => { + if (shouldPreserveScroll) { + restoreScrollPosition(); + } +}); + +// Observe the table container +resizeObserver.observe(scrollableContainer); +``` + +Result: Did not reliably maintain scroll position, and sometimes caused other rendering issues. + +### 4. Recursive Restoration Approach + +```typescript +let attempts = 0; +const maxAttempts = 5; + +const restore = () => { + if (tableContainerRef.current) { + tableContainerRef.current.scrollTop = y; + tableContainerRef.current.scrollLeft = x; + + attempts++; + if (attempts < maxAttempts) { + setTimeout(restore, 50 * attempts); + } + } +}; + +restore(); +``` + +Result: No improvement, scroll position still reset. + +### 5. Using React State for Scroll Position + +```typescript +const [scrollPos, setScrollPos] = useState<{top: number; left: number}>({top: 0, left: 0}); + +// Track the scroll event +useEffect(() => { + const handleScroll = () => { + if (scrollContainerRef.current) { + setScrollPos({ + top: scrollContainerRef.current.scrollTop, + left: scrollContainerRef.current.scrollLeft + }); + } + }; + + // Add scroll listener... +}, []); + +// Restore scroll position +useLayoutEffect(() => { + const container = scrollContainerRef.current; + const { top, left } = scrollPos; + + if (top > 0 || left > 0) { + requestAnimationFrame(() => { + if (container) { + container.scrollTop = top; + container.scrollLeft = left; + } + }); + } +}, [scrollPos, data]); +``` + +Result: Caused the screen to shake violently when scrolling and did not preserve position. + +### 6. Using Key Attribute for Stability + +```typescript +return ( +
+ {/* Table content */} +
+); +``` + +Result: Did not resolve the issue and may have contributed to rendering instability. + +### 7. Removing Scroll Management from Other Components + +We removed scroll position management code from: +- `useValidationState.tsx` (in the updateRow function) +- `ValidationContainer.tsx` (in the enhancedUpdateRow function) + +Result: This did not fix the issue either. + +### 8. Simple Scroll Position Management with Event Listeners + +```typescript +// Create a ref to store scroll position +const scrollPosition = useRef({ left: 0, top: 0 }); +const tableContainerRef = useRef(null); + +// Save scroll position when scrolling +const handleScroll = useCallback(() => { + if (tableContainerRef.current) { + scrollPosition.current = { + left: tableContainerRef.current.scrollLeft, + top: tableContainerRef.current.scrollTop + }; + } +}, []); + +// Add scroll listener +useEffect(() => { + const container = tableContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + } +}, [handleScroll]); + +// Restore scroll position after data changes +useLayoutEffect(() => { + const container = tableContainerRef.current; + if (container) { + const { left, top } = scrollPosition.current; + if (left > 0 || top > 0) { + container.scrollLeft = left; + container.scrollTop = top; + } + } +}, [data]); +``` + +Result: Still did not maintain scroll position during updates. + +### 9. Memoized Scroll Container Component + +```typescript +// Create a stable scroll container that won't re-render with the table +const ScrollContainer = React.memo(({ children }: { children: React.ReactNode }) => { + const containerRef = useRef(null); + const scrollPosition = useRef({ left: 0, top: 0 }); + + const handleScroll = useCallback(() => { + if (containerRef.current) { + scrollPosition.current = { + left: containerRef.current.scrollLeft, + top: containerRef.current.scrollTop + }; + } + }, []); + + useEffect(() => { + const container = containerRef.current; + if (container) { + // Set initial scroll position if it exists + if (scrollPosition.current.left > 0 || scrollPosition.current.top > 0) { + container.scrollLeft = scrollPosition.current.left; + container.scrollTop = scrollPosition.current.top; + } + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + } + }, [handleScroll]); + + return ( +
+ {children} +
+ ); +}); +``` + +Result: Still did not maintain scroll position during updates, even with a memoized container. + +### 10. Using TanStack Table State Management + +```typescript +// Track scroll state in the table instance +const [scrollState, setScrollState] = useState({ scrollLeft: 0, scrollTop: 0 }); + +const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + state: { + rowSelection, + // Include scroll position in table state + scrollLeft: scrollState.scrollLeft, + scrollTop: scrollState.scrollTop + }, + onStateChange: (updater) => { + if (typeof updater === 'function') { + const newState = updater({ + rowSelection, + scrollLeft: scrollState.scrollLeft, + scrollTop: scrollState.scrollTop + }); + if ('scrollLeft' in newState || 'scrollTop' in newState) { + setScrollState({ + scrollLeft: newState.scrollLeft ?? scrollState.scrollLeft, + scrollTop: newState.scrollTop ?? scrollState.scrollTop + }); + } + } + } +}); + +// Handle scroll events +const handleScroll = useCallback((event: React.UIEvent) => { + const target = event.target as HTMLDivElement; + setScrollState({ + scrollLeft: target.scrollLeft, + scrollTop: target.scrollTop + }); +}, []); + +// Restore scroll position after updates +useLayoutEffect(() => { + if (tableContainerRef.current) { + tableContainerRef.current.scrollLeft = scrollState.scrollLeft; + tableContainerRef.current.scrollTop = scrollState.scrollTop; + } +}, [data, scrollState]); +``` + +Result: Still did not maintain scroll position during updates, even with table state management. + +### 11. Using CSS Sticky Positioning + +```typescript +return ( +
+ + + + {table.getFlatHeaders().map((header) => ( + + {/* Header content */} + + ))} + + + + {/* Table body content */} + +
+
+); +``` + +Result: Still did not maintain scroll position during updates, even with native CSS scrolling. + +### 12. Optimized Memoization with Object.is + +```typescript +// Memoize data structures to prevent unnecessary re-renders +const memoizedData = useMemo(() => data, [data]); +const memoizedValidationErrors = useMemo(() => validationErrors, [validationErrors]); +const memoizedValidatingCells = useMemo(() => validatingCells, [validatingCells]); +const memoizedItemNumbers = useMemo(() => itemNumbers, [itemNumbers]); + +// Use Object.is for more efficient comparisons +export default React.memo(ValidationTable, (prev, next) => { + if (!Object.is(prev.data.length, next.data.length)) return false; + + if (prev.validationErrors.size !== next.validationErrors.size) return false; + for (const [key, value] of prev.validationErrors) { + if (!next.validationErrors.has(key)) return false; + if (!Object.is(value, next.validationErrors.get(key))) return false; + } + + // ... more optimized comparisons ... +}); +``` + +Result: Caused the page to crash with "TypeError: undefined has no properties" in the MemoizedCell component. + +### 13. Simplified Component Structure + +```typescript +const ValidationTable = ({ + data, + fields, + rowSelection, + setRowSelection, + updateRow, + validationErrors, + // ... other props +}) => { + const tableContainerRef = useRef(null); + const lastScrollPosition = useRef({ left: 0, top: 0 }); + + // Simple scroll position management + const handleScroll = useCallback(() => { + if (tableContainerRef.current) { + lastScrollPosition.current = { + left: tableContainerRef.current.scrollLeft, + top: tableContainerRef.current.scrollTop + }; + } + }, []); + + useEffect(() => { + const container = tableContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + } + }, [handleScroll]); + + useLayoutEffect(() => { + const container = tableContainerRef.current; + if (container) { + const { left, top } = lastScrollPosition.current; + if (left > 0 || top > 0) { + requestAnimationFrame(() => { + if (container) { + container.scrollLeft = left; + container.scrollTop = top; + } + }); + } + } + }, [data]); + + return ( +
+ + {/* ... table content ... */} + + {table.getRowModel().rows.map((row) => ( + + {/* ... row content ... */} + + ))} + +
+
+ ); +}; +``` + +Result: Still did not maintain scroll position during updates. However, this implementation restored the subtle red highlight on rows with validation errors, which is a useful visual indicator that should be preserved in future attempts. + +### 14. Portal-Based Scroll Container + +```typescript +// Create a stable container outside of React's control +const createStableContainer = () => { + const containerId = 'validation-table-container'; + let container = document.getElementById(containerId); + + if (!container) { + container = document.createElement('div'); + container.id = containerId; + container.className = 'overflow-auto'; + container.style.maxHeight = 'calc(100vh - 300px)'; + document.body.appendChild(container); + } + + return container; +}; + +const ValidationTable = ({...props}) => { + const [container] = useState(createStableContainer); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + return () => { + if (container && container.parentNode) { + container.parentNode.removeChild(container); + } + }; + }, [container]); + + // ... table configuration ... + + return createPortal(content, container); +}; +``` + +Result: The table contents failed to render at all. The portal-based approach to maintain scroll position by moving the scroll container outside of React's control was unsuccessful. + +## Current Understanding + +The scroll position issue appears to be complex and likely stems from multiple factors: + +1. React's virtual DOM reconciliation may be replacing the scroll container element during updates +2. The table uses complex memo patterns with custom equality checks that may not be working as expected +3. The data structure may be changing in ways that cause complete re-renders +4. The component hierarchy (with EnhancedValidationTable wrapper) may be affecting DOM stability + +## Next Steps to Consider + +At this point, we have tried multiple approaches without success: +1. Various scroll position management techniques +2. Memoization and optimization strategies +3. Different component structures +4. Portal-based rendering + +Given that none of these approaches have fully resolved the issue, it may be worth: +1. Investigating if there are any parent component updates forcing re-renders +2. Profiling the application to identify the exact timing of scroll position resets +3. Considering if the current table implementation could be simplified +4. Exploring if the data update patterns could be optimized to reduce re-renders + +## Conclusion + +The scroll position issue has proven resistant to multiple solution attempts. Each approach has either failed to maintain scroll position, introduced new issues, or in some cases (like the portal-based approach) prevented the table from rendering entirely. A deeper investigation into the component lifecycle and data flow may be necessary to identify the root cause. \ No newline at end of file diff --git a/inventory-server/db/setup-schema.sql b/inventory-server/db/setup-schema.sql new file mode 100644 index 0000000..b05c2d6 --- /dev/null +++ b/inventory-server/db/setup-schema.sql @@ -0,0 +1,53 @@ +-- Templates table for storing import templates +CREATE TABLE IF NOT EXISTS templates ( + id SERIAL PRIMARY KEY, + company TEXT NOT NULL, + product_type TEXT NOT NULL, + supplier TEXT, + msrp DECIMAL(10,2), + cost_each DECIMAL(10,2), + qty_per_unit INTEGER, + case_qty INTEGER, + hts_code TEXT, + description TEXT, + weight DECIMAL(10,2), + length DECIMAL(10,2), + width DECIMAL(10,2), + height DECIMAL(10,2), + tax_cat TEXT, + size_cat TEXT, + categories TEXT[], + ship_restrictions TEXT[], + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(company, product_type) +); + +-- AI Validation Performance Tracking +CREATE TABLE IF NOT EXISTS ai_validation_performance ( + id SERIAL PRIMARY KEY, + prompt_length INTEGER NOT NULL, + product_count INTEGER NOT NULL, + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + end_time TIMESTAMP WITH TIME ZONE NOT NULL, + duration_seconds DECIMAL(10,2) GENERATED ALWAYS AS (EXTRACT(EPOCH FROM (end_time - start_time))) STORED, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create index on prompt_length for efficient querying +CREATE INDEX IF NOT EXISTS idx_ai_validation_prompt_length ON ai_validation_performance(prompt_length); + +-- Function to update the updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Trigger to automatically update the updated_at column +CREATE TRIGGER update_templates_updated_at + BEFORE UPDATE ON templates + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); \ No newline at end of file diff --git a/inventory-server/package-lock.json b/inventory-server/package-lock.json index 2d0e8aa..97e0479 100755 --- a/inventory-server/package-lock.json +++ b/inventory-server/package-lock.json @@ -9,13 +9,17 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/diff": "^7.0.1", + "axios": "^1.8.1", "bcrypt": "^5.1.1", "cors": "^2.8.5", "csv-parse": "^5.6.0", + "diff": "^7.0.0", "dotenv": "^16.4.7", "express": "^4.18.2", "multer": "^1.4.5-lts.1", "mysql2": "^3.12.0", + "openai": "^4.85.3", "pg": "^8.13.3", "pm2": "^5.3.0", "ssh2": "^1.16.0", @@ -170,6 +174,27 @@ "node": ">=10" } }, + "node_modules/@pm2/agent/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@pm2/io": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz", @@ -308,6 +333,27 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/@pm2/js-api/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@pm2/pm2-version-check": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", @@ -346,12 +392,49 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/diff": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz", + "integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.76", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", + "integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -374,6 +457,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -520,6 +615,12 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -529,6 +630,17 @@ "node": ">= 6.0.0" } }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -797,6 +909,18 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", @@ -955,6 +1079,15 @@ "node": ">= 14" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -998,6 +1131,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -1087,6 +1229,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1166,6 +1323,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", @@ -1289,6 +1455,40 @@ } } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1337,20 +1537,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1542,6 +1728,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -1648,6 +1849,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2193,6 +2403,25 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2388,6 +2617,36 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "4.85.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.85.3.tgz", + "integrity": "sha512-KTMXAK6FPd2IvsPtglMt0J1GyVrjMxCYzu/mVbCPabzzquSJoZlYpHtE0p0ScZPyt11XTc757xSO4j39j5g+Xw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/pac-proxy-agent": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", @@ -3672,6 +3931,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3742,6 +4007,15 @@ "lodash": "^4.17.14" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3774,16 +4048,18 @@ "license": "ISC" }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { diff --git a/inventory-server/package.json b/inventory-server/package.json index cd0aca7..ac667aa 100755 --- a/inventory-server/package.json +++ b/inventory-server/package.json @@ -18,13 +18,17 @@ "author": "", "license": "ISC", "dependencies": { + "@types/diff": "^7.0.1", + "axios": "^1.8.1", "bcrypt": "^5.1.1", "cors": "^2.8.5", "csv-parse": "^5.6.0", + "diff": "^7.0.0", "dotenv": "^16.4.7", "express": "^4.18.2", "multer": "^1.4.5-lts.1", "mysql2": "^3.12.0", + "openai": "^4.85.3", "pg": "^8.13.3", "pm2": "^5.3.0", "ssh2": "^1.16.0", diff --git a/inventory-server/src/prompts/product-validation.txt b/inventory-server/src/prompts/product-validation.txt new file mode 100644 index 0000000..1b1b364 --- /dev/null +++ b/inventory-server/src/prompts/product-validation.txt @@ -0,0 +1,226 @@ +I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response (e.g. do not include its key or any value) unless the specific field guidelines below say otherwise. If a product appears to be from an empty or entirely invalid line, do not include it in your response. + +Your response should be a JSON object with the following structure: +{ + "correctedData": [], // Array of corrected products + "changes": [], // Array of strings describing each change made + "warnings": [] // Array of strings with warnings or suggestions for manual review (see below for details) +} + +IMPORTANT: For all fields that use IDs (categories, supplier, company, line, subline, ship_restrictions, tax_cat, artist, themes, etc.), you MUST return the ID values, not the display names. The system will handle converting IDs to display names. + +Using the provided guidelines, focus on: +1. Correcting typos and any incorrect spelling or grammar +2. Standardizing product names +3. Correcting and enhancing descriptions by adding details, keywords, and SEO-friendly language +4. Fixing any obvious errors or inconsistencies between similar products in measurements, prices, or quantities +5. Adding correct categories, themes, and colors + +Use only the provided data and your own knowledge to make changes. Do not make assumptions or make up information that you're not sure about. If you're unable to make a change you're confident about, leave the field as is. All data passed in should be validated, corrected, and returned. All values returned should be strings, not numbers. Do not leave out any fields that were present in the original data. + +Possible reasons for including a warning in the warnings array: +- If you're unable to make a change you're confident about but you believe one needs to be made +- If there are inconsistencies in the data that could be valid but need to be reviewed +- If not enough information is provided to make a change that you believe is needed +- If you infer a value for a required field based on context + + +----------PRODUCT FIELD GUIDELINES---------- + +Fields: supplier, private_notes, company, line, subline, artist +Changes: Not allowed +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, return these fields exactly as provided with no changes + +Fields: upc, supplier_no, notions_no, item_number +Changes: Formatting only +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, trim outside white space and return these fields exactly as provided with no other changes + +Fields: hts_code +Changes: Minimal, you can correct formatting, obvious errors or inconsistencies +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, trim white space and any non-numeric characters, then return as a string. Do not validate in any other way. + +Fields: image_url +Changes: Formatting only +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, convert all comma-separated values to valid https:// URLs and return + +Fields: msrp, cost_each +Changes: Minimal, you can correct formatting, obvious errors or inconsistencies +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, strip any currency symbols and return as a string with exactly two decimal places, even if the last place is a 0. + +Fields: qty_per_unit, case_qty +Changes: Minimal, you can correct formatting, obvious errors or inconsistencies +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, strip non-numeric characters and return + +Fields: ship_restrictions +Changes: Only add a value if it's not already present +Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0. +Instructions: Always return a value exactly as provided, or return 0 if no value is provided. + +Fields: eta +Changes: Minimal, you can correct formatting, obvious errors or inconsistencies +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, return a full month name, day is optional, no year ever (e.g. “January” or “March 3”). This value is not required if not provided. + +Fields: name +Changes: Allowed to conform to guidelines, to fix typos or formatting +Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most reasonable value possible based on the naming guidelines and the other information you have. +Instructions: Always return a value that is corrected and enhanced per additional guidelines below + +Fields: description +Changes: Full creative control allowed within guidelines +Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return the most accurate description possible based on the description guidelines and the other information you have. +Instructions: Always return a value that is corrected and enhanced per additional guidelines below + +Fields: weight, length, width, height +Changes: Allowed to correct obvious errors or inconsistencies or to add missing values +Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return your best guess based on the other information you have or the dimensions for similar products. +Instructions: Always return a reasonable value (weights in ounces and dimensions in inches) that is validated against similar provided products and your knowledge of general object measurements (e.g. a sheet of paper is not going to be 3 inches thick, a pack of stickers is not going to be 250 ounces, this sheet of paper is very likely going to be the same size as that other sheet of paper from the same line). If a value is unusual or unreasonable, even wildly so, change it to match similar products or to be more reasonable. When correcting unreasonable weights or dimensions, prioritize comparisons to products from the same company and product line first, then broader category matches or common knowledge if necessary.Do not return 0 or null for any of these fields. + +Fields: coo +Changes: Formatting only +Required: Return if present in the original data. Do not return if not present. +Instructions: If present, convert all country names and abbreviations to the official ISO 3166-1 alpha-2 two-character country code. Convert any value with more than two characters to two characters only (e.g. "United States" or "USA" should both return "US"). + +Fields: tax_cat +Changes: Allowed to correct obvious errors or inconsistencies or to add missing values +Required: You must always return a value for this field, even if it's not provided in the original data. If no value is provided, return 0. +Instructions: Always return a valid numerical tax code ID from the Available Tax Codes array below. Give preference to the value provided, but correct it if another value is more accurate. You must return a value for this field. 0 should be the default value in most cases. + +Fields: size_cat +Changes: Allowed to correct obvious errors or inconsistencies or to add missing values +Required: Return if present in the original data or if not present and applicable. Do not return if not applicable (e.g. if no size categories apply based on what you know about the product). +Instructions: If present or if applicable, return one valid numerical size category ID from the Available Size Categories array below. Give preference to the value provided, but correct it if another value is more accurate. If the product name contains a match for one of the size categories (such as 12x12, 6x6, 2oz, etc) you MUST return that size category with the results. A value is not required if none of the size categories apply. + +Fields: themes +Changes: Allowed to correct obvious errors or inconsistencies or to add missing values +Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no themes apply based on what you know about the product). +Instructions: If present, confirm that each provided theme matches what you understand to be a theme of the product. Remove any themes that do not match and add any themes that are missing. Most products will have zero or one theme. Return a comma-separated list of numerical theme IDs from the Available Themes array below. If you choose a sub-theme, you do not need to include its parent theme in the list. + +Fields: colors +Changes: Allowed to correct obvious errors or inconsistencies or to add missing values +Required: Return if present in the original data or if not present and applicable. Do not return any value if not applicable (e.g. if no colors apply based on what you know about the product). +Instructions: If present or if applicable, return a comma-separated list of numerical color IDs from the Available Colors array below, using the product name as the primary guide (e.g. if the name contains Blue or a blue variant, you should return the blue color ID). A value is not required if none of the colors apply. Most products will have zero colors. + +Fields: categories +Changes: Allowed to correct obvious errors or inconsistencies or to add missing values +Required: You must always return at least one value for this field, even if it's not provided in the original data. If no value is provided, return the most appropriate category or categories based on the other information you have. +Instructions: Always return a comma-separated list of one or more valid numerical category IDs from the Available Categories array below. Give preference to the values provided, particularly if the other information isn't enough to determine a category, but correct them or add new categories if another value is more accurate. Do not return categories in the Deals or Black Friday categories, and strip these from the list if present. If you choose a subcategory at any level, you do not need to include its parent categories in the list. You must return at least one category and you can return multiple categories if applicable. All categories have equal value so their order is not important. Always try to return the most specific categories possible (e.g. one in the third level of the category hierarchy is better than one in the second level). + +----------PRODUCT NAMING GUIDELINES---------- +If there's only one of this type of product in a line: [Line Name] [Product Name] - [Company] +Example: "Cosmos Infinity Chipboard - Stamperia" +Example: "Serene Petals 6x6 Paper Pad - Prima" + +Multiple similar products in a line: [Differentiator] [Product Type] - [Line Name] - [Company] +Example: "Ice & Shells Stencil - Arctic Antarctic - Stamperia" +Example: "Astronomy Paper - Cosmos Infinity - Stamperia" + +Standalone products: [Product Name] - [Company] +Example: "Hedwig Puffy Stickers - Paper House Productions" +Example: "Heart Tree Dies - Lawn Fawn" + +Color-based products: [Color] [Product Name] - [Company] +Example: "Green Valley Enamel Dots - Altenew" +Example: "Magenta Aqua Pigment - Brutus Monroe" + +Complex products: [Differentiator] [Line] [Product Type] - [Company] +Example: "Size 6 Round Black Velvet Watercolor Brush - Silver Brush Limited" (Size 6 Round is the differentiator, Black Velvet is the line, Watercolor Brush is the product type) + +These should not be included in the name, unless there are multiple products that are otherwise identical: +- Product size +- Product weight +- Number of pages +- How many are in the package + +Naming Conventions: +- Paper sizes: Use "12x12", "8x8", "6x6" (no spaces or units of measure) +- Company names must match backend exactly +- Always capitalize every word in the name, including short articles like "The" and "An" +- Use "Idea-ology" (not "idea-ology" or "Ideaology") +- All stamps are "Stamp Set" (not "Clear Stamps" or "Rubber Stamps") +- All dies are "Dies" or "Die" (not "Die Set") +- Brands with their own naming conventions should be respected, such as "Doodle Cuts" for dies from Doodlebug + +Special Brand Rules - Ranger: +Format: [Product Name] - [Designer Line] - Ranger +Possible Designers: Dylusions, Dina Wakley MEdia, Simon Hurley create., Wendy Vecchi +Example: "Stacked Stencil - Dina Wakley MEdia - Ranger" + +Special Brand Rules - Tim Holtz products from Ranger: +Format: [Color] [Product Name/Type] - Tim Holtz Distress - Ranger +Example: "Mermaid Lagoon Tim Holtz Distress Oxide Ink Pad - Ranger" + +Special Brand Rules - Tim Holtz products from Sizzix or Stampers Anonymous: +Format: [Product Name] [Product Type] by Tim Holtz - [Company] +Example: "Leaf Fragments Thinlits Dies by Tim Holtz - Sizzix" + +Special Brand Rules - Tim Holtz products from Advantus/Idea-ology: +Format: [Product Name] - Tim Holtz Idea-ology +Example: "Tiny Vials - Tim Holtz Idea-ology" + +Special Brand Rules - Dies from Sizzix: +Include die type plus "Dies" or "Die" +Examples: +"Art Nouveau 3-D Textured Impressions Embossing Folder - Sizzix" +"Pocket Pals Thinlits Dies - Sizzix" +"Butterfly Wishes Framelits Dies & Stamps - Sizzix" + +Important Notes +- Ensure that product names are consistent across all products of the same type +- Use the minimum amount of information needed to uniquely identify the product +- Put detailed specifications in the product description, not its name + +Edge Cases +- If the product is missing a company name, infer one from the other products included in the data +- If the product is missing a clear differentiator and needs one to be unique, infer and add one from the other data provided (e.g. the description, existing size categories, etc.) + +Incorrect example: MVP Rugby - Collection Pack - Photoplay +Notes: there should be no dash between the line and the product + +Incorrect Example: A2 Easel Cards - Black - Photoplay +Notes: the differentiating factor should come first: “Black A2 Easel Cards - Photoplay”. Size is ok to include here because this is the name printed on the package. + +Incorrect Example: 6” - Scriber Needle Modeling Tool +Notes: this product only comes in one size, so 6” isn’t needed. The company name should also be included. + +Incorrect Example: Slick - White - Tulip Dimensional Fabric Paint 4oz +Notes: color should be first, then type, then product, then company, so “White Slick Dimensional Fabric Paint - Tulip”. It appears there’s only one size available so no need to differentiate in the name. + +Incorrect Example: Silhouette Adhesive Cork Sheets 5”X7” 8/Pkg +Notes: should be “Adhesive Cork Sheets - Silhouette” + +Incorrect Example: Galaxy - Opaque - American Crafts Color Pour Resin Dyes +Notes: “Galaxy Opaque Dye Set - Color Pour Resin - American Crafts” + +Incorrect Example: Slate - Lion Brand Truboo Yarn +Notes: [Differentiator] [Line] [Product Type] - [Company] : “Slate Truboo Yarn - Lion Brand” + +Incorrect Example: Rose Quartz Dylusions Shimmer Paint +Notes: “Rose Quartz Shimmer Paint - Dylusions - Ranger” + + +----------PRODUCT DESCRIPTION GUIDELINES---------- +Product descriptions are an extremely important part of the listing and are the most important part of your response. Care should be taken to ensure they are correct, helpful, and SEO-friendly. + +If a description is provided in the data, use it as a starting point. Correct any spelling errors, typos, poor grammar, or awkward phrasing. If necessary and you have the information, add more details, describe how the customer could use it, etc. Use complete sentences and keep SEO in mind. + +If no description is provided, make one up using the product name, the information you have, and the other provided guidelines. At minimum, a description should be one complete sentence that starts with a capital letter and ends with a period. Unless the product is extremely complex, 2-4 sentences is usually sufficient if you have enough information. + +Important Notes: +- Every description should state exactly what's included in the product (e.g. "Includes one 12x12 sheet of patterned cardstock." or "Includes one 6x12 sheet with 27 unique stickers." or "Includes 55 pieces." or "Package includes machine, power cord, 12 sheets of cardstock, 3 dies, and project instructions.") +- Do not use the word "our" in the description (this usually shows up when we copy a description from the manufacturer). Instead use "these" or "[Company name] [product]" or similar. (e.g. don't use "Our journals are hand-made in the USA", instead use "These journals are hand made..." or "Archer & Olive journals are handmade...") +- Don't include statements that add no value like “this is perfect for all your paper crafts”. If the product helps to solve a unique problem or has a unique feature, by all means describe it, but if it’s just a normal sheet of paper or pack of stickers, you don’t have to pretend like it’s the best thing ever. At the same time, ensure that you add enough copy to ensure good SEO. +- State as many facts as you can about the product, considering the viewpoint of the customer and what they would want to know when looking at it. They probably want to know dimensions, what products it’s compatible with, how thick the paper is, how many sheets are included, whether the sheets are double-sided or not, which items are in the kit, etc. Say as much as you possibly can with the information that you have. +- !!DO NOT make up information if you aren't sure about it. A minimal correct description is better than a long incorrect one!! + +Avoid/remove: +- The word "Imported" +- Any warnings about Prop 65, choking hazards, etc +- The manufacturer's name if it's included as the very first thing in the description +- Any statement similar to "comes in a variety of colors, each sold separately" diff --git a/inventory-server/src/routes/ai-validation.js b/inventory-server/src/routes/ai-validation.js new file mode 100644 index 0000000..7d0e2db --- /dev/null +++ b/inventory-server/src/routes/ai-validation.js @@ -0,0 +1,1027 @@ +const express = require("express"); +const router = express.Router(); +const OpenAI = require("openai"); +const fs = require("fs").promises; +const path = require("path"); +const dotenv = require("dotenv"); +const mysql = require('mysql2/promise'); +const { Client } = require('ssh2'); + +// Ensure environment variables are loaded +dotenv.config({ path: path.join(__dirname, "../../.env") }); + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +if (!process.env.OPENAI_API_KEY) { + console.error("Warning: OPENAI_API_KEY is not set in environment variables"); +} + +// Helper function to setup SSH tunnel to production database +async function setupSshTunnel() { + const sshConfig = { + host: process.env.PROD_SSH_HOST, + port: process.env.PROD_SSH_PORT || 22, + username: process.env.PROD_SSH_USER, + privateKey: process.env.PROD_SSH_KEY_PATH + ? require('fs').readFileSync(process.env.PROD_SSH_KEY_PATH) + : undefined, + compress: true + }; + + const dbConfig = { + host: process.env.PROD_DB_HOST || 'localhost', + user: process.env.PROD_DB_USER, + password: process.env.PROD_DB_PASSWORD, + database: process.env.PROD_DB_NAME, + port: process.env.PROD_DB_PORT || 3306, + timezone: 'Z' + }; + + return new Promise((resolve, reject) => { + const ssh = new Client(); + + ssh.on('error', (err) => { + console.error('SSH connection error:', err); + reject(err); + }); + + ssh.on('ready', () => { + ssh.forwardOut( + '127.0.0.1', + 0, + dbConfig.host, + dbConfig.port, + (err, stream) => { + if (err) reject(err); + resolve({ ssh, stream, dbConfig }); + } + ); + }).connect(sshConfig); + }); +} + +// Debug endpoint for viewing prompt +router.post("/debug", async (req, res) => { + try { + console.log("Debug POST endpoint called"); + + const { products } = req.body; + + console.log("Received products for debug:", { + isArray: Array.isArray(products), + length: products?.length, + firstProduct: products?.[0], + lastProduct: products?.[products?.length - 1], + }); + + if (!Array.isArray(products)) { + console.error("Invalid input: products is not an array"); + return res.status(400).json({ error: "Products must be an array" }); + } + + if (products.length === 0) { + console.error("Invalid input: products array is empty"); + return res.status(400).json({ error: "Products array cannot be empty" }); + } + + // Clean the products array to remove any internal fields + const cleanedProducts = products.map((product) => { + const { __errors, __index, ...cleanProduct } = product; + return cleanProduct; + }); + + console.log("Processing debug request with cleaned products:", { + length: cleanedProducts.length, + sample: cleanedProducts[0], + }); + + try { + const debugResponse = await generateDebugResponse(cleanedProducts, res); + + // Get estimated processing time based on prompt length + if (debugResponse && debugResponse.promptLength) { + try { + // Use the pool from the app + const pool = req.app.locals.pool; + if (!pool) { + console.warn("⚠️ Local database pool not available for time estimates"); + return; + } + + try { + // Instead of looking for similar prompt lengths, calculate an average processing rate + const rateResults = await pool.query( + `SELECT + AVG(duration_seconds / prompt_length) as avg_rate_per_char, + COUNT(*) as sample_count, + AVG(duration_seconds) as avg_duration + FROM ai_validation_performance` + ); + + // Add estimated time to the response + if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) { + // Calculate estimated time based on the rate and current prompt length + const rate = rateResults.rows[0].avg_rate_per_char; + const estimatedSeconds = Math.max(15, Math.round(rate * debugResponse.promptLength)); + + debugResponse.estimatedProcessingTime = { + seconds: estimatedSeconds, + sampleCount: rateResults.rows[0].sample_count || 0, + avgRate: rate, + calculationMethod: "rate-based" + }; + console.log("📊 Calculated time estimate using rate-based method:", { + rate: rate, + promptLength: debugResponse.promptLength, + estimatedSeconds: estimatedSeconds, + sampleCount: rateResults.rows[0].sample_count + }); + } else { + // Fallback: Calculate a simple estimate based on prompt length (1 second per 1000 characters) + const estimatedSeconds = Math.max(15, Math.round(debugResponse.promptLength / 1000)); + console.log("📊 No rate data available, using fallback calculation"); + debugResponse.estimatedProcessingTime = { + seconds: estimatedSeconds, + sampleCount: 0, + isEstimate: true, + calculationMethod: "fallback" + }; + console.log("📊 Fallback time estimate:", debugResponse.estimatedProcessingTime); + } + } catch (queryError) { + console.error("⚠️ Failed to query performance metrics:", queryError); + // Check if table doesn't exist and log a more helpful message + if (queryError.code === '42P01') { + console.error("Table 'ai_validation_performance' does not exist. Make sure to run the setup-schema.sql script."); + } + } + } catch (timeEstimateError) { + console.error("Error getting time estimate:", timeEstimateError); + // Don't fail the request if time estimate fails + } + } + + return res.json(debugResponse); + } catch (generateError) { + console.error("Error generating debug response:", generateError); + return res.status(500).json({ + error: "Error generating debug response: " + generateError.message, + stack: generateError.stack, + name: generateError.name, + code: generateError.code, + sqlMessage: generateError.sqlMessage, + }); + } + } catch (error) { + console.error("Debug POST endpoint error:", error); + res.status(500).json({ + error: error.message, + stack: error.stack, + code: error.code || null, + name: error.name || null + }); + } +}); + +// Helper function to generate debug response +async function generateDebugResponse(productsToUse, res) { + let taxonomy = null; + let mysqlConnection = null; + let ssh = null; + + try { + // Load taxonomy data first + console.log("Loading taxonomy data..."); + try { + // Setup MySQL connection via SSH tunnel + const tunnel = await setupSshTunnel(); + ssh = tunnel.ssh; + + mysqlConnection = await mysql.createConnection({ + ...tunnel.dbConfig, + stream: tunnel.stream + }); + + console.log("MySQL connection established successfully"); + + taxonomy = await getTaxonomyData(mysqlConnection); + console.log("Successfully loaded taxonomy data"); + } catch (taxonomyError) { + console.error("Failed to load taxonomy data:", taxonomyError); + return res.status(500).json({ + error: "Error fetching taxonomy data: " + taxonomyError.message, + sqlMessage: taxonomyError.sqlMessage || null, + sqlState: taxonomyError.sqlState || null, + code: taxonomyError.code || null, + errno: taxonomyError.errno || null, + sql: taxonomyError.sql || null, + }); + } finally { + // Make sure we close the connection + if (mysqlConnection) await mysqlConnection.end(); + if (ssh) ssh.end(); + } + + // Verify the taxonomy data structure + console.log("Verifying taxonomy structure..."); + if (!taxonomy) { + console.error("Taxonomy data is null"); + return res.status(500).json({ error: "Taxonomy data is null" }); + } + + // Check if each taxonomy component exists + const taxonomyComponents = [ + "categories", "themes", "colors", "taxCodes", "sizeCategories", + "suppliers", "companies", "artists", "lines", "subLines" + ]; + + const missingComponents = taxonomyComponents.filter(comp => !taxonomy[comp]); + if (missingComponents.length > 0) { + console.error("Missing taxonomy components:", missingComponents); + } + + // Log detailed taxonomy stats for debugging + console.log("Taxonomy data loaded with details:", { + categories: { + length: taxonomy.categories?.length || 0, + sample: taxonomy.categories?.length > 0 ? JSON.stringify(taxonomy.categories[0]).substring(0, 100) + "..." : null + }, + themes: { + length: taxonomy.themes?.length || 0, + sample: taxonomy.themes?.length > 0 ? JSON.stringify(taxonomy.themes[0]).substring(0, 100) + "..." : null + }, + colors: { + length: taxonomy.colors?.length || 0, + sample: taxonomy.colors?.length > 0 ? JSON.stringify(taxonomy.colors[0]) : null + }, + taxCodes: { + length: taxonomy.taxCodes?.length || 0, + sample: taxonomy.taxCodes?.length > 0 ? JSON.stringify(taxonomy.taxCodes[0]) : null + }, + sizeCategories: { + length: taxonomy.sizeCategories?.length || 0, + sample: taxonomy.sizeCategories?.length > 0 ? JSON.stringify(taxonomy.sizeCategories[0]) : null + }, + suppliers: { + length: taxonomy.suppliers?.length || 0, + sample: taxonomy.suppliers?.length > 0 ? JSON.stringify(taxonomy.suppliers[0]) : null + }, + companies: { + length: taxonomy.companies?.length || 0, + sample: taxonomy.companies?.length > 0 ? JSON.stringify(taxonomy.companies[0]) : null + }, + artists: { + length: taxonomy.artists?.length || 0, + sample: taxonomy.artists?.length > 0 ? JSON.stringify(taxonomy.artists[0]) : null + } + }); + + // Load the prompt using the same function used by validation + console.log("Loading prompt..."); + + // Setup a new connection for loading the prompt + const promptTunnel = await setupSshTunnel(); + const promptConnection = await mysql.createConnection({ + ...promptTunnel.dbConfig, + stream: promptTunnel.stream + }); + + try { + const prompt = await loadPrompt(promptConnection, productsToUse); + const fullPrompt = prompt + "\n" + JSON.stringify(productsToUse); + + // Create the response with taxonomy stats + let categoriesCount = 0; + try { + categoriesCount = taxonomy?.categories?.length ? countItems(taxonomy.categories) : 0; + } catch (countError) { + console.error("Error counting categories:", countError); + categoriesCount = taxonomy?.categories?.length || 0; // Fallback to simple length + } + + const response = { + taxonomyStats: taxonomy + ? { + categories: categoriesCount, + themes: taxonomy.themes?.length || 0, + colors: taxonomy.colors?.length || 0, + taxCodes: taxonomy.taxCodes?.length || 0, + sizeCategories: taxonomy.sizeCategories?.length || 0, + suppliers: taxonomy.suppliers?.length || 0, + companies: taxonomy.companies?.length || 0, + artists: taxonomy.artists?.length || 0, + // Add filtered counts when products are provided + filtered: productsToUse + ? { + suppliers: taxonomy.suppliers?.filter(([id]) => + productsToUse.some( + (p) => Number(p.supplierid) === Number(id) + ) + )?.length || 0, + companies: taxonomy.companies?.filter(([id]) => + productsToUse.some((p) => Number(p.company) === Number(id)) + )?.length || 0, + artists: taxonomy.artists?.filter(([id]) => + productsToUse.some((p) => Number(p.artist) === Number(id)) + )?.length || 0, + } + : null, + } + : null, + basePrompt: prompt, + sampleFullPrompt: fullPrompt, + promptLength: fullPrompt.length, + }; + + console.log("Sending response with taxonomy stats:", response.taxonomyStats); + return response; + } finally { + if (promptConnection) await promptConnection.end(); + if (promptTunnel.ssh) promptTunnel.ssh.end(); + } + } catch (error) { + console.error("Error generating debug response:", error); + return res.status(500).json({ + error: error.message, + stack: error.stack, + sqlMessage: error.sqlMessage || null, + sqlState: error.sqlState || null, + code: error.code || null, + errno: error.errno || null, + taxonomyState: taxonomy ? "loaded" : "failed", + }); + } +} + +// Helper function to count total items in hierarchical structure +function countItems(items) { + return items.reduce((count, item) => { + return ( + count + 1 + (item.subcategories ? countItems(item.subcategories) : 0) + ); + }, 0); +} + +// Function to fetch and format taxonomy data +async function getTaxonomyData(connection) { + try { + console.log("Starting taxonomy data fetch..."); + // Fetch categories with hierarchy + const [categories] = await connection.query(` +SELECT cat_id,name,NULL AS master_cat_id,1 AS level_order FROM product_categories s WHERE type=10 UNION ALL SELECT c.cat_id,c.name,c.master_cat_id,2 AS level_order FROM product_categories c JOIN product_categories s ON c.master_cat_id=s.cat_id WHERE c.type=11 AND s.type=10 UNION ALL SELECT sc.cat_id,sc.name,sc.master_cat_id,3 AS level_order FROM product_categories sc JOIN product_categories c ON sc.master_cat_id=c.cat_id JOIN product_categories s ON c.master_cat_id=s.cat_id WHERE sc.type=12 AND c.type=11 AND s.type=10 UNION ALL SELECT ssc.cat_id,ssc.name,ssc.master_cat_id,4 AS level_order FROM product_categories ssc JOIN product_categories sc ON ssc.master_cat_id=sc.cat_id JOIN product_categories c ON sc.master_cat_id=c.cat_id JOIN product_categories s ON c.master_cat_id=s.cat_id WHERE ssc.type=13 AND sc.type=12 AND c.type=11 AND s.type=10 ORDER BY level_order,cat_id; + `); + console.log("Categories fetched:", categories.length); + + // Fetch themes with hierarchy + const [themes] = await connection.query(` +SELECT t.cat_id,t.name,null as master_cat_id,1 AS level_order FROM product_categories t WHERE t.type=20 UNION ALL SELECT ts.cat_id,ts.name,ts.master_cat_id,2 AS level_order FROM product_categories ts JOIN product_categories t ON ts.master_cat_id=t.cat_id WHERE ts.type=21 AND t.type=20 ORDER BY level_order,name + `); + console.log("Themes fetched:", themes.length); + + // Fetch colors + const [colors] = await connection.query( + `SELECT color, name, hex_color FROM product_color_list ORDER BY \`order\`` + ); + console.log("Colors fetched:", colors.length); + + // Fetch tax codes + const [taxCodes] = await connection.query( + `SELECT tax_code_id, name FROM product_tax_codes ORDER BY name` + ); + console.log("Tax codes fetched:", taxCodes.length); + + // Fetch size categories + const [sizeCategories] = await connection.query( + `SELECT cat_id, name FROM product_categories WHERE type=50 ORDER BY name` + ); + console.log("Size categories fetched:", sizeCategories.length); + + // Fetch suppliers + const [suppliers] = await connection.query(` + SELECT supplierid, companyname as name + FROM suppliers + WHERE companyname <> '' + ORDER BY companyname + `); + console.log("Suppliers fetched:", suppliers.length); + + // Fetch companies (type 1) + const [companies] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 1 + ORDER BY name + `); + console.log("Companies fetched:", companies.length); + + // Fetch artists (type 40) + const [artists] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 40 + ORDER BY name + `); + console.log("Artists fetched:", artists.length); + + // Fetch lines (type 2) + const [lines] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 2 + ORDER BY name + `); + console.log("Lines fetched:", lines.length); + + // Fetch sub-lines (type 3) + const [subLines] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 3 + ORDER BY name + `); + console.log("Sub-lines fetched:", subLines.length); + + // Format categories into a hierarchical structure + const formatHierarchy = (items, level = 1, parentId = null) => { + return items + .filter( + (item) => + item.level_order === level && item.master_cat_id === parentId + ) + .map((item) => { + const children = formatHierarchy(items, level + 1, item.cat_id); + return children.length > 0 + ? [item.cat_id, item.name, children] + : [item.cat_id, item.name]; + }); + }; + + // Format themes similarly but with only two levels + const formatThemes = (items) => { + return items + .filter((item) => item.level_order === 1) + .map((item) => { + const subthemes = items + .filter((subitem) => subitem.master_cat_id === item.cat_id) + .map((subitem) => [subitem.cat_id, subitem.name]); + return subthemes.length > 0 + ? [item.cat_id, item.name, subthemes] + : [item.cat_id, item.name]; + }); + }; + + // Log first item of each taxonomy category to check structure + console.log("Sample category:", categories.length > 0 ? categories[0] : "No categories"); + console.log("Sample theme:", themes.length > 0 ? themes[0] : "No themes"); + console.log("Sample color:", colors.length > 0 ? colors[0] : "No colors"); + + const formattedData = { + categories: formatHierarchy(categories), + themes: formatThemes(themes), + colors: colors.map((c) => [c.color, c.name, c.hex_color]), + taxCodes: (taxCodes || []).map((tc) => [tc.tax_code_id, tc.name]), + sizeCategories: (sizeCategories || []).map((sc) => [sc.cat_id, sc.name]), + suppliers: suppliers.map((s) => [s.supplierid, s.name]), + companies: companies.map((c) => [c.cat_id, c.name]), + artists: artists.map((a) => [a.cat_id, a.name]), + lines: lines.map((l) => [l.cat_id, l.name]), + subLines: subLines.map((sl) => [sl.cat_id, sl.name]), + }; + + // Check the formatted structure + console.log("Formatted categories count:", formattedData.categories.length); + console.log("Formatted themes count:", formattedData.themes.length); + console.log("Formatted colors count:", formattedData.colors.length); + + return formattedData; + } catch (error) { + console.error("Error fetching taxonomy data:", error); + console.error("Full error details:", { + message: error.message, + stack: error.stack, + code: error.code, + errno: error.errno, + sqlMessage: error.sqlMessage, + sqlState: error.sqlState, + sql: error.sql + }); + + // Instead of silently returning empty arrays, throw the error to be handled by the caller + throw error; + } +} + +// Load the prompt from file and inject taxonomy data +async function loadPrompt(connection, productsToValidate = null) { + try { + const promptPath = path.join( + __dirname, + "..", + "prompts", + "product-validation.txt" + ); + const basePrompt = await fs.readFile(promptPath, "utf8"); + + // Get taxonomy data using the provided MySQL connection + const taxonomy = await getTaxonomyData(connection); + + // Add system instructions to the prompt + const systemInstructions = `You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone. You have meticulous attention to detail and are a master at your craft.`; + + // If we have products to validate, create a filtered prompt + if (productsToValidate) { + console.log("Creating filtered prompt for products:", productsToValidate); + + // Extract unique values from products for non-core attributes + const uniqueValues = { + supplierIds: new Set(), + companyIds: new Set(), + artistIds: new Set(), + lineIds: new Set(), + subLineIds: new Set(), + }; + + // Collect any values that exist in the products + productsToValidate.forEach((product) => { + Object.entries(product).forEach(([key, value]) => { + if (value === undefined || value === null) return; + + // Map field names to their respective sets + const fieldMap = { + supplierid: "supplierIds", + supplier: "supplierIds", + company: "companyIds", + artist: "artistIds", + line: "lineIds", + subline: "subLineIds", + }; + + if (fieldMap[key]) { + uniqueValues[fieldMap[key]].add(Number(value)); + } + }); + }); + + console.log("Unique values collected:", { + suppliers: Array.from(uniqueValues.supplierIds), + companies: Array.from(uniqueValues.companyIds), + artists: Array.from(uniqueValues.artistIds), + lines: Array.from(uniqueValues.lineIds), + subLines: Array.from(uniqueValues.subLineIds), + }); + + // Create mixed taxonomy with filtered non-core data and full core data + const mixedTaxonomy = { + // Keep full data for core attributes + categories: taxonomy.categories, + themes: taxonomy.themes, + colors: taxonomy.colors, + taxCodes: taxonomy.taxCodes, + sizeCategories: taxonomy.sizeCategories, + // For non-core data, only include items that are actually used + suppliers: taxonomy.suppliers.filter(([id]) => + uniqueValues.supplierIds.has(Number(id)) + ), + companies: taxonomy.companies.filter(([id]) => + uniqueValues.companyIds.has(Number(id)) + ), + artists: taxonomy.artists.filter(([id]) => + uniqueValues.artistIds.has(Number(id)) + ), + lines: taxonomy.lines.filter(([id]) => + uniqueValues.lineIds.has(Number(id)) + ), + subLines: taxonomy.subLines.filter(([id]) => + uniqueValues.subLineIds.has(Number(id)) + ), + }; + + console.log("Filtered taxonomy counts:", { + suppliers: mixedTaxonomy.suppliers.length, + companies: mixedTaxonomy.companies.length, + artists: mixedTaxonomy.artists.length, + lines: mixedTaxonomy.lines.length, + subLines: mixedTaxonomy.subLines.length, + }); + + // Format taxonomy data for the prompt, only including sections with values + const taxonomySection = ` +All Available Categories: +${JSON.stringify(mixedTaxonomy.categories)} + +All Available Themes: +${JSON.stringify(mixedTaxonomy.themes)} + +All Available Colors: +${JSON.stringify(mixedTaxonomy.colors)} + +All Available Tax Codes: +${JSON.stringify(mixedTaxonomy.taxCodes)} + +All Available Size Categories: +${JSON.stringify(mixedTaxonomy.sizeCategories)}${ + mixedTaxonomy.suppliers.length + ? `\n\nSuppliers Used In This Data:\n${JSON.stringify( + mixedTaxonomy.suppliers + )}` + : "" + }${ + mixedTaxonomy.companies.length + ? `\n\nCompanies Used In This Data:\n${JSON.stringify( + mixedTaxonomy.companies + )}` + : "" + }${ + mixedTaxonomy.artists.length + ? `\n\nArtists Used In This Data:\n${JSON.stringify( + mixedTaxonomy.artists + )}` + : "" + }${ + mixedTaxonomy.lines.length + ? `\n\nLines Used In This Data:\n${JSON.stringify( + mixedTaxonomy.lines + )}` + : "" + }${ + mixedTaxonomy.subLines.length + ? `\n\nSub-Lines Used In This Data:\n${JSON.stringify( + mixedTaxonomy.subLines + )}` + : "" + } + +----------Here is the product data to validate----------`; + + // Return the filtered prompt + return systemInstructions + basePrompt + "\n" + taxonomySection; + } + + // Generate the full unfiltered prompt + const taxonomySection = ` +Available Categories: +${JSON.stringify(taxonomy.categories)} + +Available Themes: +${JSON.stringify(taxonomy.themes)} + +Available Colors: +${JSON.stringify(taxonomy.colors)} + +Available Tax Codes: +${JSON.stringify(taxonomy.taxCodes)} + +Available Size Categories: +${JSON.stringify(taxonomy.sizeCategories)} + +Available Suppliers: +${JSON.stringify(taxonomy.suppliers)} + +Available Companies: +${JSON.stringify(taxonomy.companies)} + +Available Artists: +${JSON.stringify(taxonomy.artists)} + +Here is the product data to validate:`; + + return systemInstructions + basePrompt + "\n" + taxonomySection; + } catch (error) { + console.error("Error loading prompt:", error); + throw error; // Re-throw to be handled by the calling function + } +} + +router.post("/validate", async (req, res) => { + try { + const { products } = req.body; + const startTime = new Date(); // Track start time for performance metrics + + console.log("🔍 Received products for validation:", { + isArray: Array.isArray(products), + length: products?.length, + firstProduct: products?.[0], + lastProduct: products?.[products?.length - 1], + }); + + if (!Array.isArray(products)) { + console.error("❌ Invalid input: products is not an array"); + return res.status(400).json({ error: "Products must be an array" }); + } + + if (products.length === 0) { + console.error("❌ Invalid input: products array is empty"); + return res.status(400).json({ error: "Products array cannot be empty" }); + } + + let ssh = null; + let connection = null; + let promptLength = 0; // Track prompt length for performance metrics + + try { + // Setup MySQL connection via SSH tunnel + console.log("🔄 Setting up connection to production database..."); + const tunnel = await setupSshTunnel(); + ssh = tunnel.ssh; + + connection = await mysql.createConnection({ + ...tunnel.dbConfig, + stream: tunnel.stream + }); + + console.log("🔄 MySQL connection established successfully"); + + // Load the prompt with the products data to filter taxonomy + console.log("🔄 Loading prompt with filtered taxonomy..."); + const prompt = await loadPrompt(connection, products); + const fullPrompt = prompt + "\n" + JSON.stringify(products); + promptLength = fullPrompt.length; // Store prompt length for performance metrics + console.log("📝 Generated prompt length:", promptLength); + + console.log("🤖 Sending request to OpenAI..."); + const completion = await openai.chat.completions.create({ + model: "o3-mini", + messages: [ + { + role: "user", + content: fullPrompt, + }, + ], + temperature: 0.2, + response_format: { type: "json_object" }, + }); + + console.log("✅ Received response from OpenAI"); + const rawResponse = completion.choices[0].message.content; + console.log("📄 Raw AI response length:", rawResponse.length); + + try { + const aiResponse = JSON.parse(rawResponse); + console.log( + "🔄 Parsed AI response with keys:", + Object.keys(aiResponse) + ); + + // Create a detailed comparison between original and corrected data + const changeDetails = []; + + // Compare original and corrected data + if (aiResponse.correctedData) { + console.log("📊 Changes summary:"); + + // Debug: Log the first product's fields + if (products.length > 0) { + console.log("🔍 First product fields:", Object.keys(products[0])); + } + + products.forEach((original, index) => { + const corrected = aiResponse.correctedData[index]; + if (corrected) { + const productChanges = { + productIndex: index, + title: original.name || original.title || `Product ${index + 1}`, + changes: [] + }; + + const changes = Object.keys(corrected).filter( + (key) => + JSON.stringify(original[key]) !== + JSON.stringify(corrected[key]) + ); + + if (changes.length > 0) { + console.log(`\nProduct ${index + 1} changes:`); + changes.forEach((key) => { + console.log(` ${key}:`); + console.log( + ` - Original: ${JSON.stringify(original[key])}` + ); + console.log( + ` - Corrected: ${JSON.stringify(corrected[key])}` + ); + + // Add to our detailed changes array + productChanges.changes.push({ + field: key, + original: original[key], + corrected: corrected[key] + }); + }); + + // Only add products that have changes + if (productChanges.changes.length > 0) { + changeDetails.push(productChanges); + } + } + } + }); + } + + // Record performance metrics after successful validation + const endTime = new Date(); + let performanceMetrics = { + promptLength, + productCount: products.length + }; + + try { + // Use the local PostgreSQL pool from the app instead of the MySQL connection + const pool = req.app.locals.pool; + if (!pool) { + console.warn("⚠️ Local database pool not available for recording metrics"); + return; + } + + try { + // Insert performance data into the local PostgreSQL database + await pool.query( + `INSERT INTO ai_validation_performance + (prompt_length, product_count, start_time, end_time) + VALUES ($1, $2, $3, $4)`, + [ + promptLength, + products.length, + startTime.toISOString(), + endTime.toISOString() + ] + ); + + console.log("📊 Performance metrics inserted into database"); + + // Query for average processing time based on similar prompt lengths + try { + const rateResults = await pool.query( + `SELECT + AVG(duration_seconds / prompt_length) as avg_rate_per_char, + COUNT(*) as sample_count, + AVG(duration_seconds) as avg_duration + FROM ai_validation_performance` + ); + + if (rateResults.rows && rateResults.rows[0] && rateResults.rows[0].avg_rate_per_char) { + const rate = rateResults.rows[0].avg_rate_per_char; + performanceMetrics.avgRate = rate; + performanceMetrics.estimatedSeconds = Math.round(rate * promptLength); + performanceMetrics.sampleCount = rateResults.rows[0].sample_count; + performanceMetrics.calculationMethod = "rate-based"; + } + + console.log("📊 Performance metrics with rate calculation:", performanceMetrics); + } catch (queryError) { + console.error("⚠️ Failed to query performance metrics:", queryError); + } + } catch (insertError) { + console.error("⚠️ Failed to insert performance metrics:", insertError); + // Check if table doesn't exist and log a more helpful message + if (insertError.code === '42P01') { + console.error("Table 'ai_validation_performance' does not exist. Make sure to run the setup-schema.sql script."); + } + } + } catch (metricError) { + // Don't fail the request if metrics recording fails + console.error("⚠️ Failed to record performance metrics:", metricError); + } + + // Include performance metrics in the response + res.json({ + success: true, + changeDetails: changeDetails, + performanceMetrics: performanceMetrics || { + // Fallback: calculate a simple estimate + promptLength: promptLength, + processingTimeSeconds: Math.max(15, Math.round(promptLength / 1000)), + isEstimate: true, + productCount: products.length + }, + ...aiResponse, + }); + } catch (parseError) { + console.error("❌ Error parsing AI response:", parseError); + console.error("Raw response that failed to parse:", rawResponse); + res.status(500).json({ + success: false, + error: "Error parsing AI response: " + parseError.message, + }); + } + } catch (openaiError) { + console.error("❌ OpenAI API Error:", openaiError); + res.status(500).json({ + success: false, + error: "OpenAI API Error: " + openaiError.message, + }); + } finally { + // Clean up database connection and SSH tunnel + if (connection) await connection.end(); + if (ssh) ssh.end(); + } + } catch (error) { + console.error("❌ AI Validation Error:", error); + console.error("Error details:", { + name: error.name, + message: error.message, + stack: error.stack, + }); + res.status(500).json({ + success: false, + error: error.message || "Error during AI validation", + }); + } +}); + +// Test endpoint for direct database query of taxonomy data +router.get("/test-taxonomy", async (req, res) => { + try { + console.log("Test taxonomy endpoint called"); + + let ssh = null; + let connection = null; + + try { + // Setup MySQL connection via SSH tunnel + const tunnel = await setupSshTunnel(); + ssh = tunnel.ssh; + + connection = await mysql.createConnection({ + ...tunnel.dbConfig, + stream: tunnel.stream + }); + + console.log("MySQL connection established successfully for test"); + + const results = {}; + + // Test categories query + try { + const [categories] = await connection.query(` + SELECT cat_id, name FROM product_categories WHERE type=10 LIMIT 5 + `); + results.categories = { + success: true, + count: categories.length, + sample: categories.length > 0 ? categories[0] : null + }; + } catch (error) { + results.categories = { + success: false, + error: error.message, + sqlMessage: error.sqlMessage + }; + } + + // Test themes query + try { + const [themes] = await connection.query(` + SELECT cat_id, name FROM product_categories WHERE type=20 LIMIT 5 + `); + results.themes = { + success: true, + count: themes.length, + sample: themes.length > 0 ? themes[0] : null + }; + } catch (error) { + results.themes = { + success: false, + error: error.message, + sqlMessage: error.sqlMessage + }; + } + + // Test colors query + try { + const [colors] = await connection.query(` + SELECT color, name, hex_color FROM product_color_list ORDER BY \`order\` LIMIT 5 + `); + results.colors = { + success: true, + count: colors.length, + sample: colors.length > 0 ? colors[0] : null + }; + } catch (error) { + results.colors = { + success: false, + error: error.message, + sqlMessage: error.sqlMessage + }; + } + + return res.json({ + message: "Test taxonomy queries executed", + results: results, + timestamp: new Date().toISOString() + }); + } finally { + if (connection) await connection.end(); + if (ssh) ssh.end(); + } + } catch (error) { + console.error("Test taxonomy endpoint error:", error); + return res.status(500).json({ + error: error.message, + stack: error.stack + }); + } +}); + +module.exports = router; diff --git a/inventory-server/src/routes/import.js b/inventory-server/src/routes/import.js new file mode 100644 index 0000000..28d2a03 --- /dev/null +++ b/inventory-server/src/routes/import.js @@ -0,0 +1,1117 @@ +const express = require('express'); +const router = express.Router(); +const { Client } = require('ssh2'); +const mysql = require('mysql2/promise'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +// Create uploads directory if it doesn't exist +const uploadsDir = path.join('/var/www/html/inventory/uploads/products'); +fs.mkdirSync(uploadsDir, { recursive: true }); + +// Create a Map to track image upload times and their scheduled deletion +const imageUploadMap = new Map(); + +// Connection pooling and cache configuration +const connectionCache = { + ssh: null, + dbConnection: null, + lastUsed: 0, + isConnecting: false, + connectionPromise: null, + // Cache expiration time in milliseconds (5 minutes) + expirationTime: 5 * 60 * 1000, + // Cache for query results (key: query string, value: {data, timestamp}) + queryCache: new Map(), + // Cache duration for different query types in milliseconds + cacheDuration: { + 'field-options': 30 * 60 * 1000, // 30 minutes for field options + 'product-lines': 10 * 60 * 1000, // 10 minutes for product lines + 'sublines': 10 * 60 * 1000, // 10 minutes for sublines + 'default': 60 * 1000 // 1 minute default + } +}; + +// Function to schedule image deletion after 24 hours +const scheduleImageDeletion = (filename, filePath) => { + // Delete any existing timeout for this file + if (imageUploadMap.has(filename)) { + clearTimeout(imageUploadMap.get(filename).timeoutId); + } + + // Schedule deletion after 24 hours (24 * 60 * 60 * 1000 ms) + const timeoutId = setTimeout(() => { + console.log(`Auto-deleting image after 24 hours: ${filename}`); + + // Check if file exists before trying to delete + if (fs.existsSync(filePath)) { + try { + fs.unlinkSync(filePath); + console.log(`Successfully auto-deleted image: ${filename}`); + } catch (error) { + console.error(`Error auto-deleting image ${filename}:`, error); + } + } else { + console.log(`File already deleted: ${filename}`); + } + + // Remove from tracking map + imageUploadMap.delete(filename); + }, 24 * 60 * 60 * 1000); // 24 hours + + // Store upload time and timeout ID + imageUploadMap.set(filename, { + uploadTime: new Date(), + timeoutId: timeoutId, + filePath: filePath + }); +}; + +// Function to clean up scheduled deletions on server restart +const cleanupImagesOnStartup = () => { + console.log('Checking for images to clean up...'); + + // Check if uploads directory exists + if (!fs.existsSync(uploadsDir)) { + console.log('Uploads directory does not exist'); + return; + } + + // Read all files in the directory + fs.readdir(uploadsDir, (err, files) => { + if (err) { + console.error('Error reading uploads directory:', err); + return; + } + + const now = new Date(); + let countDeleted = 0; + + files.forEach(filename => { + const filePath = path.join(uploadsDir, filename); + + // Get file stats + try { + const stats = fs.statSync(filePath); + const fileCreationTime = stats.birthtime || stats.ctime; // birthtime might not be available on all systems + const ageMs = now.getTime() - fileCreationTime.getTime(); + + // If file is older than 24 hours, delete it + if (ageMs > 24 * 60 * 60 * 1000) { + fs.unlinkSync(filePath); + countDeleted++; + console.log(`Deleted old image on startup: ${filename} (age: ${Math.round(ageMs / (60 * 60 * 1000))} hours)`); + } else { + // Schedule deletion for remaining time + const remainingMs = (24 * 60 * 60 * 1000) - ageMs; + console.log(`Scheduling deletion for ${filename} in ${Math.round(remainingMs / (60 * 60 * 1000))} hours`); + + const timeoutId = setTimeout(() => { + if (fs.existsSync(filePath)) { + try { + fs.unlinkSync(filePath); + console.log(`Successfully auto-deleted scheduled image: ${filename}`); + } catch (error) { + console.error(`Error auto-deleting scheduled image ${filename}:`, error); + } + } + imageUploadMap.delete(filename); + }, remainingMs); + + imageUploadMap.set(filename, { + uploadTime: fileCreationTime, + timeoutId: timeoutId, + filePath: filePath + }); + } + } catch (error) { + console.error(`Error processing file ${filename}:`, error); + } + }); + + console.log(`Cleanup completed: ${countDeleted} old images deleted, ${imageUploadMap.size} images scheduled for deletion`); + }); +}; + +// Run cleanup on server start +cleanupImagesOnStartup(); + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + console.log(`Saving to: ${uploadsDir}`); + cb(null, uploadsDir); + }, + filename: function (req, file, cb) { + // Create unique filename with original extension + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + + // Make sure we preserve the original file extension + let fileExt = path.extname(file.originalname).toLowerCase(); + + // Ensure there is a proper extension based on mimetype if none exists + if (!fileExt) { + switch (file.mimetype) { + case 'image/jpeg': fileExt = '.jpg'; break; + case 'image/png': fileExt = '.png'; break; + case 'image/gif': fileExt = '.gif'; break; + case 'image/webp': fileExt = '.webp'; break; + default: fileExt = '.jpg'; // Default to jpg + } + } + + const fileName = `${req.body.upc || 'product'}-${uniqueSuffix}${fileExt}`; + console.log(`Generated filename: ${fileName} with mimetype: ${file.mimetype}`); + cb(null, fileName); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB max file size + }, + fileFilter: function (req, file, cb) { + // Accept only image files + const filetypes = /jpeg|jpg|png|gif|webp/; + const mimetype = filetypes.test(file.mimetype); + const extname = filetypes.test(path.extname(file.originalname).toLowerCase()); + + if (mimetype && extname) { + return cb(null, true); + } + cb(new Error('Only image files are allowed')); + } +}); + +// Modified function to get a database connection with connection pooling +async function getDbConnection() { + const now = Date.now(); + + // Check if we need to refresh the connection due to inactivity + const needsRefresh = !connectionCache.ssh || + !connectionCache.dbConnection || + (now - connectionCache.lastUsed > connectionCache.expirationTime); + + // If connection is still valid, update last used time and return existing connection + if (!needsRefresh) { + connectionCache.lastUsed = now; + return { + ssh: connectionCache.ssh, + connection: connectionCache.dbConnection + }; + } + + // If another request is already establishing a connection, wait for that promise + if (connectionCache.isConnecting && connectionCache.connectionPromise) { + try { + await connectionCache.connectionPromise; + return { + ssh: connectionCache.ssh, + connection: connectionCache.dbConnection + }; + } catch (error) { + // If that connection attempt failed, we'll try again below + console.error('Error waiting for existing connection:', error); + } + } + + // Close existing connections if they exist + if (connectionCache.dbConnection) { + try { + await connectionCache.dbConnection.end(); + } catch (error) { + console.error('Error closing existing database connection:', error); + } + } + + if (connectionCache.ssh) { + try { + connectionCache.ssh.end(); + } catch (error) { + console.error('Error closing existing SSH connection:', error); + } + } + + // Mark that we're establishing a new connection + connectionCache.isConnecting = true; + + // Create a new promise for this connection attempt + connectionCache.connectionPromise = setupSshTunnel().then(tunnel => { + const { ssh, stream, dbConfig } = tunnel; + + return mysql.createConnection({ + ...dbConfig, + stream + }).then(connection => { + // Store the new connections + connectionCache.ssh = ssh; + connectionCache.dbConnection = connection; + connectionCache.lastUsed = Date.now(); + connectionCache.isConnecting = false; + + return { + ssh, + connection + }; + }); + }).catch(error => { + connectionCache.isConnecting = false; + throw error; + }); + + // Wait for the connection to be established + return connectionCache.connectionPromise; +} + +// Helper function to get cached query results or execute query if not cached +async function getCachedQuery(cacheKey, queryType, queryFn) { + // Get cache duration based on query type + const cacheDuration = connectionCache.cacheDuration[queryType] || connectionCache.cacheDuration.default; + + // Check if we have a valid cached result + const cachedResult = connectionCache.queryCache.get(cacheKey); + const now = Date.now(); + + if (cachedResult && (now - cachedResult.timestamp < cacheDuration)) { + console.log(`Cache hit for ${queryType} query: ${cacheKey}`); + return cachedResult.data; + } + + // No valid cache found, execute the query + console.log(`Cache miss for ${queryType} query: ${cacheKey}`); + const result = await queryFn(); + + // Cache the result + connectionCache.queryCache.set(cacheKey, { + data: result, + timestamp: now + }); + + return result; +} + +// Helper function to setup SSH tunnel - ONLY USED BY getDbConnection NOW +async function setupSshTunnel() { + const sshConfig = { + host: process.env.PROD_SSH_HOST, + port: process.env.PROD_SSH_PORT || 22, + username: process.env.PROD_SSH_USER, + privateKey: process.env.PROD_SSH_KEY_PATH + ? require('fs').readFileSync(process.env.PROD_SSH_KEY_PATH) + : undefined, + compress: true + }; + + const dbConfig = { + host: process.env.PROD_DB_HOST || 'localhost', + user: process.env.PROD_DB_USER, + password: process.env.PROD_DB_PASSWORD, + database: process.env.PROD_DB_NAME, + port: process.env.PROD_DB_PORT || 3306, + timezone: 'Z' + }; + + return new Promise((resolve, reject) => { + const ssh = new Client(); + + ssh.on('error', (err) => { + console.error('SSH connection error:', err); + reject(err); + }); + + ssh.on('ready', () => { + ssh.forwardOut( + '127.0.0.1', + 0, + dbConfig.host, + dbConfig.port, + (err, stream) => { + if (err) reject(err); + resolve({ ssh, stream, dbConfig }); + } + ); + }).connect(sshConfig); + }); +} + +// Image upload endpoint +router.post('/upload-image', upload.single('image'), (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No image file provided' }); + } + + // Log file information + console.log('File uploaded:', { + filename: req.file.filename, + originalname: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size, + path: req.file.path + }); + + // Ensure the file exists + const filePath = path.join(uploadsDir, req.file.filename); + if (!fs.existsSync(filePath)) { + return res.status(500).json({ error: 'File was not saved correctly' }); + } + + // Log file access permissions + fs.access(filePath, fs.constants.R_OK, (err) => { + if (err) { + console.error('File permission issue:', err); + } else { + console.log('File is readable'); + } + }); + + // Create URL for the uploaded file - using an absolute URL with domain + // This will generate a URL like: https://inventory.acot.site/uploads/products/filename.jpg + const baseUrl = 'https://inventory.acot.site'; + const imageUrl = `${baseUrl}/uploads/products/${req.file.filename}`; + + // Schedule this image for deletion in 24 hours + scheduleImageDeletion(req.file.filename, filePath); + + // Return success response with image URL + res.status(200).json({ + success: true, + imageUrl, + fileName: req.file.filename, + mimetype: req.file.mimetype, + fullPath: filePath, + message: 'Image uploaded successfully (will auto-delete after 24 hours)' + }); + + } catch (error) { + console.error('Error uploading image:', error); + res.status(500).json({ error: error.message || 'Failed to upload image' }); + } +}); + +// Image deletion endpoint +router.delete('/delete-image', (req, res) => { + try { + const { filename } = req.body; + + if (!filename) { + return res.status(400).json({ error: 'Filename is required' }); + } + + const filePath = path.join(uploadsDir, filename); + + // Check if file exists + if (!fs.existsSync(filePath)) { + return res.status(404).json({ error: 'File not found' }); + } + + // Delete the file + fs.unlinkSync(filePath); + + // Clear any scheduled deletion for this file + if (imageUploadMap.has(filename)) { + clearTimeout(imageUploadMap.get(filename).timeoutId); + imageUploadMap.delete(filename); + } + + // Return success response + res.status(200).json({ + success: true, + message: 'Image deleted successfully' + }); + + } catch (error) { + console.error('Error deleting image:', error); + res.status(500).json({ error: error.message || 'Failed to delete image' }); + } +}); + +// Get all options for import fields +router.get('/field-options', async (req, res) => { + try { + // Use cached connection + const { connection } = await getDbConnection(); + + const cacheKey = 'field-options'; + const result = await getCachedQuery(cacheKey, 'field-options', async () => { + // Fetch companies (type 1) + const [companies] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 1 + ORDER BY name + `); + + // Fetch artists (type 40) + const [artists] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 40 + ORDER BY name + `); + + // Fetch sizes (type 50) + const [sizes] = await connection.query(` + SELECT cat_id, name + FROM product_categories + WHERE type = 50 + ORDER BY name + `); + + // Fetch themes with subthemes + const [themes] = await connection.query(` + SELECT t.cat_id, t.name AS display_name, t.type, t.name AS sort_theme, + '' AS sort_subtheme, 1 AS level_order + FROM product_categories t + WHERE t.type = 20 + UNION ALL + SELECT ts.cat_id, CONCAT(t.name,' - ',ts.name) AS display_name, ts.type, + t.name AS sort_theme, ts.name AS sort_subtheme, 2 AS level_order + FROM product_categories ts + JOIN product_categories t ON ts.master_cat_id = t.cat_id + WHERE ts.type = 21 AND t.type = 20 + ORDER BY sort_theme, sort_subtheme + `); + + // Fetch categories with all levels + const [categories] = await connection.query(` + SELECT s.cat_id, s.name AS display_name, s.type, s.name AS sort_section, + '' AS sort_category, '' AS sort_subcategory, '' AS sort_subsubcategory, + 1 AS level_order + FROM product_categories s + WHERE s.type = 10 + UNION ALL + SELECT c.cat_id, CONCAT(s.name,' - ',c.name) AS display_name, c.type, + s.name AS sort_section, c.name AS sort_category, '' AS sort_subcategory, + '' AS sort_subsubcategory, 2 AS level_order + FROM product_categories c + JOIN product_categories s ON c.master_cat_id = s.cat_id + WHERE c.type = 11 AND s.type = 10 + UNION ALL + SELECT sc.cat_id, CONCAT(s.name,' - ',c.name,' - ',sc.name) AS display_name, + sc.type, s.name AS sort_section, c.name AS sort_category, + sc.name AS sort_subcategory, '' AS sort_subsubcategory, 3 AS level_order + FROM product_categories sc + JOIN product_categories c ON sc.master_cat_id = c.cat_id + JOIN product_categories s ON c.master_cat_id = s.cat_id + WHERE sc.type = 12 AND c.type = 11 AND s.type = 10 + UNION ALL + SELECT ssc.cat_id, CONCAT(s.name,' - ',c.name,' - ',sc.name,' - ',ssc.name) AS display_name, + ssc.type, s.name AS sort_section, c.name AS sort_category, + sc.name AS sort_subcategory, ssc.name AS sort_subsubcategory, 4 AS level_order + FROM product_categories ssc + JOIN product_categories sc ON ssc.master_cat_id = sc.cat_id + JOIN product_categories c ON sc.master_cat_id = c.cat_id + JOIN product_categories s ON c.master_cat_id = s.cat_id + WHERE ssc.type = 13 AND sc.type = 12 AND c.type = 11 AND s.type = 10 + ORDER BY sort_section, sort_category, sort_subcategory, sort_subsubcategory + `); + + // Fetch colors + const [colors] = await connection.query(` + SELECT color, name, hex_color + FROM product_color_list + ORDER BY \`order\` + `); + + // Fetch suppliers + const [suppliers] = await connection.query(` + SELECT supplierid as value, companyname as label + FROM suppliers + WHERE companyname <> '' + ORDER BY companyname + `); + + // Fetch tax categories + const [taxCategories] = await connection.query(` + SELECT CAST(tax_code_id AS CHAR) as value, name as label + FROM product_tax_codes + ORDER BY tax_code_id = 0 DESC, name + `); + + // Format and return all options + return { + companies: companies.map(c => ({ label: c.name, value: c.cat_id.toString() })), + artists: artists.map(a => ({ label: a.name, value: a.cat_id.toString() })), + sizes: sizes.map(s => ({ label: s.name, value: s.cat_id.toString() })), + themes: themes.map(t => ({ + label: t.display_name, + value: t.cat_id.toString(), + type: t.type, + level: t.level_order + })), + categories: categories.map(c => ({ + label: c.display_name, + value: c.cat_id.toString(), + type: c.type, + level: c.level_order + })), + colors: colors.map(c => ({ + label: c.name, + value: c.color, + hexColor: c.hex_color + })), + suppliers: suppliers, + taxCategories: taxCategories, + shippingRestrictions: [ + { label: "None", value: "0" }, + { label: "US Only", value: "1" }, + { label: "Limited Quantity", value: "2" }, + { label: "US/CA Only", value: "3" }, + { label: "No FedEx 2 Day", value: "4" }, + { label: "North America Only", value: "5" } + ] + }; + }); + + // Add debugging to verify category types + console.log(`Returning ${result.categories.length} categories with types: ${Array.from(new Set(result.categories.map(c => c.type))).join(', ')}`); + + res.json(result); + } catch (error) { + console.error('Error fetching import field options:', error); + res.status(500).json({ error: 'Failed to fetch import field options' }); + } +}); + +// Get product lines for a specific company +router.get('/product-lines/:companyId', async (req, res) => { + try { + // Use cached connection + const { connection } = await getDbConnection(); + + const companyId = req.params.companyId; + const cacheKey = `product-lines-${companyId}`; + + const lines = await getCachedQuery(cacheKey, 'product-lines', async () => { + const [queryResult] = await connection.query(` + SELECT cat_id as value, name as label + FROM product_categories + WHERE type = 2 + AND master_cat_id = ? + ORDER BY name + `, [companyId]); + + return queryResult.map(l => ({ label: l.label, value: l.value.toString() })); + }); + + res.json(lines); + } catch (error) { + console.error('Error fetching product lines:', error); + res.status(500).json({ error: 'Failed to fetch product lines' }); + } +}); + +// Get sublines for a specific product line +router.get('/sublines/:lineId', async (req, res) => { + try { + // Use cached connection + const { connection } = await getDbConnection(); + + const lineId = req.params.lineId; + const cacheKey = `sublines-${lineId}`; + + const sublines = await getCachedQuery(cacheKey, 'sublines', async () => { + const [queryResult] = await connection.query(` + SELECT cat_id as value, name as label + FROM product_categories + WHERE type = 3 + AND master_cat_id = ? + ORDER BY name + `, [lineId]); + + return queryResult.map(s => ({ label: s.label, value: s.value.toString() })); + }); + + res.json(sublines); + } catch (error) { + console.error('Error fetching sublines:', error); + res.status(500).json({ error: 'Failed to fetch sublines' }); + } +}); + +// Add a simple endpoint to check file existence and permissions +router.get('/check-file/:filename', (req, res) => { + const { filename } = req.params; + + // Prevent directory traversal + if (filename.includes('..') || filename.includes('/')) { + return res.status(400).json({ error: 'Invalid filename' }); + } + + const filePath = path.join(uploadsDir, filename); + + try { + // Check if file exists + if (!fs.existsSync(filePath)) { + return res.status(404).json({ + error: 'File not found', + path: filePath, + exists: false, + readable: false + }); + } + + // Check if file is readable + fs.accessSync(filePath, fs.constants.R_OK); + + // Get file stats + const stats = fs.statSync(filePath); + + return res.json({ + filename, + path: filePath, + exists: true, + readable: true, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + size: stats.size, + created: stats.birthtime, + modified: stats.mtime, + permissions: stats.mode.toString(8) + }); + } catch (error) { + return res.status(500).json({ + error: error.message, + path: filePath, + exists: fs.existsSync(filePath), + readable: false + }); + } +}); + +// List all files in uploads directory +router.get('/list-uploads', (req, res) => { + try { + if (!fs.existsSync(uploadsDir)) { + return res.status(404).json({ error: 'Uploads directory not found', path: uploadsDir }); + } + + const files = fs.readdirSync(uploadsDir); + const fileDetails = files.map(file => { + const filePath = path.join(uploadsDir, file); + try { + const stats = fs.statSync(filePath); + return { + filename: file, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + size: stats.size, + created: stats.birthtime, + modified: stats.mtime, + permissions: stats.mode.toString(8) + }; + } catch (error) { + return { filename: file, error: error.message }; + } + }); + + return res.json({ + directory: uploadsDir, + count: files.length, + files: fileDetails + }); + } catch (error) { + return res.status(500).json({ error: error.message, path: uploadsDir }); + } +}); + +// Search products from production database +router.get('/search-products', async (req, res) => { + const { q, company, dateRange } = req.query; + + if (!q) { + return res.status(400).json({ error: 'Search term is required' }); + } + + try { + const { connection } = await getDbConnection(); + + // Build WHERE clause with additional filters + let whereClause = ` + WHERE ( + p.description LIKE ? OR + p.itemnumber LIKE ? OR + p.upc LIKE ? OR + pc1.name LIKE ? OR + s.companyname LIKE ? + )`; + + // Add company filter if provided + if (company) { + whereClause += ` AND p.company = ${connection.escape(company)}`; + } + + // Add date range filter if provided + if (dateRange) { + let dateCondition; + const now = new Date(); + + switch(dateRange) { + case '1week': + // Last week: date is after (current date - 7 days) + const weekAgo = new Date(now); + weekAgo.setDate(now.getDate() - 7); + dateCondition = `p.datein >= ${connection.escape(weekAgo.toISOString().slice(0, 10))}`; + break; + case '1month': + // Last month: date is after (current date - 30 days) + const monthAgo = new Date(now); + monthAgo.setDate(now.getDate() - 30); + dateCondition = `p.datein >= ${connection.escape(monthAgo.toISOString().slice(0, 10))}`; + break; + case '2months': + // Last 2 months: date is after (current date - 60 days) + const twoMonthsAgo = new Date(now); + twoMonthsAgo.setDate(now.getDate() - 60); + dateCondition = `p.datein >= ${connection.escape(twoMonthsAgo.toISOString().slice(0, 10))}`; + break; + case '3months': + // Last 3 months: date is after (current date - 90 days) + const threeMonthsAgo = new Date(now); + threeMonthsAgo.setDate(now.getDate() - 90); + dateCondition = `p.datein >= ${connection.escape(threeMonthsAgo.toISOString().slice(0, 10))}`; + break; + case '6months': + // Last 6 months: date is after (current date - 180 days) + const sixMonthsAgo = new Date(now); + sixMonthsAgo.setDate(now.getDate() - 180); + dateCondition = `p.datein >= ${connection.escape(sixMonthsAgo.toISOString().slice(0, 10))}`; + break; + case '1year': + // Last year: date is after (current date - 365 days) + const yearAgo = new Date(now); + yearAgo.setDate(now.getDate() - 365); + dateCondition = `p.datein >= ${connection.escape(yearAgo.toISOString().slice(0, 10))}`; + break; + default: + // If an unrecognized value is provided, don't add a date condition + dateCondition = null; + } + + if (dateCondition) { + whereClause += ` AND ${dateCondition}`; + } + } + + // Special case for wildcard search + const isWildcardSearch = q === '*'; + const searchPattern = isWildcardSearch ? '%' : `%${q}%`; + const exactPattern = isWildcardSearch ? '%' : q; + + // Search for products based on various fields + const query = ` + SELECT + p.pid, + p.description AS title, + p.notes AS description, + p.itemnumber AS sku, + p.upc AS barcode, + p.harmonized_tariff_code, + pcp.price_each AS price, + p.sellingprice AS regular_price, + CASE + WHEN EXISTS (SELECT 1 FROM product_inventory WHERE pid = p.pid AND count > 0) + THEN (SELECT ROUND(AVG(costeach), 5) FROM product_inventory WHERE pid = p.pid AND count > 0) + ELSE (SELECT costeach FROM product_inventory WHERE pid = p.pid ORDER BY daterec DESC LIMIT 1) + END AS cost_price, + s.companyname AS vendor, + sid.supplier_itemnumber AS vendor_reference, + sid.notions_itemnumber AS notions_reference, + sid.supplier_id AS supplier, + sid.notions_case_pack AS case_qty, + pc1.name AS brand, + p.company AS brand_id, + pc2.name AS line, + p.line AS line_id, + pc3.name AS subline, + p.subline AS subline_id, + pc4.name AS artist, + p.artist AS artist_id, + COALESCE(CASE + WHEN sid.supplier_id = 92 THEN sid.notions_qty_per_unit + ELSE sid.supplier_qty_per_unit + END, sid.notions_qty_per_unit) AS moq, + p.weight, + p.length, + p.width, + p.height, + p.country_of_origin, + ci.totalsold AS total_sold, + p.datein AS first_received, + pls.date_sold AS date_last_sold, + IF(p.tax_code IS NULL, '', CAST(p.tax_code AS CHAR)) AS tax_code, + CAST(p.size_cat AS CHAR) AS size_cat, + CAST(p.shipping_restrictions AS CHAR) AS shipping_restrictions + FROM products p + LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1 + LEFT JOIN supplier_item_data sid ON p.pid = sid.pid + LEFT JOIN suppliers s ON sid.supplier_id = s.supplierid + LEFT JOIN product_categories pc1 ON p.company = pc1.cat_id + LEFT JOIN product_categories pc2 ON p.line = pc2.cat_id + LEFT JOIN product_categories pc3 ON p.subline = pc3.cat_id + LEFT JOIN product_categories pc4 ON p.artist = pc4.cat_id + LEFT JOIN product_last_sold pls ON p.pid = pls.pid + LEFT JOIN current_inventory ci ON p.pid = ci.pid + ${whereClause} + GROUP BY p.pid + ${isWildcardSearch ? 'ORDER BY p.datein DESC' : ` + ORDER BY + CASE + WHEN p.description LIKE ? THEN 1 + WHEN p.itemnumber = ? THEN 2 + WHEN p.upc = ? THEN 3 + WHEN pc1.name LIKE ? THEN 4 + WHEN s.companyname LIKE ? THEN 5 + ELSE 6 + END + `} + `; + + // Prepare query parameters based on whether it's a wildcard search + let queryParams; + if (isWildcardSearch) { + queryParams = [ + searchPattern, // LIKE for description + searchPattern, // LIKE for itemnumber + searchPattern, // LIKE for upc + searchPattern, // LIKE for brand name + searchPattern // LIKE for company name + ]; + } else { + queryParams = [ + searchPattern, // LIKE for description + searchPattern, // LIKE for itemnumber + searchPattern, // LIKE for upc + searchPattern, // LIKE for brand name + searchPattern, // LIKE for company name + // For ORDER BY clause + searchPattern, // LIKE for description + exactPattern, // Exact match for itemnumber + exactPattern, // Exact match for upc + searchPattern, // LIKE for brand name + searchPattern // LIKE for company name + ]; + } + + const [results] = await connection.query(query, queryParams); + + // Debug log to check values + if (results.length > 0) { + console.log('Product search result sample fields:', { + pid: results[0].pid, + tax_code: results[0].tax_code, + tax_code_type: typeof results[0].tax_code, + tax_code_value: `Value: '${results[0].tax_code}'`, + size_cat: results[0].size_cat, + shipping_restrictions: results[0].shipping_restrictions, + supplier: results[0].supplier, + case_qty: results[0].case_qty, + moq: results[0].moq + }); + } + + res.json(results); + } catch (error) { + console.error('Error searching products:', error); + res.status(500).json({ error: 'Failed to search products' }); + } +}); + +// Endpoint to check UPC and generate item number +router.get('/check-upc-and-generate-sku', async (req, res) => { + const { upc, supplierId } = req.query; + + if (!upc || !supplierId) { + return res.status(400).json({ error: 'UPC and supplier ID are required' }); + } + + try { + const { connection } = await getDbConnection(); + + // Step 1: Check if the UPC already exists + const [upcCheck] = await connection.query( + 'SELECT pid, itemnumber FROM products WHERE upc = ? LIMIT 1', + [upc] + ); + + if (upcCheck.length > 0) { + return res.status(409).json({ + error: 'UPC already exists', + existingProductId: upcCheck[0].pid, + existingItemNumber: upcCheck[0].itemnumber + }); + } + + // Step 2: Generate item number - supplierId-last5DigitsOfUPC minus last digit + let itemNumber = ''; + const upcStr = String(upc); + + // Extract the last 5 digits of the UPC, removing the last digit (checksum) + // So we get 5 digits from positions: length-6 to length-2 + if (upcStr.length >= 6) { + const lastFiveMinusOne = upcStr.substring(upcStr.length - 6, upcStr.length - 1); + itemNumber = `${supplierId}-${lastFiveMinusOne}`; + } else if (upcStr.length >= 5) { + // If UPC is shorter, use as many digits as possible + const digitsToUse = upcStr.substring(0, upcStr.length - 1); + itemNumber = `${supplierId}-${digitsToUse}`; + } else { + // Very short UPC, just use the whole thing + itemNumber = `${supplierId}-${upcStr}`; + } + + // Step 3: Check if the generated item number exists + const [itemNumberCheck] = await connection.query( + 'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1', + [itemNumber] + ); + + // Step 4: If the item number exists, modify it to use the last 5 digits of the UPC + if (itemNumberCheck.length > 0) { + console.log(`Item number ${itemNumber} already exists, using alternative format`); + + if (upcStr.length >= 5) { + // Use the last 5 digits (including the checksum) + const lastFive = upcStr.substring(upcStr.length - 5); + itemNumber = `${supplierId}-${lastFive}`; + + // Check again if this new item number also exists + const [altItemNumberCheck] = await connection.query( + 'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1', + [itemNumber] + ); + + if (altItemNumberCheck.length > 0) { + // If even the alternative format exists, add a timestamp suffix for uniqueness + const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp + itemNumber = `${supplierId}-${timestamp}`; + console.log(`Alternative item number also exists, using timestamp: ${itemNumber}`); + } + } else { + // For very short UPCs, add a timestamp + const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp + itemNumber = `${supplierId}-${timestamp}`; + } + } + + // Return the generated item number + res.json({ + success: true, + itemNumber, + upc, + supplierId + }); + + } catch (error) { + console.error('Error checking UPC and generating item number:', error); + res.status(500).json({ + error: 'Failed to check UPC and generate item number', + details: error.message + }); + } +}); + +// Get product categories for a specific product +router.get('/product-categories/:pid', async (req, res) => { + try { + const { pid } = req.params; + + if (!pid || isNaN(parseInt(pid))) { + return res.status(400).json({ error: 'Valid product ID is required' }); + } + + // Use the getDbConnection function instead of getPool + const { connection } = await getDbConnection(); + + // Query to get categories for a specific product + const query = ` + SELECT pc.cat_id, pc.name, pc.type, pc.combined_name, pc.master_cat_id + FROM product_category_index pci + JOIN product_categories pc ON pci.cat_id = pc.cat_id + WHERE pci.pid = ? + ORDER BY pc.type, pc.name + `; + + const [rows] = await connection.query(query, [pid]); + + // Add debugging to log category types + const categoryTypes = rows.map(row => row.type); + const uniqueTypes = [...new Set(categoryTypes)]; + console.log(`Product ${pid} has ${rows.length} categories with types: ${uniqueTypes.join(', ')}`); + console.log('Categories:', rows.map(row => ({ id: row.cat_id, name: row.name, type: row.type }))); + + // Check for parent categories to filter out deals and black friday + const sectionQuery = ` + SELECT pc.cat_id, pc.name + FROM product_categories pc + WHERE pc.type = 10 AND (LOWER(pc.name) LIKE '%deal%' OR LOWER(pc.name) LIKE '%black friday%') + `; + + const [dealSections] = await connection.query(sectionQuery); + const dealSectionIds = dealSections.map(section => section.cat_id); + + console.log('Filtering out categories from deal sections:', dealSectionIds); + + // Filter out categories from deals and black friday sections + const filteredCategories = rows.filter(category => { + // Direct check for top-level deal sections + if (category.type === 10) { + return !dealSectionIds.some(id => id === category.cat_id); + } + + // For categories (type 11), check if their parent is a deal section + if (category.type === 11) { + return !dealSectionIds.some(id => id === category.master_cat_id); + } + + // For subcategories (type 12), get their parent category first + if (category.type === 12) { + const parentId = category.master_cat_id; + // Find the parent category in our rows + const parentCategory = rows.find(c => c.cat_id === parentId); + // If parent not found or parent's parent is not a deal section, keep it + return !parentCategory || !dealSectionIds.some(id => id === parentCategory.master_cat_id); + } + + // For subsubcategories (type 13), check their hierarchy manually + if (category.type === 13) { + const parentId = category.master_cat_id; + // Find the parent subcategory + const parentSubcategory = rows.find(c => c.cat_id === parentId); + if (!parentSubcategory) return true; + + // Find the grandparent category + const grandparentId = parentSubcategory.master_cat_id; + const grandparentCategory = rows.find(c => c.cat_id === grandparentId); + // If grandparent not found or grandparent's parent is not a deal section, keep it + return !grandparentCategory || !dealSectionIds.some(id => id === grandparentCategory.master_cat_id); + } + + // Keep all other category types + return true; + }); + + console.log(`Filtered out ${rows.length - filteredCategories.length} deal/black friday categories`); + + // Format the response to match the expected format in the frontend + const categories = filteredCategories.map(category => ({ + value: category.cat_id.toString(), + label: category.name, + type: category.type, + combined_name: category.combined_name + })); + + res.json(categories); + } catch (error) { + console.error('Error fetching product categories:', error); + res.status(500).json({ + error: 'Failed to fetch product categories', + details: error.message + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/inventory-server/src/routes/templates.js b/inventory-server/src/routes/templates.js new file mode 100644 index 0000000..999f960 --- /dev/null +++ b/inventory-server/src/routes/templates.js @@ -0,0 +1,283 @@ +const express = require('express'); +const { getPool } = require('../utils/db'); +const dotenv = require('dotenv'); +const path = require('path'); + +dotenv.config({ path: path.join(__dirname, "../../.env") }); + +const router = express.Router(); + +// Get all templates +router.get('/', async (req, res) => { + try { + const pool = getPool(); + if (!pool) { + throw new Error('Database pool not initialized'); + } + + const result = await pool.query(` + SELECT * FROM templates + ORDER BY company ASC, product_type ASC + `); + res.json(result.rows); + } catch (error) { + console.error('Error fetching templates:', error); + res.status(500).json({ + error: 'Failed to fetch templates', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +// Get template by company and product type +router.get('/:company/:productType', async (req, res) => { + try { + const { company, productType } = req.params; + const pool = getPool(); + if (!pool) { + throw new Error('Database pool not initialized'); + } + + const result = await pool.query(` + SELECT * FROM templates + WHERE company = $1 AND product_type = $2 + `, [company, productType]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Template not found' }); + } + + res.json(result.rows[0]); + } catch (error) { + console.error('Error fetching template:', error); + res.status(500).json({ + error: 'Failed to fetch template', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +// Create new template +router.post('/', async (req, res) => { + try { + const { + company, + product_type, + supplier, + msrp, + cost_each, + qty_per_unit, + case_qty, + hts_code, + description, + weight, + length, + width, + height, + tax_cat, + size_cat, + categories, + ship_restrictions + } = req.body; + + // Validate required fields + if (!company || !product_type) { + return res.status(400).json({ error: 'Company and Product Type are required' }); + } + + const pool = getPool(); + if (!pool) { + throw new Error('Database pool not initialized'); + } + + const result = await pool.query(` + INSERT INTO templates ( + company, + product_type, + supplier, + msrp, + cost_each, + qty_per_unit, + case_qty, + hts_code, + description, + weight, + length, + width, + height, + tax_cat, + size_cat, + categories, + ship_restrictions + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + RETURNING * + `, [ + company, + product_type, + supplier, + msrp, + cost_each, + qty_per_unit, + case_qty, + hts_code, + description, + weight, + length, + width, + height, + tax_cat, + size_cat, + categories, + ship_restrictions + ]); + + res.status(201).json(result.rows[0]); + } catch (error) { + console.error('Error creating template:', error); + // Check for unique constraint violation + if (error instanceof Error && error.message.includes('unique constraint')) { + return res.status(409).json({ + error: 'Template already exists for this company and product type', + details: error.message + }); + } + res.status(500).json({ + error: 'Failed to create template', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +// Update template +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { + company, + product_type, + supplier, + msrp, + cost_each, + qty_per_unit, + case_qty, + hts_code, + description, + weight, + length, + width, + height, + tax_cat, + size_cat, + categories, + ship_restrictions + } = req.body; + + // Validate required fields + if (!company || !product_type) { + return res.status(400).json({ error: 'Company and Product Type are required' }); + } + + const pool = getPool(); + if (!pool) { + throw new Error('Database pool not initialized'); + } + + const result = await pool.query(` + UPDATE templates + SET + company = $1, + product_type = $2, + supplier = $3, + msrp = $4, + cost_each = $5, + qty_per_unit = $6, + case_qty = $7, + hts_code = $8, + description = $9, + weight = $10, + length = $11, + width = $12, + height = $13, + tax_cat = $14, + size_cat = $15, + categories = $16, + ship_restrictions = $17 + WHERE id = $18 + RETURNING * + `, [ + company, + product_type, + supplier, + msrp, + cost_each, + qty_per_unit, + case_qty, + hts_code, + description, + weight, + length, + width, + height, + tax_cat, + size_cat, + categories, + ship_restrictions, + id + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Template not found' }); + } + + res.json(result.rows[0]); + } catch (error) { + console.error('Error updating template:', error); + // Check for unique constraint violation + if (error instanceof Error && error.message.includes('unique constraint')) { + return res.status(409).json({ + error: 'Template already exists for this company and product type', + details: error.message + }); + } + res.status(500).json({ + error: 'Failed to update template', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +// Delete template +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const pool = getPool(); + if (!pool) { + throw new Error('Database pool not initialized'); + } + + const result = await pool.query('DELETE FROM templates WHERE id = $1 RETURNING *', [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Template not found' }); + } + + res.json({ message: 'Template deleted successfully' }); + } catch (error) { + console.error('Error deleting template:', error); + res.status(500).json({ + error: 'Failed to delete template', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +// Error handling middleware +router.use((err, req, res, next) => { + console.error('Template route error:', err); + res.status(500).json({ + error: 'Internal server error', + details: err.message + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index 77f1c38..879dfa3 100755 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -15,6 +15,9 @@ const configRouter = require('./routes/config'); const metricsRouter = require('./routes/metrics'); const vendorsRouter = require('./routes/vendors'); const categoriesRouter = require('./routes/categories'); +const importRouter = require('./routes/import'); +const aiValidationRouter = require('./routes/ai-validation'); +const templatesRouter = require('./routes/templates'); // Get the absolute path to the .env file const envPath = '/var/www/html/inventory/.env'; @@ -62,67 +65,78 @@ app.use((req, res, next) => { app.use(corsMiddleware); // Body parser middleware -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); -// Initialize database pool -const poolPromise = initPool({ - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - port: process.env.DB_PORT || 5432, - max: process.env.NODE_ENV === 'production' ? 20 : 10, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, - ssl: process.env.DB_SSL === 'true' ? { - rejectUnauthorized: false - } : false -}); +// Initialize database pool and start server +async function startServer() { + try { + // Initialize database pool + const pool = await initPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT || 5432, + max: process.env.NODE_ENV === 'production' ? 20 : 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + ssl: process.env.DB_SSL === 'true' ? { + rejectUnauthorized: false + } : false + }); -// Make pool available to routes once initialized -poolPromise.then(pool => { - app.locals.pool = pool; -}).catch(err => { - console.error('[Database] Failed to initialize pool:', err); - process.exit(1); -}); + // Make pool available to routes + app.locals.pool = pool; -// Routes -app.use('/api/products', productsRouter); -app.use('/api/dashboard', dashboardRouter); -app.use('/api/orders', ordersRouter); -app.use('/api/csv', csvRouter); -app.use('/api/analytics', analyticsRouter); -app.use('/api/purchase-orders', purchaseOrdersRouter); -app.use('/api/config', configRouter); -app.use('/api/metrics', metricsRouter); -app.use('/api/vendors', vendorsRouter); -app.use('/api/categories', categoriesRouter); + // Set up routes after pool is initialized + app.use('/api/products', productsRouter); + app.use('/api/dashboard', dashboardRouter); + app.use('/api/orders', ordersRouter); + app.use('/api/csv', csvRouter); + app.use('/api/analytics', analyticsRouter); + app.use('/api/purchase-orders', purchaseOrdersRouter); + app.use('/api/config', configRouter); + app.use('/api/metrics', metricsRouter); + app.use('/api/vendors', vendorsRouter); + app.use('/api/categories', categoriesRouter); + app.use('/api/import', importRouter); + app.use('/api/ai-validation', aiValidationRouter); + app.use('/api/templates', templatesRouter); -// Basic health check route -app.get('/health', (req, res) => { - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - environment: process.env.NODE_ENV - }); -}); + // Basic health check route + app.get('/health', (req, res) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV + }); + }); -// CORS error handler - must be before other error handlers -app.use(corsErrorHandler); + // CORS error handler - must be before other error handlers + app.use(corsErrorHandler); -// Error handling middleware - MUST be after routes and CORS error handler -app.use((err, req, res, next) => { - console.error(`[${new Date().toISOString()}] Error:`, err); - - // Send detailed error in development, generic in production - const error = process.env.NODE_ENV === 'production' - ? 'An internal server error occurred' - : err.message || err; - - res.status(err.status || 500).json({ error }); -}); + // Error handling middleware - MUST be after routes and CORS error handler + app.use((err, req, res, next) => { + console.error(`[${new Date().toISOString()}] Error:`, err); + + // Send detailed error in development, generic in production + const error = process.env.NODE_ENV === 'production' + ? 'An internal server error occurred' + : err.message || err; + + res.status(err.status || 500).json({ error }); + }); + + const PORT = process.env.PORT || 3000; + app.listen(PORT, () => { + console.log(`[Server] Running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} // Handle uncaught exceptions process.on('uncaughtException', (err) => { @@ -184,62 +198,5 @@ const setupSSE = (req, res) => { } }; -// Update the status endpoint to include reset-metrics -app.get('/csv/status', (req, res) => { - res.json({ - active: !!currentOperation, - type: currentOperation?.type || null, - progress: currentOperation ? { - status: currentOperation.status, - operation: currentOperation.operation, - current: currentOperation.current, - total: currentOperation.total, - percentage: currentOperation.percentage - } : null - }); -}); - -// Update progress endpoint mapping -app.get('/csv/:type/progress', (req, res) => { - const { type } = req.params; - if (!['import', 'update', 'reset', 'reset-metrics'].includes(type)) { - res.status(400).json({ error: 'Invalid operation type' }); - return; - } - - setupSSE(req, res); -}); - -// Update the cancel endpoint to handle reset-metrics -app.post('/csv/cancel', (req, res) => { - const { operation } = req.query; - - if (!currentOperation) { - res.status(400).json({ error: 'No operation in progress' }); - return; - } - - if (operation && operation.toLowerCase() !== currentOperation.type) { - res.status(400).json({ error: 'Operation type mismatch' }); - return; - } - - try { - // Handle cancellation based on operation type - if (currentOperation.type === 'reset-metrics') { - // Reset metrics doesn't need special cleanup - currentOperation = null; - res.json({ message: 'Reset metrics cancelled' }); - } else { - // ... existing cancellation logic for other operations ... - } - } catch (error) { - console.error('Error during cancellation:', error); - res.status(500).json({ error: 'Failed to cancel operation' }); - } -}); - -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`[Server] Running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`); -}); \ No newline at end of file +// Start the server +startServer(); \ No newline at end of file diff --git a/inventory-server/src/utils/db.js b/inventory-server/src/utils/db.js index 3b6454f..0ca0f63 100644 --- a/inventory-server/src/utils/db.js +++ b/inventory-server/src/utils/db.js @@ -1,63 +1,10 @@ -const { Pool, Client } = require('pg'); +const mysql = require('mysql2/promise'); let pool; function initPool(config) { - // Log config without sensitive data - const safeConfig = { - host: config.host, - user: config.user, - database: config.database, - port: config.port, - max: config.max, - idleTimeoutMillis: config.idleTimeoutMillis, - connectionTimeoutMillis: config.connectionTimeoutMillis, - ssl: config.ssl, - password: config.password ? '[password set]' : '[no password]' - }; - console.log('[Database] Initializing pool with config:', safeConfig); - - // Try creating a client first to test the connection - const testClient = new Client({ - host: config.host, - user: config.user, - password: config.password, - database: config.database, - port: config.port, - ssl: config.ssl - }); - - console.log('[Database] Testing connection with Client...'); - return testClient.connect() - .then(() => { - console.log('[Database] Test connection with Client successful'); - return testClient.end(); - }) - .then(() => { - // If client connection worked, create the pool - console.log('[Database] Creating pool...'); - pool = new Pool({ - host: config.host, - user: config.user, - password: config.password, - database: config.database, - port: config.port, - max: config.max, - idleTimeoutMillis: config.idleTimeoutMillis, - connectionTimeoutMillis: config.connectionTimeoutMillis, - ssl: config.ssl - }); - return pool.connect(); - }) - .then(poolClient => { - console.log('[Database] Pool connection successful'); - poolClient.release(); - return pool; - }) - .catch(err => { - console.error('[Database] Connection failed:', err); - throw err; - }); + pool = mysql.createPool(config); + return pool; } async function getConnection() { diff --git a/inventory/package-lock.json b/inventory/package-lock.json index d3f5a14..ba69ecc 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -11,55 +11,72 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "@shadcn/ui": "^0.0.4", "@tabler/icons-react": "^3.28.1", - "@tanstack/react-query": "^5.63.0", + "@tanstack/react-query": "^5.66.7", "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.11.2", "@tanstack/virtual-core": "^3.11.2", + "@types/js-levenshtein": "^1.1.3", + "@types/uuid": "^10.0.0", + "axios": "^1.8.1", "chart.js": "^4.4.7", "chartjs-adapter-date-fns": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", + "diff": "^7.0.0", + "framer-motion": "^12.4.4", + "js-levenshtein": "^1.1.6", + "lodash": "^4.17.21", "lucide-react": "^0.469.0", "motion": "^11.18.0", "next-themes": "^0.4.4", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", + "react-data-grid": "^7.0.0-beta.13", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-dropzone": "^14.3.5", + "react-icons": "^5.4.0", "react-router-dom": "^7.1.1", "recharts": "^2.15.0", "sonner": "^1.7.1", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "tanstack": "^1.0.0", - "vaul": "^1.1.2" + "uuid": "^11.0.5", + "vaul": "^1.1.2", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^9.17.0", "@types/fs-extra": "^11.0.4", - "@types/lodash": "^4.17.14", + "@types/lodash": "^4.17.15", "@types/node": "^22.10.5", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -107,7 +124,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -119,9 +135,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, "license": "MIT", "engines": { @@ -129,22 +145,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -159,15 +175,21 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -177,13 +199,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -197,7 +219,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -226,9 +247,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -239,7 +260,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -249,7 +269,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -266,27 +285,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", - "dev": true, + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.26.9" }, "bin": { "parser": "bin/babel-parser.js" @@ -328,9 +346,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -340,32 +358,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -377,17 +393,15 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -450,10 +464,156 @@ "react": ">=16.8.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", "cpu": [ "ppc64" ], @@ -468,9 +628,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", "cpu": [ "arm" ], @@ -485,9 +645,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", "cpu": [ "arm64" ], @@ -502,9 +662,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", "cpu": [ "x64" ], @@ -519,9 +679,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", "cpu": [ "arm64" ], @@ -536,9 +696,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", "cpu": [ "x64" ], @@ -553,9 +713,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", "cpu": [ "arm64" ], @@ -570,9 +730,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", "cpu": [ "x64" ], @@ -587,9 +747,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", "cpu": [ "arm" ], @@ -604,9 +764,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", "cpu": [ "arm64" ], @@ -621,9 +781,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", "cpu": [ "ia32" ], @@ -638,9 +798,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", "cpu": [ "loong64" ], @@ -655,9 +815,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", "cpu": [ "mips64el" ], @@ -672,9 +832,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", "cpu": [ "ppc64" ], @@ -689,9 +849,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", "cpu": [ "riscv64" ], @@ -706,9 +866,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", "cpu": [ "s390x" ], @@ -723,9 +883,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", "cpu": [ "x64" ], @@ -740,9 +900,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", "cpu": [ "arm64" ], @@ -757,9 +917,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", "cpu": [ "x64" ], @@ -774,9 +934,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", "cpu": [ "arm64" ], @@ -791,9 +951,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", "cpu": [ "x64" ], @@ -808,9 +968,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", "cpu": [ "x64" ], @@ -825,9 +985,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", "cpu": [ "arm64" ], @@ -842,9 +1002,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", "cpu": [ "ia32" ], @@ -859,9 +1019,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], @@ -918,13 +1078,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -933,9 +1093,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -983,9 +1143,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -993,9 +1153,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1003,18 +1163,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -1248,19 +1422,19 @@ "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz", - "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.3.tgz", + "integrity": "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collapsible": "1.1.2", - "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { @@ -1279,17 +1453,17 @@ } }, "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz", - "integrity": "sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dialog": "1.1.4", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1307,12 +1481,12 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", - "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", @@ -1330,13 +1504,13 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz", - "integrity": "sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz", + "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==", "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -1355,10 +1529,40 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", - "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.3.tgz", + "integrity": "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", @@ -1366,7 +1570,7 @@ "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -1386,15 +1590,15 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", - "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1442,25 +1646,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", - "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -1493,14 +1697,14 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", - "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, @@ -1520,17 +1724,17 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz", - "integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.4", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { @@ -1564,13 +1768,13 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", - "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { @@ -1607,12 +1811,12 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", - "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", @@ -1630,29 +1834,29 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz", - "integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.1", - "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-roving-focus": "1.1.1", - "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -1670,26 +1874,26 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz", - "integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.1", - "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -1707,16 +1911,16 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", - "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", @@ -1739,12 +1943,12 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", - "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { @@ -1787,12 +1991,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", - "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1810,13 +2014,45 @@ } }, "node_modules/@radix-ui/react-progress": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz", - "integrity": "sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", + "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -1834,18 +2070,18 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", - "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, @@ -1865,9 +2101,9 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz", - "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.0", @@ -1876,7 +2112,7 @@ "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -1896,32 +2132,32 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz", - "integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.1", - "@radix-ui/react-portal": "1.1.3", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -1939,12 +2175,12 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", - "integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", + "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", @@ -1962,9 +2198,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" @@ -1980,15 +2216,15 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", - "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", + "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" @@ -2009,9 +2245,9 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", - "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", @@ -2019,8 +2255,8 @@ "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { @@ -2038,14 +2274,48 @@ } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz", - "integrity": "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==", + "node_modules/@radix-ui/react-toast": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz", + "integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { @@ -2064,17 +2334,17 @@ } }, "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.1.tgz", - "integrity": "sha512-OgDLZEA30Ylyz8YSXvnGqIHtERqnUt1KUYTKdw/y8u7Ci6zGiJfXc02jahmcSNK3YcErqioj/9flWC9S1ihfwg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz", + "integrity": "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-roving-focus": "1.1.1", - "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-toggle": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { @@ -2093,23 +2363,23 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz", - "integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.1", - "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.1" + "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -2244,12 +2514,12 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", - "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", @@ -2273,9 +2543,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", - "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", "cpu": [ "arm" ], @@ -2287,9 +2557,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", - "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", "cpu": [ "arm64" ], @@ -2301,9 +2571,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", - "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", "cpu": [ "arm64" ], @@ -2315,9 +2585,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", - "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", "cpu": [ "x64" ], @@ -2329,9 +2599,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", - "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", "cpu": [ "arm64" ], @@ -2343,9 +2613,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", - "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", "cpu": [ "x64" ], @@ -2357,9 +2627,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", - "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", "cpu": [ "arm" ], @@ -2371,9 +2641,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", - "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", "cpu": [ "arm" ], @@ -2385,9 +2655,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", - "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", "cpu": [ "arm64" ], @@ -2399,9 +2669,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", - "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", "cpu": [ "arm64" ], @@ -2413,9 +2683,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", - "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", "cpu": [ "loong64" ], @@ -2427,9 +2697,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", - "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", "cpu": [ "ppc64" ], @@ -2441,9 +2711,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", - "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", "cpu": [ "riscv64" ], @@ -2455,9 +2725,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", - "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", "cpu": [ "s390x" ], @@ -2469,9 +2739,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", - "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", "cpu": [ "x64" ], @@ -2483,9 +2753,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", - "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", "cpu": [ "x64" ], @@ -2497,9 +2767,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", - "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", "cpu": [ "arm64" ], @@ -2511,9 +2781,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", - "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", "cpu": [ "ia32" ], @@ -2525,9 +2795,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", - "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", "cpu": [ "x64" ], @@ -2557,31 +2827,10 @@ "ui": "dist/index.js" } }, - "node_modules/@shadcn/ui/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@shadcn/ui/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@tabler/icons": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.28.1.tgz", - "integrity": "sha512-h7nqKEvFooLtFxhMOC1/2eiV+KRXhBUuDUUJrJlt6Ft6tuMw2eU/9GLQgrTk41DNmIEzp/LI83K9J9UUU8YBYQ==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.30.0.tgz", + "integrity": "sha512-c8OKLM48l00u9TFbh2qhSODMONIzML8ajtCyq95rW8vzkWcBrKRPM61tdkThz2j4kd5u17srPGIjqdeRUZdfdw==", "license": "MIT", "funding": { "type": "github", @@ -2589,12 +2838,12 @@ } }, "node_modules/@tabler/icons-react": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.28.1.tgz", - "integrity": "sha512-KNBpA2kbxr3/2YK5swt7b/kd/xpDP1FHYZCxDFIw54tX8slELRFEf95VMxsccQHZeIcUbdoojmUUuYSbt/sM5Q==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.30.0.tgz", + "integrity": "sha512-9KZ9D1UNAyjlLkkYp2HBPHdf6lAJ2aelDqh8YYAnnmLF3xwprWKxxW8+zw5jlI0IwdfN4XFFuzqePkaw+DpIOg==", "license": "MIT", "dependencies": { - "@tabler/icons": "3.28.1" + "@tabler/icons": "3.30.0" }, "funding": { "type": "github", @@ -2605,9 +2854,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.62.16", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.16.tgz", - "integrity": "sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g==", + "version": "5.66.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz", + "integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==", "license": "MIT", "funding": { "type": "github", @@ -2615,12 +2864,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.63.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.63.0.tgz", - "integrity": "sha512-QWizLzSiog8xqIRYmuJRok9VELlXVBAwtINgVCgW1SNvamQwWDO5R0XFSkjoBEj53x9Of1KAthLRBUC5xmtVLQ==", + "version": "5.66.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.7.tgz", + "integrity": "sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.62.16" + "@tanstack/query-core": "5.66.4" }, "funding": { "type": "github", @@ -2631,12 +2880,12 @@ } }, "node_modules/@tanstack/react-table": { - "version": "8.20.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", - "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==", + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz", + "integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==", "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.20.5" + "@tanstack/table-core": "8.21.2" }, "engines": { "node": ">=12" @@ -2651,12 +2900,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz", - "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz", + "integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.11.2" + "@tanstack/virtual-core": "3.13.0" }, "funding": { "type": "github", @@ -2668,9 +2917,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.20.5", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", - "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz", + "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==", "license": "MIT", "engines": { "node": ">=12" @@ -2681,9 +2930,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", - "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz", + "integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==", "license": "MIT", "funding": { "type": "github", @@ -2769,15 +3018,15 @@ } }, "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -2822,6 +3071,12 @@ "@types/node": "*" } }, + "node_modules/@types/js-levenshtein": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz", + "integrity": "sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2840,22 +3095,28 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -2884,22 +3145,28 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", - "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", + "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.1", - "@typescript-eslint/type-utils": "8.19.1", - "@typescript-eslint/utils": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/type-utils": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2915,16 +3182,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", - "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", + "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.1", - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/typescript-estree": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "engines": { @@ -2940,14 +3207,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", - "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1" + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2958,16 +3225,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", - "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", + "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.1", - "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2982,9 +3249,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", - "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -2996,20 +3263,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", - "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3049,9 +3316,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -3062,16 +3329,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", - "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.1", - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/typescript-estree": "8.19.1" + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3086,13 +3353,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", - "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3146,6 +3413,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3176,15 +3452,12 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -3234,6 +3507,21 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3272,6 +3560,32 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3401,11 +3715,23 @@ "ieee754": "^1.2.1" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3421,9 +3747,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "dev": true, "funding": [ { @@ -3441,18 +3767,26 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" }, "engines": { - "node": ">=10" + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", + "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -4005,6 +4339,15 @@ } } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4023,13 +4366,25 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=14" } }, "node_modules/concat-map": { @@ -4040,10 +4395,9 @@ "license": "MIT" }, "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/cookie": { @@ -4055,6 +4409,43 @@ "node": ">=18" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4231,7 +4622,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4270,6 +4660,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4282,6 +4681,15 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4298,6 +4706,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4305,9 +4727,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.79", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.79.tgz", - "integrity": "sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==", + "version": "1.5.102", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", + "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", "dev": true, "license": "ISC" }, @@ -4317,10 +4739,64 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4331,31 +4807,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/escalade": { @@ -4372,7 +4848,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4382,19 +4857,19 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -4455,9 +4930,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz", - "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==", + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4494,6 +4969,39 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", @@ -4587,12 +5095,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4652,9 +5154,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -4696,6 +5198,18 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4708,6 +5222,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4740,12 +5260,32 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4762,6 +5302,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -4774,6 +5341,15 @@ "node": ">=12.20.0" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4789,13 +5365,13 @@ } }, "node_modules/framer-motion": { - "version": "11.18.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.0.tgz", - "integrity": "sha512-Vmjl5Al7XqKHzDFnVqzi1H9hzn5w4eN/bdqXTymVpU2UuMQuz9w6UPdsL9dFBeH7loBlnu4qcEXME+nvbkcIOw==", + "version": "12.4.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.4.tgz", + "integrity": "sha512-JWkVwbJBgVkeZHNcnk8ififgwTF+5de9wbJnTLI+g9YqaGo75Xd5uRVDm9FR8chqRDOKcXv/71f40CGescYVmg==", "license": "MIT", "dependencies": { - "motion-dom": "^11.16.4", - "motion-utils": "^11.16.0", + "motion-dom": "^12.4.4", + "motion-utils": "^12.0.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -4816,9 +5392,9 @@ } }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -4862,6 +5438,30 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4871,6 +5471,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4940,9 +5553,9 @@ } }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -4952,6 +5565,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4975,6 +5600,33 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4987,6 +5639,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -5027,10 +5688,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -5068,6 +5728,12 @@ "node": ">=12" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5200,6 +5866,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5223,7 +5898,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5239,6 +5913,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5374,18 +6054,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5417,6 +6085,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5445,6 +6122,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -5480,12 +6178,12 @@ } }, "node_modules/motion": { - "version": "11.18.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-11.18.0.tgz", - "integrity": "sha512-uJ4zNXh/4K9C5wftxHKlXLHC0Rc9dHSHPyO1P6T9XE2bTn2z8C2lOZX/M8vAmFp0gtJTJ3aYkv44lTtJSfv6+A==", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-11.18.2.tgz", + "integrity": "sha512-JLjvFDuFr42NFtcVoMAyC2sEjnpA8xpy6qWPyzQvCloznAyQ8FIXioxWfHiLtgYhoVpfUqSWpn1h9++skj9+Wg==", "license": "MIT", "dependencies": { - "framer-motion": "^11.18.0", + "framer-motion": "^11.18.2", "tslib": "^2.4.0" }, "peerDependencies": { @@ -5506,25 +6204,66 @@ } }, "node_modules/motion-dom": { - "version": "11.16.4", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.16.4.tgz", - "integrity": "sha512-2wuCie206pCiP2K23uvwJeci4pMFfyQKpWI0Vy6HrCTDzDCer4TsYtT7IVnuGbDeoIV37UuZiUr6SZMHEc1Vww==", + "version": "12.4.4", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.4.tgz", + "integrity": "sha512-D8Kjp8oqUNqxoAVmIlOH+YCMov/4koBAmG4OJs0VWfh18xkQEIsx9+S7yrXyx0XaMBEPtre6e9LiSW2Zs7vIhA==", "license": "MIT", "dependencies": { - "motion-utils": "^11.16.0" + "motion-utils": "^12.0.0" } }, "node_modules/motion-utils": { - "version": "11.16.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.16.0.tgz", - "integrity": "sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==", + "license": "MIT" + }, + "node_modules/motion/node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion/node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion/node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -5737,18 +6476,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5791,7 +6518,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -5800,6 +6526,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5847,6 +6591,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5884,9 +6637,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -5903,7 +6656,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6060,10 +6813,10 @@ "react-is": "^16.13.1" } }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/punycode": { @@ -6118,6 +6871,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-data-grid": { + "version": "7.0.0-beta.13", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.13.tgz", + "integrity": "sha512-vhBdkC2KqAawmmzYTcNlfhfjcYFQsinNr5pPTUG6/3DzLfYWo1S6nl48wgPWgyD9uDclV3H5NWvKSSwQTTsYMQ==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0", + "react-dom": "^16.14 || ^17.0 || ^18.0" + } + }, + "node_modules/react-data-grid/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-day-picker": { "version": "8.10.1", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", @@ -6145,10 +6920,36 @@ "react": "^18.3.1" } }, + "node_modules/react-dropzone": { + "version": "14.3.5", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", + "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/react-refresh": { @@ -6162,16 +6963,16 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" + "use-sidecar": "^1.1.3" }, "engines": { "node": ">=10" @@ -6209,9 +7010,9 @@ } }, "node_modules/react-router": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", - "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz", + "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==", "license": "MIT", "dependencies": { "@types/cookie": "^0.6.0", @@ -6233,12 +7034,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz", - "integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.5.tgz", + "integrity": "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ==", "license": "MIT", "dependencies": { - "react-router": "7.1.1" + "react-router": "7.1.5" }, "engines": { "node": ">=20.0.0" @@ -6337,16 +7138,16 @@ } }, "node_modules/recharts": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", - "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", - "react-smooth": "^4.0.0", + "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" @@ -6368,6 +7169,12 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -6398,7 +7205,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6444,12 +7250,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6461,9 +7261,9 @@ } }, "node_modules/rollup": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", - "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6477,25 +7277,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.30.1", - "@rollup/rollup-android-arm64": "4.30.1", - "@rollup/rollup-darwin-arm64": "4.30.1", - "@rollup/rollup-darwin-x64": "4.30.1", - "@rollup/rollup-freebsd-arm64": "4.30.1", - "@rollup/rollup-freebsd-x64": "4.30.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", - "@rollup/rollup-linux-arm-musleabihf": "4.30.1", - "@rollup/rollup-linux-arm64-gnu": "4.30.1", - "@rollup/rollup-linux-arm64-musl": "4.30.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", - "@rollup/rollup-linux-riscv64-gnu": "4.30.1", - "@rollup/rollup-linux-s390x-gnu": "4.30.1", - "@rollup/rollup-linux-x64-gnu": "4.30.1", - "@rollup/rollup-linux-x64-musl": "4.30.1", - "@rollup/rollup-win32-arm64-msvc": "4.30.1", - "@rollup/rollup-win32-ia32-msvc": "4.30.1", - "@rollup/rollup-win32-x64-msvc": "4.30.1", + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" } }, @@ -6589,16 +7389,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/sisteransi": { "version": "1.0.5", @@ -6607,15 +7401,24 @@ "license": "MIT" }, "node_modules/sonner": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.1.tgz", - "integrity": "sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6625,6 +7428,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", @@ -6770,6 +7585,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -6792,6 +7613,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6919,9 +7749,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", - "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", "engines": { @@ -6977,15 +7807,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.1.tgz", - "integrity": "sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz", + "integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.19.1", - "@typescript-eslint/parser": "8.19.1", - "@typescript-eslint/utils": "8.19.1" + "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/parser": "8.24.1", + "@typescript-eslint/utils": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7105,6 +7935,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -7141,15 +7984,15 @@ } }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", + "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" @@ -7245,6 +8088,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7299,6 +8160,21 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7331,16 +8207,25 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" } }, "node_modules/yallist": { @@ -7376,9 +8261,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/inventory/package.json b/inventory/package.json index 097e57b..4ddf1e5 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -10,58 +10,75 @@ "preview": "vite preview" }, "dependencies": { - "@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", + "@emotion/styled": "^11.14.0", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "@shadcn/ui": "^0.0.4", "@tabler/icons-react": "^3.28.1", - "@tanstack/react-query": "^5.63.0", + "@tanstack/react-query": "^5.66.7", "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.11.2", "@tanstack/virtual-core": "^3.11.2", + "@types/js-levenshtein": "^1.1.3", + "@types/uuid": "^10.0.0", + "axios": "^1.8.1", "chart.js": "^4.4.7", "chartjs-adapter-date-fns": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", + "diff": "^7.0.0", + "framer-motion": "^12.4.4", + "js-levenshtein": "^1.1.6", + "lodash": "^4.17.21", "lucide-react": "^0.469.0", "motion": "^11.18.0", "next-themes": "^0.4.4", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", + "react-data-grid": "^7.0.0-beta.13", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-dropzone": "^14.3.5", + "react-icons": "^5.4.0", "react-router-dom": "^7.1.1", "recharts": "^2.15.0", "sonner": "^1.7.1", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "tanstack": "^1.0.0", - "vaul": "^1.1.2" + "uuid": "^11.0.5", + "vaul": "^1.1.2", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^9.17.0", "@types/fs-extra": "^11.0.4", - "@types/lodash": "^4.17.14", + "@types/lodash": "^4.17.15", "@types/node": "^22.10.5", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/inventory/scripts/copyBuild.ts b/inventory/scripts/copyBuild.ts deleted file mode 100644 index e39f65c..0000000 --- a/inventory/scripts/copyBuild.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index 898750f..e38ef11 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -15,6 +15,8 @@ import { RequireAuth } from './components/auth/RequireAuth'; import Forecasting from "@/pages/Forecasting"; import { Vendors } from '@/pages/Vendors'; import { Categories } from '@/pages/Categories'; +import { Import } from '@/pages/Import'; +import { AiValidationDebug } from "@/pages/AiValidationDebug" const queryClient = new QueryClient(); @@ -60,6 +62,7 @@ function App() { }> } /> } /> + } /> } /> } /> } /> @@ -67,6 +70,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/inventory/src/components/analytics/CategoryPerformance.tsx b/inventory/src/components/analytics/CategoryPerformance.tsx index 1f627a0..01e502c 100644 --- a/inventory/src/components/analytics/CategoryPerformance.tsx +++ b/inventory/src/components/analytics/CategoryPerformance.tsx @@ -99,7 +99,7 @@ export function CategoryPerformance() { ))} [ + formatter={(value: number, _: string, props: any) => [ `$${value.toLocaleString()}`,
Category Path:
@@ -143,7 +143,7 @@ export function CategoryPerformance() { /> `${value}%`} /> [ + formatter={(value: number, _: string, props: any) => [ `${value.toFixed(1)}%`,
Category Path:
diff --git a/inventory/src/components/analytics/ProfitAnalysis.tsx b/inventory/src/components/analytics/ProfitAnalysis.tsx index 17deac4..e8710ac 100644 --- a/inventory/src/components/analytics/ProfitAnalysis.tsx +++ b/inventory/src/components/analytics/ProfitAnalysis.tsx @@ -96,7 +96,7 @@ export function ProfitAnalysis() { /> `${value}%`} /> [ + formatter={(value: number, _: string, props: any) => [ `${value.toFixed(1)}%`,
Category Path:
diff --git a/inventory/src/components/dashboard/BestSellers.tsx b/inventory/src/components/dashboard/BestSellers.tsx index 412dc0c..bd387ea 100644 --- a/inventory/src/components/dashboard/BestSellers.tsx +++ b/inventory/src/components/dashboard/BestSellers.tsx @@ -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[] diff --git a/inventory/src/components/dashboard/LowStockAlerts.tsx b/inventory/src/components/dashboard/LowStockAlerts.tsx index f37aca5..e41a468 100644 --- a/inventory/src/components/dashboard/LowStockAlerts.tsx +++ b/inventory/src/components/dashboard/LowStockAlerts.tsx @@ -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({ queryKey: ["low-stock"], diff --git a/inventory/src/components/dashboard/PurchaseMetrics.tsx b/inventory/src/components/dashboard/PurchaseMetrics.tsx index b4852c4..4235277 100644 --- a/inventory/src/components/dashboard/PurchaseMetrics.tsx +++ b/inventory/src/components/dashboard/PurchaseMetrics.tsx @@ -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 diff --git a/inventory/src/components/dashboard/TrendingProducts.tsx b/inventory/src/components/dashboard/TrendingProducts.tsx index d9f2a74..b095988 100644 --- a/inventory/src/components/dashboard/TrendingProducts.tsx +++ b/inventory/src/components/dashboard/TrendingProducts.tsx @@ -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 ( <> diff --git a/inventory/src/components/forecasting/columns.tsx b/inventory/src/components/forecasting/columns.tsx index 8d26393..054af27 100644 --- a/inventory/src/components/forecasting/columns.tsx +++ b/inventory/src/components/forecasting/columns.tsx @@ -169,7 +169,7 @@ export const renderSubComponent = ({ row }: { row: any }) => { - {products.map((product) => ( + {products.map((product: Product) => ( > = { + autoMapHeaders: true, + autoMapSelectValues: false, + allowInvalidSubmit: true, + autoMapDistance: 2, + isNavigationEnabled: false, + translations: translations, + uploadStepHook: async (value) => value, + selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }), + matchColumnsStepHook: async (table) => table, + dateFormat: "yyyy-mm-dd", // ISO 8601, + parseRaw: true, +} as const + +export const ReactSpreadsheetImport = (propsWithoutDefaults: RsiProps) => { + const props = merge({}, defaultRSIProps, propsWithoutDefaults) + const mergedTranslations = + props.translations !== translations ? merge(translations, props.translations) : translations + + return ( + + + + + + ) +} diff --git a/inventory/src/components/product-import/components/ModalWrapper.tsx b/inventory/src/components/product-import/components/ModalWrapper.tsx new file mode 100644 index 0000000..fbb4363 --- /dev/null +++ b/inventory/src/components/product-import/components/ModalWrapper.tsx @@ -0,0 +1,104 @@ +import type React from "react" +import { + Dialog, + DialogContent, + DialogOverlay, + DialogPortal, + DialogClose, +} from "@/components/ui/dialog" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + AlertDialogPortal, + AlertDialogOverlay, +} from "@/components/ui/alert-dialog" +import { useRsi } from "../hooks/useRsi" +import { useState, useCallback } from "react" + +type Props = { + children: React.ReactNode + isOpen: boolean + onClose: () => void +} + +export const ModalWrapper = ({ children, isOpen, onClose }: Props) => { + const { rtl, translations } = useRsi() + const [showCloseAlert, setShowCloseAlert] = useState(false) + + // Create a handler that resets scroll positions before closing + const handleClose = useCallback(() => { + // Reset all scroll positions in the dialog + const scrollContainers = document.querySelectorAll('.overflow-auto, .overflow-scroll'); + scrollContainers.forEach(container => { + if (container instanceof HTMLElement) { + // Reset scroll position to top-left + container.scrollTop = 0; + container.scrollLeft = 0; + } + }); + + // Call the original onClose handler + onClose(); + }, [onClose]); + + return ( + <> + setShowCloseAlert(true)} modal> + + + { + e.preventDefault() + setShowCloseAlert(true) + }} + onPointerDownOutside={(e) => e.preventDefault()} + className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-[calc(100%-2rem)] h-[calc(100%-2rem)] max-w-[100vw] max-h-[100vh] flex flex-col overflow-hidden rounded-lg border bg-background p-0 shadow-lg sm:w-[calc(100%-3rem)] sm:h-[calc(100%-3rem)] md:w-[calc(100%-4rem)] md:h-[calc(100%-4rem)]" + > + + + { + e.preventDefault() + setShowCloseAlert(true) + }} /> + + +
+ {children} +
+
+
+
+ + + + + + + + {translations.alerts.confirmClose.headerTitle} + + + {translations.alerts.confirmClose.bodyText} + + + + setShowCloseAlert(false)}> + {translations.alerts.confirmClose.cancelButtonTitle} + + + {translations.alerts.confirmClose.exitButtonTitle} + + + + + + + ) +} diff --git a/inventory/src/components/product-import/components/Providers.tsx b/inventory/src/components/product-import/components/Providers.tsx new file mode 100644 index 0000000..b0af03d --- /dev/null +++ b/inventory/src/components/product-import/components/Providers.tsx @@ -0,0 +1,24 @@ +import { createContext } from "react" +import type { RsiProps } from "../types" + +export const RsiContext = createContext({} as any) + +type ProvidersProps = { + children: React.ReactNode + rsiValues: RsiProps +} + +// No need for a root ID as we're not using Chakra anymore +export const rootId = "rsi-modal-root" + +export const Providers = ({ children, rsiValues }: ProvidersProps) => { + if (!rsiValues.fields) { + throw new Error("Fields must be provided to react-spreadsheet-import") + } + + return ( + + {children} + + ) +} diff --git a/inventory/src/components/product-import/components/Table.tsx b/inventory/src/components/product-import/components/Table.tsx new file mode 100644 index 0000000..083d39c --- /dev/null +++ b/inventory/src/components/product-import/components/Table.tsx @@ -0,0 +1,23 @@ +import type { DataGridProps, Column } from "react-data-grid" +import DataGrid from "react-data-grid" +import { useRsi } from "../hooks/useRsi" + +export type { Column } + +export type Props = DataGridProps & { + rowHeight?: number + hiddenHeader?: boolean + className?: string + style?: React.CSSProperties +} + +export const Table = ({ className, ...props }: Props) => { + const { rtl } = useRsi() + return ( + + ) +} diff --git a/inventory/src/components/product-import/hooks/useRsi.ts b/inventory/src/components/product-import/hooks/useRsi.ts new file mode 100644 index 0000000..173109a --- /dev/null +++ b/inventory/src/components/product-import/hooks/useRsi.ts @@ -0,0 +1,9 @@ +import { useContext } from "react" +import { RsiContext } from "../components/Providers" +import type { RsiProps } from "../types" +import type { MarkRequired } from "ts-essentials" +import type { defaultRSIProps } from "../ReactSpreadsheetImport" +import type { Translations } from "../translationsRSIProps" + +export const useRsi = () => + useContext, keyof typeof defaultRSIProps> & { translations: Translations }>(RsiContext) diff --git a/inventory/src/components/product-import/index.ts b/inventory/src/components/product-import/index.ts new file mode 100644 index 0000000..6f803f2 --- /dev/null +++ b/inventory/src/components/product-import/index.ts @@ -0,0 +1,3 @@ +export { StepType } from "./steps/UploadFlow" +export { ReactSpreadsheetImport } from "./ReactSpreadsheetImport" +export * from "./types" diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx new file mode 100644 index 0000000..9a6594d --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx @@ -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; +} + +export const ImageUploadStep = ({ + data, + file, + onBack, + onSubmit +}: Props) => { + useRsi(); + const [isSubmitting, setIsSubmitting] = useState(false); + const fileInputRefs = useRef<{ [key: number]: React.RefObject }>({}); + + // 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(); + } + }); + }, [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 ( +
+ {/* Header - fixed at top */} +
+

Add Product Images

+

+ Drag images to reorder them or move them between products. +

+
+ + {/* Content area - only this part scrolls */} +
+
+
+ setShowUnassigned(true)} + /> +
+ +
+ setShowUnassigned(false)} + onAssign={assignImageToProduct} + onRemove={removeUnassignedImage} + /> +
+ + {/* Scrollable product cards */} +
+ +
+ {data.map((product: Product, index: number) => ( + 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} + /> + ))} +
+ + + {activeImage && ( +
+ {activeImage.fileName} +
+ )} +
+
+
+
+
+ + {/* Footer - fixed at bottom */} +
+ {onBack && ( + + )} + +
+
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/DroppableContainer.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/DroppableContainer.tsx new file mode 100644 index 0000000..e2a5225 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/DroppableContainer.tsx @@ -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 ( +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/GenericDropzone.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/GenericDropzone.tsx new file mode 100644 index 0000000..a9c70e1 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/GenericDropzone.tsx @@ -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 ( +
+ +
+ {processingBulk ? ( + <> + +

Processing images...

+ + ) : isDragActive ? ( + <> + +

Drop images here

+

 

+ + ) : ( + <> + +

Drop images here or click to select

+

Images dropped here will be automatically assigned to products based on filename

+ {unassignedImages.length > 0 && !showUnassigned && ( + + )} + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/CopyButton.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/CopyButton.tsx new file mode 100644 index 0000000..208ae4d --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/CopyButton.tsx @@ -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 ( + + ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ImageDropzone.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ImageDropzone.tsx new file mode 100644 index 0000000..647a340 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ImageDropzone.tsx @@ -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 ( +
+ + {isDragActive ? ( +
Drop images here
+ ) : ( + <> + + Add Images + + )} +
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx new file mode 100644 index 0000000..a39e3f7 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx @@ -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; + 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 ( + + +
+
+
+

{product.name || `Product #${index + 1}`}

+
+ UPC: {product.upc || 'N/A'} + + {' | '} + Supplier #: {product.supplier_no || 'N/A'} + +
+
+
+
+ onUrlInputChange(e.target.value)} + className="!text-xs h-8 w-[180px]" + /> + +
+
+
+ +
+
+ +
+ +
+ + {getProductImages().length > 0 ? ( + img.id)} + strategy={horizontalListSortingStrategy} + > + {getProductImages().map((image, imgIndex) => ( + + ))} + + ) : ( +
+ )} +
+
+
+ + e.target.files && onImageUpload(e.target.files)} + /> +
+
+
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/SortableImage.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/SortableImage.tsx new file mode 100644 index 0000000..b56ea7d --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/SortableImage.tsx @@ -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(null); + const zoomButtonRef = useRef(null); + + const displayName = productName || `Product #${productIndex + 1}`; + + return ( +
{ + // This ensures the native drag doesn't interfere + e.preventDefault(); + e.stopPropagation(); + }} + > + {image.loading ? ( +
+ + {image.fileName} +
+ ) : ( + <> + {`${displayName} +
+
+ +
+ + + + + + + +
+ +
+ {`${displayName} +
+
+ {`${displayName} - Image ${imgIndex + 1}`} +
+
+
+
+ + )} +
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/UnassignedImagesSection.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/UnassignedImagesSection.tsx new file mode 100644 index 0000000..ac2804c --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/UnassignedImagesSection.tsx @@ -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 ( +
+
+
+
+ +

+ Unassigned Images ({unassignedImages.length}) +

+
+ +
+ +
+ {unassignedImages.map((image, index) => ( + + ))} +
+
+
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/UnassignedImagesSection/UnassignedImageItem.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/UnassignedImagesSection/UnassignedImageItem.tsx new file mode 100644 index 0000000..2ba1c00 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/UnassignedImagesSection/UnassignedImageItem.tsx @@ -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 ( +
+ {`Unassigned +
+

{image.file.name}

+
+ + +
+
+ {/* Zoom button for unassigned images */} + + + + + +
+ +
+ {`Unassigned +
+
+ {`Unassigned image: ${image.file.name}`} +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts new file mode 100644 index 0000000..89e0c9e --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useBulkImageUpload.ts @@ -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; + +interface UseBulkImageUploadProps { + data: Product[]; + handleImageUpload: HandleImageUploadFn; +} + +export const useBulkImageUpload = ({ data, handleImageUpload }: UseBulkImageUploadProps) => { + const [unassignedImages, setUnassignedImages] = useState([]); + 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 + }; +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useDragAndDrop.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useDragAndDrop.ts new file mode 100644 index 0000000..8f3d076 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useDragAndDrop.ts @@ -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>; + 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(null); + const [activeImage, setActiveImage] = useState(null); + const [activeDroppableId, setActiveDroppableId] = useState(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 + }; +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts new file mode 100644 index 0000000..f47775c --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts @@ -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>; +} + +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, + }; +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImagesInit.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImagesInit.ts new file mode 100644 index 0000000..5a3280b --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImagesInit.ts @@ -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(() => { + // 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 + }; +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts new file mode 100644 index 0000000..db1a287 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts @@ -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>; + 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 + }; +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/types.ts b/inventory/src/components/product-import/steps/ImageUploadStep/types.ts new file mode 100644 index 0000000..17f9b23 --- /dev/null +++ b/inventory/src/components/product-import/steps/ImageUploadStep/types.ts @@ -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[]; +} \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx new file mode 100644 index 0000000..2250366 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -0,0 +1,1689 @@ +import React, { useCallback, useEffect, useMemo, useState, memo, useRef, useLayoutEffect } from "react" +import { useRsi } from "../../hooks/useRsi" +import { setColumn } from "./utils/setColumn" +import { setIgnoreColumn } from "./utils/setIgnoreColumn" +import { setSubColumn } from "./utils/setSubColumn" +import { normalizeTableData } from "./utils/normalizeTableData" +import type { Field, RawData, Fields } from "../../types" +import { getMatchedColumns } from "./utils/getMatchedColumns" +import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields" +import { toast } from "sonner" +import { DeepReadonly as TsDeepReadonly } from "ts-essentials" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useQuery } from "@tanstack/react-query" +import config from "@/config" +import { Button } from "@/components/ui/button" +import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, CheckIcon, ChevronsUpDown } from "lucide-react" +import { Separator } from "@/components/ui/separator" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { cn } from "@/lib/utils" + +export type MatchColumnsProps = { + data: RawData[] + headerValues: RawData + onContinue: (data: any[], rawData: RawData[], columns: Columns, globalSelections?: GlobalSelections) => void + onBack?: () => void + initialGlobalSelections?: GlobalSelections +} + +export type GlobalSelections = { + supplier?: string + company?: string + line?: string + subline?: string +} + +export enum ColumnType { + empty, + ignored, + matched, + matchedCheckbox, + matchedSelect, + matchedSelectOptions, + matchedMultiInput, + matchedMultiSelect, +} + +export type MatchedOptions = { + entry: string + value: T +} + +type EmptyColumn = { type: ColumnType.empty; index: number; header: string } +type IgnoredColumn = { type: ColumnType.ignored; index: number; header: string } +type MatchedColumn = { type: ColumnType.matched; index: number; header: string; value: T } +type MatchedSwitchColumn = { type: ColumnType.matchedCheckbox; index: number; header: string; value: T } +export type MatchedSelectColumn = { + type: ColumnType.matchedSelect + index: number + header: string + value: T + matchedOptions: Partial>[] +} +export type MatchedSelectOptionsColumn = { + type: ColumnType.matchedSelectOptions + index: number + header: string + value: T + matchedOptions: MatchedOptions[] +} +export type MatchedMultiInputColumn = { + type: ColumnType.matchedMultiInput + index: number + header: string + value: T +} +export type MatchedMultiSelectColumn = { + type: ColumnType.matchedMultiSelect + index: number + header: string + value: T + matchedOptions: MatchedOptions[] +} + +export type Column = + | EmptyColumn + | IgnoredColumn + | MatchedColumn + | MatchedSwitchColumn + | MatchedSelectColumn + | MatchedSelectOptionsColumn + | MatchedMultiInputColumn + | MatchedMultiSelectColumn + +export type Columns = Column[] + +// Extract components to reduce re-renders +const ColumnActions = memo(({ + column, + onIgnore, + toggleValueMapping, + isExpanded, + canExpandValues +}: { + column: any, + onIgnore: (index: number) => void, + toggleValueMapping: (index: number) => void, + isExpanded: boolean, + canExpandValues: boolean +}) => { + // Create stable callback references to prevent unnecessary re-renders + const handleIgnore = useCallback(() => { + onIgnore(column.index); + }, [onIgnore, column.index]); + + const handleToggleMapping = useCallback(() => { + toggleValueMapping(column.index); + }, [toggleValueMapping, column.index]); + + return ( +
+ {canExpandValues && ( + + )} + +
+ ); +}); + +// Additional performance optimization - use a memoized row renderer for column sample data +const MemoizedColumnSamplePreview = React.memo(({ samples }: { samples: any[] }) => { + return ( +
+ + + + + + +
+ {samples.map((sample, i) => ( +
+ {String(sample || '(empty)')} + {i < samples.length - 1 && } +
+ ))} +
+
+
+
+
+ ); +}); + +// Replace the original ColumnSamplePreview with more optimized version +const ColumnSamplePreview = MemoizedColumnSamplePreview; + +// Add a memoized component for value mappings +const ValueMappings = memo(({ + column, + fieldOptions, + onSubChange +}: { + column: any, + fieldOptions: any[], + onSubChange: (value: string, columnIndex: number, entry: string) => void +}) => { + // Use a React.useMemo for expensive calculations + const matchedOptions = useMemo(() => column.matchedOptions || [], [column.matchedOptions]); + const columnIndex = useMemo(() => column.index, [column.index]); + + if (!fieldOptions || fieldOptions.length === 0) { + return ( +
+

+ No options available for this field. Options mapping is not required. +

+
+ ); + } + + return ( +
+
+

Map Values from Column to Field Options

+

+ Match values found in your spreadsheet to options available in the system +

+
+
+ {matchedOptions.map((matched: any, i: number) => { + // Set default value if none exists + const currentValue = (matched.value as string) || ""; + // Ensure entry is a string + const entryValue = matched.entry || ""; + const isUnmapped = !currentValue; + + // Use stable callback for value change + const handleValueChange = useCallback((value: string) => { + onSubChange(value, columnIndex, entryValue); + }, [onSubChange, columnIndex, entryValue]); + + return ( +
+
+ {entryValue || '(empty)'} +
+
+ +
+ +
+ ); + })} +
+
+ ); +}); + +// Add these new components before the MatchColumnsStep component +const SupplierSelector = React.memo(({ + value, + onChange, + suppliers +}: { + value?: string; + onChange: (value: string) => void; + suppliers: any[] +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No suppliers found. + + {suppliers?.map((supplier: any) => ( + { + onChange(supplier.value); + setOpen(false); // Close popover after selection + }} + > + {supplier.label} + + + ))} + + + + + + ); +}); + +const CompanySelector = React.memo(({ + value, + onChange, + companies +}: { + value?: string; + onChange: (value: string) => void; + companies: any[] +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No companies found. + + {companies?.map((company: any) => ( + { + onChange(company.value); + setOpen(false); // Close popover after selection + }} + > + {company.label} + + + ))} + + + + + + ); +}); + +const LineSelector = React.memo(({ + value, + onChange, + lines, + disabled +}: { + value?: string; + onChange: (value: string) => void; + lines: any[]; + disabled: boolean; +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No lines found. + + {lines?.map((line: any) => ( + { + onChange(line.value); + setOpen(false); // Close popover after selection + }} + > + {line.label} + + + ))} + + + + + + ); +}); + +const SubLineSelector = React.memo(({ + value, + onChange, + sublines, + disabled +}: { + value?: string; + onChange: (value: string) => void; + sublines: any[]; + disabled: boolean; +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No sub lines found. + + {sublines?.map((subline: any) => ( + { + onChange(subline.value); + setOpen(false); // Close popover after selection + }} + > + {subline.label} + + + ))} + + + + + + ); +}); + +// Add this new component before the MatchColumnsStep component +const FieldSelector = React.memo(({ + column, + fieldCategories, + allFields, + onChange, + isFieldMappedToOtherColumn, + handleCommandListWheel +}: { + column: any; + isUnmapped?: boolean; + fieldCategories: any[]; + allFields: any[]; + onChange: (value: string) => void; + isFieldMappedToOtherColumn: (fieldKey: string, currentColumnIndex: number) => { isMapped: boolean, columnHeader?: string }; + handleCommandListWheel: (e: React.WheelEvent) => void; +}) => { + const [open, setOpen] = useState(false); + + // For ignored columns, show a badge + if (column.type === ColumnType.ignored) { + return Ignored; + } + + // Get the current value if this is a mapped column + const currentValue = "value" in column ? column.value as string : undefined; + + return ( + + + + + + + + + No fields found. + {fieldCategories.map(category => ( + + {category.fields.map((field: { key: string; label: string }) => { + const { isMapped, columnHeader } = isFieldMappedToOtherColumn(field.key as string, column.index); + return ( + { + onChange(value); + setOpen(false); // Close the popover after selection + }} + className={isMapped ? "opacity-70" : ""} + > +
+ {field.label} + {isMapped ? ( + + (mapped to "{columnHeader}") + + ) : null} +
+ {currentValue === field.key && ( + + )} +
+ ); + })} +
+ ))} +
+
+
+
+ ); +}); + +export const MatchColumnsStep = React.memo(({ + data, + headerValues, + onContinue, + onBack, + initialGlobalSelections +}: MatchColumnsProps): JSX.Element => { + const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations } = useRsi() + const [isLoading, setIsLoading] = useState(false) + const [columns, setColumns] = useState>( + // Do not remove spread, it indexes empty array elements, otherwise map() skips over them + ([...headerValues] as string[]).map((value, index) => ({ type: ColumnType.empty, index, header: value ?? "" })), + ) + const [globalSelections, setGlobalSelections] = useState(initialGlobalSelections || {}) + const [showAllColumns, setShowAllColumns] = useState(true) + const [expandedValues, setExpandedValues] = useState([]) + const [userCollapsedColumns, setUserCollapsedColumns] = useState([]) + + // Toggle with immediate visual feedback + const toggleValueMappingOptimized = useCallback((columnIndex: number) => { + if (expandedValues.includes(columnIndex)) { + // User is collapsing this column - add to userCollapsedColumns + setUserCollapsedColumns(prev => [...prev, columnIndex]); + setExpandedValues(prev => prev.filter(idx => idx !== columnIndex)); + } else { + // User is expanding this column - remove from userCollapsedColumns + setUserCollapsedColumns(prev => prev.filter(idx => idx !== columnIndex)); + setExpandedValues(prev => [...prev, columnIndex]); + } + }, [expandedValues]); + + // Check if column is expandable (has value mappings) + const isExpandable = useCallback((column: Column) => { + return ( + column.type === ColumnType.matchedSelect || + column.type === ColumnType.matchedSelectOptions || + column.type === ColumnType.matchedMultiSelect + ); + }, []); + + // Initialize with any provided global selections + useEffect(() => { + if (initialGlobalSelections) { + setGlobalSelections(initialGlobalSelections) + } + }, [initialGlobalSelections]) + + // Fetch product lines when company is selected + const { data: productLines } = useQuery({ + queryKey: ["product-lines", globalSelections.company], + queryFn: async () => { + if (!globalSelections.company) return []; + const response = await fetch(`${config.apiUrl}/import/product-lines/${globalSelections.company}`); + if (!response.ok) { + throw new Error("Failed to fetch product lines"); + } + return response.json(); + }, + enabled: !!globalSelections.company, + staleTime: 600000, // 10 minutes (increased from 60 seconds) + }); + + // Fetch sublines when line is selected + const { data: sublines } = useQuery({ + queryKey: ["sublines", globalSelections.line], + queryFn: async () => { + if (!globalSelections.line) return []; + const response = await fetch(`${config.apiUrl}/import/sublines/${globalSelections.line}`); + if (!response.ok) { + throw new Error("Failed to fetch sublines"); + } + return response.json(); + }, + enabled: !!globalSelections.line, + staleTime: 600000, // 10 minutes (increased from 60 seconds) + }); + + // Find mapped column for a specific field + const findMappedColumnForField = useCallback((fieldKey: string) => { + return columns.find(col => { + // Check if it's a matched column with value property + return "value" in col && col.value === fieldKey && + (col.type === ColumnType.matched || + col.type === ColumnType.matchedCheckbox || + col.type === ColumnType.matchedSelect || + col.type === ColumnType.matchedSelectOptions || + col.type === ColumnType.matchedMultiInput || + col.type === ColumnType.matchedMultiSelect); + }); + }, [columns]); + + // Get mapped company value (if company is mapped to a column) + const mappedCompanyColumn = useMemo(() => findMappedColumnForField('company'), [findMappedColumnForField]); + const mappedCompanyValue = useMemo(() => { + // If using global selection, return that + if (globalSelections.company) return globalSelections.company; + + // If company is mapped to a column, get the first value + if (mappedCompanyColumn && "matchedOptions" in mappedCompanyColumn) { + const firstEntry = data[0]?.[mappedCompanyColumn.index]; + // Find the mapped value for this entry + const mappedOption = mappedCompanyColumn.matchedOptions.find(opt => opt.entry === firstEntry); + return mappedOption?.value as string || null; + } + + return null; + }, [globalSelections.company, mappedCompanyColumn, data]); + + // Get mapped line value (if line is mapped to a column) + const mappedLineColumn = useMemo(() => findMappedColumnForField('line'), [findMappedColumnForField]); + const mappedLineValue = useMemo(() => { + // If using global selection, return that + if (globalSelections.line) return globalSelections.line; + + // If line is mapped to a column, get the first value + if (mappedLineColumn && "matchedOptions" in mappedLineColumn) { + const firstEntry = data[0]?.[mappedLineColumn.index]; + // Find the mapped value for this entry + const mappedOption = mappedLineColumn.matchedOptions.find(opt => opt.entry === firstEntry); + return mappedOption?.value as string || null; + } + + return null; + }, [globalSelections.line, mappedLineColumn, data]); + + // Fetch product lines for mapped company + const { data: mappedProductLines } = useQuery({ + queryKey: ["product-lines-mapped", mappedCompanyValue], + queryFn: async () => { + if (!mappedCompanyValue) return []; + const response = await fetch(`${config.apiUrl}/import/product-lines/${mappedCompanyValue}`); + if (!response.ok) { + throw new Error("Failed to fetch product lines for mapped company"); + } + return response.json(); + }, + enabled: !!mappedCompanyValue && mappedCompanyValue !== globalSelections.company, + staleTime: 600000, // 10 minutes (increased from 60 seconds) + }); + + // Fetch sublines for mapped line + const { data: mappedSublines } = useQuery({ + queryKey: ["sublines-mapped", mappedLineValue], + queryFn: async () => { + if (!mappedLineValue) return []; + const response = await fetch(`${config.apiUrl}/import/sublines/${mappedLineValue}`); + if (!response.ok) { + throw new Error("Failed to fetch sublines for mapped line"); + } + return response.json(); + }, + enabled: !!mappedLineValue && mappedLineValue !== globalSelections.line, + staleTime: 600000, // 10 minutes (increased from 60 seconds) + }); + + // Get field options for suppliers and companies + const { data: fieldOptionsData } = useQuery({ + queryKey: ["field-options"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/import/field-options`); + if (!response.ok) { + throw new Error("Failed to fetch field options"); + } + return response.json(); + }, + staleTime: 600000, // 10 minutes (increased from 60 seconds) + }); + + // Safely access field options + const fieldOptions = fieldOptionsData || { suppliers: [], companies: [] }; + + // Create a stable identity for these queries to avoid re-renders + const stableProductLines = useMemo(() => productLines || [], [productLines]); + const stableSublines = useMemo(() => sublines || [], [sublines]); + const stableMappedProductLines = useMemo(() => mappedProductLines || [], [mappedProductLines]); + const stableMappedSublines = useMemo(() => mappedSublines || [], [mappedSublines]); + + // Check if a field is covered by global selections + const isFieldCoveredByGlobalSelections = useCallback((key: string) => { + return (key === 'supplier' && !!globalSelections.supplier) || + (key === 'company' && !!globalSelections.company) || + (key === 'line' && !!globalSelections.line) || + (key === 'subline' && !!globalSelections.subline); + }, [ + globalSelections.supplier, + globalSelections.company, + globalSelections.line, + globalSelections.subline + ]); + + // Create a stable version of getFieldOptions with improved memoization + const getFieldOptions = useCallback((fieldKey: string) => { + const fieldsArray = Array.isArray(fields) ? fields : [fields]; + const field = fieldsArray.find(f => f.key === fieldKey); + + if (!field) { + return []; + } + + // Handle special hierarchical fields + if (fieldKey === 'line') { + // For line, return the appropriate product lines based on the company + if (globalSelections.company) { + return stableProductLines; + } else if (mappedCompanyValue) { + return stableMappedProductLines; + } + } else if (fieldKey === 'subline') { + // For subline, return the appropriate sublines based on the line + if (globalSelections.line) { + return stableSublines; + } else if (mappedLineValue) { + return stableMappedSublines; + } + } + + // For other fields, check in both places - directly on the field or in fieldType.options + let options = field.options || []; + + // If no options at the root level, check in fieldType + if ((!options || options.length === 0) && field.fieldType && field.fieldType.options) { + options = field.fieldType.options; + } + + return options || []; + }, [ + fields, + globalSelections.company, + globalSelections.line, + stableProductLines, + stableSublines, + mappedCompanyValue, + mappedLineValue, + stableMappedProductLines, + stableMappedSublines + ]); + + // Check if a column has unmapped values (for select fields) + const hasUnmappedValues = useCallback((column: Column) => { + if (!isExpandable(column) || !("matchedOptions" in column) || !("value" in column)) { + return false; + } + + const fieldOptions = getFieldOptions(column.value as string); + + // If there are options available but some values aren't mapped, consider it unmapped + return fieldOptions.length > 0 && + column.matchedOptions.some(option => !option.value); + }, [isExpandable, getFieldOptions]); + + // Optimize columnsWithUnmappedValuesMap calculation - prevent deep comparisons + const columnsWithUnmappedValuesMap = useMemo(() => { + const map = new Map(); + // Prevent recalculation by checking if we already have this value mapped + columns.forEach(col => { + if (!map.has(col.index)) { + map.set(col.index, hasUnmappedValues(col)); + } + }); + return map; + }, [columns, hasUnmappedValues]); + + // Get matched, unmapped, and columns with unmapped values + const { matchedColumns, unmatchedColumns, columnsWithUnmappedValues } = useMemo(() => { + // First identify columns with unmapped values (they need special treatment) + const withUnmappedValues = columns.filter(col => + col.type !== ColumnType.empty && + col.type !== ColumnType.ignored && + columnsWithUnmappedValuesMap.get(col.index) + ); + + // These are columns that are mapped AND have all their values properly mapped + const fullyMapped = columns.filter(col => + col.type !== ColumnType.empty && + col.type !== ColumnType.ignored && + !columnsWithUnmappedValuesMap.get(col.index) + ); + + // Unmapped columns + const unmatched = columns.filter(col => col.type === ColumnType.empty); + + return { + matchedColumns: fullyMapped, + unmatchedColumns: unmatched, + columnsWithUnmappedValues: withUnmappedValues + }; + }, [columns, columnsWithUnmappedValuesMap]); + + // Get ignored columns + const ignoredColumns = useMemo(() => { + return columns.filter(col => col.type === ColumnType.ignored); + }, [columns]); + + // Get mapping information for required fields + const requiredFieldMappings = useMemo(() => { + const fieldsArray = Array.isArray(fields) ? fields : [fields]; + const mappings = new Map(); + + // Check global selections + fieldsArray.forEach(field => { + const key = field.key as string; + if (isFieldCoveredByGlobalSelections(key)) { + mappings.set(key, { isGlobal: true }); + } + }); + + // Check column mappings + columns.forEach(column => { + if ("value" in column) { + const key = column.value as string; + if (!mappings.has(key) || !mappings.get(key)?.isGlobal) { + mappings.set(key, { + isGlobal: false, + columnHeader: column.header, + columnIndex: column.index + }); + } + } + }); + + return mappings; + }, [columns, fields, isFieldCoveredByGlobalSelections]); + + // Available fields for mapping (including already mapped fields) + const availableFields = useMemo(() => { + const fieldsArray = Array.isArray(fields) ? fields : [fields]; + // Don't filter out mapped fields, only filter global selections + return fieldsArray.filter(field => + !isFieldCoveredByGlobalSelections(field.key) + ); + }, [fields, isFieldCoveredByGlobalSelections]); + + // All available fields including already mapped ones (for editing mapped columns) + const allFields = useMemo(() => { + const fieldsArray = Array.isArray(fields) ? fields : [fields]; + return fieldsArray.filter(field => !isFieldCoveredByGlobalSelections(field.key)); + }, [fields, isFieldCoveredByGlobalSelections]); + + // For filtering fields by type + const getFieldsByKeyPrefix = useCallback((prefix: string, fieldsToUse = availableFields) => { + return fieldsToUse.filter(field => + typeof field.key === 'string' && field.key.startsWith(prefix) + ); + }, [availableFields]); + + // Group all fields by category (for editing mapped columns) + + // Group available fields by category (for unmapped columns) + const availableFieldCategories = useMemo(() => { + return [ + { name: "Basic Info", fields: getFieldsByKeyPrefix('basic') }, + { name: "Product", fields: getFieldsByKeyPrefix('product') }, + { name: "Inventory", fields: getFieldsByKeyPrefix('inventory') }, + { name: "Pricing", fields: getFieldsByKeyPrefix('pricing') }, + { name: "Other", fields: availableFields.filter(f => + !f.key.startsWith('basic') && + !f.key.startsWith('product') && + !f.key.startsWith('inventory') && + !f.key.startsWith('pricing') + ) } + ].filter(category => category.fields.length > 0); + }, [availableFields, getFieldsByKeyPrefix]); + + // Override onChange to handle the new interface + const onChange = useCallback( + (value: T, columnIndex: number) => { + const field = (fields as Fields).find((f) => f.key === value) + if (!field) return + + const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key) + + setColumns( + columns.map>((column, index) => { + if (columnIndex === index) { + // Set the new column value + const updatedColumn = setColumn(column, field as Field, data, autoMapSelectValues); + + // Auto-map values if this is a field with options + if (isExpandable(updatedColumn) && "matchedOptions" in updatedColumn) { + // Get available options for this field + const fieldOptions = getFieldOptions(field.key as string); + + if (fieldOptions && fieldOptions.length > 0) { + // Try to auto-map each value + updatedColumn.matchedOptions = updatedColumn.matchedOptions.map(option => { + // If already mapped, keep it + if (option.value) return option; + + const entryValue = String(option.entry || '').trim(); + + // Try to find a case-insensitive match + const matchingOption = fieldOptions.find((opt: { label: string, value: string }) => + String(opt.label).toLowerCase() === entryValue.toLowerCase() || + String(opt.value).toLowerCase() === entryValue.toLowerCase() + ); + + if (matchingOption) { + console.log(`Auto-matched "${entryValue}" to "${matchingOption.label}" (${matchingOption.value})`); + return { + ...option, + value: matchingOption.value + }; + } + + return option; + }); + } + } + + return updatedColumn; + } else if (index === existingFieldIndex) { + // Clear the old column that had this field + toast.warning(translations.matchColumnsStep.duplicateColumnWarningTitle, { + description: translations.matchColumnsStep.duplicateColumnWarningDescription, + }) + return setColumn(column) + } else { + // Leave other columns unchanged + return column + } + }), + ) + }, + [ + autoMapSelectValues, + columns, + data, + fields, + getFieldOptions, + isExpandable, + translations.matchColumnsStep.duplicateColumnWarningDescription, + translations.matchColumnsStep.duplicateColumnWarningTitle, + ], + ) + + // Auto-map values for all columns + const autoMapAllValues = useCallback(() => { + setColumns( + columns.map>((column) => { + // Only process expandable columns with unmatched options + if (isExpandable(column) && "matchedOptions" in column && "value" in column) { + const fieldKey = column.value as string; + const fieldOptions = getFieldOptions(fieldKey); + + if (fieldOptions && fieldOptions.length > 0) { + // Create a copy of the column + const updatedColumn = { ...column }; + + // Try to auto-map each value + updatedColumn.matchedOptions = updatedColumn.matchedOptions.map(option => { + // If already mapped, keep it + if (option.value) return option; + + const entryValue = String(option.entry || '').trim(); + + // Try to find a case-insensitive match + const matchingOption = fieldOptions.find((opt: { label: string, value: string }) => + String(opt.label).toLowerCase() === entryValue.toLowerCase() || + String(opt.value).toLowerCase() === entryValue.toLowerCase() + ); + + if (matchingOption) { + console.log(`Auto-matched "${entryValue}" to "${matchingOption.label}" (${matchingOption.value})`); + return { + ...option, + value: matchingOption.value + }; + } + + return option; + }); + + return updatedColumn; + } + } + + return column; + }), + ) + }, [columns, getFieldOptions, isExpandable]); + + // Run auto-mapping only when needed - when columns change or field options change + useEffect(() => { + // Check if we have columns that need mapping + const needsMapping = columns.some(column => + isExpandable(column) && + "matchedOptions" in column && + column.matchedOptions.some(option => !option.value) + ); + + if (needsMapping) { + autoMapAllValues(); + } + }, [autoMapAllValues, columns, fields, productLines, sublines]); + + const onIgnore = useCallback( + (columnIndex: number) => { + setColumns(columns.map((column, index) => (columnIndex === index ? setIgnoreColumn(column) : column))) + }, + [columns, setColumns], + ) + + const onRevertIgnore = useCallback( + (columnIndex: number) => { + setColumns(columns.map((column, index) => (columnIndex === index ? setColumn(column) : column))) + }, + [columns, setColumns], + ) + + const onSubChange = useCallback( + (value: string, columnIndex: number, entry: string) => { + setColumns( + columns.map((column, index) => + columnIndex === index && "matchedOptions" in column + ? setSubColumn(column as MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn, entry, value) + : column, + ), + ) + }, + [columns, setColumns], + ) + + // Get all required fields + const requiredFields = useMemo(() => { + // Convert the fields to the expected type + const fieldsArray = Array.isArray(fields) ? fields : [fields]; + + // Log the fields for debugging + console.log("All fields:", fieldsArray); + + // Log validation rules for each field + fieldsArray.forEach(field => { + if (field.validations && Array.isArray(field.validations)) { + console.log(`Field ${field.key} validations:`, field.validations.map((v: any) => v.rule)); + } else { + console.log(`Field ${field.key} has no validations`); + } + }); + + // Check for required fields based on validations + const required = fieldsArray.filter(field => { + // Check if the field has validations + if (!field.validations || !Array.isArray(field.validations)) { + return false; + } + + // Check for any validation with rule: 'required' or required: true + const isRequired = field.validations.some( + (v: any) => v.rule === 'required' || v.required === true + ); + + console.log(`Field ${field.key} required:`, isRequired, field.validations); + + return isRequired; + }); + + console.log("Required fields:", required); + return required; + }, [fields]); + + // Get unmatched required fields + const unmatchedRequiredFields = useMemo(() => { + // Convert the fields to the expected type + const fieldsArray = Array.isArray(fields) ? fields : [fields] + const typedFields = fieldsArray.map(field => ({ + ...field, + key: field.key as TsDeepReadonly + })) as unknown as Fields + + const unmatched = findUnmatchedRequiredFields(typedFields, columns); + return unmatched; + }, [fields, columns]) + + // Get matched required fields + const matchedRequiredFields = useMemo(() => { + const matched = requiredFields + .map(field => field.key) + .filter(key => { + // Type assertion to handle the DeepReadonly vs string type mismatch + return !unmatchedRequiredFields.includes(key as any); + }); + return matched; + }, [requiredFields, unmatchedRequiredFields]); + + // Get field label by key + const getFieldLabel = useCallback((key: string) => { + const fieldsArray = Array.isArray(fields) ? fields : [fields]; + const field = fieldsArray.find(f => f.key === key); + return field?.label || key; + }, [fields]); + + // Fix handleOnContinue - it should be useCallback, not useEffect + const handleOnContinue = useCallback(async () => { + setIsLoading(true) + // Normalize the data with global selections before continuing + const normalizedData = normalizeTableData(columns, data, fields) + await onContinue(normalizedData, data, columns, globalSelections) + setIsLoading(false) + }, [onContinue, columns, data, fields, globalSelections]) + + useEffect( + () => { + if (autoMapHeaders) { + setColumns(getMatchedColumns(columns, fields, data, autoMapDistance, autoMapSelectValues)) + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) + + // Missing dataExample - let's create a data sample from the actual data + const dataSample = useMemo(() => { + // Take first 5 rows as sample data + return data.slice(0, 5); + }, [data]); + + // Optimize expensive getColumnSamples calls with memoization and ref + const columnSamplesCache = useRef(new Map()); + + // Enhanced version with caching and a more stable identity + const getColumnSamples = useCallback((columnIndex: number) => { + if (!columnSamplesCache.current.has(columnIndex)) { + // Only build samples from data when needed + const samples = dataSample.slice(0, 5).map(row => row[columnIndex] || ''); + columnSamplesCache.current.set(columnIndex, samples); + } + return columnSamplesCache.current.get(columnIndex) || []; + }, [dataSample]); // Only depends on dataSample, not on data directly + + // Clear the cache when the data changes significantly + useEffect(() => { + columnSamplesCache.current.clear(); + }, [data.length]); // Only clear when data length changes, not on every data reference change + + // Optimize the renderSamplePreview function + const renderSamplePreview = useCallback((columnIndex: number) => { + const samples = getColumnSamples(columnIndex); + return ; + }, [getColumnSamples]); + + // Automatically expand columns with unmapped values - use layoutEffect to avoid flashing + useLayoutEffect(() => { + // Track the current unmapped column indexes + const currentUnmappedIndexes = columnsWithUnmappedValues.map(col => col.index); + + // Find columns that are newly unmapped (weren't in expandedValues or userCollapsedColumns) + const newlyUnmappedColumns = currentUnmappedIndexes.filter(index => + !expandedValues.includes(index) && !userCollapsedColumns.includes(index) + ); + + if (newlyUnmappedColumns.length > 0) { + setExpandedValues(prev => [...prev, ...newlyUnmappedColumns]); + } + }, [columnsWithUnmappedValues, expandedValues, userCollapsedColumns]); + + // Create a stable mapping of column index to change handlers + const columnChangeHandlers = useMemo(() => { + const handlers = new Map void>(); + + columns.forEach(column => { + handlers.set(column.index, (value: string) => { + onChange(value as T, column.index); + }); + }); + + return handlers; + }, [columns, onChange]); + + // Add a function to check if a field is already mapped to another column + const isFieldMappedToOtherColumn = useCallback((fieldKey: string, currentColumnIndex: number) => { + const matchedColumnForField = columns.find(col => + col.type !== ColumnType.empty && + col.type !== ColumnType.ignored && + "value" in col && + col.value === fieldKey && + col.index !== currentColumnIndex + ); + return matchedColumnForField ? { isMapped: true, columnHeader: matchedColumnForField.header } : { isMapped: false }; + }, [columns]); + + // Add a wheel handler function for command lists + const handleCommandListWheel = useCallback((e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }, []); + + // Replace the renderFieldSelector function with a more stable version + const renderFieldSelector = useCallback((column: Column, isUnmapped: boolean = false) => { + // For ignored columns, show a badge + if (column.type === ColumnType.ignored) { + return Ignored; + } + + // Get the pre-created onChange handler for this column + const handleChange = columnChangeHandlers.get(column.index); + + return ( + { + if (handleChange) handleChange(value); + }} + isFieldMappedToOtherColumn={isFieldMappedToOtherColumn} + handleCommandListWheel={handleCommandListWheel} + /> + ); + }, [availableFieldCategories, allFields, columnChangeHandlers, isFieldMappedToOtherColumn, handleCommandListWheel]); + + // Replace the renderValueMappings function with a memoized version + const renderValueMappings = useCallback((column: Column) => { + if (!isExpandable(column) || !("matchedOptions" in column) || !("value" in column)) { + return null; + } + + const fieldOptions = getFieldOptions(column.value as string); + return ; + }, [isExpandable, getFieldOptions, onSubChange]); + + // Remove virtualization references and helpers + const renderTable = useMemo(() => { + return ( + + + + + Imported Column + Sample Data + + Map To Field + Ignore + + + + {/* Always show columns with unmapped values */} + {columnsWithUnmappedValues.map((column) => { + const isExpanded = expandedValues.includes(column.index); + + return ( + + + {column.header} + + {renderSamplePreview(column.index)} + + + + + + {renderFieldSelector(column)} + + + + + + + {/* Value mappings row */} + {isExpanded && ( + + + {renderValueMappings(column)} + + + )} + + ); + })} + + {/* Always show unmapped columns */} + {unmatchedColumns.map((column) => ( + + {column.header} + + {renderSamplePreview(column.index)} + + + + + + {renderFieldSelector(column, true)} + + + + + + ))} + + {/* Show matched columns if showAllColumns is true */} + {showAllColumns && matchedColumns.map((column) => { + const isExpanded = expandedValues.includes(column.index); + const canExpandValues = isExpandable(column); + + return ( + + + {column.header} + + {renderSamplePreview(column.index)} + + + + + + {renderFieldSelector(column)} + + + + + + + {/* Value mappings row */} + {isExpanded && canExpandValues && ( + + + {renderValueMappings(column)} + + + )} + + ); + })} + + {/* Show ignored columns if showAllColumns is true */} + {showAllColumns && ignoredColumns.map((column) => ( + + {column.header} + + {renderSamplePreview(column.index)} + + + + + + Ignored + + + + + + ))} + + {/* Show a message if all columns are mapped/ignored */} + {unmatchedColumns.length === 0 && columnsWithUnmappedValues.length === 0 && !showAllColumns && ( + + + All columns have been mapped or ignored. + + + + )} + +
+
+ ); + }, [ + columnsWithUnmappedValues, + unmatchedColumns, + matchedColumns, + ignoredColumns, + showAllColumns, + expandedValues, + renderSamplePreview, + renderFieldSelector, + renderValueMappings, + onIgnore, + onRevertIgnore, + toggleValueMappingOptimized, + isExpandable + ]); + + return ( +
+
+
+
+
+ {/* Left panel - Global selections & Required fields */} +
+
+

Global Settings

+

+ These values will be applied to all imported items +

+ +
+
+ + setGlobalSelections(prev => ({ ...prev, supplier: value }))} + suppliers={fieldOptions?.suppliers || []} + /> +
+ +
+ + setGlobalSelections(prev => ({ + ...prev, + company: value, + line: undefined, + subline: undefined + }))} + companies={fieldOptions?.companies || []} + /> +
+ +
+ + setGlobalSelections(prev => ({ + ...prev, + line: value, + subline: undefined + }))} + lines={productLines || []} + disabled={!globalSelections.company} + /> +
+ +
+ + setGlobalSelections(prev => ({ + ...prev, + subline: value + }))} + sublines={sublines || []} + disabled={!globalSelections.line} + /> +
+
+
+ + {/* Required Fields Section - Updated to show source column */} +
+
+

Matched Fields

+
+ +
+ {requiredFields.length > 0 ? ( + requiredFields.map(field => { + const isMatched = matchedRequiredFields.includes(field.key); + const fieldMapping = requiredFieldMappings.get(field.key); + const isCoveredByGlobal = fieldMapping?.isGlobal; + const isAccountedFor = isMatched || isCoveredByGlobal; + + return ( +
+ {isAccountedFor ? ( + + ) : ( + + )} + + {getFieldLabel(field.key)} + + + {isCoveredByGlobal ? + "(global)" : + fieldMapping?.columnHeader ? + `(from "${fieldMapping.columnHeader}")` : + "" + } + +
+ ); + }) + ) : ( +
+ No required fields found. +
+ )} +
+
+
+ + {/* Right panel - Column mapping interface */} +
+
+
+

Map Spreadsheet Columns

+ {columnsWithUnmappedValues.length > 0 && ( + + {columnsWithUnmappedValues.length} with unmapped values + + )} +
+ +
+ +
+
+ +
+ {renderTable} +
+
+
+
+
+
+ +
+
+ + {onBack && ( + + )} + + +
+
+
+ ) +}) diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/components/MatchIcon.tsx b/inventory/src/components/product-import/steps/MatchColumnsStep/components/MatchIcon.tsx new file mode 100644 index 0000000..c524f78 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/components/MatchIcon.tsx @@ -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 ( +
+ {isChecked && ( + + + + )} +
+ ) +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/components/TemplateColumn.tsx b/inventory/src/components/product-import/steps/MatchColumnsStep/components/TemplateColumn.tsx new file mode 100644 index 0000000..002ddf0 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -0,0 +1,133 @@ +import { useRsi } from "../../../hooks/useRsi" +import type { Column } from "../MatchColumnsStep" +import { ColumnType } from "../MatchColumnsStep" +import type { Fields } from "../../../types" +import { + Card, + CardContent, + CardHeader, +} from "@/components/ui/card" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { Check } from "lucide-react" + +type TemplateColumnProps = { + column: Column + onChange: (value: T, columnIndex: number) => void + onSubChange: (value: string, columnIndex: number, entry: string) => void +} + +const getAccordionTitle = (fields: Fields, column: Column, translations: any) => { + const field = fields.find((f) => "value" in column && f.key === column.value) + if (!field) return "" + return `${translations.matchColumnsStep.matchDropdownTitle} ${field.label} (${ + "matchedOptions" in column ? column.matchedOptions.filter((option) => !option.value).length : 0 + } ${translations.matchColumnsStep.unmatched})` +} + +export const TemplateColumn = ({ column, onChange, onSubChange }: TemplateColumnProps) => { + const { translations, fields } = useRsi() + const isIgnored = column.type === ColumnType.ignored + const isChecked = + column.type === ColumnType.matched || + column.type === ColumnType.matchedCheckbox || + column.type === ColumnType.matchedSelectOptions + const isSelect = "matchedOptions" in column + const selectOptions = fields.map(({ label, key }) => ({ value: key, label })) + const selectValue = column.type === ColumnType.empty ? undefined : + selectOptions.find(({ value }) => "value" in column && column.value === value)?.value + + if (isIgnored) { + return null + } + + return ( + + +
+ +
+ {isChecked && ( +
+ +
+ )} +
+ {isSelect && ( + + + + + {getAccordionTitle(fields, column, translations)} + + +
+ {column.matchedOptions.map((option) => ( +
+

+ {option.entry} +

+ +
+ ))} +
+
+
+
+
+ )} +
+ ) +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/findMatch.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/findMatch.ts new file mode 100644 index 0000000..06be615 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/findMatch.ts @@ -0,0 +1,27 @@ +import lavenstein from "js-levenshtein" +import type { Fields } from "../../../types" + +type AutoMatchAccumulator = { + distance: number + value: T +} + +export const findMatch = ( + header: string, + fields: Fields, + autoMapDistance: number, +): T | undefined => { + const headerLower = header.toLowerCase() + const smallestValue = fields.reduce>((acc, field) => { + const distance = Math.min( + ...[ + lavenstein(field.key.toLowerCase(), headerLower), + ...(field.alternateMatches?.map((alternate) => lavenstein(alternate.toLowerCase(), headerLower)) || []), + ], + ) + return distance < acc.distance || acc.distance === undefined + ? ({ value: field.key, distance } as AutoMatchAccumulator) + : acc + }, {} as AutoMatchAccumulator) + return smallestValue.distance <= autoMapDistance ? smallestValue.value : undefined +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/findUnmatchedRequiredFields.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/findUnmatchedRequiredFields.ts new file mode 100644 index 0000000..4c74bf1 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/findUnmatchedRequiredFields.ts @@ -0,0 +1,15 @@ +import type { Fields } from "../../../types" +import type { Columns } from "../MatchColumnsStep" + +export const findUnmatchedRequiredFields = (fields: Fields, columns: Columns) => { + // Get all required fields + const requiredFields = fields + .filter((field) => field.validations?.some((validation: any) => + validation.rule === "required" || validation.required === true + )) + + // Find which required fields are not matched in columns + return requiredFields + .filter((field) => columns.findIndex((column) => "value" in column && column.value === field.key) === -1) + .map((field) => field.key) || [] +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/getFieldOptions.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/getFieldOptions.ts new file mode 100644 index 0000000..8793557 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/getFieldOptions.ts @@ -0,0 +1,6 @@ +import type { Fields } from "../../../types" + +export const getFieldOptions = (fields: Fields, fieldKey: string) => { + const field = fields.find(({ key }) => fieldKey === key)! + return field.fieldType.type === "select" ? field.fieldType.options : [] +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/getMatchedColumns.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/getMatchedColumns.ts new file mode 100644 index 0000000..4fdea0c --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/getMatchedColumns.ts @@ -0,0 +1,41 @@ +import lavenstein from "js-levenshtein" +import { findMatch } from "./findMatch" +import type { Field, Fields } from "../../../types" +import { setColumn } from "./setColumn" +import type { Column, Columns } from "../MatchColumnsStep" +import type { MatchColumnsProps } from "../MatchColumnsStep" + +export const getMatchedColumns = ( + columns: Columns, + fields: Fields, + data: MatchColumnsProps["data"], + autoMapDistance: number, + autoMapSelectValues?: boolean, +) => + columns.reduce[]>((arr, column) => { + const autoMatch = findMatch(column.header, fields, autoMapDistance) + if (autoMatch) { + const field = fields.find((field) => field.key === autoMatch) as Field + const duplicateIndex = arr.findIndex((column) => "value" in column && column.value === field.key) + const duplicate = arr[duplicateIndex] + if (duplicate && "value" in duplicate) { + return lavenstein(duplicate.value, duplicate.header) < lavenstein(autoMatch, column.header) + ? [ + ...arr.slice(0, duplicateIndex), + setColumn(arr[duplicateIndex], field, data, autoMapSelectValues), + ...arr.slice(duplicateIndex + 1), + setColumn(column), + ] + : [ + ...arr.slice(0, duplicateIndex), + setColumn(arr[duplicateIndex]), + ...arr.slice(duplicateIndex + 1), + setColumn(column, field, data, autoMapSelectValues), + ] + } else { + return [...arr, setColumn(column, field, data, autoMapSelectValues)] + } + } else { + return [...arr, column] + } + }, []) diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeCheckboxValue.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeCheckboxValue.ts new file mode 100644 index 0000000..62a434d --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeCheckboxValue.ts @@ -0,0 +1,13 @@ +const booleanWhitelist: Record = { + yes: true, + no: false, + true: true, + false: false, +} + +export const normalizeCheckboxValue = (value: string | undefined): boolean => { + if (value && value.toLowerCase() in booleanWhitelist) { + return booleanWhitelist[value.toLowerCase()] + } + return false +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts new file mode 100644 index 0000000..46fa4dd --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/normalizeTableData.ts @@ -0,0 +1,67 @@ +import type { Columns } from "../MatchColumnsStep" +import { ColumnType } from "../MatchColumnsStep" +import type { Data, Fields, RawData } from "../../../types" +import { normalizeCheckboxValue } from "./normalizeCheckboxValue" + +export const normalizeTableData = (columns: Columns, data: RawData[], fields: Fields) => + data.map((row) => + columns.reduce((acc, column, index) => { + const curr = row[index] + switch (column.type) { + case ColumnType.matchedCheckbox: { + const field = fields.find((field) => field.key === column.value)! + if ("booleanMatches" in field.fieldType && Object.keys(field.fieldType).length) { + const booleanMatchKey = Object.keys(field.fieldType.booleanMatches || []).find( + (key) => key.toLowerCase() === curr?.toLowerCase(), + )! + const booleanMatch = field.fieldType.booleanMatches?.[booleanMatchKey] + acc[column.value] = (booleanMatchKey ? booleanMatch : normalizeCheckboxValue(curr)) as Data[T] + } else { + acc[column.value] = normalizeCheckboxValue(curr) as Data[T] + } + return acc + } + case ColumnType.matched: { + acc[column.value] = (curr === "" ? undefined : curr) as Data[T] + return acc + } + case ColumnType.matchedMultiInput: { + const field = fields.find((field) => field.key === column.value)! + if (curr) { + const separator = field.fieldType.type === "multi-input" ? field.fieldType.separator || "," : "," + acc[column.value] = curr.split(separator).map(v => v.trim()).filter(Boolean) as Data[T] + } else { + acc[column.value] = undefined as Data[T] + } + return acc + } + case ColumnType.matchedSelect: + case ColumnType.matchedSelectOptions: { + const matchedOption = column.matchedOptions.find(({ entry }) => entry === curr) + acc[column.value] = (matchedOption?.value || undefined) as Data[T] + return acc + } + case ColumnType.matchedMultiSelect: { + const field = fields.find((field) => field.key === column.value)! + if (curr) { + const separator = field.fieldType.type === "multi-select" ? field.fieldType.separator || "," : "," + const entries = curr.split(separator).map(v => v.trim()).filter(Boolean) + const values = entries.map(entry => { + const matchedOption = column.matchedOptions.find(({ entry: optEntry }) => optEntry === entry) + return matchedOption?.value + }).filter(Boolean) as string[] + acc[column.value] = (values.length ? values : undefined) as Data[T] + } else { + acc[column.value] = undefined as Data[T] + } + return acc + } + case ColumnType.empty: + case ColumnType.ignored: { + return acc + } + default: + return acc + } + }, {} as Data), + ) diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setColumn.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setColumn.ts new file mode 100644 index 0000000..cd3d6ff --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setColumn.ts @@ -0,0 +1,65 @@ +import type { Field, MultiSelect } from "../../../types" +import { Column, ColumnType, MatchColumnsProps, MatchedOptions } from "../MatchColumnsStep" +import { uniqueEntries } from "./uniqueEntries" + +export const setColumn = ( + oldColumn: Column, + field?: Field, + data?: MatchColumnsProps["data"], + autoMapSelectValues?: boolean, +): Column => { + switch (field?.fieldType.type) { + case "select": + const fieldOptions = field.fieldType.options + const uniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] + const matchedOptions = autoMapSelectValues + ? uniqueData.map((record) => { + const value = fieldOptions.find( + (fieldOption) => fieldOption.value === record.entry || fieldOption.label === record.entry, + )?.value + return value ? ({ ...record, value } as MatchedOptions) : (record as MatchedOptions) + }) + : uniqueData + const allMatched = matchedOptions.filter((o) => o.value).length == uniqueData?.length + + return { + ...oldColumn, + type: allMatched ? ColumnType.matchedSelectOptions : ColumnType.matchedSelect, + value: field.key, + matchedOptions, + } + case "multi-select": + const multiSelectFieldType = field.fieldType as MultiSelect + const multiSelectFieldOptions = multiSelectFieldType.options + const multiSelectUniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] + const multiSelectMatchedOptions = autoMapSelectValues + ? multiSelectUniqueData.map((record) => { + // Split the entry by the separator (default to comma) + const entries = record.entry.split(multiSelectFieldType.separator || ",").map(e => e.trim()) + // Try to match each entry to an option + const values = entries.map(entry => { + const value = multiSelectFieldOptions.find( + (fieldOption) => fieldOption.value === entry || fieldOption.label === entry, + )?.value + return value + }).filter(Boolean) as T[] + return { ...record, value: values.length ? values[0] : undefined } as MatchedOptions + }) + : multiSelectUniqueData + + return { + ...oldColumn, + type: ColumnType.matchedMultiSelect, + value: field.key, + matchedOptions: multiSelectMatchedOptions, + } + case "checkbox": + return { index: oldColumn.index, type: ColumnType.matchedCheckbox, value: field.key, header: oldColumn.header } + case "input": + return { index: oldColumn.index, type: ColumnType.matched, value: field.key, header: oldColumn.header } + case "multi-input": + return { index: oldColumn.index, type: ColumnType.matchedMultiInput, value: field.key, header: oldColumn.header } + default: + return { index: oldColumn.index, header: oldColumn.header, type: ColumnType.empty } + } +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setIgnoreColumn.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setIgnoreColumn.ts new file mode 100644 index 0000000..2404d12 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setIgnoreColumn.ts @@ -0,0 +1,7 @@ +import { Column, ColumnType } from "../MatchColumnsStep" + +export const setIgnoreColumn = ({ header, index }: Column): Column => ({ + header, + index, + type: ColumnType.ignored, +}) diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setSubColumn.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setSubColumn.ts new file mode 100644 index 0000000..2186524 --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/setSubColumn.ts @@ -0,0 +1,20 @@ +import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn, MatchedMultiSelectColumn } from "../MatchColumnsStep" + +export const setSubColumn = ( + oldColumn: MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn, + entry: string, + value: string, +): MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn => { + const options = oldColumn.matchedOptions.map((option) => (option.entry === entry ? { ...option, value } : option)) + const allMatched = options.every(({ value }) => !!value) + + if (oldColumn.type === ColumnType.matchedMultiSelect) { + return { ...oldColumn, matchedOptions: options as MatchedOptions[] } + } + + if (allMatched) { + return { ...oldColumn, matchedOptions: options as MatchedOptions[], type: ColumnType.matchedSelectOptions } + } else { + return { ...oldColumn, matchedOptions: options as MatchedOptions[], type: ColumnType.matchedSelect } + } +} diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/utils/uniqueEntries.ts b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/uniqueEntries.ts new file mode 100644 index 0000000..d44739c --- /dev/null +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/utils/uniqueEntries.ts @@ -0,0 +1,11 @@ +import uniqBy from "lodash/uniqBy" +import type { MatchColumnsProps, MatchedOptions } from "../MatchColumnsStep" + +export const uniqueEntries = ( + data: MatchColumnsProps["data"], + index: number, +): Partial>[] => + uniqBy( + data.map((row) => ({ entry: row[index] })), + "entry", + ).filter(({ entry }) => !!entry) diff --git a/inventory/src/components/product-import/steps/SelectHeaderStep/SelectHeaderStep.tsx b/inventory/src/components/product-import/steps/SelectHeaderStep/SelectHeaderStep.tsx new file mode 100644 index 0000000..dfce26b --- /dev/null +++ b/inventory/src/components/product-import/steps/SelectHeaderStep/SelectHeaderStep.tsx @@ -0,0 +1,194 @@ +import { useCallback, useState } from "react" +import { SelectHeaderTable } from "./components/SelectHeaderTable" +import { useRsi } from "../../hooks/useRsi" +import type { RawData } from "../../types" +import { Button } from "@/components/ui/button" +import { useToast } from "@/hooks/use-toast" + +type SelectHeaderProps = { + data: RawData[] + onContinue: (headerValues: RawData, data: RawData[]) => Promise + onBack?: () => void +} + +export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps) => { + const { translations } = useRsi() + const { toast } = useToast() + const [selectedRows, setSelectedRows] = useState>(new Set([0])) + const [isLoading, setIsLoading] = useState(false) + const [localData, setLocalData] = useState(data) + + const handleContinue = useCallback(async () => { + const [selectedRowIndex] = selectedRows + // We consider data above header to be redundant + const trimmedData = localData.slice(selectedRowIndex + 1) + setIsLoading(true) + await onContinue(localData[selectedRowIndex], trimmedData) + setIsLoading(false) + }, [onContinue, localData, selectedRows]) + + const discardEmptyAndDuplicateRows = useCallback(() => { + // Helper function to count non-empty values in a row + const countNonEmptyValues = (values: Record): number => { + return Object.values(values).filter(val => + val !== undefined && + val !== null && + (typeof val === 'string' ? val.trim() !== '' : true) + ).length; + }; + + // Helper function to normalize row values for case-insensitive comparison + const normalizeRowForComparison = (row: Record): Record => { + return Object.entries(row).reduce((acc, [key, value]) => { + // Convert string values to lowercase for case-insensitive comparison + if (typeof value === 'string') { + acc[key.toLowerCase()] = value.toLowerCase().trim(); + } else { + acc[key.toLowerCase()] = value; + } + return acc; + }, {} as Record); + }; + + // First, analyze all rows to determine if we have rows with multiple values + const rowsWithValues = localData.map(row => { + return countNonEmptyValues(row); + }); + + // Check if we have any rows with more than one value + const hasMultiValueRows = rowsWithValues.some(count => count > 1); + + // Get the selected header row + const [selectedRowIndex] = selectedRows; + const selectedHeaderRow = localData[selectedRowIndex]; + + // Debug: Log the selected header row + console.log("Selected header row:", selectedHeaderRow); + + const normalizedHeaderRow = normalizeRowForComparison(selectedHeaderRow); + + // Debug: Log the normalized header row + console.log("Normalized header row:", normalizedHeaderRow); + + const selectedHeaderStr = JSON.stringify(Object.entries(normalizedHeaderRow).sort()); + + // Filter out empty rows, rows with single values (if we have multi-value rows), + // and duplicate rows (including duplicates of the header row) + const seen = new Set(); + // Add the selected header row to the seen set first + seen.add(selectedHeaderStr); + + // Debug: Track which rows are being removed and why + const removedRows: { index: number; reason: string; row: any }[] = []; + + const filteredRows = localData.filter((row, index) => { + // Always keep the selected header row + if (index === selectedRowIndex) { + return true; + } + + // Check if it's empty or has only one value + const nonEmptyCount = rowsWithValues[index]; + if (nonEmptyCount === 0 || (hasMultiValueRows && nonEmptyCount <= 1)) { + removedRows.push({ index, reason: "Empty or single value", row }); + return false; + } + + // Check if it's a duplicate (case-insensitive) + const normalizedRow = normalizeRowForComparison(row); + + // Debug: If this row might be a duplicate of the header, log it + if (index < 5 || index === selectedRowIndex + 1 || index === selectedRowIndex - 1) { + console.log(`Row ${index} normalized:`, normalizedRow); + } + + const rowStr = JSON.stringify(Object.entries(normalizedRow).sort()); + + if (seen.has(rowStr)) { + removedRows.push({ + index, + reason: "Duplicate", + row + }); + return false; + } + + seen.add(rowStr); + return true; + }); + + // Debug: Log removed rows + console.log("Removed rows:", removedRows); + + // Only update if we actually removed any rows + if (filteredRows.length < localData.length) { + // Adjust the selected row index if needed + const newSelectedIndex = filteredRows.findIndex(row => + JSON.stringify(Object.entries(normalizeRowForComparison(row)).sort()) === selectedHeaderStr + ); + + // Debug: Log the new selected index + console.log("New selected index:", newSelectedIndex); + + setLocalData(filteredRows); + setSelectedRows(new Set([newSelectedIndex])); + + toast({ + title: "Rows removed", + description: `Removed ${localData.length - filteredRows.length} empty, single-value, or duplicate rows`, + variant: "default" + }); + } else { + toast({ + title: "No rows removed", + description: "No empty, single-value, or duplicate rows were found", + variant: "default" + }); + } + }, [localData, selectedRows, toast]); + + return ( +
+
+
+

+ {translations.selectHeaderStep.title} +

+

+ Select the row that contains your column headers +

+
+ +
+
+
+ +
+
+
+ {onBack && ( + + )} + +
+
+ ) +} diff --git a/inventory/src/components/product-import/steps/SelectHeaderStep/components/SelectHeaderTable.tsx b/inventory/src/components/product-import/steps/SelectHeaderStep/components/SelectHeaderTable.tsx new file mode 100644 index 0000000..e387673 --- /dev/null +++ b/inventory/src/components/product-import/steps/SelectHeaderStep/components/SelectHeaderTable.tsx @@ -0,0 +1,86 @@ +import { useMemo } from "react" +import type { RawData } from "../../../types" +import { + Table, + TableBody, + TableCell, + TableRow, +} from "@/components/ui/table" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" + +interface Props { + data: RawData[] + selectedRows: ReadonlySet + setSelectedRows: (rows: ReadonlySet) => void +} + +export const SelectHeaderTable = ({ data, selectedRows, setSelectedRows }: Props) => { + const columns = useMemo(() => { + const longestRowLength = data.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0) + return Array.from(Array(longestRowLength), (_, index) => ({ + key: index.toString(), + name: `Column ${index + 1}`, + })) + }, [data]) + + if (!data || data.length === 0) { + return ( +
+

No data available to select headers from.

+
+ ) + } + + const selectedRowIndex = Array.from(selectedRows)[0] + const gridTemplateColumns = `60px repeat(${columns.length}, minmax(150px, 300px))` + + return ( +
+ +
+ + + + setSelectedRows(new Set([parseInt(value)]))} + > + {data.map((row, rowIndex) => ( + + +
+ + +
+
+ {columns.map((column, colIndex) => ( + +
+ {row[colIndex] || ""} +
+
+ ))} +
+ ))} +
+
+
+
+
+ ) +} diff --git a/inventory/src/components/product-import/steps/SelectHeaderStep/components/columns.tsx b/inventory/src/components/product-import/steps/SelectHeaderStep/components/columns.tsx new file mode 100644 index 0000000..a042ba3 --- /dev/null +++ b/inventory/src/components/product-import/steps/SelectHeaderStep/components/columns.tsx @@ -0,0 +1,67 @@ +import { Column, FormatterProps, useRowSelection } from "react-data-grid" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Label } from "@/components/ui/label" +import type { RawData } from "../../../types" + +const SELECT_COLUMN_KEY = "select-row" + +function SelectFormatter(props: FormatterProps) { + const [isRowSelected, onRowSelectionChange] = useRowSelection() + + return ( +
+ +
+ { + onRowSelectionChange({ + row: props.row, + checked: !isRowSelected, + isShiftClick: (event.nativeEvent as MouseEvent).shiftKey, + }) + }} + /> + +
+
+
+ ) +} + +export const SelectColumn: Column = { + key: SELECT_COLUMN_KEY, + name: "Select Header", + width: 100, + minWidth: 100, + maxWidth: 100, + resizable: false, + sortable: false, + frozen: true, + cellClass: "rdg-radio", + formatter: SelectFormatter, +} + +export const generateSelectionColumns = (data: RawData[]) => { + const longestRowLength = data.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0) + return [ + SelectColumn, + ...Array.from(Array(longestRowLength), (_, index) => ({ + key: index.toString(), + name: `Column ${index + 1}`, + width: 150, + formatter: ({ row }: { row: RawData }) => ( +
+ {row[index]} +
+ ), + })), + ] +} diff --git a/inventory/src/components/product-import/steps/SelectSheetStep/SelectSheetStep.tsx b/inventory/src/components/product-import/steps/SelectSheetStep/SelectSheetStep.tsx new file mode 100644 index 0000000..1e43ad5 --- /dev/null +++ b/inventory/src/components/product-import/steps/SelectSheetStep/SelectSheetStep.tsx @@ -0,0 +1,77 @@ +import { useCallback, useState } from "react" +import { useRsi } from "../../hooks/useRsi" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Label } from "@/components/ui/label" +import { Button } from "@/components/ui/button" +import { ChevronLeft } from "lucide-react" + +type SelectSheetProps = { + sheetNames: string[] + onContinue: (sheetName: string) => Promise + onBack?: () => void +} + +export const SelectSheetStep = ({ sheetNames, onContinue, onBack }: SelectSheetProps) => { + const [isLoading, setIsLoading] = useState(false) + const { translations } = useRsi() + const [value, setValue] = useState(sheetNames[0]) + + const handleOnContinue = useCallback( + async (data: typeof value) => { + setIsLoading(true) + await onContinue(data) + setIsLoading(false) + }, + [onContinue], + ) + + return ( +
+
+
+
+

+ {translations.uploadStep.selectSheet.title} +

+
+ + {sheetNames.map((sheetName) => ( +
+ + +
+ ))} +
+
+
+
+ {onBack && ( + + )} +
+ +
+
+ ) +} diff --git a/inventory/src/components/product-import/steps/Steps.tsx b/inventory/src/components/product-import/steps/Steps.tsx new file mode 100644 index 0000000..d007f32 --- /dev/null +++ b/inventory/src/components/product-import/steps/Steps.tsx @@ -0,0 +1,103 @@ +import { StepState, StepType, UploadFlow } from "./UploadFlow" +import { useRsi } from "../hooks/useRsi" +import { useRef, useState, useEffect } from "react" +import { steps, stepTypeToStepIndex, stepIndexToStepType } from "../utils/steps" +import { CgCheck } from "react-icons/cg" + +const CheckIcon = ({ color }: { color: string }) => + +export const Steps = () => { + const { initialStepState, translations, isNavigationEnabled, isOpen } = useRsi() + const initialStep = stepTypeToStepIndex(initialStepState?.type) + const [activeStep, setActiveStep] = useState(initialStep) + const [state, setState] = useState(initialStepState || { type: StepType.upload }) + const history = useRef([]) + const prevIsOpen = useRef(isOpen) + + // Reset state when dialog is reopened + useEffect(() => { + // Check if dialog was closed and is now open again + if (isOpen && !prevIsOpen.current) { + // Reset to initial state + setActiveStep(initialStep) + setState(initialStepState || { type: StepType.upload }) + history.current = [] + } + + // Update previous isOpen value + prevIsOpen.current = isOpen + }, [isOpen, initialStep, initialStepState]) + + const onClickStep = (stepIndex: number) => { + const type = stepIndexToStepType(stepIndex) + const historyIdx = history.current.findIndex((v) => v.type === type) + if (historyIdx === -1) return + const nextHistory = history.current.slice(0, historyIdx + 1) + history.current = nextHistory + setState(nextHistory[nextHistory.length - 1]) + setActiveStep(stepIndex) + } + + const onBack = () => { + onClickStep(Math.max(activeStep - 1, 0)) + } + + const onNext = (v: StepState) => { + history.current.push(state) + setState(v) + + if (v.type === StepType.validateData && 'isFromScratch' in v && v.isFromScratch) { + // If starting from scratch, jump directly to the validation step + const validationStepIndex = steps.indexOf('validationStep') + setActiveStep(validationStepIndex) + } else if (v.type !== StepType.selectSheet) { + setActiveStep(activeStep + 1) + } + } + + return ( + <> +
+ +
+ + + ) +} diff --git a/inventory/src/components/product-import/steps/UploadFlow.tsx b/inventory/src/components/product-import/steps/UploadFlow.tsx new file mode 100644 index 0000000..cd4bc1c --- /dev/null +++ b/inventory/src/components/product-import/steps/UploadFlow.tsx @@ -0,0 +1,258 @@ +import { useCallback, useState } from "react" +import type XLSX from "xlsx" +import { UploadStep } from "./UploadStep/UploadStep" +import { SelectHeaderStep } from "./SelectHeaderStep/SelectHeaderStep" +import { SelectSheetStep } from "./SelectSheetStep/SelectSheetStep" +import { mapWorkbook } from "../utils/mapWorkbook" +import { ValidationStepNew } from "./ValidationStepNew" +import { ImageUploadStep } from "./ImageUploadStep/ImageUploadStep" +import { MatchColumnsStep, type GlobalSelections } from "./MatchColumnsStep/MatchColumnsStep" +import { exceedsMaxRecords } from "../utils/exceedsMaxRecords" +import { useRsi } from "../hooks/useRsi" +import type { RawData, Data } from "../types" +import { Progress } from "@/components/ui/progress" +import { useToast } from "@/hooks/use-toast" +import { addErrorsAndRunHooks } from "./ValidationStepNew/utils/dataMutations" + +export enum StepType { + upload = "upload", + selectSheet = "selectSheet", + selectHeader = "selectHeader", + matchColumns = "matchColumns", + validateData = "validateData", + imageUpload = "imageUpload", +} + +export type StepState = + | { + type: StepType.upload + } + | { + type: StepType.selectSheet + workbook: XLSX.WorkBook + } + | { + type: StepType.selectHeader + data: RawData[] + } + | { + type: StepType.matchColumns + data: RawData[] + headerValues: RawData + globalSelections?: GlobalSelections + } + | { + type: StepType.validateData + data: any[] + globalSelections?: GlobalSelections + isFromScratch?: boolean + } + | { + type: StepType.imageUpload + data: any[] + file: File + globalSelections?: GlobalSelections + } + +interface Props { + state: StepState + onNext: (v: StepState) => void + onBack?: () => void +} + +export const UploadFlow = ({ state, onNext, onBack }: Props) => { + const { + maxRecords, + translations, + uploadStepHook, + selectHeaderStepHook, + matchColumnsStepHook, + fields, + rowHook, + tableHook, + onSubmit } = useRsi() + const [uploadedFile, setUploadedFile] = useState(null) + const { toast } = useToast() + const errorToast = useCallback( + (description: string) => { + toast({ + variant: "destructive", + title: translations.alerts.toast.error, + description, + }) + }, + [toast, translations], + ) + + // Keep track of global selections across steps + const [persistedGlobalSelections, setPersistedGlobalSelections] = useState( + state.type === StepType.validateData || state.type === StepType.matchColumns + ? state.globalSelections + : undefined + ) + + + switch (state.type) { + case StepType.upload: + return ( + { + setUploadedFile(file) + const isSingleSheet = workbook.SheetNames.length === 1 + if (isSingleSheet) { + if (maxRecords && exceedsMaxRecords(workbook.Sheets[workbook.SheetNames[0]], maxRecords)) { + errorToast(translations.uploadStep.maxRecordsExceeded(maxRecords.toString())) + return + } + try { + const mappedWorkbook = await uploadStepHook(mapWorkbook(workbook)) + onNext({ + type: StepType.selectHeader, + data: mappedWorkbook, + }) + } catch (e) { + errorToast((e as Error).message) + } + } else { + onNext({ type: StepType.selectSheet, workbook }) + } + }} + setInitialState={(state) => { + // Ensure the state has the correct type + if (state.type === StepType.validateData) { + onNext({ + type: StepType.validateData, + data: state.data, + isFromScratch: state.isFromScratch, + globalSelections: undefined + }); + } + }} + /> + ) + case StepType.selectSheet: + return ( + { + if (maxRecords && exceedsMaxRecords(state.workbook.Sheets[sheetName], maxRecords)) { + errorToast(translations.uploadStep.maxRecordsExceeded(maxRecords.toString())) + return + } + try { + const mappedWorkbook = await uploadStepHook(mapWorkbook(state.workbook, sheetName)) + onNext({ + type: StepType.selectHeader, + data: mappedWorkbook, + }) + } catch (e) { + errorToast((e as Error).message) + } + }} + onBack={onBack} + /> + ) + case StepType.selectHeader: + return ( + { + try { + const { data, headerValues } = await selectHeaderStepHook(...args) + onNext({ + type: StepType.matchColumns, + data, + headerValues, + globalSelections: persistedGlobalSelections, + }) + } catch (e) { + errorToast((e as Error).message) + } + }} + onBack={onBack} + /> + ) + case StepType.matchColumns: + return ( + { + try { + const data = await matchColumnsStepHook(values, rawData, columns) + const dataWithMeta = await addErrorsAndRunHooks(data, fields, rowHook, tableHook) + + // Apply global selections to each row of data if they exist + const dataWithGlobalSelections = globalSelections + ? dataWithMeta.map((row: Data & { __index?: string }) => { + const newRow = { ...row }; + if (globalSelections.supplier) newRow.supplier = globalSelections.supplier; + if (globalSelections.company) newRow.company = globalSelections.company; + return newRow; + }) + : dataWithMeta; + + setPersistedGlobalSelections(globalSelections) + onNext({ + type: StepType.validateData, + data: dataWithGlobalSelections, + globalSelections, + }) + } catch (e) { + errorToast((e as Error).message) + } + }} + onBack={onBack} + /> + ) + case StepType.validateData: + // Always use the new ValidationStepNew component + return ( + { + // If we started from scratch, we need to go back to the upload step + if (state.isFromScratch) { + onNext({ + type: StepType.upload + }); + } else if (onBack) { + // Use the provided onBack function + onBack(); + } + }} + onNext={(validatedData: any[]) => { + // Go to image upload step with the validated data + onNext({ + type: StepType.imageUpload, + data: validatedData, + file: uploadedFile || new File([], "empty.xlsx"), + globalSelections: state.globalSelections + }); + }} + isFromScratch={state.isFromScratch} + /> + ) + case StepType.imageUpload: + return ( + { + // Create a Result object from the array data + const result = { + validData: data as Data[], + invalidData: [] as Data[], + all: data as Data[] + }; + onSubmit(result, file); + }} + /> + ) + default: + return + } +} diff --git a/inventory/src/components/product-import/steps/UploadStep/UploadStep.tsx b/inventory/src/components/product-import/steps/UploadStep/UploadStep.tsx new file mode 100644 index 0000000..ea5744f --- /dev/null +++ b/inventory/src/components/product-import/steps/UploadStep/UploadStep.tsx @@ -0,0 +1,61 @@ +import type XLSX from "xlsx" +import { useCallback, useState } from "react" +import { useRsi } from "../../hooks/useRsi" +import { DropZone } from "./components/DropZone" +import { StepType } from "../UploadFlow" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" + +type UploadProps = { + onContinue: (data: XLSX.WorkBook, file: File) => Promise + setInitialState?: (state: { type: StepType; data: any[]; isFromScratch?: boolean }) => void +} + +export const UploadStep = ({ onContinue, setInitialState }: UploadProps) => { + const [isLoading, setIsLoading] = useState(false) + const { translations } = useRsi() + + const handleOnContinue = useCallback( + async (data: XLSX.WorkBook, file: File) => { + setIsLoading(true) + await onContinue(data, file) + setIsLoading(false) + }, + [onContinue], + ) + + const handleStartFromScratch = useCallback(() => { + if (setInitialState) { + setInitialState({ type: StepType.validateData, data: [{}], isFromScratch: true }) + } + }, [setInitialState]) + + return ( +
+

{translations.uploadStep.title}

+ +
+
+ +
+ +
+ + OR + +
+ +
+ +
+
+
+ ) +} diff --git a/inventory/src/components/product-import/steps/UploadStep/components/DropZone.tsx b/inventory/src/components/product-import/steps/UploadStep/components/DropZone.tsx new file mode 100644 index 0000000..c080aec --- /dev/null +++ b/inventory/src/components/product-import/steps/UploadStep/components/DropZone.tsx @@ -0,0 +1,85 @@ +import { useDropzone } from "react-dropzone" +import * as XLSX from "xlsx" +import { useState } from "react" +import { useRsi } from "../../../hooks/useRsi" +import { readFileAsync } from "../utils/readFilesAsync" +import { Button } from "@/components/ui/button" +import { useToast } from "@/hooks/use-toast" +import { cn } from "@/lib/utils" + +type DropZoneProps = { + onContinue: (data: XLSX.WorkBook, file: File) => void + isLoading: boolean +} + +export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => { + const { translations, maxFileSize, dateFormat, parseRaw } = useRsi() + const { toast } = useToast() + const [loading, setLoading] = useState(false) + const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ + noClick: true, + noKeyboard: true, + maxFiles: 1, + maxSize: maxFileSize, + accept: { + "application/vnd.ms-excel": [".xls"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], + "text/csv": [".csv"], + }, + onDropRejected: (fileRejections) => { + setLoading(false) + fileRejections.forEach((fileRejection) => { + toast({ + variant: "destructive", + title: `${fileRejection.file.name} ${translations.uploadStep.dropzone.errorToastDescription}`, + description: fileRejection.errors[0].message, + }) + }) + }, + onDropAccepted: async ([file]) => { + setLoading(true) + const arrayBuffer = await readFileAsync(file) + const workbook = XLSX.read(arrayBuffer, { + cellDates: true, + dateNF: dateFormat, + raw: parseRaw, + dense: true, + type: 'array', + codepage: 65001, // UTF-8 + WTF: false // Don't throw on errors + }) + setLoading(false) + onContinue(workbook, file) + }, + }) + + return ( +
+ + {isDragActive ? ( +

+ {translations.uploadStep.dropzone.activeDropzoneTitle} +

+ ) : loading || isLoading ? ( +

+ {translations.uploadStep.dropzone.loadingTitle} +

+ ) : ( + <> +

+ {translations.uploadStep.dropzone.title} +

+ + + )} +
+ ) +} diff --git a/inventory/src/components/product-import/steps/UploadStep/components/columns.tsx b/inventory/src/components/product-import/steps/UploadStep/components/columns.tsx new file mode 100644 index 0000000..e31333a --- /dev/null +++ b/inventory/src/components/product-import/steps/UploadStep/components/columns.tsx @@ -0,0 +1,44 @@ +import type { Column } from "react-data-grid" +import type { Fields } from "../../../types" +import { CgInfo } from "react-icons/cg" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +export const generateColumns = (fields: Fields) => + fields.map( + (column): Column => ({ + key: column.key, + name: column.label, + minWidth: 150, + headerRenderer: () => ( +
+
+ {column.label} +
+ {column.description && ( + + + +
+ +
+
+ + {column.description} + +
+
+ )} +
+ ), + formatter: ({ row }) => ( +
+ {row[column.key]} +
+ ), + }), + ) diff --git a/inventory/src/components/product-import/steps/UploadStep/utils/readFilesAsync.ts b/inventory/src/components/product-import/steps/UploadStep/utils/readFilesAsync.ts new file mode 100644 index 0000000..aa4d209 --- /dev/null +++ b/inventory/src/components/product-import/steps/UploadStep/utils/readFilesAsync.ts @@ -0,0 +1,13 @@ +export const readFileAsync = (file: File) => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + + reader.onload = () => { + resolve(reader.result) + } + + reader.onerror = reject + + reader.readAsArrayBuffer(file) + }) +} diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/README.md b/inventory/src/components/product-import/steps/ValidationStepNew/README.md new file mode 100644 index 0000000..3ef2e75 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/README.md @@ -0,0 +1,58 @@ +# ValidationStepNew Component + +This is a refactored version of the original ValidationStep component with improved architecture, performance, and maintainability. + +## Overview + +The ValidationStepNew component is designed to replace the original ValidationStep component in the React Spreadsheet Import library. It provides the same functionality but with a more modular and maintainable codebase. + +## Features + +- Field-level validation (required, regex, unique) +- Row-level validation (supplier, company fields) +- UPC validation with API integration +- Template management (saving, loading, applying) +- Filtering and sorting capabilities +- Error display and management +- Special field handling (input, multi-input, select, checkbox) + +## Usage + +To use the new ValidationStepNew component, select the "Use new validation component" checkbox in the MatchColumnsStep. This will route you to the new implementation instead of the original one. + +## Architecture + +The component is structured as follows: + +- **ValidationContainer**: Main container component that coordinates all subcomponents +- **ValidationTable**: Handles data display, filtering, and column configuration +- **ValidationCell**: Cell-level component with specialized rendering based on field type +- **ValidationToolbar**: Top toolbar with actions and statistics +- **ValidationSidebar**: Contains filters, actions, and other UI controls + +## State Management + +State is managed through custom hooks: + +- **useValidationState**: Main state management hook +- **useValidation**: Validation logic +- **useTemplates**: Template management +- **useFilters**: Filtering logic +- **useUpcValidation**: UPC validation + +## Development + +This component is still under development. The goal is to eventually replace the original ValidationStep component completely once all functionality is implemented and tested. + +## Known Issues + +- Some TypeScript errors in the UploadFlow component when integrating with the new component +- Not all features from the original component are fully implemented yet + +## Roadmap + +1. Complete implementation of all features from the original component +2. Add comprehensive tests +3. Improve performance with virtualization for large datasets +4. Add more customization options +5. Replace the original component completely \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx new file mode 100644 index 0000000..b15ebcc --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx @@ -0,0 +1,248 @@ +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Button } from '@/components/ui/button'; +import { Loader2, CheckIcon } from 'lucide-react'; +import { Code } from '@/components/ui/code'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { AiValidationDetails, AiValidationProgress, CurrentPrompt } from '../hooks/useAiValidation'; + +interface AiValidationDialogsProps { + aiValidationProgress: AiValidationProgress; + aiValidationDetails: AiValidationDetails; + currentPrompt: CurrentPrompt; + setAiValidationProgress: React.Dispatch>; + setAiValidationDetails: React.Dispatch>; + setCurrentPrompt: React.Dispatch>; + revertAiChange: (productIndex: number, fieldKey: string) => void; + isChangeReverted: (productIndex: number, fieldKey: string) => boolean; + getFieldDisplayValueWithHighlight: (fieldKey: string, originalValue: any, correctedValue: any) => { originalHtml: string, correctedHtml: string }; + fields: readonly any[]; +} + +export const AiValidationDialogs: React.FC = ({ + aiValidationProgress, + aiValidationDetails, + currentPrompt, + setAiValidationProgress, + setAiValidationDetails, + setCurrentPrompt, + revertAiChange, + isChangeReverted, + getFieldDisplayValueWithHighlight, + fields +}) => { + return ( + <> + {/* Current Prompt Dialog */} + setCurrentPrompt(prev => ({ ...prev, isOpen: open }))} + > + + + Current AI Prompt + + This is the exact prompt that would be sent to the AI for validation + + + + {currentPrompt.isLoading ? ( +
+ +
+ ) : ( + {currentPrompt.prompt} + )} +
+
+
+ + {/* AI Validation Progress Dialog */} + { + // Only allow closing if validation failed + if (!open && aiValidationProgress.step === -1) { + setAiValidationProgress(prev => ({ ...prev, isOpen: false })); + } + }} + > + + + AI Validation Progress + +
+
+
+
+
+
+
+
+ {aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`} +
+
+

+ {aiValidationProgress.status} +

+ {(() => { + // Only show time remaining if we have an estimate and are in progress + return aiValidationProgress.estimatedSeconds && + aiValidationProgress.elapsedSeconds !== undefined && + aiValidationProgress.step > 0 && + aiValidationProgress.step < 5 && ( +
+ {(() => { + // Calculate time remaining using the elapsed seconds + const elapsedSeconds = aiValidationProgress.elapsedSeconds; + const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds; + const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds); + + // Format time remaining + if (remainingSeconds < 60) { + return `Approximately ${Math.round(remainingSeconds)} seconds remaining`; + } else { + const minutes = Math.floor(remainingSeconds / 60); + const seconds = Math.round(remainingSeconds % 60); + return `Approximately ${minutes}m ${seconds}s remaining`; + } + })()} + {aiValidationProgress.promptLength && ( +

+ Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters +

+ )} +
+ ); + })()} +
+ +
+ + {/* AI Validation Results Dialog */} + setAiValidationDetails(prev => ({ ...prev, isOpen: open }))} + > + + + AI Validation Results + + Review the changes and warnings suggested by the AI + + + + {aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? ( +
+

Detailed Changes:

+ {aiValidationDetails.changeDetails.map((product, i) => { + // Find the title change if it exists + const titleChange = product.changes.find(c => c.field === 'title'); + const titleValue = titleChange ? titleChange.corrected : product.title; + + return ( +
+

+ {titleValue || `Product ${product.productIndex + 1}`} +

+ + + + Field + Original Value + Corrected Value + Action + + + + {product.changes.map((change, j) => { + const field = fields.find(f => f.key === change.field); + const fieldLabel = field ? field.label : change.field; + const isReverted = isChangeReverted(product.productIndex, change.field); + + // Get highlighted differences + const { originalHtml, correctedHtml } = getFieldDisplayValueWithHighlight( + change.field, + change.original, + change.corrected + ); + + return ( + + {fieldLabel} + +
+ + +
+ + +
+ {isReverted ? ( + + ) : ( + + )} +
+
+ + ); + })} + +
+
+ ); + })} +
+ ) : ( +
+ {aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? ( +
+

No changes were made, but the AI provided some warnings:

+
    + {aiValidationDetails.warnings.map((warning, i) => ( +
  • {warning}
  • + ))} +
+
+ ) : ( +

No changes or warnings were suggested by the AI.

+ )} +
+ )} +
+
+
+ + ); +}; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/BaseCellContent.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/BaseCellContent.tsx new file mode 100644 index 0000000..27593af --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/BaseCellContent.tsx @@ -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 ( + + ); + } + + return null; +}; + +export default BaseCellContent; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx new file mode 100644 index 0000000..6507b3e --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/SearchableTemplateSelect.tsx @@ -0,0 +1,328 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react' +import { Template } from '../hooks/validationTypes' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { ScrollArea } from '@/components/ui/scroll-area' +import { cn } from '@/lib/utils' +import { Check, ChevronsUpDown } from 'lucide-react' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +interface SearchableTemplateSelectProps { + templates: Template[] | undefined; + value: string; + onValueChange: (value: string) => void; + getTemplateDisplayText: (templateId: string | null) => string; + placeholder?: string; + className?: string; + triggerClassName?: string; + defaultBrand?: string; +} + +const SearchableTemplateSelect: React.FC = ({ + templates = [], + value, + onValueChange, + getTemplateDisplayText, + placeholder = "Select template", + className, + triggerClassName, + defaultBrand, +}) => { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedBrand, setSelectedBrand] = useState(null); + const [open, setOpen] = useState(false); + + // Set default brand when component mounts or defaultBrand changes + useEffect(() => { + if (defaultBrand) { + setSelectedBrand(defaultBrand); + } + }, [defaultBrand]); + + // Force a re-render when templates change from empty to non-empty + useEffect(() => { + if (templates && templates.length > 0) { + // Force a re-render by updating state + setSearchTerm(""); + } + }, [templates]); + + // Handle wheel events for scrolling + const handleWheel = (e: React.WheelEvent) => { + const scrollArea = e.currentTarget; + scrollArea.scrollTop += e.deltaY; + }; + + // Extract unique brands from templates + const brands = useMemo(() => { + try { + if (!Array.isArray(templates) || templates.length === 0) { + return []; + } + + const brandSet = new Set(); + const brandNames: {id: string, name: string}[] = []; + + templates.forEach(template => { + if (!template?.company) return; + + const companyId = template.company; + if (!brandSet.has(companyId)) { + brandSet.add(companyId); + + // Try to get the company name from the template display text + try { + const displayText = getTemplateDisplayText(template.id.toString()); + const companyName = displayText.split(' - ')[0]; + brandNames.push({ id: companyId, name: companyName || companyId }); + } catch (err) { + brandNames.push({ id: companyId, name: companyId }); + } + } + }); + + return brandNames.sort((a, b) => a.name.localeCompare(b.name)); + } catch (err) { + return []; + } + }, [templates, getTemplateDisplayText]); + + // Group templates by company for better organization + const groupedTemplates = useMemo(() => { + try { + if (!Array.isArray(templates) || templates.length === 0) return {}; + + const groups: Record = {}; + + templates.forEach(template => { + if (!template?.company) return; + + const companyId = template.company; + if (!groups[companyId]) { + groups[companyId] = []; + } + groups[companyId].push(template); + }); + + return groups; + } catch (err) { + return {}; + } + }, [templates]); + + // Filter templates based on selected brand and search term + const filteredTemplates = useMemo(() => { + try { + if (!Array.isArray(templates) || templates.length === 0) return []; + + // First filter by brand if selected + let brandFiltered = templates; + if (selectedBrand) { + // Check if the selected brand has any templates + const brandTemplates = templates.filter(t => t?.company === selectedBrand); + + // If the selected brand has templates, use them; otherwise, show all templates + brandFiltered = brandTemplates.length > 0 ? brandTemplates : templates; + } + + // Then filter by search term if provided + if (!searchTerm.trim()) return brandFiltered; + + const lowerSearchTerm = searchTerm.toLowerCase(); + return brandFiltered.filter(template => { + if (!template?.id) return false; + try { + const displayText = getTemplateDisplayText(template.id.toString()); + const productType = template.product_type?.toLowerCase() || ''; + + return displayText.toLowerCase().includes(lowerSearchTerm) || + productType.includes(lowerSearchTerm); + } catch (error) { + return false; + } + }); + } catch (err) { + return []; + } + }, [templates, selectedBrand, searchTerm, getTemplateDisplayText]); + + // Handle errors gracefully + const getDisplayText = useCallback(() => { + try { + if (!value) return placeholder; + const template = templates.find(t => t.id.toString() === value); + if (!template) return placeholder; + + // Get the original display text + const originalText = getTemplateDisplayText(value); + + // Check if it has the expected format "Brand - Product Type" + if (originalText.includes(' - ')) { + const [brand, productType] = originalText.split(' - ', 2); + // Reverse the order to "Product Type - Brand" + return `${productType} - ${brand}`; + } + + // If it doesn't match the expected format, return the original text + return originalText; + } catch (err) { + console.error('Error getting display text:', err); + return placeholder; + } + }, [getTemplateDisplayText, placeholder, value, templates]); + + // Safe render function for CommandItem + const renderCommandItem = useCallback((template: Template) => { + if (!template?.id) return null; + + try { + const displayText = getTemplateDisplayText(template.id.toString()); + + return ( + { + try { + onValueChange(currentValue); + setOpen(false); + setSearchTerm(""); + } catch (err) { + console.error('Error selecting template:', err); + } + }} + className="flex items-center justify-between" + > + {displayText} + {value === template.id.toString() && } + + ); + } catch (err) { + console.error('Error rendering template item:', err); + return null; + } + }, [onValueChange, value, getTemplateDisplayText]); + + return ( + + + + + + +
+ {brands.length > 0 && ( +
+ +
+ )} + + + +
+ +
+
+ + +
+

No templates found.

+
+
+ + + + {!searchTerm ? ( + selectedBrand ? ( + groupedTemplates[selectedBrand]?.length > 0 ? ( + b.id === selectedBrand)?.name || selectedBrand}> + {groupedTemplates[selectedBrand]?.map(template => renderCommandItem(template))} + + ) : ( + // If selected brand has no templates, show all brands + Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => { + const brand = brands.find(b => b.id === companyId); + const companyName = brand ? brand.name : companyId; + + return ( + + {companyTemplates.map(template => renderCommandItem(template))} + + ); + }) + ) + ) : ( + Object.entries(groupedTemplates).map(([companyId, companyTemplates]) => { + const brand = brands.find(b => b.id === companyId); + const companyName = brand ? brand.name : companyId; + + return ( + + {companyTemplates.map(template => renderCommandItem(template))} + + ); + }) + ) + ) : ( + + {filteredTemplates.map(template => renderCommandItem(template))} + + )} + + +
+
+
+ ); +}; + +export default SearchableTemplateSelect; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx new file mode 100644 index 0000000..80980c4 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/UpcValidationTableAdapter.tsx @@ -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 { + data: any[] + fields: Fields + validationErrors: Map> + rowSelection: RowSelectionState + setRowSelection: React.Dispatch> + 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 + isLoadingTemplates: boolean + rowProductLines: Record + rowSublines: Record + isLoadingLines: Record + isLoadingSublines: Record + upcValidation: { + validatingRows: Set + getItemNumber: (rowIndex: number) => string | undefined + } + itemNumbers?: Map +} + +/** + * 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({ + data, + fields, + validationErrors, + rowSelection, + setRowSelection, + updateRow, + filters, + templates, + applyTemplate, + getTemplateDisplayText, + isValidatingUpc, + validatingUpcRows, + copyDown, + validatingCells: externalValidatingCells, + isLoadingTemplates, + rowProductLines, + rowSublines, + isLoadingLines, + isLoadingSublines, + upcValidation, + itemNumbers +}: UpcValidationTableAdapterProps) { + // Prepare the validation table with UPC data + + // Create combined validatingCells set from validating rows and external cells + const combinedValidatingCells = useMemo(() => { + const combined = new Set(); + + // 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(); + + // 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(); + + // 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 ( + 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 \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationCell.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationCell.tsx new file mode 100644 index 0000000..0c5fd91 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationCell.tsx @@ -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 }) => ( + + + +
+ +
+
+ +

{error.message}

+
+
+
+)); + +ValidationIcon.displayName = 'ValidationIcon'; + +// Memoized base cell content component +const BaseCellContent = React.memo(({ + field, + value, + onChange, + hasErrors, + options = [], + className = '', + fieldKey = '' +}: { + field: Field; + 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 ( + + ); + } + + if (fieldType === 'select') { + return ( + + ); + } + + if (fieldType === 'multi-select' || fieldType === 'multi-input') { + return ( + + ); + } + + return ( + + ); +}, (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 + 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 ( + setIsTargetRowHovered(true) : undefined} + onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined} + > +
+ {shouldShowErrorIcon && !isInTargetRow && ( +
+ +
+ )} + {!shouldShowErrorIcon && copyDown && !isEmpty(displayValue) && !copyDownContext.isInCopyDownMode && ( +
+ + + + + + +
+

Copy value to rows below

+
+
+
+
+
+ )} + {isSourceCell && ( +
+ + + + + + +

Cancel copy down

+
+
+
+
+ )} + {isLoading ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ ); +}, (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; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx new file mode 100644 index 0000000..8749643 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -0,0 +1,1228 @@ +import React, { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react' +import { useValidationState } from '../hooks/useValidationState' +import { Props } from '../hooks/validationTypes' +import { Button } from '@/components/ui/button' +import { Loader2, X, Plus, Edit3, Sparkles, FileText } from 'lucide-react' +import { toast } from 'sonner' +import { Switch } from '@/components/ui/switch' +import { useRsi } from '../../../hooks/useRsi' +import SearchableTemplateSelect from './SearchableTemplateSelect' +import { useAiValidation } from '../hooks/useAiValidation' +import { AiValidationDialogs } from './AiValidationDialogs' +import { Fields } from '../../../types' +import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog' +import { TemplateForm } from '@/components/templates/TemplateForm' +import axios from 'axios' +import { RowSelectionState } from '@tanstack/react-table' +import { useUpcValidation } from '../hooks/useUpcValidation' +import { useProductLinesFetching } from '../hooks/useProductLinesFetching' +import UpcValidationTableAdapter from './UpcValidationTableAdapter' +import { Skeleton } from '@/components/ui/skeleton' +/** + * ValidationContainer component - the main wrapper for the validation step + * + * This component is responsible for: + * - Managing global state using hooks + * - Coordinating between subcomponents + * - Handling navigation events (next, back) + */ +const ValidationContainer = ({ + initialData, + file, + onBack, + onNext, + isFromScratch +}: Props) => { + // Use our main validation state hook + const validationState = useValidationState({ + initialData, + file, + onBack, + onNext, + isFromScratch + }) + + const { + data, + filteredData, + validationErrors, + rowSelection, + setRowSelection, + templates, + selectedTemplateId, + applyTemplate, + applyTemplateToSelected, + getTemplateDisplayText, + filters, + updateFilters, + loadTemplates, + setData, + fields, + isLoadingTemplates, + validatingCells, + setValidatingCells + } = validationState + + // Use product lines fetching hook + const { + rowProductLines, + rowSublines, + isLoadingLines, + isLoadingSublines, + fetchProductLines, + fetchSublines + } = useProductLinesFetching(data); + + // Use UPC validation hook + const upcValidation = useUpcValidation(data, setData); + + // Function to check if a specific row is being validated - memoized + const isRowValidatingUpc = upcValidation.isRowValidatingUpc; + + // Apply all pending updates to the data state + + // Use AI validation hook + const aiValidation = useAiValidation( + data, + setData, + fields as Fields, + // Create a wrapper function that adapts the rowHook to the expected signature + validationState.rowHook ? + async (row) => { + // Call the original rowHook and return the row itself instead of just Meta + await validationState.rowHook(row, 0, data); + return row; + } : + undefined, + // Create a wrapper function that adapts the tableHook to the expected signature + validationState.tableHook ? + async (rows) => { + // Call the original tableHook and return the rows themselves + await validationState.tableHook(rows); + return rows; + } : + undefined + ); + + const { translations } = useRsi() + + // State for product search dialog + const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false) + + // Add new state for template form dialog + const [isTemplateFormOpen, setIsTemplateFormOpen] = useState(false) + const [templateFormInitialData, setTemplateFormInitialData] = useState(null) + const [fieldOptions, setFieldOptions] = useState(null) + + // Track fields that need revalidation due to value changes + const [fieldsToRevalidate, setFieldsToRevalidate] = useState>(new Set()); + const [fieldsToRevalidateMap, setFieldsToRevalidateMap] = useState<{[rowIndex: number]: string[]}>({}); + + // Function to mark a row for revalidation + const markRowForRevalidation = useCallback((rowIndex: number, fieldKey?: string) => { + setFieldsToRevalidate(prev => { + const newSet = new Set(prev); + newSet.add(rowIndex); + return newSet; + }); + + // Also track which specific field needs to be revalidated + if (fieldKey) { + setFieldsToRevalidateMap(prev => { + const newMap = { ...prev }; + if (!newMap[rowIndex]) { + newMap[rowIndex] = []; + } + if (!newMap[rowIndex].includes(fieldKey)) { + newMap[rowIndex] = [...newMap[rowIndex], fieldKey]; + } + return newMap; + }); + } + }, []); + + // Add a ref to track the last validation time + + // Trigger revalidation only for specifically marked fields + useEffect(() => { + if (fieldsToRevalidate.size === 0) return; + + // Revalidate the marked rows + const rowsToRevalidate = Array.from(fieldsToRevalidate); + + // Clear the revalidation set + setFieldsToRevalidate(new Set()); + + // Get the fields map for revalidation + const fieldsMap = { ...fieldsToRevalidateMap }; + + // Clear the fields map + setFieldsToRevalidateMap({}); + + console.log(`Validating ${rowsToRevalidate.length} rows with specific fields`); + + // Revalidate each row with specific fields information + validationState.revalidateRows(rowsToRevalidate, fieldsMap); + }, [fieldsToRevalidate, validationState, fieldsToRevalidateMap]); + + // Function to fetch field options for template form + const fetchFieldOptions = useCallback(async () => { + try { + const response = await axios.get('/api/import/field-options'); + console.log('Field options from API:', response.data); + + // Check if suppliers are included in the response + if (response.data && response.data.suppliers) { + console.log('Suppliers available:', response.data.suppliers.length); + } else { + console.warn('No suppliers found in field options response'); + } + + setFieldOptions(response.data); + return response.data; + } catch (error) { + console.error('Error fetching field options:', error); + toast.error('Failed to load field options'); + return null; + } + }, []); + + // Function to prepare row data for the template form + const prepareRowDataForTemplateForm = useCallback(() => { + // Get the selected row key (should be only one) + const selectedKey = Object.entries(rowSelection) + .filter(([_, selected]) => selected === true) + .map(([key, _]) => key)[0]; + + if (!selectedKey) return null; + + // Try to find the row in the data array + let selectedRow; + + // First check if the key is an index in filteredData + const numericIndex = parseInt(selectedKey); + if (!isNaN(numericIndex) && numericIndex >= 0 && numericIndex < filteredData.length) { + selectedRow = filteredData[numericIndex]; + } + + // If not found by index, try to find it by __index property + if (!selectedRow) { + selectedRow = data.find(row => row.__index === selectedKey); + } + + // If still not found, return null + if (!selectedRow) { + console.error('Selected row not found:', selectedKey); + return null; + } + + // TemplateForm expects supplier as a NUMBER - the field options have numeric values + // Convert the supplier to a number if possible + let supplierValue; + if (selectedRow.supplier) { + const numSupplier = Number(selectedRow.supplier); + supplierValue = !isNaN(numSupplier) ? numSupplier : selectedRow.supplier; + } else { + supplierValue = undefined; + } + + // Create template form data with the correctly typed supplier value + return { + company: selectedRow.company || '', + product_type: selectedRow.product_type || '', + supplier: supplierValue, + msrp: selectedRow.msrp ? Number(Number(selectedRow.msrp).toFixed(2)) : undefined, + cost_each: selectedRow.cost_each ? Number(Number(selectedRow.cost_each).toFixed(2)) : undefined, + qty_per_unit: selectedRow.qty_per_unit ? Number(selectedRow.qty_per_unit) : undefined, + case_qty: selectedRow.case_qty ? Number(selectedRow.case_qty) : undefined, + hts_code: selectedRow.hts_code || undefined, + description: selectedRow.description || undefined, + weight: selectedRow.weight ? Number(Number(selectedRow.weight).toFixed(2)) : undefined, + length: selectedRow.length ? Number(Number(selectedRow.length).toFixed(2)) : undefined, + width: selectedRow.width ? Number(Number(selectedRow.width).toFixed(2)) : undefined, + height: selectedRow.height ? Number(Number(selectedRow.height).toFixed(2)) : undefined, + tax_cat: selectedRow.tax_cat ? String(selectedRow.tax_cat) : undefined, + size_cat: selectedRow.size_cat ? String(selectedRow.size_cat) : undefined, + categories: Array.isArray(selectedRow.categories) ? selectedRow.categories : + (selectedRow.categories ? [selectedRow.categories] : []), + ship_restrictions: selectedRow.ship_restrictions ? String(selectedRow.ship_restrictions) : undefined + }; + }, [data, filteredData, rowSelection]); + + // Add useEffect to fetch field options when template form opens + useEffect(() => { + if (isTemplateFormOpen && !fieldOptions) { + fetchFieldOptions(); + } + }, [isTemplateFormOpen, fieldOptions, fetchFieldOptions]); + + // Function to handle opening the template form + const openTemplateForm = useCallback(async () => { + const templateData = prepareRowDataForTemplateForm(); + if (!templateData) return; + + setTemplateFormInitialData(templateData); + + // Always fetch fresh field options to ensure supplier list is up to date + try { + const options = await fetchFieldOptions(); + if (options && options.suppliers) { + console.log(`Loaded ${options.suppliers.length} suppliers for template form`); + + // Log if we can find a match for our supplier + if (templateData.supplier !== undefined) { + // Need to compare numeric values since supplier options have numeric values + const supplierMatch = options.suppliers.find((s: { value: string | number }) => + s.value === templateData.supplier || + Number(s.value) === Number(templateData.supplier) + ); + + console.log('Found supplier match:', supplierMatch ? 'Yes' : 'No', + 'For supplier value:', templateData.supplier, + 'Type:', typeof templateData.supplier); + + if (supplierMatch) { + console.log('Matched supplier:', supplierMatch.label); + } + } + + setIsTemplateFormOpen(true); + } else { + console.error('Failed to load suppliers for template form'); + toast.error('Could not load supplier options'); + } + } catch (error) { + console.error('Error loading field options:', error); + toast.error('Failed to prepare template form'); + } + }, [prepareRowDataForTemplateForm, fetchFieldOptions]); + + // Create a function to validate uniqueness if validateUniqueItemNumbers is not available + + // Apply item numbers to data and trigger revalidation for uniqueness + + // Handle next button click - memoized + const handleNext = useCallback(() => { + // Make sure any pending item numbers are applied + upcValidation.applyItemNumbersToData(updatedRowIds => { + // Mark updated rows for revalidation + updatedRowIds.forEach(rowIndex => { + markRowForRevalidation(rowIndex, 'item_number'); + }); + + // Small delay to ensure all validations complete before proceeding + setTimeout(() => { + // Call the onNext callback with the validated data + onNext?.(data); + }, 100); + }); + + // If no item numbers to apply, just proceed + if (upcValidation.validatingRows.size === 0) { + onNext?.(data); + } + }, [onNext, data, upcValidation, markRowForRevalidation]); + + const deleteSelectedRows = useCallback(() => { + // 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 for deletion:', selectedKeys); + + if (selectedKeys.length === 0) { + toast.error("No rows selected"); + return; + } + + // Map UUID keys to array indices + const selectedIndices = 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 for deletion:', selectedIndices); + + if (selectedIndices.length === 0) { + toast.error('Could not find selected rows'); + return; + } + + // Sort indices in descending order to avoid index shifting during removal + const sortedIndices = [...selectedIndices].sort((a, b) => b - a); + + // Create a new array without the selected rows + const newData = [...data]; + + // Remove rows from bottom up to avoid index issues + sortedIndices.forEach(index => { + if (index >= 0 && index < newData.length) { + newData.splice(index, 1); + } + }); + + // Update the data with rows removed + setData(newData); + + // Clear row selection + setRowSelection({}); + + // Show success message + toast.success( + selectedIndices.length === 1 + ? "Row deleted" + : `${selectedIndices.length} rows deleted` + ); + + // Reindex the data in the next render cycle + requestAnimationFrame(() => { + // Update indices to maintain consistency + setData(current => + current.map((row, newIndex) => ({ + ...row, + __index: String(newIndex) + })) + ); + }); + }, [data, rowSelection, setData, setRowSelection]); + + // Memoize handlers + // This function is defined for potential future use but not currently used + // eslint-disable-next-line @typescript-eslint/no-unused-vars + + const handleRowSelectionChange = useCallback( + (value: React.SetStateAction) => { + setRowSelection(value); + }, + [setRowSelection] + ); + + // Add scroll container ref at the container level + const scrollContainerRef = useRef(null); + const lastScrollPosition = useRef({ left: 0, top: 0 }); + + // Track if we're currently validating a UPC + + // Track last UPC update to prevent conflicting changes + + // Add these ref declarations here, at component level + + // Memoize scroll handlers - simplified to avoid performance issues + const handleScroll = useCallback((event: React.UIEvent | Event) => { + // Store scroll position directly without conditions + const target = event.currentTarget as HTMLDivElement; + lastScrollPosition.current = { + left: target.scrollLeft, + top: target.scrollTop + }; + }, []); + + // Add scroll event listener + useEffect(() => { + const container = scrollContainerRef.current; + if (container) { + // Convert React event handler to native event handler + const nativeHandler = ((evt: Event) => { + handleScroll(evt); + }) as EventListener; + + container.addEventListener('scroll', nativeHandler, { passive: true }); + return () => container.removeEventListener('scroll', nativeHandler); + } + }, [handleScroll]); + + // Use a ref to track if we need to restore scroll position + const needScrollRestore = useRef(false); + + // Set flag when data changes + useEffect(() => { + needScrollRestore.current = true; + // Only restore scroll on layout effects to avoid triggering rerenders + }, []); + + // Use layout effect for DOM manipulations + useLayoutEffect(() => { + if (!needScrollRestore.current) return; + + const container = scrollContainerRef.current; + if (container && (lastScrollPosition.current.left > 0 || lastScrollPosition.current.top > 0)) { + container.scrollLeft = lastScrollPosition.current.left; + container.scrollTop = lastScrollPosition.current.top; + needScrollRestore.current = false; + } + }, []); + + // Ensure manual edits to item numbers persist with minimal changes to validation logic + const handleUpdateRow = useCallback(async (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 in the data + const rowData = filteredData[rowIndex]; + if (!rowData) { + console.error(`No row data found for index ${rowIndex}`); + return; + } + + // Use __index to find the actual row in the full data array + const rowId = rowData.__index; + const originalIndex = data.findIndex(item => item.__index === rowId); + + // Detect if this is a direct item_number edit + const isItemNumberEdit = key === 'item_number' as T; + + // For item_number edits, we need special handling to ensure they persist + if (isItemNumberEdit) { + console.log(`Manual edit to item_number: ${value}`); + + // First, update data immediately to ensure edit takes effect + setData(prevData => { + const newData = [...prevData]; + if (originalIndex >= 0 && originalIndex < newData.length) { + newData[originalIndex] = { + ...newData[originalIndex], + [key]: processedValue + }; + } + return newData; + }); + + // Mark for revalidation after a delay to ensure data update completes first + setTimeout(() => { + markRowForRevalidation(rowIndex, key as string); + }, 0); + + // Return early to prevent double-updating + return; + } + + // For all other fields, use standard approach + // Always use setData for updating - immediate update for better UX + const updatedRow = { ...rowData, [key]: processedValue }; + + // Mark this row for revalidation to clear any existing errors + markRowForRevalidation(rowIndex, key as string); + + // Update the data immediately to show the change + setData(prevData => { + const newData = [...prevData]; + if (originalIndex >= 0 && originalIndex < newData.length) { + // Create a new row object with the updated field + newData[originalIndex] = { + ...newData[originalIndex], + [key]: processedValue + }; + } + return newData; + }); + + // Secondary effects - using a timeout to ensure UI updates first + setTimeout(() => { + // Handle company change - clear line/subline and fetch product lines + if (key === 'company' && value) { + console.log(`Company changed to ${value} for row ${rowIndex}, updating lines and sublines`); + + // Clear any existing line/subline values immediately + setData(prevData => { + const newData = [...prevData]; + const idx = newData.findIndex(item => item.__index === rowId); + if (idx >= 0) { + console.log(`Clearing line and subline values for row with ID ${rowId}`); + newData[idx] = { + ...newData[idx], + line: undefined, + subline: undefined + }; + } else { + console.warn(`Could not find row with ID ${rowId} to clear line/subline values`); + } + return newData; + }); + + // Fetch product lines for the new company + if (rowId && value !== undefined) { + const companyId = value.toString(); + + // Force immediate fetch for better UX + console.log(`Immediately fetching product lines for company ${companyId} for row ${rowId}`); + + // Set loading state first + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.add(`${rowIndex}-line`); + return newSet; + }); + + fetchProductLines(rowId, companyId) + .then(lines => { + console.log(`Successfully loaded ${lines.length} product lines for company ${companyId}`); + }) + .catch(err => { + console.error(`Error fetching product lines for company ${companyId}:`, err); + toast.error("Failed to load product lines"); + }) + .finally(() => { + // Clear loading indicator + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.delete(`${rowIndex}-line`); + return newSet; + }); + }); + } + } + + // Handle supplier + UPC validation - using the most recent values + if (key === 'supplier' && value) { + // Get the latest UPC value from the updated row + const upcValue = updatedRow.upc || updatedRow.barcode; + + if (upcValue) { + console.log(`Validating UPC: rowIndex=${rowIndex}, supplier=${value}, upc=${upcValue}`); + + // Mark the item_number cell as being validated + const cellKey = `${rowIndex}-item_number`; + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.add(cellKey); + return newSet; + }); + + // Use a regular promise-based approach instead of await + upcValidation.validateUpc(rowIndex, value.toString(), upcValue.toString()) + .then(result => { + if (result.success) { + console.log(`UPC validation successful for row ${rowIndex}`); + upcValidation.applyItemNumbersToData(); + + // Mark for revalidation after item numbers are updated + setTimeout(() => { + markRowForRevalidation(rowIndex, 'item_number'); + }, 50); + } + }) + .catch(err => { + console.error("Error validating UPC:", err); + }) + .finally(() => { + // Clear validation state for the item_number cell + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.delete(cellKey); + return newSet; + }); + }); + } + } + + // Handle line change - clear subline and fetch sublines + if (key === 'line' && value) { + console.log(`Line changed to ${value} for row ${rowIndex}, updating sublines`); + + // Clear any existing subline value + setData(prevData => { + const newData = [...prevData]; + const idx = newData.findIndex(item => item.__index === rowId); + if (idx >= 0) { + console.log(`Clearing subline values for row with ID ${rowId}`); + newData[idx] = { + ...newData[idx], + subline: undefined + }; + } else { + console.warn(`Could not find row with ID ${rowId} to clear subline values`); + } + return newData; + }); + + // Fetch sublines for the new line + if (rowId && value !== undefined) { + const lineId = value.toString(); + + // Force immediate fetch for better UX + console.log(`Immediately fetching sublines for line ${lineId} for row ${rowId}`); + + // Set loading state first + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.add(`${rowIndex}-subline`); + return newSet; + }); + + fetchSublines(rowId, lineId) + .then(sublines => { + console.log(`Successfully loaded ${sublines.length} sublines for line ${lineId}`); + }) + .catch(err => { + console.error(`Error fetching sublines for line ${lineId}:`, err); + toast.error("Failed to load sublines"); + }) + .finally(() => { + // Clear loading indicator + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.delete(`${rowIndex}-subline`); + return newSet; + }); + }); + } + } + + // Add the UPC/barcode validation handler back: + // Handle UPC/barcode + supplier validation + if ((key === 'upc' || key === 'barcode') && value) { + // Get latest supplier from the updated row + const supplier = updatedRow.supplier; + + if (supplier) { + console.log(`Validating UPC from UPC change: rowIndex=${rowIndex}, supplier=${supplier}, upc=${value}`); + + // Mark the item_number cell as being validated + const cellKey = `${rowIndex}-item_number`; + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.add(cellKey); + return newSet; + }); + + // Use a regular promise-based approach + upcValidation.validateUpc(rowIndex, supplier.toString(), value.toString()) + .then(result => { + if (result.success) { + console.log(`UPC validation successful for row ${rowIndex}`); + upcValidation.applyItemNumbersToData(); + + // Mark for revalidation after item numbers are updated + setTimeout(() => { + markRowForRevalidation(rowIndex, 'item_number'); + }, 50); + } + }) + .catch(err => { + console.error("Error validating UPC:", err); + }) + .finally(() => { + // Clear validation state for the item_number cell + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.delete(cellKey); + return newSet; + }); + }); + } + } + }, 0); // Using 0ms timeout to defer execution until after the UI update + }, [data, filteredData, setData, fetchProductLines, fetchSublines, upcValidation, markRowForRevalidation]); + + // Fix the missing loading indicator clear code + const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => { + // Get the value to copy from the source row + const sourceRow = data[rowIndex]; + if (!sourceRow) { + console.error(`Source row ${rowIndex} not found for copyDown`); + return; + } + + const valueToCopy = sourceRow[fieldKey]; + + // Create a proper copy of the value to avoid reference issues, especially for arrays (MultiSelectCell) + const valueCopy = Array.isArray(valueToCopy) ? [...valueToCopy] : valueToCopy; + + // Get all rows below the source row, up to endRowIndex if specified + const lastRowIndex = endRowIndex !== undefined ? Math.min(endRowIndex, data.length - 1) : data.length - 1; + const rowsToUpdate = Array.from({ length: lastRowIndex - rowIndex }, (_, i) => rowIndex + i + 1); + + // Mark all cells as updating at once + const updatingCells = new Set(); + rowsToUpdate.forEach(targetRowIndex => { + updatingCells.add(`${targetRowIndex}-${fieldKey}`); + }); + + setValidatingCells(prev => { + const newSet = new Set(prev); + updatingCells.forEach(cell => newSet.add(cell)); + return newSet; + }); + + // Update all rows at once efficiently with a single state update + setData(prevData => { + // Create a new copy of the data + const newData = [...prevData]; + + // Update all rows at once + rowsToUpdate.forEach(targetRowIndex => { + // Find the original row using __index + const rowData = filteredData[targetRowIndex]; + if (!rowData) return; + + const rowId = rowData.__index; + const originalIndex = newData.findIndex(item => item.__index === rowId); + + if (originalIndex !== -1) { + // Update the specific field on this row + newData[originalIndex] = { + ...newData[originalIndex], + [fieldKey]: valueCopy + }; + } else { + // Fall back to direct index if __index not found + if (targetRowIndex < newData.length) { + newData[targetRowIndex] = { + ...newData[targetRowIndex], + [fieldKey]: valueCopy + }; + } + } + }); + + return newData; + }); + + // Mark rows for revalidation + rowsToUpdate.forEach(targetRowIndex => { + markRowForRevalidation(targetRowIndex, fieldKey); + }); + + // Clear the loading state for all cells after a short delay + setTimeout(() => { + setValidatingCells(prev => { + if (prev.size === 0) return prev; + const newSet = new Set(prev); + updatingCells.forEach(cell => newSet.delete(cell)); + return newSet; + }); + }, 100); + + // If copying UPC or supplier fields, validate UPC for all rows + if (fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'supplier') { + // Process each row in parallel + const validationsToRun: {rowIndex: number, supplier: string, upc: string}[] = []; + + // Process each row separately to collect validation tasks + rowsToUpdate.forEach(targetRowIndex => { + const rowData = filteredData[targetRowIndex]; + if (!rowData) return; + + // Only validate if both UPC and supplier are present after the update + const updatedRow = { + ...rowData, + [fieldKey]: valueCopy + }; + + const hasUpc = updatedRow.upc || updatedRow.barcode; + const hasSupplier = updatedRow.supplier; + + if (hasUpc && hasSupplier) { + const upcValue = updatedRow.upc || updatedRow.barcode; + const supplierId = updatedRow.supplier; + + // Queue this validation if both values are defined + if (supplierId !== undefined && upcValue !== undefined) { + validationsToRun.push({ + rowIndex: targetRowIndex, + supplier: supplierId.toString(), + upc: upcValue.toString() + }); + } + } + }); + + // Run validations in parallel but limit the batch size + if (validationsToRun.length > 0) { + console.log(`Running ${validationsToRun.length} UPC validations for copyDown`); + + // Mark all cells as validating + validationsToRun.forEach(({ rowIndex }) => { + const cellKey = `${rowIndex}-item_number`; + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.add(cellKey); + return newSet; + }); + }); + + // Process in smaller batches to avoid overwhelming the system + const BATCH_SIZE = 5; // Process 5 validations at a time + const processBatch = (startIdx: number) => { + const endIdx = Math.min(startIdx + BATCH_SIZE, validationsToRun.length); + const batch = validationsToRun.slice(startIdx, endIdx); + + Promise.all( + batch.map(({ rowIndex, supplier, upc }) => + upcValidation.validateUpc(rowIndex, supplier, upc) + .then(result => { + if (result.success) { + // Apply immediately for better UX + if (startIdx + BATCH_SIZE >= validationsToRun.length) { + // Apply all updates at the end with callback to mark for revalidation + upcValidation.applyItemNumbersToData(updatedRowIds => { + // Mark these rows for revalidation after a delay + setTimeout(() => { + updatedRowIds.forEach(rowIdx => { + markRowForRevalidation(rowIdx, 'item_number'); + }); + }, 100); + }); + } + } + return { rowIndex, success: result.success }; + }) + .catch(err => { + console.error(`Error validating UPC for row ${rowIndex}:`, err); + return { rowIndex, success: false }; + }) + .finally(() => { + // Clear validation state for this cell + const cellKey = `${rowIndex}-item_number`; + setValidatingCells(prev => { + if (!prev.has(cellKey)) return prev; + const newSet = new Set(prev); + newSet.delete(cellKey); + return newSet; + }); + }) + ) + ).then(() => { + // If there are more validations to run, process the next batch + if (endIdx < validationsToRun.length) { + // Add a small delay between batches to prevent UI freezing + setTimeout(() => processBatch(endIdx), 100); + } else { + console.log(`Completed all ${validationsToRun.length} UPC validations`); + // Final application of all item numbers if not done by individual batches + upcValidation.applyItemNumbersToData(updatedRowIds => { + // Mark these rows for revalidation after a delay + setTimeout(() => { + updatedRowIds.forEach(rowIdx => { + markRowForRevalidation(rowIdx, 'item_number'); + }); + }, 100); + }); + } + }); + }; + + // Start processing the first batch + processBatch(0); + } + } + }, [data, filteredData, setData, setValidatingCells, upcValidation, markRowForRevalidation]); + + // Memoize the rendered validation table + const renderValidationTable = useMemo(() => { + // Create wrapper for applyTemplate that matches the expected interface + const applyTemplateWrapper = (templateId: string, rowIndexes: number[]) => { + if (rowIndexes.length === 1) { + // Single row apply - pass the array with a single index + applyTemplate(templateId, rowIndexes); + } else if (rowIndexes.length > 1) { + // Multiple rows - use applyTemplateToSelected + applyTemplateToSelected(templateId); + } + }; + + return ( + } + validationErrors={validationErrors} + rowSelection={rowSelection} + setRowSelection={handleRowSelectionChange} + updateRow={handleUpdateRow} + filters={filters} + templates={templates} + applyTemplate={applyTemplateWrapper} + getTemplateDisplayText={getTemplateDisplayText} + isValidatingUpc={isRowValidatingUpc} + validatingUpcRows={Array.from(upcValidation.validatingRows)} + copyDown={handleCopyDown} + validatingCells={validatingCells} + isLoadingTemplates={isLoadingTemplates} + rowProductLines={rowProductLines} + rowSublines={rowSublines} + isLoadingLines={isLoadingLines} + isLoadingSublines={isLoadingSublines} + upcValidation={upcValidation} + itemNumbers={upcValidation.itemNumbers} + /> + ); + }, [ + filteredData, + fields, + validationErrors, + rowSelection, + handleRowSelectionChange, + handleUpdateRow, + filters, + templates, + applyTemplate, + applyTemplateToSelected, + getTemplateDisplayText, + isRowValidatingUpc, + upcValidation, + handleCopyDown, + validatingCells, + isLoadingTemplates, + rowProductLines, + rowSublines, + isLoadingLines, + isLoadingSublines + ]); + + return ( +
+
+
+
+
+ {/* Header section */} +
+
+
+

+ {translations.validationStep.title || "Validate Data"} +

+ +
+ {isFromScratch && ( + + )} + +
+ updateFilters({ showErrorsOnly: checked })} + id="filter-errors" + /> + +
+
+
+
+
+ + {/* Main table section */} +
+
+
+
+ {renderValidationTable} +
+
+
+
+
+
+ + {/* Selection Action Bar - only shown when items are selected */} + {Object.keys(rowSelection).length > 0 && ( +
+
+
+
+ {Object.keys(rowSelection).length} selected +
+ + +
+ +
+ {isLoadingTemplates ? ( + + ) : templates && templates.length > 0 ? ( + { + if (value) { + applyTemplateToSelected(value); + } + }} + getTemplateDisplayText={getTemplateDisplayText} + placeholder="Apply template to selected rows" + triggerClassName="w-[250px] text-xs h-8" + /> + ) : ( + + )} +
+ + {Object.keys(rowSelection).length === 1 && ( + + )} + + +
+
+ )} +
+
+ + {/* Footer with navigation buttons */} +
+
+ {onBack && ( + + )} +
+ {/* Show Prompt Button */} + + + {/* AI Validate Button */} + + + +
+
+
+ + {/* AI Validation Dialogs */} + + + {/* Product Search Dialog */} + setIsProductSearchDialogOpen(false)} + onTemplateCreated={loadTemplates} + /> + + {/* Template Form Dialog */} + setIsTemplateFormOpen(false)} + onSuccess={() => { + loadTemplates(); + setIsTemplateFormOpen(false); + }} + initialData={templateFormInitialData} + mode="create" + fieldOptions={fieldOptions} + /> +
+ ) +} + +export default ValidationContainer \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx new file mode 100644 index 0000000..b82d1e0 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx @@ -0,0 +1,696 @@ +import React, { useMemo, useCallback, useState } from 'react' +import { + useReactTable, + getCoreRowModel, + flexRender, + RowSelectionState, + ColumnDef +} from '@tanstack/react-table' +import { Fields, Field } from '../../../types' +import { RowData, Template } from '../hooks/validationTypes' +import ValidationCell, { CopyDownContext } from './ValidationCell' +import { useRsi } from '../../../hooks/useRsi' +import SearchableTemplateSelect from './SearchableTemplateSelect' +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 { Skeleton } from '@/components/ui/skeleton' + +// Define a simple Error type locally to avoid import issues +type ErrorType = { + message: string; + level: string; + source?: string; +} + +interface ValidationTableProps { + data: RowData[] + fields: Fields + rowSelection: RowSelectionState + setRowSelection: React.Dispatch> + updateRow: (rowIndex: number, key: T, value: any) => void + validationErrors: Map> + isValidatingUpc: (rowIndex: number) => boolean + validatingUpcRows: number[] + filters?: { showErrorsOnly?: boolean } + templates: Template[] + applyTemplate: (templateId: string, rowIndexes: number[]) => void + getTemplateDisplayText: (templateId: string | null) => string + rowProductLines?: Record + rowSublines?: Record + isLoadingLines?: Record + isLoadingSublines?: Record + upcValidationResults: Map + validatingCells: Set + itemNumbers: Map + isLoadingTemplates?: boolean + copyDown: (rowIndex: number, key: string, endRowIndex?: number) => void + [key: string]: any +} + +// Create a memoized wrapper for template selects to prevent unnecessary re-renders +const MemoizedTemplateSelect = React.memo(({ + templates, + value, + onValueChange, + getTemplateDisplayText, + defaultBrand, + isLoading +}: { + templates: Template[], + value: string, + onValueChange: (value: string) => void, + getTemplateDisplayText: (value: string | null) => string, + defaultBrand?: string, + isLoading?: boolean +}) => { + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( + + ); +}, (prev, next) => { + return ( + prev.value === next.value && + prev.templates === next.templates && + prev.defaultBrand === next.defaultBrand && + prev.isLoading === next.isLoading + ); +}); + +MemoizedTemplateSelect.displayName = 'MemoizedTemplateSelect'; + +// Create a memoized cell component +const MemoizedCell = React.memo(({ + field, + value, + onChange, + errors, + isValidating, + fieldKey, + options, + itemNumber, + width, + rowIndex, + copyDown, + totalRows +}: { + field: Field, + value: any, + onChange: (value: any) => void, + errors: ErrorType[], + isValidating?: boolean, + fieldKey: string, + options?: readonly any[], + itemNumber?: string, + width: number, + rowIndex: number, + copyDown?: (endRowIndex?: number) => void, + totalRows: number +}) => { + return ( + + ); +}, (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 + 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'; + +const ValidationTable = ({ + data, + fields, + rowSelection, + setRowSelection, + updateRow, + validationErrors, + filters, + templates, + applyTemplate, + getTemplateDisplayText, + validatingCells, + itemNumbers, + isLoadingTemplates = false, + copyDown, + rowProductLines = {}, + rowSublines = {}, + isLoadingLines = {}, + isLoadingSublines = {}, + isValidatingUpc, + validatingUpcRows = [], + upcValidationResults +}: ValidationTableProps) => { + const { translations } = useRsi(); + + // Add state for copy down selection mode + const [isInCopyDownMode, setIsInCopyDownMode] = useState(false); + const [sourceRowIndex, setSourceRowIndex] = useState(null); + const [sourceFieldKey, setSourceFieldKey] = useState(null); + const [targetRowIndex, setTargetRowIndex] = useState(null); + + // Handle copy down completion + const handleCopyDownComplete = useCallback((sourceRowIndex: number, fieldKey: string, targetRowIndex: number) => { + // Call the copyDown function with the source row index, field key, and target row index + copyDown(sourceRowIndex, fieldKey, targetRowIndex); + + // Reset the copy down selection mode + setIsInCopyDownMode(false); + setSourceRowIndex(null); + setSourceFieldKey(null); + setTargetRowIndex(null); + }, [copyDown]); + + // Create copy down context value + const copyDownContextValue = useMemo(() => ({ + isInCopyDownMode, + sourceRowIndex, + sourceFieldKey, + targetRowIndex, + setIsInCopyDownMode, + setSourceRowIndex, + setSourceFieldKey, + setTargetRowIndex, + handleCopyDownComplete + }), [ + isInCopyDownMode, + sourceRowIndex, + sourceFieldKey, + targetRowIndex, + handleCopyDownComplete + ]); + + // Update targetRowIndex when hovering over rows in copy down mode + const handleRowMouseEnter = useCallback((rowIndex: number) => { + if (isInCopyDownMode && sourceRowIndex !== null && rowIndex > sourceRowIndex) { + setTargetRowIndex(rowIndex); + } + }, [isInCopyDownMode, sourceRowIndex]); + + // Memoize the selection column with stable callback + const handleSelectAll = useCallback((value: boolean, table: any) => { + table.toggleAllPageRowsSelected(!!value); + }, []); + + const handleRowSelect = useCallback((value: boolean, row: any) => { + row.toggleSelected(!!value); + }, []); + + const selectionColumn = useMemo((): ColumnDef, any> => ({ + id: 'select', + header: ({ table }) => ( +
+ handleSelectAll(!!value, table)} + aria-label="Select all" + /> +
+ ), + cell: ({ row }) => ( +
+ handleRowSelect(!!value, row)} + aria-label="Select row" + /> +
+ ), + enableSorting: false, + enableHiding: false, + size: 50, + }), [handleSelectAll, handleRowSelect]); + + // Memoize template selection handler + const handleTemplateChange = useCallback((value: string, rowIndex: number) => { + applyTemplate(value, [rowIndex]); + }, [applyTemplate]); + + // Memoize the template column with stable callback + const templateColumn = useMemo((): ColumnDef, any> => ({ + accessorKey: '__template', + header: 'Template', + size: 200, + cell: ({ row }) => { + const templateValue = row.original.__template || null; + const defaultBrand = row.original.company || undefined; + const rowIndex = data.findIndex(r => r === row.original); + + return ( + +
+ handleTemplateChange(value, rowIndex)} + getTemplateDisplayText={getTemplateDisplayText} + defaultBrand={defaultBrand} + isLoading={isLoadingTemplates} + /> +
+
+ ); + } + }), [templates, handleTemplateChange, getTemplateDisplayText, isLoadingTemplates, data]); + + // Cache options by field key to avoid recreating arrays + const optionsCache = useMemo(() => { + const cache = new Map(); + + fields.forEach((field) => { + // Get the field key + 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 || []); + } + }); + + return cache; + }, [fields]); + + // Memoize the field update handler + const handleFieldUpdate = useCallback((rowIndex: number, fieldKey: T, value: any) => { + updateRow(rowIndex, fieldKey, value); + }, [updateRow]); + + // Memoize the copyDown handler + const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => { + 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, any> | null => { + // Don't filter out disabled fields, just pass the disabled state to the cell component + + const fieldWidth = field.width || ( + field.fieldType.type === "checkbox" ? 80 : + field.fieldType.type === "select" ? 150 : + field.fieldType.type === "multi-select" ? 200 : + (field.fieldType.type === "input" || field.fieldType.type === "multi-input") && + (field.fieldType as any).multiline ? 300 : + 150 + ); + + const fieldKey = String(field.key); + // Get cached options for this field + const fieldOptions = optionsCache.get(fieldKey) || []; + + return { + accessorKey: fieldKey, + header: field.label || fieldKey, + size: fieldWidth, + cell: ({ row }) => { + // Get row-specific options for line and subline fields + let options = fieldOptions; + const rowId = row.original.__index; + + if (fieldKey === 'line' && rowId && rowProductLines[rowId]) { + options = rowProductLines[rowId]; + } else if (fieldKey === 'subline' && rowId && rowSublines[rowId]) { + options = rowSublines[rowId]; + } + + // 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 + else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) { + isLoading = true; + } + else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) { + isLoading = true; + } + + // Get validation errors for this cell + const cellErrors = validationErrors.get(row.index)?.[fieldKey] || []; + + // Create a copy of the field with guaranteed field type for line and subline fields + let fieldWithType = field; + + // Ensure line and subline fields always have the correct fieldType + if (fieldKey === 'line' || fieldKey === 'subline') { + // Create a deep clone of the field to prevent any reference issues + fieldWithType = { + ...JSON.parse(JSON.stringify(field)), // Ensure deep clone + fieldType: { + type: 'select', + options: options + }, + // Explicitly mark as not disabled to ensure dropdown works + disabled: false + }; + + } + + // CRITICAL CHANGE: Get item number directly from row data first for item_number fields + let itemNumber; + if (fieldKey === 'item_number') { + // Check directly in row data first - this is the most accurate source + const directValue = row.original[fieldKey]; + if (directValue) { + itemNumber = directValue; + } else { + // Fall back to centralized getter that checks all sources + itemNumber = getRowUpcResult(row.index); + } + } + + // CRITICAL: For item_number fields, create a unique key that includes the itemNumber value + // This forces a complete re-render when the itemNumber changes + const cellKey = fieldKey === 'item_number' + ? `cell-${row.index}-${fieldKey}-${itemNumber || 'empty'}-${Date.now()}` // Force re-render on every render cycle for item_number + : `cell-${row.index}-${fieldKey}`; + + return ( + } + 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={cellErrors} + isValidating={isLoading} + fieldKey={fieldKey} + options={options} + itemNumber={itemNumber} + width={fieldWidth} + rowIndex={row.index} + copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)} + totalRows={data.length} + /> + ); + } + }; + }).filter((col): col is ColumnDef, any> => col !== null), + [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]); + + const table = useReactTable({ + data, + columns, + state: { + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getRowId: useCallback((_row: RowData, index: number) => String(index), []), + }); + + // Calculate total table width for stable horizontal scrolling + const totalWidth = useMemo(() => { + return columns.reduce((total, col) => total + (col.size || 0), 0); + }, [columns]); + + // Don't render if no data + if (data.length === 0) { + return ( +
+

+ {filters?.showErrorsOnly + ? translations.validationStep.noRowsMessageWhenFiltered || "No rows with errors" + : translations.validationStep.noRowsMessage || "No data to display"} +

+
+ ); + } + + return ( + +
+ {/* Add global styles for copy down mode */} + {isInCopyDownMode && ( + + )} + {isInCopyDownMode && sourceRowIndex !== null && sourceFieldKey !== null && ( +
+
{ + // Find the column index + const colIndex = columns.findIndex(col => + 'accessorKey' in col && col.accessorKey === sourceFieldKey + ); + + // If column not found, position at a default location + if (colIndex === -1) return '50px'; + + // Calculate position based on column widths + let position = 0; + for (let i = 0; i < colIndex; i++) { + position += columns[i].size || 0; + } + + // Add half of the current column width to center it + position += (columns[colIndex].size || 0) / 2; + + // Adjust to center the notification + position -= 120; // Half of the notification width + + return `${Math.max(50, position)}px`; + })() + }} + > +
+ Click on the last row you want to copy to +
+ +
+
+ )} +
+ {/* Custom Table Header - Always Visible with GPU acceleration */} +
+
+ {table.getFlatHeaders().map((header) => { + const width = header.getSize(); + return ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ ); + })} +
+
+ + {/* Table Body - With optimized rendering */} + + + {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 ( + handleRowMouseEnter(parseInt(row.id))} + > + {row.getVisibleCells().map((cell: any) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); + })} + +
+
+
+
+ ); +}; + +// Optimize memo comparison with more efficient checks +const areEqual = (prev: ValidationTableProps, next: ValidationTableProps) => { + // Check reference equality for simple props first + if (prev.fields !== next.fields) return false; + if (prev.templates !== next.templates) return false; + if (prev.isLoadingTemplates !== next.isLoadingTemplates) return false; + if (prev.filters?.showErrorsOnly !== next.filters?.showErrorsOnly) return false; + + // Fast path: data length change always means re-render + if (prev.data.length !== next.data.length) return false; + + // Efficiently check row selection changes + const prevSelectionKeys = Object.keys(prev.rowSelection); + const nextSelectionKeys = Object.keys(next.rowSelection); + if (prevSelectionKeys.length !== nextSelectionKeys.length) return false; + if (!prevSelectionKeys.every(key => prev.rowSelection[key] === next.rowSelection[key])) return false; + + // Use size for Map comparisons instead of deeper checks + if (prev.validationErrors.size !== next.validationErrors.size) return false; + if (prev.validatingCells.size !== next.validatingCells.size) return false; + if (prev.itemNumbers.size !== next.itemNumbers.size) return false; + + // If values haven't changed, component doesn't need to re-render + return true; +}; + +export default React.memo(ValidationTable, areEqual); \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/CheckboxCell.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/CheckboxCell.tsx new file mode 100644 index 0000000..db18398 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/CheckboxCell.tsx @@ -0,0 +1,145 @@ +import { useState, useEffect, useCallback } from 'react' +import { Field } from '../../../../types' +import { Checkbox } from '@/components/ui/checkbox' +import { cn } from '@/lib/utils' +import React from 'react' + +interface CheckboxCellProps { + field: Field + value: any + onChange: (value: any) => void + hasErrors?: boolean + booleanMatches?: Record + className?: string +} + +const CheckboxCell = ({ + field, + value, + onChange, + hasErrors, + booleanMatches = {}, + className = '' +}: CheckboxCellProps) => { + const [checked, setChecked] = useState(false) + // Add state for hover + const [isHovered, setIsHovered] = useState(false) + + // Initialize checkbox state + useEffect(() => { + if (value === undefined || value === null) { + setChecked(false) + return + } + + if (typeof value === 'boolean') { + setChecked(value) + return + } + + // Handle string values using booleanMatches + if (typeof value === 'string') { + // First try the field's booleanMatches + const fieldBooleanMatches = field.fieldType.type === 'checkbox' + ? field.fieldType.booleanMatches || {} + : {} + + // Merge with the provided booleanMatches, with the provided ones taking precedence + const allMatches = { ...fieldBooleanMatches, ...booleanMatches } + + // Try to find the value in the matches + const matchEntry = Object.entries(allMatches).find(([k]) => + k.toLowerCase() === value.toLowerCase()) + + if (matchEntry) { + setChecked(matchEntry[1]) + return + } + + // If no match found, use common true/false strings + const trueStrings = ['yes', 'true', '1', 'y'] + const falseStrings = ['no', 'false', '0', 'n'] + + if (trueStrings.includes(value.toLowerCase())) { + setChecked(true) + return + } + + if (falseStrings.includes(value.toLowerCase())) { + setChecked(false) + return + } + } + + // For any other values, try to convert to boolean + setChecked(!!value) + }, [value, field.fieldType, booleanMatches]) + + // Helper function to check if a class is present in the className string + const hasClass = (cls: string): boolean => { + const classNames = (className || '').split(' '); + return classNames.includes(cls); + }; + + // Handle checkbox change + const handleChange = useCallback((checked: boolean) => { + setChecked(checked) + onChange(checked) + }, [onChange]) + + // Add outline even when not in focus + const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0" + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + +
+ ) +} + +export default React.memo(CheckboxCell, (prev, next) => { + if (prev.hasErrors !== next.hasErrors) return false; + if (prev.field !== next.field) return false; + if (prev.value !== next.value) return false; + if (prev.className !== next.className) return false; + + // Compare booleanMatches objects + const prevMatches = prev.booleanMatches || {}; + const nextMatches = next.booleanMatches || {}; + const prevKeys = Object.keys(prevMatches); + const nextKeys = Object.keys(nextMatches); + + if (prevKeys.length !== nextKeys.length) return false; + + for (const key of prevKeys) { + if (prevMatches[key] !== nextMatches[key]) return false; + } + + return true; +}); \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/InputCell.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/InputCell.tsx new file mode 100644 index 0000000..9f94035 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/InputCell.tsx @@ -0,0 +1,299 @@ +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' +import MultilineInput from './MultilineInput' + +interface InputCellProps { + field: Field + value: any + onChange: (value: any) => void + onStartEdit?: () => void + onEndEdit?: () => void + hasErrors?: boolean + isMultiline?: boolean + isPrice?: boolean + disabled?: boolean + className?: string +} + +// Add efficient price formatting utility +const formatPrice = (value: string): string => { + // Remove any non-numeric characters except decimal point + const numericValue = value.replace(/[^\d.]/g, ''); + + // Parse as float and format to 2 decimal places + const numValue = parseFloat(numericValue); + if (!isNaN(numValue)) { + return numValue.toFixed(2); + } + + return numericValue; +}; + +const InputCell = ({ + field, + value, + onChange, + onStartEdit, + onEndEdit, + hasErrors, + isMultiline = false, + isPrice = false, + disabled = false, + className = '' +}: InputCellProps) => { + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(''); + const [isPending, startTransition] = useTransition(); + + // Use a ref to track if we need to process the value + const needsProcessingRef = useRef(false); + + // Track local display value to avoid waiting for validation + const [localDisplayValue, setLocalDisplayValue] = useState(null); + + // Add state for hover + const [isHovered, setIsHovered] = useState(false); + + // Helper function to check if a class is present in the className string + const hasClass = (cls: string): boolean => { + const classNames = className.split(' '); + return classNames.includes(cls); + }; + + // Initialize localDisplayValue on mount and when value changes externally + useEffect(() => { + if (localDisplayValue === null || + (typeof value === 'string' && typeof localDisplayValue === 'string' && + value.trim() !== localDisplayValue.trim())) { + setLocalDisplayValue(value); + } + }, [value, localDisplayValue]); + + // Efficiently handle price formatting without multiple rerenders + useEffect(() => { + if (isPrice && needsProcessingRef.current && !isEditing) { + needsProcessingRef.current = false; + + // Do price processing only when needed + const formattedValue = formatPrice(value); + if (formattedValue !== value) { + onChange(formattedValue); + } + } + }, [value, isPrice, isEditing, onChange]); + + // Handle focus event - optimized to be synchronous + const handleFocus = useCallback(() => { + setIsEditing(true); + + // For price fields, strip formatting when focusing + if (value !== undefined && value !== null) { + if (isPrice) { + // Remove any non-numeric characters except decimal point + const numericValue = String(value).replace(/[^\d.]/g, ''); + setEditValue(numericValue); + } else { + setEditValue(String(value)); + } + } else { + setEditValue(''); + } + + onStartEdit?.(); + }, [value, onStartEdit, isPrice]); + + // 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 = finalValue; + + if (isPrice && processedValue) { + needsProcessingRef.current = true; + } + + // Update local display value immediately to prevent UI flicker + setLocalDisplayValue(processedValue); + + // Commit the change to parent component + onChange(processedValue); + onEndEdit?.(); + }); + }, [editValue, onChange, onEndEdit, isPrice]); + + // Handle direct input change - optimized to be synchronous for typing + const handleChange = useCallback((e: React.ChangeEvent) => { + const newValue = isPrice ? e.target.value.replace(/[$,]/g, '') : e.target.value; + setEditValue(newValue); + }, [isPrice]); + + // Get the display value - prioritize local display value + const displayValue = useMemo(() => { + // First priority: local display value (for immediate updates) + if (localDisplayValue !== null) { + if (isPrice) { + // Format price value + const numValue = parseFloat(localDisplayValue); + return !isNaN(numValue) ? numValue.toFixed(2) : localDisplayValue; + } + return localDisplayValue; + } + + // Second priority: handle price formatting for the actual value + if (isPrice && value) { + if (typeof value === 'number') { + return value.toFixed(2); + } else if (typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value)) { + return parseFloat(value).toFixed(2); + } + } + + // Default: use the actual value or empty string + return value ?? ''; + }, [isPrice, value, localDisplayValue]); + + // Add outline even when not in focus + const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0"; + + // If disabled, just render the value without any interactivity + if (disabled) { + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {displayValue} +
+ ); + } + + // Render multiline fields using the dedicated MultilineInput component + if (isMultiline) { + return ( + + ); + } + + // Original component for non-multiline fields + return ( +
+ {isEditing ? ( + + ) : ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {displayValue} +
+ )} +
+ ) +} + +// Optimize memo comparison to focus on essential props +export default React.memo(InputCell, (prev, next) => { + if (prev.hasErrors !== next.hasErrors) return false; + if (prev.isMultiline !== next.isMultiline) return false; + if (prev.isPrice !== next.isPrice) return false; + if (prev.disabled !== next.disabled) return false; + if (prev.field !== next.field) return false; + + // Only check value if not editing (to avoid expensive rerender during editing) + if (prev.value !== next.value) { + // For price values, do a more intelligent comparison + if (prev.isPrice) { + // Convert both to numeric values for comparison + const prevNum = typeof prev.value === 'number' ? prev.value : + typeof prev.value === 'string' ? parseFloat(prev.value) : 0; + const nextNum = typeof next.value === 'number' ? next.value : + typeof next.value === 'string' ? parseFloat(next.value) : 0; + + // Only update if the actual numeric values differ + if (!isNaN(prevNum) && !isNaN(nextNum) && + Math.abs(prevNum - nextNum) > 0.001) { + return false; + } + } else { + return false; + } + } + + return true; +}); \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx new file mode 100644 index 0000000..f7e0503 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx @@ -0,0 +1,545 @@ +import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react' +import { Field } from '../../../../types' +import { cn } from '@/lib/utils' +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Button } from '@/components/ui/button' +import { Check, ChevronsUpDown } from 'lucide-react' +import { Badge } from '@/components/ui/badge' + +// Define a type for field options +interface FieldOption { + label: string; + value: string; +} + +interface MultiSelectCellProps { + field: Field + value: string[] + onChange: (value: string[]) => void + onStartEdit?: () => void + onEndEdit?: () => void + hasErrors?: boolean + options?: readonly FieldOption[] + disabled?: boolean + className?: string +} + +// Memoized option item to prevent unnecessary renders for large option lists +const OptionItem = React.memo(({ + option, + isSelected, + onSelect +}: { + option: FieldOption, + isSelected: boolean, + onSelect: (value: string) => void +}) => ( + onSelect(option.value)} + className="flex w-full" + > +
+ + {option.label} +
+
+), (prev, next) => { + return prev.option.value === next.option.value && + prev.isSelected === next.isSelected; +}); + +OptionItem.displayName = 'OptionItem'; + +// Create a virtualized list component for large option lists +const VirtualizedOptions = React.memo(({ + options, + selectedValues, + onSelect, + maxHeight = 200 +}: { + options: FieldOption[], + selectedValues: Set, + onSelect: (value: string) => void, + maxHeight?: number +}) => { + const listRef = useRef(null); + + // Only render visible options for better performance with large lists + const [visibleOptions, setVisibleOptions] = useState([]); + const [scrollPosition, setScrollPosition] = useState(0); + + // Constants for virtualization + const itemHeight = 32; // Height of each option item in pixels + const visibleCount = Math.ceil(maxHeight / itemHeight) + 2; // Number of visible items + buffer + + // Handle scroll events + const handleScroll = useCallback(() => { + if (listRef.current) { + setScrollPosition(listRef.current.scrollTop); + } + }, []); + + // Update visible options based on scroll position + useEffect(() => { + if (options.length <= visibleCount) { + // If fewer options than visible count, just show all + setVisibleOptions(options); + return; + } + + // Calculate start and end indices + const startIndex = Math.floor(scrollPosition / itemHeight); + const endIndex = Math.min(startIndex + visibleCount, options.length); + + // Update visible options + setVisibleOptions(options.slice(Math.max(0, startIndex), endIndex)); + }, [options, scrollPosition, visibleCount, itemHeight]); + + // If fewer than the threshold, render all directly + if (options.length <= 100) { + return ( +
+ {options.map(option => ( + + ))} +
+ ); + } + + return ( +
+
+
+ {visibleOptions.map(option => ( + + ))} +
+
+
+ ); +}); + +VirtualizedOptions.displayName = 'VirtualizedOptions'; + +const MultiSelectCell = ({ + field, + value = [], + onChange, + onStartEdit, + onEndEdit, + hasErrors, + options: providedOptions, + disabled = false, + className = '' +}: MultiSelectCellProps) => { + const [open, setOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + // Add internal state for tracking selections - ensure value is always an array + const [internalValue, setInternalValue] = useState(Array.isArray(value) ? value : []) + // Ref for the command list to enable scrolling + const commandListRef = useRef(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(() => { + // 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, 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 + // 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) { + // 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 + setOpen(false); + } + }, [open, internalValue, value, onChange, onStartEdit, onEndEdit]); + + // Memoize field options to prevent unnecessary recalculations + const selectOptions = useMemo(() => { + const fieldType = field.fieldType; + const fieldOptions = fieldType && + (fieldType.type === 'select' || fieldType.type === 'multi-select') && + fieldType.options ? + fieldType.options : + []; + + // Use provided options or field options, ensuring they have the correct shape + // Skip this work if we have a large number of options and they didn't change + if (providedOptions && providedOptions.length > 0) { + // Check if options are already in the right format + if (typeof providedOptions[0] === 'object' && 'label' in providedOptions[0] && 'value' in providedOptions[0]) { + return providedOptions as FieldOption[]; + } + + return providedOptions.map(option => ({ + label: option.label || String(option.value), + value: String(option.value) + })); + } + + // Check field options format + if (fieldOptions.length > 0) { + if (typeof fieldOptions[0] === 'object' && 'label' in fieldOptions[0] && 'value' in fieldOptions[0]) { + return fieldOptions as FieldOption[]; + } + + return fieldOptions.map(option => ({ + label: option.label || String(option.value), + value: String(option.value) + })); + } + + // Add default option if no options available + return [{ label: 'No options available', value: '' }]; + }, [field.fieldType, providedOptions]); + + // Use deferredValue for search to prevent UI blocking with large lists + const deferredSearchQuery = React.useDeferredValue(searchQuery); + + // Memoize filtered options based on search query - efficient filtering algorithm + const filteredOptions = useMemo(() => { + // If no search query, return all options + if (!deferredSearchQuery.trim()) return selectOptions; + + const query = deferredSearchQuery.toLowerCase(); + + // Use faster algorithm for large option lists + if (selectOptions.length > 100) { + return selectOptions.filter(option => { + // First check starting with the query (most relevant) + if (option.label.toLowerCase().startsWith(query)) return true; + + // Then check includes for more general matches + return option.label.toLowerCase().includes(query); + }); + } + + // For smaller lists, do full text search + return selectOptions.filter(option => + option.label.toLowerCase().includes(query) + ); + }, [selectOptions, deferredSearchQuery]); + + // Sort options with selected items at the top for the dropdown - only for smaller lists + const sortedOptions = useMemo(() => { + // Skip expensive sorting for large lists + if (selectOptions.length > 100) return filteredOptions; + + return [...filteredOptions].sort((a, b) => { + const aSelected = selectedValueSet.has(a.value); + const bSelected = selectedValueSet.has(b.value); + + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return a.label.localeCompare(b.label); + }); + }, [filteredOptions, selectedValueSet, selectOptions.length]); + + // Memoize selected values display + const selectedValues = useMemo(() => { + // Use a map for looking up options by value for better performance + const optionsMap = new Map(selectOptions.map(opt => [opt.value, opt])); + + return internalValue.map(v => { + const option = optionsMap.get(v); + return { + value: v, + label: option ? option.label : String(v) + }; + }); + }, [internalValue, selectOptions]); + + // 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)) { + // Remove the value + newValue = prev.filter(v => v !== selectedValue); + } else { + // 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 + const handleWheel = useCallback((e: React.WheelEvent) => { + if (commandListRef.current) { + e.stopPropagation(); + commandListRef.current.scrollTop += e.deltaY; + } + }, []); + + // Helper function to check if a class is present in the className string + const hasClass = (cls: string): boolean => { + const classNames = className.split(' '); + return classNames.includes(cls); + }; + + // If disabled, just render the value without any interactivity + if (disabled) { + const displayValue = internalValue.length > 0 + ? internalValue.map(val => { + const option = selectOptions.find(opt => opt.value === val); + return option ? option.label : val; + }).join(', ') + : ''; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {displayValue || ""} +
+ ); + } + + return ( + { + // Only open the popover if we're not in copy down mode + if (!hasClass('!bg-blue-100') && !hasClass('!bg-blue-200') && !hasClass('hover:!bg-blue-100')) { + setOpen(o); + handleOpenChange(o); + } + }}> + + + + + + + + No results found. + + {sortedOptions.map((option) => ( + handleSelect(option.value)} + className="cursor-pointer" + > + {option.label} + {selectedValueSet.has(option.value) && ( + + )} + + ))} + + + + + + ); +}; + +MultiSelectCell.displayName = 'MultiSelectCell'; + +export default React.memo(MultiSelectCell, (prev, next) => { + // Check primitive props first (cheap comparisons) + if (prev.hasErrors !== next.hasErrors) return false; + if (prev.disabled !== next.disabled) return false; + if (prev.className !== next.className) return false; + + // Check field reference + if (prev.field !== next.field) return false; + + // Check value arrays (potentially expensive for large arrays) + // Handle undefined or null values safely + const prevValue = prev.value || []; + const nextValue = next.value || []; + + if (prevValue.length !== nextValue.length) return false; + for (let i = 0; i < prevValue.length; i++) { + if (prevValue[i] !== nextValue[i]) return false; + } + + // Check options (potentially expensive for large option lists) + const prevOptions = prev.options || []; + const nextOptions = next.options || []; + if (prevOptions.length !== nextOptions.length) return false; + + // For large option lists, just compare references + if (prevOptions.length > 100) { + return prevOptions === nextOptions; + } + + // For smaller lists, do a shallow comparison + for (let i = 0; i < prevOptions.length; i++) { + if (prevOptions[i] !== nextOptions[i]) return false; + } + + return true; +}); \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/MultilineInput.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/MultilineInput.tsx new file mode 100644 index 0000000..65b3d65 --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/cells/MultilineInput.tsx @@ -0,0 +1,238 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react' +import { Field } from '../../../../types' +import { Textarea } from '@/components/ui/textarea' +import { cn } from '@/lib/utils' +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' +import { X } from 'lucide-react' +import { Button } from '@/components/ui/button' + +interface MultilineInputProps { + field: Field + value: any + onChange: (value: any) => void + hasErrors?: boolean + disabled?: boolean + className?: string +} + +const MultilineInput = ({ + field, + value, + onChange, + hasErrors = false, + disabled = false, + className = '' +}: MultilineInputProps) => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [editValue, setEditValue] = useState(''); + const [localDisplayValue, setLocalDisplayValue] = useState(null); + const cellRef = useRef(null); + const preventReopenRef = useRef(false); + const pendingChangeRef = useRef(null); + + // Add state for hover + const [isHovered, setIsHovered] = useState(false); + + // Helper function to check if a class is present in the className string + const hasClass = (cls: string): boolean => { + const classNames = (className || '').split(' '); + return classNames.includes(cls); + }; + + // Initialize localDisplayValue on mount and when value changes externally + useEffect(() => { + if (localDisplayValue === null || + (typeof value === 'string' && typeof localDisplayValue === 'string' && + value.trim() !== localDisplayValue.trim())) { + setLocalDisplayValue(value); + } + }, [value, localDisplayValue]); + + // Process any pending changes in the background + useEffect(() => { + if (pendingChangeRef.current !== null && !popoverOpen) { + const newValue = pendingChangeRef.current; + pendingChangeRef.current = null; + // Apply changes after the popover is closed + if (newValue !== value) { + onChange(newValue); + } + } + }, [popoverOpen, onChange, value]); + + // Handle trigger click to toggle the popover + const handleTriggerClick = useCallback((e: React.MouseEvent) => { + if (preventReopenRef.current) { + e.preventDefault(); + e.stopPropagation(); + preventReopenRef.current = false; + return; + } + + // Only process if not already open + if (!popoverOpen) { + setPopoverOpen(true); + // Initialize edit value from the current display + setEditValue(localDisplayValue || value || ''); + } + }, [popoverOpen, value, localDisplayValue]); + + // Handle immediate close of popover + const handleClosePopover = useCallback(() => { + // Only process if we have changes + if (editValue !== value || editValue !== localDisplayValue) { + // Store pending changes for async processing + pendingChangeRef.current = editValue; + + // Update local display immediately + setLocalDisplayValue(editValue); + + // Queue up the change to be processed in the background + setTimeout(() => { + onChange(editValue); + }, 0); + } + + // Immediately close popover + setPopoverOpen(false); + + // Prevent reopening + preventReopenRef.current = true; + setTimeout(() => { + preventReopenRef.current = false; + }, 100); + }, [editValue, value, localDisplayValue, onChange]); + + // Handle clicking outside the popover + const handleInteractOutside = useCallback(() => { + handleClosePopover(); + }, [handleClosePopover]); + + // Handle popover open/close + const handlePopoverOpenChange = useCallback((open: boolean) => { + if (!open && popoverOpen) { + // Just call the close handler + handleClosePopover(); + } else if (open && !popoverOpen) { + // When opening, set edit value from current display + setEditValue(localDisplayValue || value || ''); + setPopoverOpen(true); + } + }, [value, popoverOpen, handleClosePopover, localDisplayValue]); + + // Handle direct input change + const handleChange = useCallback((e: React.ChangeEvent) => { + setEditValue(e.target.value); + }, []); + + // Calculate display value + const displayValue = localDisplayValue !== null ? localDisplayValue : (value ?? ''); + + // Add outline even when not in focus + const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0"; + + // If disabled, just render the value without any interactivity + if (disabled) { + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {displayValue} +
+ ); + } + + return ( +
+ + +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {displayValue} +
+
+ +
+ + +