Tweak import results UI
This commit is contained in:
@@ -26,13 +26,13 @@
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--info: 217.2 91.2% 59.8%;
|
||||
--info-foreground: 210 40% 98%;
|
||||
--info-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--success: 142.1 76.2% 36.3%;
|
||||
--success-foreground: 210 40% 98%;
|
||||
--success-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--warning: 45.4 93.4% 47.5%;
|
||||
--warning-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
@@ -8,8 +8,10 @@ import { toast } from "sonner";
|
||||
import { motion } from "framer-motion";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import config from "@/config";
|
||||
import { Loader2, AlertCircle, AlertTriangle, Info, CheckCircle } from "lucide-react";
|
||||
import { Loader2, AlertCircle, AlertTriangle, Info, CheckCircle, ExternalLink } from "lucide-react";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { Data, DataValue, FieldType, Result, SubmitOptions } from "@/components/product-import/types";
|
||||
import { BASE_IMPORT_FIELDS, type ImportFieldKey } from "@/components/product-import/config";
|
||||
import { submitNewProducts, type SubmitNewProductsResponse } from "@/services/apiv2";
|
||||
@@ -254,6 +256,199 @@ export function Import() {
|
||||
const { user } = useContext(AuthContext);
|
||||
const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug"));
|
||||
|
||||
// ========== TEMPORARY TEST DATA ==========
|
||||
// Uncomment the useEffect below to test the results page without submitting actual data
|
||||
|
||||
// useEffect(() => {
|
||||
// // Test scenario: Mix of successful and failed products
|
||||
// const testSubmittedProducts: NormalizedProduct[] = [
|
||||
// {
|
||||
// name: "Test Product 1",
|
||||
// upc: "123456789012",
|
||||
// item_number: "ITEM-001",
|
||||
// company: "Test Company",
|
||||
// line: "Test Line",
|
||||
// subline: "Test Subline",
|
||||
// product_images: ["https://picsum.photos/200/200?random=1"],
|
||||
// short_description: "This is a test product",
|
||||
// retail: "29.99",
|
||||
// wholesale: "15.00",
|
||||
// weight: "1.5",
|
||||
// categories: ["Category 1", "Category 2"],
|
||||
// colors: ["Red", "Blue"],
|
||||
// size_cat: "Medium",
|
||||
// tax_cat: "Taxable",
|
||||
// ship_restrictions: "None",
|
||||
// supplier: "Test Supplier",
|
||||
// artist: null,
|
||||
// themes: ["Theme 1"],
|
||||
// vendor_sku: "VS-001",
|
||||
// publish: true,
|
||||
// list_on_marketplace: false,
|
||||
// },
|
||||
// {
|
||||
// name: "Test Product 2",
|
||||
// upc: "234567890123",
|
||||
// item_number: "ITEM-002",
|
||||
// company: "Test Company",
|
||||
// line: "Test Line",
|
||||
// subline: "Test Subline",
|
||||
// product_images: ["https://picsum.photos/200/200?random=2"],
|
||||
// short_description: "Another test product",
|
||||
// retail: "49.99",
|
||||
// wholesale: "25.00",
|
||||
// weight: "2.0",
|
||||
// categories: ["Category 3"],
|
||||
// colors: ["Green"],
|
||||
// size_cat: "Large",
|
||||
// tax_cat: "Taxable",
|
||||
// ship_restrictions: "None",
|
||||
// supplier: "Test Supplier",
|
||||
// artist: "Test Artist",
|
||||
// themes: [],
|
||||
// vendor_sku: "VS-002",
|
||||
// publish: true,
|
||||
// list_on_marketplace: true,
|
||||
// },
|
||||
// {
|
||||
// name: "Failed Product 1",
|
||||
// upc: "345678901234",
|
||||
// item_number: "ITEM-003",
|
||||
// company: "Test Company",
|
||||
// line: "Test Line",
|
||||
// subline: null,
|
||||
// product_images: ["https://picsum.photos/200/200?random=3"],
|
||||
// short_description: "This product will fail",
|
||||
// retail: "19.99",
|
||||
// wholesale: "10.00",
|
||||
// weight: "0.5",
|
||||
// categories: [],
|
||||
// colors: [],
|
||||
// size_cat: null,
|
||||
// tax_cat: "Taxable",
|
||||
// ship_restrictions: null,
|
||||
// supplier: null,
|
||||
// artist: null,
|
||||
// themes: [],
|
||||
// vendor_sku: "VS-003",
|
||||
// publish: false,
|
||||
// list_on_marketplace: false,
|
||||
// },
|
||||
// {
|
||||
// name: "Failed Product 2",
|
||||
// upc: "456789012345",
|
||||
// item_number: "ITEM-004",
|
||||
// company: "Test Company",
|
||||
// line: null,
|
||||
// subline: null,
|
||||
// product_images: null,
|
||||
// description: "Another failed product",
|
||||
// msrp: "99.99",
|
||||
// cost_each: "50.00",
|
||||
// weight: "5.0",
|
||||
// categories: ["Category 1"],
|
||||
// colors: ["Yellow"],
|
||||
// size_cat: "Small",
|
||||
// tax_cat: null,
|
||||
// ship_restrictions: "Hazmat",
|
||||
// supplier: "Test Supplier",
|
||||
// artist: null,
|
||||
// themes: [],
|
||||
// vendor_sku: null,
|
||||
// publish: true,
|
||||
// list_on_marketplace: false,
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const testSubmittedRows: Data<string>[] = testSubmittedProducts.map(product => ({ ...product } as Data<string>));
|
||||
|
||||
// //Scenario 1: All successful
|
||||
// const testResponse: SubmitNewProductsResponse = {
|
||||
// success: true,
|
||||
// message: "Successfully created 4 products",
|
||||
// data: {
|
||||
// created: [
|
||||
// { pid: 12345, upc: "123456789012", item_number: "ITEM-001" },
|
||||
// { pid: 12346, upc: "234567890123", item_number: "ITEM-002" },
|
||||
// { pid: 12347, upc: "345678901234", item_number: "ITEM-003" },
|
||||
// { pid: 12348, upc: "456789012345", item_number: "ITEM-004" },
|
||||
// ],
|
||||
// errored: [],
|
||||
// },
|
||||
// };
|
||||
|
||||
// // Scenario 2: Partial success (2 created, 2 failed)
|
||||
// const testResponse: SubmitNewProductsResponse = {
|
||||
// success: true,
|
||||
// message: "Created 2 of 4 products. 2 products had errors.",
|
||||
// data: {
|
||||
// created: [
|
||||
// { pid: 12345, upc: "123456789012", item_number: "ITEM-001" },
|
||||
// { pid: 12346, upc: "234567890123", item_number: "ITEM-002" },
|
||||
// ],
|
||||
// errored: [
|
||||
// {
|
||||
// upc: "345678901234",
|
||||
// item_number: "ITEM-003",
|
||||
// error_msg: "Missing required field: supplier",
|
||||
// errors: {
|
||||
// supplier: ["Supplier is required for this product line"],
|
||||
// categories: ["At least one category must be selected"],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// upc: "456789012345",
|
||||
// item_number: "ITEM-004",
|
||||
// error_msg: "Invalid product configuration",
|
||||
// errors: {
|
||||
// line: ["Product line is required"],
|
||||
// tax_cat: ["Tax category must be specified"],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// };
|
||||
|
||||
// // Scenario 3: Complete failure
|
||||
// const testResponse: SubmitNewProductsResponse = {
|
||||
// success: false,
|
||||
// message: "Failed to create products. Please check the errors below.",
|
||||
// data: {
|
||||
// created: [],
|
||||
// errored: [
|
||||
// {
|
||||
// upc: "123456789012",
|
||||
// item_number: "ITEM-001",
|
||||
// error_msg: "UPC already exists in the system",
|
||||
// },
|
||||
// {
|
||||
// upc: "234567890123",
|
||||
// item_number: "ITEM-002",
|
||||
// error_msg: "Invalid wholesale price",
|
||||
// },
|
||||
// {
|
||||
// upc: "345678901234",
|
||||
// item_number: "ITEM-003",
|
||||
// error_msg: "Missing required field: supplier",
|
||||
// },
|
||||
// {
|
||||
// upc: "456789012345",
|
||||
// item_number: "ITEM-004",
|
||||
// error_msg: "Invalid product configuration",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// };
|
||||
|
||||
// setImportOutcome({
|
||||
// submittedProducts: testSubmittedProducts,
|
||||
// submittedRows: testSubmittedRows,
|
||||
// response: testResponse,
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// ========== END TEST DATA ==========
|
||||
|
||||
// Fetch initial field options from the API
|
||||
const { data: fieldOptions, isLoading: isLoadingOptions } = useQuery({
|
||||
queryKey: ["import-field-options"],
|
||||
@@ -759,7 +954,7 @@ export function Import() {
|
||||
<CardTitle>Import Results</CardTitle>
|
||||
{hasDebugPermission && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setIsDebugDataVisible((prev) => !prev)}
|
||||
>
|
||||
@@ -769,32 +964,32 @@ export function Import() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{importOutcome.response.success === false ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Alert className="border-destructive bg-destructive/10">
|
||||
<AlertCircle className="h-4 w-4" style={{ color: 'hsl(var(--destructive))' }} />
|
||||
<AlertTitle className="text-destructive-foreground">Error</AlertTitle>
|
||||
<AlertDescription className="text-destructive-foreground">
|
||||
{summaryMessage ?? "Products not created - please review details and fix."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : createdProducts.length === totalSubmitted && totalSubmitted > 0 ? (
|
||||
<Alert className="border-success bg-success/10">
|
||||
<CheckCircle className="h-4 w-4" style={{ color: 'hsl(var(--success))' }} />
|
||||
<AlertTitle className="text-success">Success</AlertTitle>
|
||||
<AlertDescription className="text-success">All products created successfully.</AlertDescription>
|
||||
<AlertTitle className="text-success-foreground">Success</AlertTitle>
|
||||
<AlertDescription className="text-success-foreground">All products created successfully.</AlertDescription>
|
||||
</Alert>
|
||||
) : createdProducts.length > 0 && erroredProducts.length > 0 ? (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Partial Success</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Alert className="border-warning bg-warning/10">
|
||||
<AlertTriangle className="h-4 w-4" style={{ color: 'hsl(var(--warning))' }} />
|
||||
<AlertTitle className="text-warning-foreground">Partial Success</AlertTitle>
|
||||
<AlertDescription className="text-warning-foreground">
|
||||
{createdProducts.length} product{createdProducts.length === 1 ? "" : "s"} created successfully. {erroredProducts.length} product{erroredProducts.length === 1 ? "" : "s"} need attention.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : erroredProducts.length > 0 ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Alert className="border-destructive bg-destructive/10">
|
||||
<AlertCircle className="h-4 w-4" style={{ color: 'hsl(var(--destructive))' }} />
|
||||
<AlertTitle className="text-destructive-foreground">Error</AlertTitle>
|
||||
<AlertDescription className="text-destructive-foreground">
|
||||
{summaryMessage ?? "Products not created - please review details and fix."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -805,19 +1000,6 @@ export function Import() {
|
||||
<AlertDescription>{summaryMessage}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{createdProducts.length > 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Created {createdProducts.length} of {totalSubmitted} products.
|
||||
{erroredProducts.length > 0
|
||||
? ` ${erroredProducts.length} product${erroredProducts.length === 1 ? "" : "s"} ${erroredProducts.length === 1 ? "needs" : "need"} attention.`
|
||||
: ""}
|
||||
</p>
|
||||
) : null}
|
||||
{erroredProducts.length > 0 && hasErroredRowsForEditing && (
|
||||
<Button size="sm" onClick={handleResumeErroredProducts}>
|
||||
Fix products with errors
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{createdProducts.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
@@ -834,7 +1016,7 @@ export function Import() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} className="flex items-start gap-4 rounded-md border p-4">
|
||||
<div key={key} className="flex items-start gap-4 rounded-md p-1 transition-colors">
|
||||
{product.url ? (
|
||||
<a
|
||||
href={product.url}
|
||||
@@ -849,19 +1031,33 @@ export function Import() {
|
||||
{imageContent}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{product.url ? (
|
||||
<a
|
||||
href={product.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
{product.name}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm font-medium">{product.name}</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
{product.url ? (
|
||||
<a
|
||||
href={product.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm font-medium text-primary hover:underline flex items-center gap-1.5"
|
||||
>
|
||||
{product.name}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ExternalLink className="h-3.5 w-3.5 shrink-0" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>View in Backend</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm font-medium">{product.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
UPC: {product.upc}
|
||||
</span>
|
||||
@@ -876,10 +1072,17 @@ export function Import() {
|
||||
|
||||
{erroredProducts.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold text-destructive">Products with Errors</h3>
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-destructive">Products with Errors</h3>
|
||||
{hasErroredRowsForEditing && (
|
||||
<Button size="sm" onClick={handleResumeErroredProducts} variant="default">
|
||||
Fix products with errors
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-3 border border-destructive rounded-md p-2">
|
||||
{erroredProducts.map((product, index) => (
|
||||
<div key={product.upc ?? product.itemNumber ?? index} className="flex items-start gap-4 rounded-md border border-destructive/40 p-4">
|
||||
<div key={product.upc ?? product.itemNumber ?? index} className="flex items-start gap-4 p-1">
|
||||
<div className="block h-16 w-16 shrink-0 overflow-hidden rounded-md border bg-muted">
|
||||
{product.imageUrl ? (
|
||||
<img src={product.imageUrl} alt={product.name} className="h-full w-full object-cover" />
|
||||
@@ -891,10 +1094,15 @@ export function Import() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium">{product.name}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-muted-foreground">UPC: {product.upc}</span>
|
||||
<Separator orientation="vertical" className="h-3" />
|
||||
<span className="text-xs text-muted-foreground">Item #: {product.itemNumber}</span>
|
||||
</div>
|
||||
{product.errorDetails && (
|
||||
<span className="text-xs text-destructive">{product.errorDetails}</span>
|
||||
<div className="px-2 pb-0.5 rounded-md bg-destructive/10 border border-destructive/20">
|
||||
<span className="text-xs text-destructive">{product.errorDetails}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user