Validation step tweaks, remove remaining references to old version
This commit is contained in:
@@ -1394,7 +1394,7 @@ router.get('/check-upc-and-generate-sku', async (req, res) => {
|
|||||||
|
|
||||||
if (upcCheck.length > 0) {
|
if (upcCheck.length > 0) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
error: 'UPC already exists',
|
error: 'A product with this UPC already exists',
|
||||||
existingProductId: upcCheck[0].pid,
|
existingProductId: upcCheck[0].pid,
|
||||||
existingItemNumber: upcCheck[0].itemnumber
|
existingItemNumber: upcCheck[0].itemnumber
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useRsi } from "../hooks/useRsi"
|
|||||||
import type { RawData, Data } from "../types"
|
import type { RawData, Data } from "../types"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
import { addErrorsAndRunHooks } from "./ValidationStepNew/utils/dataMutations"
|
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
|
||||||
|
|
||||||
export enum StepType {
|
export enum StepType {
|
||||||
upload = "upload",
|
upload = "upload",
|
||||||
|
|||||||
@@ -324,20 +324,55 @@ const CellWrapper = memo(({
|
|||||||
|
|
||||||
// Only check uniqueness if value is not empty
|
// Only check uniqueness if value is not empty
|
||||||
if (stringValue !== '') {
|
if (stringValue !== '') {
|
||||||
const isDuplicate = rows.some((row, idx) => {
|
// Find ALL rows with the same value (including current row)
|
||||||
if (idx === rowIndex) return false;
|
const duplicateRowIndices: number[] = [];
|
||||||
const otherValue = String(row[field.key] ?? '').toLowerCase().trim();
|
rows.forEach((row, idx) => {
|
||||||
return otherValue === stringValue;
|
// For current row, use the new value being saved; for other rows, use stored value
|
||||||
|
const cellValue = idx === rowIndex ? valueToSave : row[field.key];
|
||||||
|
const otherValue = String(cellValue ?? '').toLowerCase().trim();
|
||||||
|
if (otherValue === stringValue) {
|
||||||
|
duplicateRowIndices.push(idx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDuplicate = duplicateRowIndices.length > 1;
|
||||||
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
setError(rowIndex, field.key, {
|
// Set error on ALL duplicate rows (bidirectional)
|
||||||
|
const errorObj = {
|
||||||
message: (uniqueValidation as { errorMessage?: string }).errorMessage || 'Must be unique',
|
message: (uniqueValidation as { errorMessage?: string }).errorMessage || 'Must be unique',
|
||||||
level: (uniqueValidation as { level?: 'error' | 'warning' | 'info' }).level || 'error',
|
level: (uniqueValidation as { level?: 'error' | 'warning' | 'info' }).level || 'error',
|
||||||
source: ErrorSource.Table,
|
source: ErrorSource.Table,
|
||||||
type: ErrorType.Unique,
|
type: ErrorType.Unique,
|
||||||
|
};
|
||||||
|
duplicateRowIndices.forEach((idx) => {
|
||||||
|
setError(idx, field.key, errorObj);
|
||||||
});
|
});
|
||||||
hasError = true;
|
hasError = true;
|
||||||
|
} else {
|
||||||
|
// Value is now unique - clear any existing unique errors on other rows
|
||||||
|
// that might have had this value before
|
||||||
|
rows.forEach((row, idx) => {
|
||||||
|
if (idx !== rowIndex) {
|
||||||
|
const existingErrors = useValidationStore.getState().errors.get(idx);
|
||||||
|
const fieldErrors = existingErrors?.[field.key];
|
||||||
|
if (fieldErrors?.some(e => e.type === ErrorType.Unique)) {
|
||||||
|
// Re-validate this row's uniqueness
|
||||||
|
const otherValue = String(row[field.key] ?? '').toLowerCase().trim();
|
||||||
|
if (otherValue !== '') {
|
||||||
|
const stillHasDuplicate = rows.some((r, i) => {
|
||||||
|
if (i === idx) return false;
|
||||||
|
const cellValue = i === rowIndex ? valueToSave : r[field.key];
|
||||||
|
const v = String(cellValue ?? '').toLowerCase().trim();
|
||||||
|
return v === otherValue;
|
||||||
|
});
|
||||||
|
if (!stillHasDuplicate) {
|
||||||
|
clearFieldError(idx, field.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,6 +383,69 @@ const CellWrapper = memo(({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger product lines fetch if company changed
|
||||||
|
if (field.key === 'company' && valueToSave) {
|
||||||
|
const companyId = String(valueToSave);
|
||||||
|
const state = useValidationStore.getState();
|
||||||
|
const cached = state.productLinesCache.get(companyId);
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
// Start loading state and fetch product lines
|
||||||
|
state.setLoadingProductLines(companyId, true);
|
||||||
|
fetch(`/api/import/product-lines/${companyId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(lines => {
|
||||||
|
const opts = lines.map((line: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({
|
||||||
|
label: line.name || line.label || String(line.value || line.id),
|
||||||
|
value: String(line.value || line.id),
|
||||||
|
}));
|
||||||
|
state.setProductLines(companyId, opts);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error prefetching product lines:', err);
|
||||||
|
state.setProductLines(companyId, []);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
state.setLoadingProductLines(companyId, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear line and subline when company changes (they're no longer valid)
|
||||||
|
updateCell(rowIndex, 'line', '');
|
||||||
|
updateCell(rowIndex, 'subline', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger sublines fetch if line changed
|
||||||
|
if (field.key === 'line' && valueToSave) {
|
||||||
|
const lineId = String(valueToSave);
|
||||||
|
const state = useValidationStore.getState();
|
||||||
|
const cached = state.sublinesCache.get(lineId);
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
// Start loading state and fetch sublines
|
||||||
|
state.setLoadingSublines(lineId, true);
|
||||||
|
fetch(`/api/import/sublines/${lineId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(sublines => {
|
||||||
|
const opts = sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({
|
||||||
|
label: subline.name || subline.label || String(subline.value || subline.id),
|
||||||
|
value: String(subline.value || subline.id),
|
||||||
|
}));
|
||||||
|
state.setSublines(lineId, opts);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error prefetching sublines:', err);
|
||||||
|
state.setSublines(lineId, []);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
state.setLoadingSublines(lineId, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear subline when line changes (it's no longer valid)
|
||||||
|
updateCell(rowIndex, 'subline', '');
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger UPC validation if supplier or UPC changed
|
// Trigger UPC validation if supplier or UPC changed
|
||||||
if (field.key === 'supplier' || field.key === 'upc') {
|
if (field.key === 'supplier' || field.key === 'upc') {
|
||||||
const currentRow = useValidationStore.getState().rows[rowIndex];
|
const currentRow = useValidationStore.getState().rows[rowIndex];
|
||||||
@@ -379,7 +477,7 @@ const CellWrapper = memo(({
|
|||||||
if (response.status === 409) {
|
if (response.status === 409) {
|
||||||
// UPC already exists
|
// UPC already exists
|
||||||
setError(rowIndex, 'upc', {
|
setError(rowIndex, 'upc', {
|
||||||
message: 'UPC already exists in database',
|
message: 'A product with this UPC already exists',
|
||||||
level: 'error',
|
level: 'error',
|
||||||
source: ErrorSource.Upc,
|
source: ErrorSource.Upc,
|
||||||
type: ErrorType.Unique,
|
type: ErrorType.Unique,
|
||||||
@@ -707,7 +805,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa
|
|||||||
if (response.status === 409) {
|
if (response.status === 409) {
|
||||||
// UPC already exists
|
// UPC already exists
|
||||||
setError(rowIndex, 'upc', {
|
setError(rowIndex, 'upc', {
|
||||||
message: 'UPC already exists in database',
|
message: 'A product with this UPC already exists',
|
||||||
level: 'error',
|
level: 'error',
|
||||||
source: ErrorSource.Upc,
|
source: ErrorSource.Upc,
|
||||||
type: ErrorType.Unique,
|
type: ErrorType.Unique,
|
||||||
@@ -809,6 +907,25 @@ const VirtualRow = memo(({
|
|||||||
useCallback((state) => state.validatingCells.has(`${rowIndex}-item_number`), [rowIndex])
|
useCallback((state) => state.validatingCells.has(`${rowIndex}-item_number`), [rowIndex])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Subscribe to loading states for line/subline fields
|
||||||
|
// These need reactive updates so loading spinners clear when API calls complete
|
||||||
|
const company = rowData?.company;
|
||||||
|
const line = rowData?.line;
|
||||||
|
|
||||||
|
const isLoadingProductLinesForCompany = useValidationStore(
|
||||||
|
useCallback(
|
||||||
|
(state) => (company ? state.loadingProductLines.has(String(company)) : false),
|
||||||
|
[company]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoadingSublineForLine = useValidationStore(
|
||||||
|
useCallback(
|
||||||
|
(state) => (line ? state.loadingSublines.has(String(line)) : false),
|
||||||
|
[line]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Subscribe to selection status
|
// Subscribe to selection status
|
||||||
const isSelected = useValidationStore(
|
const isSelected = useValidationStore(
|
||||||
useCallback((state) => state.selectedRows.has(rowId), [rowId])
|
useCallback((state) => state.selectedRows.has(rowId), [rowId])
|
||||||
@@ -821,8 +938,7 @@ const VirtualRow = memo(({
|
|||||||
|
|
||||||
// DON'T subscribe to caches - read via getState() when needed
|
// DON'T subscribe to caches - read via getState() when needed
|
||||||
// Subscribing to caches causes ALL rows with same company to re-render when cache updates!
|
// Subscribing to caches causes ALL rows with same company to re-render when cache updates!
|
||||||
const company = rowData?.company;
|
// Note: company and line are already declared above for loading state subscriptions
|
||||||
const line = rowData?.line;
|
|
||||||
const supplier = rowData?.supplier;
|
const supplier = rowData?.supplier;
|
||||||
|
|
||||||
// Get action via getState() - no need to subscribe
|
// Get action via getState() - no need to subscribe
|
||||||
@@ -899,12 +1015,12 @@ const VirtualRow = memo(({
|
|||||||
const needsLine = field.key === 'subline';
|
const needsLine = field.key === 'subline';
|
||||||
const needsSupplier = field.key === 'upc';
|
const needsSupplier = field.key === 'upc';
|
||||||
|
|
||||||
// Check loading state for dependent dropdowns via getState()
|
// Check loading state for dependent dropdowns - uses subscribed values for reactivity
|
||||||
let isLoadingOptions = false;
|
let isLoadingOptions = false;
|
||||||
if (needsCompany && company) {
|
if (needsCompany) {
|
||||||
isLoadingOptions = useValidationStore.getState().loadingProductLines.has(String(company));
|
isLoadingOptions = isLoadingProductLinesForCompany;
|
||||||
} else if (needsLine && line) {
|
} else if (needsLine) {
|
||||||
isLoadingOptions = useValidationStore.getState().loadingSublines.has(String(line));
|
isLoadingOptions = isLoadingSublineForLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate copy-down state for this cell
|
// Calculate copy-down state for this cell
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { useValidationStore } from '../store/validationStore';
|
import { useValidationStore } from '../store/validationStore';
|
||||||
import {
|
import {
|
||||||
useFilters,
|
useFilters,
|
||||||
useSelectedRowCount,
|
|
||||||
useFields,
|
useFields,
|
||||||
} from '../store/selectors';
|
} from '../store/selectors';
|
||||||
import { CreateProductCategoryDialog, type CreatedCategoryInfo } from '../../../CreateProductCategoryDialog';
|
import { CreateProductCategoryDialog, type CreatedCategoryInfo } from '../../../CreateProductCategoryDialog';
|
||||||
@@ -38,7 +37,6 @@ export const ValidationToolbar = ({
|
|||||||
rowsWithErrors,
|
rowsWithErrors,
|
||||||
}: ValidationToolbarProps) => {
|
}: ValidationToolbarProps) => {
|
||||||
const filters = useFilters();
|
const filters = useFilters();
|
||||||
const selectedRowCount = useSelectedRowCount();
|
|
||||||
const fields = useFields();
|
const fields = useFields();
|
||||||
|
|
||||||
// State for the product search template dialog
|
// State for the product search template dialog
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Only visible to users with admin:debug permission.
|
* Only visible to users with admin:debug permission.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export const useUpcValidation = () => {
|
|||||||
// Set specific error for conflicts
|
// Set specific error for conflicts
|
||||||
if (result.code === 'conflict') {
|
if (result.code === 'conflict') {
|
||||||
setError(rowIndex, 'upc', {
|
setError(rowIndex, 'upc', {
|
||||||
message: 'UPC already exists in database',
|
message: 'A product with this UPC already exists',
|
||||||
level: 'error',
|
level: 'error',
|
||||||
source: ErrorSource.Upc,
|
source: ErrorSource.Upc,
|
||||||
type: ErrorType.Unique,
|
type: ErrorType.Unique,
|
||||||
@@ -262,7 +262,7 @@ export const useUpcValidation = () => {
|
|||||||
|
|
||||||
if (result.code === 'conflict') {
|
if (result.code === 'conflict') {
|
||||||
setError(index, 'upc', {
|
setError(index, 'upc', {
|
||||||
message: 'UPC already exists in database',
|
message: 'A product with this UPC already exists',
|
||||||
level: 'error',
|
level: 'error',
|
||||||
source: ErrorSource.Upc,
|
source: ErrorSource.Upc,
|
||||||
type: ErrorType.Unique,
|
type: ErrorType.Unique,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Data, Fields, Info, RowHook, TableHook } from "../../../types"
|
import type { Data, Fields, Info, RowHook, TableHook, Meta, Errors } from "../../../types"
|
||||||
import type { Meta, Errors } from "../../ValidationStepNew/types"
|
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { ErrorSources, ErrorType } from "../../../types"
|
import { ErrorSources, ErrorType } from "../../../types"
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { Meta } from "./steps/ValidationStepNew/types"
|
|
||||||
import type { DeepReadonly } from "ts-essentials"
|
import type { DeepReadonly } from "ts-essentials"
|
||||||
import type { TranslationsRSIProps } from "./translationsRSIProps"
|
import type { TranslationsRSIProps } from "./translationsRSIProps"
|
||||||
import type { Columns } from "./steps/MatchColumnsStep/types"
|
import type { Columns } from "./steps/MatchColumnsStep/types"
|
||||||
import type { StepState } from "./steps/UploadFlow"
|
import type { StepState } from "./steps/UploadFlow"
|
||||||
|
|
||||||
|
// Meta type for row data with unique index
|
||||||
|
export type Meta = { __index: string }
|
||||||
|
|
||||||
export type SubmitOptions = {
|
export type SubmitOptions = {
|
||||||
targetEnvironment: "dev" | "prod"
|
targetEnvironment: "dev" | "prod"
|
||||||
useTestDataSource: boolean
|
useTestDataSource: boolean
|
||||||
@@ -200,6 +202,10 @@ export type InfoWithSource = Info & {
|
|||||||
type: ErrorType;
|
type: ErrorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy error types used by dataMutations utility
|
||||||
|
export type FieldError = { [key: string]: InfoWithSource }
|
||||||
|
export type Errors = { [id: string]: FieldError }
|
||||||
|
|
||||||
export type Result<T extends string> = {
|
export type Result<T extends string> = {
|
||||||
validData: Data<T>[]
|
validData: Data<T>[]
|
||||||
invalidData: Data<T>[]
|
invalidData: Data<T>[]
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export function Import() {
|
|||||||
// {
|
// {
|
||||||
// upc: "123456789012",
|
// upc: "123456789012",
|
||||||
// item_number: "ITEM-001",
|
// item_number: "ITEM-001",
|
||||||
// error_msg: "UPC already exists in the system",
|
// error_msg: "A product with this UPC already exists",
|
||||||
// },
|
// },
|
||||||
// {
|
// {
|
||||||
// upc: "234567890123",
|
// upc: "234567890123",
|
||||||
|
|||||||
Reference in New Issue
Block a user