garage-erp/apps/dashboard/shared/components/import-results-dialog.tsx

139 lines
5.7 KiB
TypeScript

"use client"
import * as React from "react"
import { CheckCircle2, XCircle } from "lucide-react"
import type { ImportFailureRow } from "@garage/api"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import { Button } from "@/shared/components/ui/button"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/components/ui/table"
export type { ImportFailureRow }
export type ImportResultsDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
importedCount: number
failedCount: number
failedRows: ImportFailureRow[]
entityLabel?: string
}
function formatValue(value: unknown): string {
if (value === null || value === undefined || value === "") return "—"
if (typeof value === "string") return value
if (typeof value === "number" || typeof value === "boolean") return String(value)
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
export function ImportResultsDialog({
open,
onOpenChange,
importedCount,
failedCount,
failedRows,
entityLabel = "Records",
}: ImportResultsDialogProps) {
const hasFailures = failedCount > 0
const flatFailures = failedRows.flatMap((r) =>
r.errors.map((e) => ({ row: r.row, ...e })),
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[min(96vw,64rem)] sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Import results</DialogTitle>
<DialogDescription>
{hasFailures
? "Some rows could not be imported. Fix the issues below and re-upload only the failing rows."
: `All ${entityLabel.toLowerCase()} imported successfully.`}
</DialogDescription>
</DialogHeader>
<div className="flex gap-3">
<div className="flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">
<CheckCircle2 className="size-4" />
<span>
<strong>{importedCount}</strong> imported
</span>
</div>
{hasFailures && (
<div className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
<XCircle className="size-4" />
<span>
<strong>{failedCount}</strong> failed
</span>
</div>
)}
</div>
{hasFailures && (
<ScrollArea className="max-h-96 rounded-md border">
<Table className="table-fixed w-full">
<TableHeader>
<TableRow>
<TableHead className="w-16">Row</TableHead>
<TableHead className="w-40">Field</TableHead>
<TableHead className="w-56">Value</TableHead>
<TableHead>Reason</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{flatFailures.map((f, idx) => (
<TableRow key={`${f.row}-${f.field}-${idx}`}>
<TableCell className="font-mono text-xs">{f.row}</TableCell>
<TableCell className="font-medium">{f.label}</TableCell>
<TableCell className="text-muted-foreground break-all whitespace-pre-wrap">
{formatValue(f.value)}
</TableCell>
<TableCell className="break-words">
<div>{f.message}</div>
{f.valid_examples && f.valid_examples.length > 0 && (
<details className="mt-1 text-xs text-muted-foreground">
<summary className="cursor-pointer">
Try one of: {f.valid_examples.slice(0, 3).join(", ")}
{f.valid_examples.length > 3 ? "…" : ""}
</summary>
<div className="mt-1">
{f.valid_examples.join(", ")}
</div>
</details>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}