Tweak import results UI

This commit is contained in:
2025-10-24 12:58:20 -04:00
parent 0b5f3162c7
commit d56beb5143
2 changed files with 258 additions and 50 deletions

View File

@@ -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%;

View File

@@ -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>