AI validation tweaks, add templates settings page and schema and routes
This commit is contained in:
@@ -660,75 +660,83 @@ export function DataManagement() {
|
||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{importHistory.slice(0, 20).map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-transparent">
|
||||
<TableCell className="w-full p-0">
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem
|
||||
value={`import-${record.id}`}
|
||||
className="border-0"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2">
|
||||
<div className="flex justify-between items-start w-full pr-4">
|
||||
<span className="font-medium min-w-[60px]">
|
||||
#{record.id}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 min-w-[120px]">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
<span className="text-sm min-w-[100px]">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`min-w-[80px] ${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2">
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">End Time:</span>
|
||||
<span>
|
||||
{record.end_time
|
||||
? formatDate(record.end_time)
|
||||
: "N/A"}
|
||||
{importHistory.length > 0 ? (
|
||||
importHistory.slice(0, 20).map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-transparent">
|
||||
<TableCell className="w-full p-0">
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem
|
||||
value={`import-${record.id}`}
|
||||
className="border-0"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2">
|
||||
<div className="flex justify-between items-start w-full pr-4">
|
||||
<span className="font-medium min-w-[60px]">
|
||||
#{record.id}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 min-w-[120px]">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
<span className="text-sm min-w-[100px]">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`min-w-[80px] ${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Records:</span>
|
||||
<span>
|
||||
{record.records_added} added,{" "}
|
||||
{record.records_updated} updated
|
||||
</span>
|
||||
</div>
|
||||
{record.error_message && (
|
||||
<div className="text-sm text-red-600 mt-2">
|
||||
{record.error_message}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2">
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">End Time:</span>
|
||||
<span>
|
||||
{record.end_time
|
||||
? formatDate(record.end_time)
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{record.additional_info &&
|
||||
formatJsonData(record.additional_info)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Records:</span>
|
||||
<span>
|
||||
{record.records_added} added,{" "}
|
||||
{record.records_updated} updated
|
||||
</span>
|
||||
</div>
|
||||
{record.error_message && (
|
||||
<div className="text-sm text-red-600 mt-2">
|
||||
{record.error_message}
|
||||
</div>
|
||||
)}
|
||||
{record.additional_info &&
|
||||
formatJsonData(record.additional_info)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="text-center text-sm text-muted-foreground py-4">
|
||||
No import history available
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
@@ -742,90 +750,98 @@ export function DataManagement() {
|
||||
<CardContent className="px-4 mb-4 max-h-[300px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{calculateHistory.slice(0, 20).map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-transparent">
|
||||
<TableCell className="w-full p-0">
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem
|
||||
value={`calc-${record.id}`}
|
||||
className="border-0"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2">
|
||||
<div className="flex justify-between items-start w-full pr-4">
|
||||
<span className="font-medium min-w-[60px]">
|
||||
#{record.id}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 min-w-[120px]">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
<span className="text-sm min-w-[100px]">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
{calculateHistory.length > 0 ? (
|
||||
calculateHistory.slice(0, 20).map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-transparent">
|
||||
<TableCell className="w-full p-0">
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem
|
||||
value={`calc-${record.id}`}
|
||||
className="border-0"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2">
|
||||
<div className="flex justify-between items-start w-full pr-4">
|
||||
<span className="font-medium min-w-[60px]">
|
||||
#{record.id}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 min-w-[120px]">
|
||||
{formatDate(record.start_time)}
|
||||
</span>
|
||||
<span className="text-sm min-w-[100px]">
|
||||
{formatDurationWithSeconds(
|
||||
record.duration_minutes,
|
||||
record.status === "running",
|
||||
record.start_time
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`min-w-[80px] ${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2">
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">End Time:</span>
|
||||
<span>
|
||||
{record.end_time
|
||||
? formatDate(record.end_time)
|
||||
: "N/A"}
|
||||
<span
|
||||
className={`min-w-[80px] ${
|
||||
record.status === "completed"
|
||||
? "text-green-600"
|
||||
: record.status === "failed"
|
||||
? "text-red-600"
|
||||
: record.status === "cancelled"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
>
|
||||
{record.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Products:
|
||||
</span>
|
||||
<span>{record.processed_products}</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-2">
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">End Time:</span>
|
||||
<span>
|
||||
{record.end_time
|
||||
? formatDate(record.end_time)
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Products:
|
||||
</span>
|
||||
<span>{record.processed_products}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Orders:
|
||||
</span>
|
||||
<span>{record.processed_orders}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Purchase Orders:
|
||||
</span>
|
||||
<span>{record.processed_purchase_orders}</span>
|
||||
</div>
|
||||
{record.error_message && (
|
||||
<div className="text-sm text-red-600 mt-2">
|
||||
{record.error_message}
|
||||
</div>
|
||||
)}
|
||||
{record.additional_info &&
|
||||
formatJsonData(record.additional_info)}
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Orders:
|
||||
</span>
|
||||
<span>{record.processed_orders}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
Processed Purchase Orders:
|
||||
</span>
|
||||
<span>{record.processed_purchase_orders}</span>
|
||||
</div>
|
||||
{record.error_message && (
|
||||
<div className="text-sm text-red-600 mt-2">
|
||||
{record.error_message}
|
||||
</div>
|
||||
)}
|
||||
{record.additional_info &&
|
||||
formatJsonData(record.additional_info)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="text-center text-sm text-muted-foreground py-4">
|
||||
No calculation history available
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
422
inventory/src/components/settings/TemplateManagement.tsx
Normal file
422
inventory/src/components/settings/TemplateManagement.tsx
Normal file
@@ -0,0 +1,422 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { toast } from "sonner";
|
||||
import config from "@/config";
|
||||
|
||||
interface Template {
|
||||
id: number;
|
||||
company: string;
|
||||
product_type: string;
|
||||
supplier?: string;
|
||||
msrp?: number;
|
||||
cost_each?: number;
|
||||
qty_per_unit?: number;
|
||||
case_qty?: number;
|
||||
hts_code?: string;
|
||||
description?: string;
|
||||
weight?: number;
|
||||
length?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
tax_cat?: string;
|
||||
size_cat?: string;
|
||||
categories?: string[];
|
||||
ship_restrictions?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TemplateFormData extends Omit<Template, 'id' | 'created_at' | 'updated_at'> {}
|
||||
|
||||
export function TemplateManagement() {
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [formData, setFormData] = useState<TemplateFormData>({
|
||||
company: "",
|
||||
product_type: "",
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: templates, isLoading } = useQuery<Template[]>({
|
||||
queryKey: ["templates"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/templates`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch templates");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data: TemplateFormData) => {
|
||||
const response = await fetch(`${config.apiUrl}/templates`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create template");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
setIsCreateOpen(false);
|
||||
setFormData({ company: "", product_type: "" });
|
||||
toast.success("Template created successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to create template");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const response = await fetch(`${config.apiUrl}/templates/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete template");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||
toast.success("Template deleted successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to delete template");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
createMutation.mutate(formData);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleArrayInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value.split(",").map((item) => item.trim()),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleNumberInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value === "" ? undefined : Number(value),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Import Templates</h2>
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Create Template</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Import Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new template for importing products. Company and Product Type combination must be unique.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ScrollArea className="h-[60vh]">
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company">Company *</Label>
|
||||
<Input
|
||||
id="company"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="product_type">Product Type *</Label>
|
||||
<Input
|
||||
id="product_type"
|
||||
name="product_type"
|
||||
value={formData.product_type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="supplier">Supplier</Label>
|
||||
<Input
|
||||
id="supplier"
|
||||
name="supplier"
|
||||
value={formData.supplier || ""}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="msrp">MSRP</Label>
|
||||
<Input
|
||||
id="msrp"
|
||||
name="msrp"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.msrp || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cost_each">Cost Each</Label>
|
||||
<Input
|
||||
id="cost_each"
|
||||
name="cost_each"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.cost_each || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="qty_per_unit">Quantity per Unit</Label>
|
||||
<Input
|
||||
id="qty_per_unit"
|
||||
name="qty_per_unit"
|
||||
type="number"
|
||||
value={formData.qty_per_unit || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="case_qty">Case Quantity</Label>
|
||||
<Input
|
||||
id="case_qty"
|
||||
name="case_qty"
|
||||
type="number"
|
||||
value={formData.case_qty || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hts_code">HTS Code</Label>
|
||||
<Input
|
||||
id="hts_code"
|
||||
name="hts_code"
|
||||
value={formData.hts_code || ""}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description || ""}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="weight">Weight</Label>
|
||||
<Input
|
||||
id="weight"
|
||||
name="weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.weight || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="length">Length</Label>
|
||||
<Input
|
||||
id="length"
|
||||
name="length"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.length || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<Input
|
||||
id="width"
|
||||
name="width"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.width || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="height">Height</Label>
|
||||
<Input
|
||||
id="height"
|
||||
name="height"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.height || ""}
|
||||
onChange={handleNumberInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tax_cat">Tax Category</Label>
|
||||
<Input
|
||||
id="tax_cat"
|
||||
name="tax_cat"
|
||||
value={formData.tax_cat || ""}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="size_cat">Size Category</Label>
|
||||
<Input
|
||||
id="size_cat"
|
||||
name="size_cat"
|
||||
value={formData.size_cat || ""}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="categories">Categories (comma-separated)</Label>
|
||||
<Input
|
||||
id="categories"
|
||||
name="categories"
|
||||
value={formData.categories?.join(", ") || ""}
|
||||
onChange={handleArrayInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ship_restrictions">
|
||||
Shipping Restrictions (comma-separated)
|
||||
</Label>
|
||||
<Input
|
||||
id="ship_restrictions"
|
||||
name="ship_restrictions"
|
||||
value={formData.ship_restrictions?.join(", ") || ""}
|
||||
onChange={handleArrayInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? "Creating..." : "Create Template"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div>Loading templates...</div>
|
||||
) : (
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Company</TableHead>
|
||||
<TableHead>Product Type</TableHead>
|
||||
<TableHead>Supplier</TableHead>
|
||||
<TableHead>Last Updated</TableHead>
|
||||
<TableHead className="w-[100px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{templates?.map((template) => (
|
||||
<TableRow key={template.id}>
|
||||
<TableCell>{template.company}</TableCell>
|
||||
<TableCell>{template.product_type}</TableCell>
|
||||
<TableCell>{template.supplier || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{new Date(template.updated_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete this template?"
|
||||
)
|
||||
) {
|
||||
deleteMutation.mutate(template.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{templates?.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center">
|
||||
No templates found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -744,6 +744,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
prompt: null,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
|
||||
// Memoize filtered data to prevent recalculation on every render
|
||||
const filteredData = useMemo(() => {
|
||||
@@ -893,6 +894,59 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
}
|
||||
}, [rowSelection, data, updateData]);
|
||||
|
||||
const discardEmptyAndDuplicateRows = useCallback(() => {
|
||||
// Helper function to count non-empty values in a row
|
||||
const countNonEmptyValues = (values: Record<string, any>): number => {
|
||||
return Object.values(values).filter(val =>
|
||||
val !== undefined &&
|
||||
val !== null &&
|
||||
(typeof val === 'string' ? val.trim() !== '' : true)
|
||||
).length;
|
||||
};
|
||||
|
||||
// First, analyze all rows to determine if we have rows with multiple values
|
||||
const rowsWithValues = data.map(row => {
|
||||
const { __index, __errors, ...values } = row;
|
||||
return countNonEmptyValues(values);
|
||||
});
|
||||
|
||||
// Check if we have any rows with more than one value
|
||||
const hasMultiValueRows = rowsWithValues.some(count => count > 1);
|
||||
|
||||
// Filter out empty rows and rows with single values (if we have multi-value rows)
|
||||
const nonEmptyRows = data.filter((row, index) => {
|
||||
const nonEmptyCount = rowsWithValues[index];
|
||||
|
||||
// Keep the row if:
|
||||
// 1. It has more than one value, OR
|
||||
// 2. It has exactly one value AND we don't have any rows with multiple values
|
||||
return nonEmptyCount > 0 && (!hasMultiValueRows || nonEmptyCount > 1);
|
||||
});
|
||||
|
||||
// Then, remove duplicates by creating a unique string representation of each row
|
||||
const seen = new Set<string>();
|
||||
const uniqueRows = nonEmptyRows.filter(row => {
|
||||
const { __index, __errors, ...values } = row;
|
||||
const rowStr = JSON.stringify(Object.entries(values).sort());
|
||||
if (seen.has(rowStr)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(rowStr);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Only update if we actually removed any rows
|
||||
if (uniqueRows.length < data.length) {
|
||||
updateData(uniqueRows);
|
||||
setRowSelection({});
|
||||
toast({
|
||||
title: "Rows removed",
|
||||
description: `Removed ${data.length - uniqueRows.length} empty, single-value, or duplicate rows`,
|
||||
variant: "default"
|
||||
});
|
||||
}
|
||||
}, [data, updateData, toast]);
|
||||
|
||||
const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => {
|
||||
if (field.fieldType.type === "checkbox") {
|
||||
if (typeof value === "boolean") return value
|
||||
@@ -1218,7 +1272,12 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialog>
|
||||
<Dialog open={aiValidationProgress.isOpen} onOpenChange={() => {}}>
|
||||
<Dialog open={aiValidationProgress.isOpen} onOpenChange={(open) => {
|
||||
// Only allow closing if validation failed
|
||||
if (!open && aiValidationProgress.step === -1) {
|
||||
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Validation Progress</DialogTitle>
|
||||
@@ -1302,6 +1361,13 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
>
|
||||
{translations.validationStep.discardButtonTitle}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={discardEmptyAndDuplicateRows}
|
||||
>
|
||||
Remove Empty/Duplicates
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
||||
@@ -37,7 +37,24 @@ export function AiValidationDebug() {
|
||||
const fetchDebugData = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/debug`)
|
||||
// Use a sample product to avoid loading full taxonomy
|
||||
const sampleProduct = {
|
||||
title: "Sample Product",
|
||||
description: "A sample product for testing",
|
||||
SKU: "SAMPLE-001",
|
||||
price: "9.99",
|
||||
cost_each: "5.00",
|
||||
qty_per_unit: "1",
|
||||
case_qty: "12"
|
||||
}
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/debug`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ products: [sampleProduct] })
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch debug data')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DataManagement } from "@/components/settings/DataManagement";
|
||||
import { StockManagement } from "@/components/settings/StockManagement";
|
||||
import { PerformanceMetrics } from "@/components/settings/PerformanceMetrics";
|
||||
import { CalculationSettings } from "@/components/settings/CalculationSettings";
|
||||
import { TemplateManagement } from "@/components/settings/TemplateManagement";
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
export function Settings() {
|
||||
@@ -22,6 +23,9 @@ export function Settings() {
|
||||
<TabsTrigger value="calculation-settings">
|
||||
Calculation Settings
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="templates">
|
||||
Import Templates
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="data-management">
|
||||
@@ -39,6 +43,10 @@ export function Settings() {
|
||||
<TabsContent value="calculation-settings">
|
||||
<CalculationSettings />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="templates">
|
||||
<TemplateManagement />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user