Connect with database for dropdowns, more validate data step fixes

This commit is contained in:
2025-02-19 21:32:23 -05:00
parent 43d7775d08
commit 24e2d01ccc
8 changed files with 646 additions and 180 deletions

View File

@@ -101,7 +101,6 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
}
const isRequired = field.validations?.some(v => v.rule === "required")
const isRequiredAndEmpty = isRequired && !value
// Determine the current validation state
const getValidationState = () => {
@@ -170,6 +169,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
"w-full justify-between",
currentError ? "border-destructive text-destructive" : "border-input"
)}
disabled={field.disabled}
>
{value
? field.fieldType.options.find((option) => option.value === value)?.label
@@ -189,6 +189,9 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
value={option.value}
onSelect={(currentValue) => {
onChange(currentValue)
if (field.onChange) {
field.onChange(currentValue)
}
setIsEditing(false)
}}
>
@@ -335,7 +338,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
return (
<div
onClick={() => {
if (field.fieldType.type !== "checkbox") {
if (field.fieldType.type !== "checkbox" && !field.disabled) {
setIsEditing(true)
setInputValue(Array.isArray(value) ? value.join(", ") : value ?? "")
}
@@ -343,14 +346,15 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
className={cn(
"min-h-[36px] cursor-text p-2 rounded-md border bg-background",
currentError ? "border-destructive" : "border-input",
field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between"
field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between",
field.disabled && "opacity-50 cursor-not-allowed bg-muted"
)}
>
<div className={cn(!value && "text-muted-foreground")}>
{value ? getDisplayValue(value, field.fieldType) : ""}
</div>
{(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && (
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
<ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} />
)}
{currentError && (
<div className="absolute left-0 -bottom-5 text-xs text-destructive">
@@ -376,7 +380,7 @@ const ColumnHeader = <T extends string>({
<div className="flex-1 overflow-hidden text-ellipsis">
{field.label}
</div>
{data.length > 1 && (
{data.length > 1 && !field.disabled && (
<Button
variant="ghost"
size="icon"

View File

@@ -58,7 +58,7 @@ export type Data<T extends string> = { [key in T]: string | boolean | undefined
// Data model RSI uses for spreadsheet imports
export type Fields<T extends string> = DeepReadonly<Field<T>[]>
export type Field<T extends string> = {
export type Field<T extends string = string> = {
// UI-facing field label
label: string
// Field's unique identifier
@@ -73,6 +73,9 @@ export type Field<T extends string> = {
fieldType: Checkbox | Select | Input | MultiInput | MultiSelect
// UI-facing values shown to user as field examples pre-upload phase
example?: string
width?: number
disabled?: boolean
onChange?: (value: string) => void
}
export type Checkbox = {

View File

@@ -1,27 +1,27 @@
import { useState } from "react";
import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src";
import type { Field, Fields, Validation, ErrorLevel } from "@/lib/react-spreadsheet-import/src/types";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Code } from "@/components/ui/code";
import { toast } from "sonner";
import { motion } from "framer-motion";
import { useQuery } from "@tanstack/react-query";
import config from "@/config";
import { StepType } from "@/lib/react-spreadsheet-import/src/steps/UploadFlow";
const IMPORT_FIELDS = [
// Define base fields without dynamic options
const BASE_IMPORT_FIELDS = [
{
label: "Supplier",
key: "supplier",
description: "Primary supplier/manufacturer of the product",
fieldType: {
type: "select",
options: [
{ label: "Acme Corp", value: "acme" },
{ label: "Global Supplies", value: "global" },
{ label: "Best Manufacturers", value: "best" },
{ label: "Quality Goods", value: "quality" },
],
type: "select" as const,
options: [], // Will be populated from API
},
width: 200,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }],
},
{
label: "UPC",
@@ -143,12 +143,7 @@ const IMPORT_FIELDS = [
description: "Product tax category",
fieldType: {
type: "multi-select",
options: [
{ label: "Standard", value: "standard" },
{ label: "Reduced", value: "reduced" },
{ label: "Zero", value: "zero" },
{ label: "Exempt", value: "exempt" },
],
options: [], // Will be populated from API
},
width: 150,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
@@ -157,7 +152,10 @@ const IMPORT_FIELDS = [
label: "Company",
key: "company",
description: "Company/Brand name",
fieldType: { type: "input" },
fieldType: {
type: "select",
options: [], // Will be populated from API
},
width: 200,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
@@ -165,21 +163,31 @@ const IMPORT_FIELDS = [
label: "Line",
key: "line",
description: "Product line",
fieldType: { type: "input" },
fieldType: {
type: "select",
options: [], // Will be populated dynamically based on company selection
},
width: 150,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
},
{
label: "Sub Line",
key: "subline",
description: "Product sub-line",
fieldType: { type: "input" },
fieldType: {
type: "select",
options: [], // Will be populated dynamically based on line selection
},
width: 150,
},
{
label: "Artist",
key: "artist",
description: "Artist/Designer name",
fieldType: { type: "input" },
fieldType: {
type: "select",
options: [], // Will be populated from API
},
width: 200,
},
{
@@ -240,12 +248,7 @@ const IMPORT_FIELDS = [
description: "Product shipping restrictions",
fieldType: {
type: "select",
options: [
{ label: "None", value: "none" },
{ label: "Hazmat", value: "hazmat" },
{ label: "Oversize", value: "oversize" },
{ label: "Restricted", value: "restricted" },
],
options: [], // Will be populated from API
},
width: 150,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
@@ -278,12 +281,7 @@ const IMPORT_FIELDS = [
description: "Product size category",
fieldType: {
type: "select",
options: [
{ label: "Small", value: "small" },
{ label: "Medium", value: "medium" },
{ label: "Large", value: "large" },
{ label: "Extra Large", value: "xl" },
],
options: [], // Will be populated from API
},
width: 150,
},
@@ -308,12 +306,7 @@ const IMPORT_FIELDS = [
description: "Product categories",
fieldType: {
type: "select",
options: [
{ label: "Art Supplies", value: "art" },
{ label: "Crafts", value: "crafts" },
{ label: "Home Decor", value: "home" },
{ label: "Stationery", value: "stationery" },
],
options: [], // Will be populated from API
},
width: 200,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
@@ -324,12 +317,7 @@ const IMPORT_FIELDS = [
description: "Product themes/styles",
fieldType: {
type: "select",
options: [
{ label: "Modern", value: "modern" },
{ label: "Vintage", value: "vintage" },
{ label: "Nature", value: "nature" },
{ label: "Abstract", value: "abstract" },
],
options: [], // Will be populated from API
},
width: 200,
},
@@ -339,20 +327,180 @@ const IMPORT_FIELDS = [
description: "Product colors",
fieldType: {
type: "select",
options: [
{ label: "Red", value: "red" },
{ label: "Blue", value: "blue" },
{ label: "Green", value: "green" },
{ label: "Multi", value: "multi" },
],
options: [], // Will be populated from API
},
width: 150,
},
];
] as const;
type ImportField = typeof BASE_IMPORT_FIELDS[number];
type ImportFieldKey = ImportField["key"];
export function Import() {
const [isOpen, setIsOpen] = useState(false);
const [importedData, setImportedData] = useState<any[] | null>(null);
const [selectedCompany, setSelectedCompany] = useState<string | null>(null);
const [selectedLine, setSelectedLine] = useState<string | null>(null);
const [startFromScratch, setStartFromScratch] = useState(false);
// Fetch initial field options from the API
const { data: fieldOptions, isLoading: isLoadingOptions } = useQuery({
queryKey: ["import-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();
},
});
// Fetch product lines when company is selected
const { data: productLines } = useQuery({
queryKey: ["product-lines", selectedCompany],
queryFn: async () => {
if (!selectedCompany) return [];
const response = await fetch(`${config.apiUrl}/import/product-lines/${selectedCompany}`);
if (!response.ok) {
throw new Error("Failed to fetch product lines");
}
return response.json();
},
enabled: !!selectedCompany,
});
// Fetch sublines when line is selected
const { data: sublines } = useQuery({
queryKey: ["sublines", selectedLine],
queryFn: async () => {
if (!selectedLine) return [];
const response = await fetch(`${config.apiUrl}/import/sublines/${selectedLine}`);
if (!response.ok) {
throw new Error("Failed to fetch sublines");
}
return response.json();
},
enabled: !!selectedLine,
});
// Handle field value changes
const handleFieldChange = (field: string, value: any) => {
console.log('Field change:', field, value);
if (field === "company") {
setSelectedCompany(value);
setSelectedLine(null); // Reset line when company changes
} else if (field === "line") {
setSelectedLine(value);
}
};
// Merge base fields with dynamic options
const importFields = BASE_IMPORT_FIELDS.map(field => {
if (!fieldOptions) return field;
switch (field.key) {
case "company":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.companies || [],
},
onChange: (value: string) => {
console.log('Company selected:', value);
handleFieldChange("company", value);
},
};
case "line":
return {
...field,
fieldType: {
type: "select" as const,
options: productLines || [],
},
onChange: (value: string) => {
console.log('Line selected:', value);
handleFieldChange("line", value);
},
disabled: !selectedCompany,
};
case "subline":
return {
...field,
fieldType: {
type: "select" as const,
options: sublines || [],
},
disabled: !selectedLine,
};
case "colors":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.colors || [],
},
};
case "tax_cat":
return {
...field,
fieldType: {
type: "multi-select" as const,
options: fieldOptions.taxCategories || [],
},
};
case "ship_restrictions":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.shippingRestrictions || [],
},
};
case "supplier":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.suppliers || [],
},
};
case "artist":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.artists || [],
},
};
case "categories":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.categories || [],
},
};
case "themes":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.themes || [],
},
};
case "size_cat":
return {
...field,
fieldType: {
type: "select" as const,
options: fieldOptions.sizes || [],
},
};
default:
return field;
}
});
const handleData = async (data: any, file: File) => {
try {
@@ -367,6 +515,14 @@ export function Import() {
}
};
if (isLoadingOptions) {
return (
<div className="container mx-auto py-6">
<h1 className="text-3xl font-bold tracking-tight">Loading import options...</h1>
</div>
);
}
return (
<motion.div
layout
@@ -390,10 +546,20 @@ export function Import() {
<CardHeader>
<CardTitle>Import Data</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="space-y-4">
<Button onClick={() => setIsOpen(true)} className="w-full">
Upload Spreadsheet
</Button>
<Button
onClick={() => {
setStartFromScratch(true);
setIsOpen(true);
}}
variant="outline"
className="w-full"
>
Start From Scratch
</Button>
</CardContent>
</Card>
@@ -412,9 +578,13 @@ export function Import() {
<ReactSpreadsheetImport
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onClose={() => {
setIsOpen(false);
setStartFromScratch(false);
}}
onSubmit={handleData}
fields={IMPORT_FIELDS}
fields={importFields}
initialStepState={startFromScratch ? { type: StepType.validateData, data: [{}] } : undefined}
/>
</motion.div>
);