Validation step tweaks, remove remaining references to old version

This commit is contained in:
2026-01-19 13:11:35 -05:00
parent 43d76e011d
commit 7218e7cc3f
43 changed files with 144 additions and 25 deletions

View File

@@ -1394,7 +1394,7 @@ router.get('/check-upc-and-generate-sku', async (req, res) => {
if (upcCheck.length > 0) {
return res.status(409).json({
error: 'UPC already exists',
error: 'A product with this UPC already exists',
existingProductId: upcCheck[0].pid,
existingItemNumber: upcCheck[0].itemnumber
});

View File

@@ -13,7 +13,7 @@ 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"
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
export enum StepType {
upload = "upload",

View File

@@ -324,20 +324,55 @@ const CellWrapper = memo(({
// Only check uniqueness if value is not empty
if (stringValue !== '') {
const isDuplicate = rows.some((row, idx) => {
if (idx === rowIndex) return false;
const otherValue = String(row[field.key] ?? '').toLowerCase().trim();
return otherValue === stringValue;
// Find ALL rows with the same value (including current row)
const duplicateRowIndices: number[] = [];
rows.forEach((row, idx) => {
// 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) {
setError(rowIndex, field.key, {
// Set error on ALL duplicate rows (bidirectional)
const errorObj = {
message: (uniqueValidation as { errorMessage?: string }).errorMessage || 'Must be unique',
level: (uniqueValidation as { level?: 'error' | 'warning' | 'info' }).level || 'error',
source: ErrorSource.Table,
type: ErrorType.Unique,
};
duplicateRowIndices.forEach((idx) => {
setError(idx, field.key, errorObj);
});
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
if (field.key === 'supplier' || field.key === 'upc') {
const currentRow = useValidationStore.getState().rows[rowIndex];
@@ -379,7 +477,7 @@ const CellWrapper = memo(({
if (response.status === 409) {
// UPC already exists
setError(rowIndex, 'upc', {
message: 'UPC already exists in database',
message: 'A product with this UPC already exists',
level: 'error',
source: ErrorSource.Upc,
type: ErrorType.Unique,
@@ -707,7 +805,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa
if (response.status === 409) {
// UPC already exists
setError(rowIndex, 'upc', {
message: 'UPC already exists in database',
message: 'A product with this UPC already exists',
level: 'error',
source: ErrorSource.Upc,
type: ErrorType.Unique,
@@ -809,6 +907,25 @@ const VirtualRow = memo(({
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
const isSelected = useValidationStore(
useCallback((state) => state.selectedRows.has(rowId), [rowId])
@@ -821,8 +938,7 @@ const VirtualRow = memo(({
// 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!
const company = rowData?.company;
const line = rowData?.line;
// Note: company and line are already declared above for loading state subscriptions
const supplier = rowData?.supplier;
// Get action via getState() - no need to subscribe
@@ -899,12 +1015,12 @@ const VirtualRow = memo(({
const needsLine = field.key === 'subline';
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;
if (needsCompany && company) {
isLoadingOptions = useValidationStore.getState().loadingProductLines.has(String(company));
} else if (needsLine && line) {
isLoadingOptions = useValidationStore.getState().loadingSublines.has(String(line));
if (needsCompany) {
isLoadingOptions = isLoadingProductLinesForCompany;
} else if (needsLine) {
isLoadingOptions = isLoadingSublineForLine;
}
// Calculate copy-down state for this cell

View File

@@ -19,7 +19,6 @@ import { Badge } from '@/components/ui/badge';
import { useValidationStore } from '../store/validationStore';
import {
useFilters,
useSelectedRowCount,
useFields,
} from '../store/selectors';
import { CreateProductCategoryDialog, type CreatedCategoryInfo } from '../../../CreateProductCategoryDialog';
@@ -38,7 +37,6 @@ export const ValidationToolbar = ({
rowsWithErrors,
}: ValidationToolbarProps) => {
const filters = useFilters();
const selectedRowCount = useSelectedRowCount();
const fields = useFields();
// State for the product search template dialog

View File

@@ -10,7 +10,7 @@
* Only visible to users with admin:debug permission.
*/
import { useState, useMemo } from 'react';
import { useState } from 'react';
import {
Dialog,
DialogContent,

View File

@@ -154,7 +154,7 @@ export const useUpcValidation = () => {
// Set specific error for conflicts
if (result.code === 'conflict') {
setError(rowIndex, 'upc', {
message: 'UPC already exists in database',
message: 'A product with this UPC already exists',
level: 'error',
source: ErrorSource.Upc,
type: ErrorType.Unique,
@@ -262,7 +262,7 @@ export const useUpcValidation = () => {
if (result.code === 'conflict') {
setError(index, 'upc', {
message: 'UPC already exists in database',
message: 'A product with this UPC already exists',
level: 'error',
source: ErrorSource.Upc,
type: ErrorType.Unique,

View File

@@ -1,5 +1,4 @@
import type { Data, Fields, Info, RowHook, TableHook } from "../../../types"
import type { Meta, Errors } from "../../ValidationStepNew/types"
import type { Data, Fields, Info, RowHook, TableHook, Meta, Errors } from "../../../types"
import { v4 } from "uuid"
import { ErrorSources, ErrorType } from "../../../types"

View File

@@ -1,9 +1,11 @@
import type { Meta } from "./steps/ValidationStepNew/types"
import type { DeepReadonly } from "ts-essentials"
import type { TranslationsRSIProps } from "./translationsRSIProps"
import type { Columns } from "./steps/MatchColumnsStep/types"
import type { StepState } from "./steps/UploadFlow"
// Meta type for row data with unique index
export type Meta = { __index: string }
export type SubmitOptions = {
targetEnvironment: "dev" | "prod"
useTestDataSource: boolean
@@ -200,6 +202,10 @@ export type InfoWithSource = Info & {
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> = {
validData: Data<T>[]
invalidData: Data<T>[]

View File

@@ -431,7 +431,7 @@ export function Import() {
// {
// upc: "123456789012",
// item_number: "ITEM-001",
// error_msg: "UPC already exists in the system",
// error_msg: "A product with this UPC already exists",
// },
// {
// upc: "234567890123",