"use client"; import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { adminFetch } from "@/lib/admin-fetch"; import { previewAutoCatalog } from "@/lib/furni/auto-catalog"; import { compareFurniture } from "@/lib/furni/studio-inspection"; import { FurnitureComparison } from "./furniture-inspector"; import type { FurniItem } from "./studio-types"; import { useFurnitureInspection } from "./use-furniture-inspection"; import { useSourceAssetChecks } from "./use-source-asset-checks"; export function ImportReview({ items, sourceId, source, translation, onCancel, busy = false, onConfirm, }: { items: FurniItem[]; sourceId?: string; source: string; translation: string; onCancel: () => void; busy?: boolean; onConfirm: (items: FurniItem[]) => void; }) { const [attachments, setAttachments] = useState>({}); const [uploading, setUploading] = useState(null); async function attach(item: FurniItem, file: File) { setUploading(item.classname); try { const form = new FormData(); form.set("classname", item.classname); form.set("file", file); const response = await adminFetch("/api/admin/studio/import-attachment", { method: "POST", body: form, }); const data = await response.json(); if (!response.ok) throw Error(data.error || "Upload failed"); setAttachments((current) => ({ ...current, [item.classname]: data.attachmentId, })); toast.success("Matching .nitro attached to this import"); } catch (error) { toast.error(error instanceof Error ? error.message : "Upload failed"); } finally { setUploading(null); } } const inspection = useFurnitureInspection( items.map((item) => item.classname), ); const assets = useSourceAssetChecks(items, sourceId, inspection); const unavailable = assets.items.filter( (item) => item.state === "missing" && !attachments[item.classname], ); const [expanded, setExpanded] = useState(items[0]?.classname ?? ""); const [step, setStep] = useState<"review" | "confirm">("review"); const [page, setPage] = useState(0); const localByName = new Map( inspection.items.map((item) => [item.classname, item]), ); const complete = items.every((item) => localByName.has(item.classname)); const ready = items.filter((item) => { const local = localByName.get(item.classname); return local && compareFurniture(item, local).state !== "conflict"; }); const blocked = items.filter((item) => { const local = localByName.get(item.classname); return local && compareFurniture(item, local).state === "conflict"; }).length; const existing = items.filter((item) => { const local = localByName.get(item.classname); return ( local && local.sql.length > 0 && compareFurniture(item, local).state !== "conflict" ); }).length; const pageItems = (step === "review" ? items : ready).slice( page * 50, (page + 1) * 50, ); const pageCount = Math.max( 1, Math.ceil((step === "review" ? items : ready).length / 50), ); return ( { if (!open && !busy && !uploading) onCancel(); }} > {step === "review" ? "Review furniture import" : "Confirm furniture import"} 1. Select → 2. Review → 3. Import

Source: {source}

Destination: local catalog, furnidata, .nitro and icons

Translation: {translation}

Selection: {items.length} furniture items

{assets.loading && !inspection.loading && (

Checking source files… {assets.items.length} checked

)} {assets.error && (

{assets.error}

)} {assets.items.some((item) => item.state === "unknown") && (

Some sources could not be verified. The import will still try to download their files.

)} {unavailable.map((check) => (

Source files not found by preliminary check: {check.classname}

You can continue. The importer will try the original furniture files and verify them before creating its SQL entry.

))} {inspection.loading ? (

Checking local data and assets…

) : inspection.error ? (

{inspection.error}

) : !complete ? (

Inspection is incomplete. Close and retry before importing.

) : ( <>

{ready.length} ready to import · {existing} already in SQL (complete missing data) · {blocked} conflicts ·{" "} {unavailable.length} source file warnings

{inspection.items.some( (item) => !item.furnidataReadable || item.icon.exists === null || item.nitro.exists === null, ) && (

Some local files could not be verified. Review the item details before continuing.

)} {step === "review" ? (
{pageItems.map((item) => { const local = localByName.get(item.classname); if (!local) return null; const comparison = compareFurniture(item, local); return (
{expanded === item.classname && (
{local.nitro.exists !== true && ( )}
)}
); })}
) : ( <>

Only the {ready.length} eligible items below will be submitted. Existing furniture keeps its local IDs, SQL settings and catalog offers. Missing assets and catalog entries are completed; furnidata is refreshed. Conflicts are excluded.

Automatic categories and prices below are estimates. Source rules may override them; final results appear in the import summary.

    {pageItems.map((item) => { const placement = previewAutoCatalog( item.classname, item.type, ); return (
  • {item.name}

    {placement.categoryLabel} · {placement.credits}{" "} credits + {placement.points} points

  • ); })}
)} )}
{!inspection.loading && !inspection.error && pageCount > 1 && (
Page {page + 1} / {pageCount}
)}
); }