Validation step styles and functionality tweaks, add initial AI functionality

This commit is contained in:
2025-02-20 15:11:14 -05:00
parent bba7362641
commit 45a52cbc33
9 changed files with 800 additions and 129 deletions

View File

@@ -15,7 +15,7 @@ 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';
import { Import } from '@/pages/Import';
import { ChakraProvider } from '@chakra-ui/react';
const queryClient = new QueryClient();

View File

@@ -35,7 +35,7 @@ export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>
</Button>
<div
className="vertical-text font-medium text-muted-foreground"
style={{ writingMode: 'vertical-rl', textOrientation: 'mixed', transform: 'rotate(180deg)' }}
style={{ writingMode: 'vertical-rl', textOrientation: 'mixed', transform: 'rotate(0deg)' }}
>
{header}
</div>

View File

@@ -1,9 +1,9 @@
import { useCallback, useMemo, useState, useEffect } from "react"
import { useCallback, useMemo, useState, useEffect, memo } from "react"
import { useRsi } from "../../hooks/useRsi"
import type { Meta } from "./types"
import { addErrorsAndRunHooks } from "./utils/dataMutations"
import type { Data, Field, SelectOption, MultiInput } from "../../types"
import { Check, ChevronsUpDown, ArrowDown, AlertCircle } from "lucide-react"
import type { Data, SelectOption } from "../../types"
import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2 } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Command,
@@ -56,12 +56,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import * as Select from "@radix-ui/react-select"
import {
Dialog,
DialogContent,
DialogTrigger,
} from "@/components/ui/dialog"
import config from "@/config"
type Props<T extends string> = {
initialData: (Data<T> & Meta)[]
@@ -69,19 +64,80 @@ type Props<T extends string> = {
onBack?: () => void
}
type CellProps = {
value: any,
onChange: (value: any) => void,
error?: { level: string, message: string },
field: Field<string>
type BaseFieldType = {
multiline?: boolean;
price?: boolean;
}
const EditableCell = ({ value, onChange, error, field }: CellProps) => {
type InputFieldType = BaseFieldType & {
type: "input";
}
type MultiInputFieldType = BaseFieldType & {
type: "multi-input";
separator?: string;
}
type SelectFieldType = {
type: "select" | "multi-select";
options: SelectOption[];
}
type CheckboxFieldType = {
type: "checkbox";
booleanMatches?: { [key: string]: boolean };
}
type FieldType = InputFieldType | MultiInputFieldType | SelectFieldType | CheckboxFieldType;
type Field<T extends string> = {
label: string;
key: T;
description?: string;
alternateMatches?: string[];
validations?: ({ rule: string } & Record<string, any>)[];
fieldType: FieldType;
width?: number;
disabled?: boolean;
onChange?: (value: string) => void;
}
type CellProps = {
value: any;
onChange: (value: any) => void;
error?: { level: string; message: string };
field: Field<string>;
}
// Define ValidationIcon before EditableCell
const ValidationIcon = memo(({ error }: { error: { level: string, message: string } }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="absolute right-2 top-2 text-destructive">
<AlertCircle className="h-4 w-4" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{error.message}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))
// Wrap EditableCell with memo to avoid unnecessary re-renders
const EditableCell = memo(({ value, onChange, error, field }: CellProps) => {
const [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(value ?? "")
const [searchQuery, setSearchQuery] = useState("")
const [localValues, setLocalValues] = useState<string[]>([])
const handleWheel = useCallback((e: React.WheelEvent) => {
const commandList = e.currentTarget;
commandList.scrollTop += e.deltaY;
e.stopPropagation();
}, []);
// Update input value when external value changes and we're not editing
useEffect(() => {
if (!isEditing) {
@@ -97,6 +153,12 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
}
}, [value, field.fieldType.type])
const formatPrice = (value: string) => {
if (!value) return value
// Remove dollar signs and trim
return value.replace(/^\$/, '').trim()
}
const validateRegex = (val: any) => {
// Handle non-string values
if (val === undefined || val === null) return undefined
@@ -115,8 +177,14 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
const regexValidation = field.validations?.find(v => v.rule === "regex")
if (regexValidation) {
let testValue = strVal
// For price fields, remove dollar sign before testing
if (isPriceField(field.fieldType)) {
testValue = formatPrice(strVal)
}
const regex = new RegExp(regexValidation.value, regexValidation.flags)
if (!regex.test(strVal)) {
if (!regex.test(testValue)) {
return { level: regexValidation.level || "error", message: regexValidation.errorMessage }
}
}
@@ -134,10 +202,10 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
const currentError = getValidationError()
const getDisplayValue = (value: any, fieldType: Field<string>["fieldType"]) => {
if (fieldType.type === "select") {
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value
}
if (fieldType.type === "multi-select") {
if (fieldType.type === "select" || fieldType.type === "multi-select") {
if (fieldType.type === "select") {
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value
}
if (Array.isArray(value)) {
return value.map(v => fieldType.options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ")
}
@@ -160,6 +228,11 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
return true
}
// Handle price fields
if (isPriceField(field.fieldType)) {
newValue = formatPrice(newValue)
}
// Always commit the value
onChange(newValue)
@@ -172,27 +245,6 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
setIsEditing(false)
}
const ValidationIcon = ({ error }: { error: { level: string, message: string } }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="absolute right-2 top-2 text-destructive">
<AlertCircle className="h-4 w-4" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{error.message}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
const handleWheel = (e: React.WheelEvent) => {
const commandList = e.currentTarget;
commandList.scrollTop += e.deltaY;
e.stopPropagation();
};
if (isEditing) {
switch (field.fieldType.type) {
case "select":
@@ -353,8 +405,59 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
{currentError && <ValidationIcon error={currentError} />}
</div>
)
case "input":
case "multi-input":
default:
if (field.fieldType.multiline) {
return (
<div className="relative" id={`cell-${field.key}`}>
<Popover
open={isEditing}
onOpenChange={(open) => {
if (!open) {
validateAndCommit(inputValue)
setIsEditing(false)
}
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full h-[36px] justify-start font-normal"
>
<div className="truncate">
{inputValue || "Click to edit..."}
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[400px] p-3"
align="start"
side="bottom"
>
<div className="space-y-2">
<h3 className="font-medium text-sm">{field.label}</h3>
<textarea
className="w-full min-h-[150px] p-2 border rounded-md"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
if (validateAndCommit(inputValue)) {
setIsEditing(false)
}
}
}}
autoFocus
placeholder={`Press ${navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+Enter to save`}
/>
{currentError && <ValidationIcon error={currentError} />}
</div>
</PopoverContent>
</Popover>
</div>
)
}
return (
<div className="relative" id={`cell-${field.key}`}>
<Input
@@ -365,9 +468,9 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
onBlur={handleBlur}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (field.fieldType.type === "multi-input") {
const separator = (field.fieldType as MultiInput).separator || ","
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean)
if (isMultiInputType(field.fieldType)) {
const separator = getMultiInputSeparator(field.fieldType);
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean);
if (validateAndCommit(values.join(separator))) {
setIsEditing(false)
}
@@ -381,9 +484,47 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
currentError ? "border-destructive" : ""
)}
autoFocus
placeholder={field.fieldType.type === "multi-input"
? `Enter values separated by ${(field.fieldType as MultiInput).separator || ","}`
: undefined}
placeholder={
isMultiInputType(field.fieldType)
? `Enter values separated by ${getMultiInputSeparator(field.fieldType)}`
: undefined
}
/>
{currentError && <ValidationIcon error={currentError} />}
</div>
)
default:
return (
<div className="relative" id={`cell-${field.key}`}>
<Input
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value)
}}
onBlur={handleBlur}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (isMultiInputType(field.fieldType)) {
const separator = getMultiInputSeparator(field.fieldType);
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean);
if (validateAndCommit(values.join(separator))) {
setIsEditing(false)
}
} else if (validateAndCommit(inputValue)) {
setIsEditing(false)
}
}
}}
className={cn(
"w-full",
currentError ? "border-destructive" : ""
)}
autoFocus
placeholder={
isMultiInputType(field.fieldType)
? `Enter values separated by ${getMultiInputSeparator(field.fieldType)}`
: undefined
}
/>
{currentError && <ValidationIcon error={currentError} />}
</div>
@@ -397,38 +538,79 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
id={`cell-${field.key}`}
onClick={(e) => {
if (field.fieldType.type !== "checkbox" && !field.disabled) {
e.stopPropagation() // Prevent event bubbling
e.stopPropagation()
setIsEditing(true)
setInputValue(Array.isArray(value) ? value.join(", ") : value ?? "")
}
}}
className={cn(
"relative min-h-[36px] cursor-text p-2 rounded-md border bg-background",
(field.fieldType.type === "input" || field.fieldType.type === "multi-input") &&
field.fieldType.multiline && "max-h-[100px] overflow-y-auto",
currentError ? "border-destructive" : "border-input",
field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between",
field.disabled && "opacity-50 cursor-not-allowed bg-muted"
)}
>
<div className={cn("flex-1 overflow-hidden text-ellipsis", !value && "text-muted-foreground")}>
{value ? getDisplayValue(value, field.fieldType) : ""}
</div>
{((field.fieldType.type === "input" || field.fieldType.type === "multi-input") && field.fieldType.multiline) ? (
<div className={cn(
"flex-1 overflow-hidden",
!value && "text-muted-foreground"
)}>
<div className="line-clamp-2 whitespace-pre-wrap">
{value || ""}
</div>
{value && value.length > 100 && (
<Button
variant="link"
className="h-4 p-0 text-xs"
onClick={(e) => {
e.stopPropagation()
setIsEditing(true)
}}
>
Show more...
</Button>
)}
</div>
) : (
<div className={cn("flex-1 overflow-hidden text-ellipsis", !value && "text-muted-foreground")}>
{value ? getDisplayValue(value, field.fieldType) : ""}
</div>
)}
{(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && (
<ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} />
)}
{currentError && <ValidationIcon error={currentError} />}
</div>
)
})
// Update type guard to be more specific
function isMultiInputType(fieldType: FieldType): fieldType is MultiInputFieldType {
return fieldType.type === "multi-input";
}
// Add this component for the column header with copy down functionality
const ColumnHeader = <T extends string>({
function getMultiInputSeparator(fieldType: FieldType): string {
if (isMultiInputType(fieldType)) {
return fieldType.separator || ",";
}
return ",";
}
function isPriceField(fieldType: FieldType): fieldType is (InputFieldType | MultiInputFieldType) & { price: true } {
return (fieldType.type === "input" || fieldType.type === "multi-input") && 'price' in fieldType && fieldType.price === true;
}
// Wrap ColumnHeader with memo so that it re-renders only when its props change
const ColumnHeader = memo(({
field,
data,
onCopyDown
}: {
field: Field<T>,
data: (Data<T> & Meta)[],
onCopyDown: (key: T) => void
field: Field<string>
data: (Data<string> & Meta)[]
onCopyDown: (key: string) => void
}) => {
return (
<div className="flex items-center gap-2">
@@ -442,7 +624,7 @@ const ColumnHeader = <T extends string>({
className="h-4 w-4 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
onCopyDown(field.key as T)
onCopyDown(field.key)
}}
title="Copy first row's value down"
>
@@ -451,10 +633,10 @@ const ColumnHeader = <T extends string>({
)}
</div>
)
}
})
// Add this component for the copy down confirmation dialog
const CopyDownDialog = ({
const CopyDownDialog = memo(({
isOpen,
onClose,
onConfirm,
@@ -493,7 +675,7 @@ const CopyDownDialog = ({
</AlertDialogPortal>
</AlertDialog>
)
}
})
// Add type utilities at the top level
type DeepReadonlyField<T extends string> = {
@@ -523,6 +705,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
const [showSubmitAlert, setShowSubmitAlert] = useState(false)
const [isSubmitting, setSubmitting] = useState(false)
const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null)
const [isAiValidating, setIsAiValidating] = useState(false)
// Memoize filtered data to prevent recalculation on every render
const filteredData = useMemo(() => {
@@ -596,18 +779,22 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
<div className="flex h-full items-center justify-center">
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
</div>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
<div className="flex items-start justify-center pt-2">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
enableSorting: false,
enableHiding: false,
@@ -617,10 +804,10 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
accessorKey: field.key,
header: () => (
<div className="group">
<ColumnHeader
field={field as Field<T>}
<ColumnHeader
field={field as Field<string>}
data={data}
onCopyDown={(key) => copyValueDown(key, field.label)}
onCopyDown={(key: string) => copyValueDown(key as T, field.label)}
/>
</div>
),
@@ -659,14 +846,14 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
getCoreRowModel: getCoreRowModel(),
})
const deleteSelectedRows = () => {
const deleteSelectedRows = useCallback(() => {
if (Object.keys(rowSelection).length) {
const selectedRows = Object.keys(rowSelection).map(Number)
const newData = data.filter((_, index) => !selectedRows.includes(index))
updateData(newData)
setRowSelection({})
}
}
}, [rowSelection, data, updateData]);
const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => {
if (field.fieldType.type === "checkbox") {
@@ -694,7 +881,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
return value
}, [])
const submitData = async () => {
const submitData = useCallback(async () => {
const calculatedData = data.reduce(
(acc, value) => {
const { __index, __errors, ...values } = value
@@ -743,18 +930,93 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
} else {
onClose()
}
}
const onContinue = () => {
}, [data, fields, file, onClose, onSubmit, normalizeValue, toast, translations]);
const onContinue = useCallback(() => {
const invalidData = data.find((value) => {
if (value?.__errors) {
return !!Object.values(value.__errors)?.filter((err) => err.level === "error").length
return !!Object.values(value.__errors)?.filter((err) => err.level === "error").length;
}
return false
})
return false;
});
if (!invalidData) {
submitData()
submitData();
} else {
setShowSubmitAlert(true)
setShowSubmitAlert(true);
}
}, [data, submitData]);
// Add AI validation function
const handleAiValidation = async () => {
try {
setIsAiValidating(true)
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ products: data }),
})
if (!response.ok) {
throw new Error('AI validation failed')
}
const result = await response.json()
if (!result.success) {
throw new Error(result.error || 'AI validation failed')
}
// Update the data with AI suggestions
if (result.correctedData && Array.isArray(result.correctedData)) {
// Preserve the __index and __errors from the original data
const newData = result.correctedData.map((item: any, idx: number) => ({
...item,
__index: data[idx]?.__index,
__errors: data[idx]?.__errors,
}))
// Update the data and run validations
await updateData(newData)
}
// Show changes and warnings
if (result.changes?.length) {
toast({
title: "AI Validation Changes",
description: (
<div className="mt-2 space-y-2">
{result.changes.map((change: string, i: number) => (
<div key={i} className="text-sm">• {change}</div>
))}
</div>
),
})
}
if (result.warnings?.length) {
toast({
title: "AI Validation Warnings",
description: (
<div className="mt-2 space-y-2">
{result.warnings.map((warning: string, i: number) => (
<div key={i} className="text-sm">• {warning}</div>
))}
</div>
),
variant: "destructive",
})
}
} catch (error) {
console.error('AI Validation Error:', error)
toast({
title: "AI Validation Error",
description: error instanceof Error ? error.message : "An error occurred during AI validation",
variant: "destructive",
})
} finally {
setIsAiValidating(false)
}
}
@@ -808,6 +1070,17 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
>
{translations.validationStep.discardButtonTitle}
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleAiValidation}
disabled={isAiValidating || data.length === 0}
>
{isAiValidating && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
AI Validate
</Button>
<div className="flex items-center gap-2">
<Switch
checked={filterByErrors}
@@ -857,7 +1130,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="p-2"
className="p-2 align-top"
style={{
width: cell.column.getSize(),
minWidth: cell.column.getSize(),

View File

@@ -70,7 +70,7 @@ export type Field<T extends string = string> = {
// Validations used for field entries
validations?: Validation[]
// Field entry component
fieldType: Checkbox | Select | Input | MultiInput | MultiSelect
fieldType: FieldType
// UI-facing values shown to user as field examples pre-upload phase
example?: string
width?: number
@@ -78,17 +78,22 @@ export type Field<T extends string = string> = {
onChange?: (value: string) => void
}
export type Checkbox = {
type: "checkbox"
// Alternate values to be treated as booleans, e.g. {yes: true, no: false}
booleanMatches?: { [key: string]: boolean }
}
export type Select = {
type: "select"
// Options displayed in Select component
options: SelectOption[]
}
export type FieldType =
| {
type: "input" | "multi-input";
multiline?: boolean;
price?: boolean;
separator?: string;
}
| {
type: "select" | "multi-select";
options: readonly SelectOption[];
separator?: string;
}
| {
type: "checkbox";
booleanMatches?: { readonly [key: string]: boolean };
};
export type SelectOption = {
// UI-facing option label

View File

@@ -20,7 +20,7 @@ const BASE_IMPORT_FIELDS = [
type: "select" as const,
options: [], // Will be populated from API
},
width: 200,
width: 220,
validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }],
},
{
@@ -29,7 +29,7 @@ const BASE_IMPORT_FIELDS = [
description: "Universal Product Code/Barcode",
alternateMatches: ["barcode", "bar code", "JAN", "EAN"],
fieldType: { type: "input" },
width: 150,
width: 140,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -42,7 +42,7 @@ const BASE_IMPORT_FIELDS = [
description: "Supplier's product identifier",
alternateMatches: ["sku", "item#", "mfg item #", "item"],
fieldType: { type: "input" },
width: 120,
width: 180,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -53,7 +53,7 @@ const BASE_IMPORT_FIELDS = [
key: "notions_no",
description: "Internal notions number",
fieldType: { type: "input" },
width: 120,
width: 110,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -66,7 +66,7 @@ const BASE_IMPORT_FIELDS = [
description: "Product name/title",
alternateMatches: ["sku description"],
fieldType: { type: "input" },
width: 300,
width: 500,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -88,14 +88,17 @@ const BASE_IMPORT_FIELDS = [
key: "image_url",
description: "Product image URL(s)",
fieldType: { type: "multi-input" },
width: 250,
width: 300,
},
{
label: "MSRP",
key: "msrp",
description: "Manufacturer's Suggested Retail Price",
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. Retail"],
fieldType: { type: "input" },
fieldType: {
type: "input",
price: true
},
width: 100,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
@@ -108,7 +111,7 @@ const BASE_IMPORT_FIELDS = [
description: "Quantity of items per individual unit",
alternateMatches: ["inner pack", "inner", "min qty", "unit qty", "min. order qty"],
fieldType: { type: "input" },
width: 100,
width: 90,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
@@ -119,7 +122,10 @@ const BASE_IMPORT_FIELDS = [
key: "cost_each",
description: "Wholesale cost per unit",
alternateMatches: ["wholesale", "wholesale price"],
fieldType: { type: "input" },
fieldType: {
type: "input",
price: true
},
width: 100,
validations: [
{ rule: "required", errorMessage: "Required", level: "error" },
@@ -132,7 +138,7 @@ const BASE_IMPORT_FIELDS = [
description: "Number of units per case",
alternateMatches: ["mc qty"],
fieldType: { type: "input" },
width: 100,
width: 50,
validations: [
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
],
@@ -142,10 +148,10 @@ const BASE_IMPORT_FIELDS = [
key: "tax_cat",
description: "Product tax category",
fieldType: {
type: "multi-select",
type: "select",
options: [], // Will be populated from API
},
width: 150,
width: 180,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -167,7 +173,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated dynamically based on company selection
},
width: 150,
width: 180,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -178,7 +184,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated dynamically based on line selection
},
width: 150,
width: 180,
},
{
label: "Artist",
@@ -188,7 +194,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 200,
width: 180,
},
{
label: "ETA Date",
@@ -250,7 +256,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 150,
width: 190,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -270,9 +276,9 @@ const BASE_IMPORT_FIELDS = [
description: "Harmonized Tariff Schedule code",
alternateMatches: ["taric"],
fieldType: { type: "input" },
width: 120,
width: 130,
validations: [
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
{ rule: "regex", value: "^[0-9.]+$", errorMessage: "Must be a number", level: "error" },
],
},
{
@@ -283,13 +289,16 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 150,
width: 180,
},
{
label: "Description",
key: "description",
description: "Detailed product description",
fieldType: { type: "input" },
fieldType: {
type: "input",
multiline: true
},
width: 400,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
@@ -297,7 +306,10 @@ const BASE_IMPORT_FIELDS = [
label: "Private Notes",
key: "priv_notes",
description: "Internal notes about the product",
fieldType: { type: "input" },
fieldType: {
type: "input",
multiline: true
},
width: 300,
},
{
@@ -308,7 +320,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 200,
width: 350,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
@@ -319,7 +331,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 200,
width: 300,
},
{
label: "Colors",
@@ -329,7 +341,7 @@ const BASE_IMPORT_FIELDS = [
type: "select",
options: [], // Will be populated from API
},
width: 150,
width: 180,
},
] as const;