feat(catalog): queue furniture imports with history and matching file attachments
This commit is contained in:
1 parent
775d14f861
commit
2619bec165
16 files changed
+1016
-232
No files matched your search
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
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 {
|
||||
@@ -24,6 +26,7 @@ export function ImportReview({
|
||||
source,
|
||||
translation,
|
||||
onCancel,
|
||||
busy = false,
|
||||
onConfirm,
|
||||
}: {
|
||||
items: FurniItem[];
|
||||
@@ -31,13 +34,41 @@ export function ImportReview({
|
||||
source: string;
|
||||
translation: string;
|
||||
onCancel: () => void;
|
||||
busy?: boolean;
|
||||
onConfirm: (items: FurniItem[]) => void;
|
||||
}) {
|
||||
const [attachments, setAttachments] = useState<Record<string, string>>({});
|
||||
const [uploading, setUploading] = useState<string | null>(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");
|
||||
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);
|
||||
@@ -73,7 +104,7 @@ export function ImportReview({
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onCancel();
|
||||
if (!open && !busy && !uploading) onCancel();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="flex max-h-[90dvh] w-[calc(100vw-2rem)] max-w-3xl sm:max-w-3xl flex-col overflow-hidden border-[var(--admin-border)] bg-[var(--admin-surface)] text-[var(--admin-text)]">
|
||||
@@ -208,6 +239,27 @@ export function ImportReview({
|
||||
</button>
|
||||
{expanded === item.classname && (
|
||||
<div className="border-t border-[var(--admin-border)] p-3">
|
||||
{local.nitro.exists !== true && (
|
||||
<label className="mt-3 block text-sm">
|
||||
{attachments[item.classname]
|
||||
? "Matching .nitro attached"
|
||||
: "Attach the original .nitro"}
|
||||
<input
|
||||
className="mt-2 block max-w-full"
|
||||
type="file"
|
||||
accept=".nitro"
|
||||
disabled={busy || uploading !== null}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) void attach(item, file);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
{uploading === item.classname && (
|
||||
<span role="status">Validating file…</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
<FurnitureComparison item={item} local={local} />
|
||||
</div>
|
||||
)}
|
||||
@@ -278,6 +330,7 @@ export function ImportReview({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={busy || uploading !== null}
|
||||
onClick={
|
||||
step === "confirm"
|
||||
? () => {
|
||||
@@ -295,6 +348,8 @@ export function ImportReview({
|
||||
color: "var(--admin-accent-foreground)",
|
||||
}}
|
||||
disabled={
|
||||
busy ||
|
||||
uploading !== null ||
|
||||
inspection.loading ||
|
||||
!!inspection.error ||
|
||||
!complete ||
|
||||
@@ -304,7 +359,13 @@ export function ImportReview({
|
||||
if (step === "review") {
|
||||
setStep("confirm");
|
||||
setPage(0);
|
||||
} else onConfirm(ready);
|
||||
} else
|
||||
onConfirm(
|
||||
ready.map((item) => ({
|
||||
...item,
|
||||
attachmentId: attachments[item.classname],
|
||||
})),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{step === "review"
|
||||
|
||||
Reference in new issue
Block a user