"use client"; import { Ban, Check, Circle, CircleAlert, Loader2, RefreshCw, X, } from "lucide-react"; import { useTranslations } from "next-intl"; import { memo, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { BatchItemStatus, VerificationSummary } from "./studio-types"; const STATUS_KEYS: Record = { pending: "status.pending", started: "status.started", downloading: "status.downloading", converting: "status.converting", writing_db: "status.writingDb", done: "status.done", failed: "status.failed", }; function formatDuration(ms: number): string { if (!Number.isFinite(ms) || ms < 0) return "0s"; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const rest = s % 60; return rest > 0 ? `${m}m ${rest}s` : `${m}m`; } function formatRate(completed: number, ms: number): string { if (ms < 1500) return ""; const perMinute = completed / (ms / 60000); if (perMinute < 1) return `${(completed / (ms / 1000)).toFixed(1)}/s`; return `${Math.round(perMinute)}/min`; } export const BatchProgress = memo(function BatchProgress({ progress, done, succeeded, failed, cancelling, verification, failedClassnames, startTime, onCancel, onDismiss, onRetry, }: { progress: Map; done: boolean; succeeded: number; failed: number; cancelling?: boolean; verification?: VerificationSummary | null; startTime?: number; failedClassnames?: string[]; onCancel?: () => void; onDismiss?: () => void; onRetry?: (classnames: string[]) => void; }) { const t = useTranslations("admin.studio.batch"); const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (done) return; const id = setInterval(() => setNow(Date.now()), 500); return () => clearInterval(id); }, [done]); // Relevance-first list: newest activity on top, capped so high-volume // imports don't render hundreds of DOM rows on every progress event. const VISIBLE_ITEMS = 60; const entries = useMemo( () => [...progress.values()].slice().reverse(), [progress], ); const completed = useMemo( () => entries.filter((i) => i.status === "done" || i.status === "failed") .length, [entries], ); const total = entries.length; const percent = total ? Math.round((completed / total) * 100) : 0; const imported = done ? succeeded : entries.filter((item) => item.status === "done").length; const errors = done ? failed : entries.filter((item) => item.status === "failed").length; const unfinished = Math.max(0, total - completed); const elapsed = startTime ? now - startTime : 0; const rate = formatRate(completed, elapsed); const heading = !done ? cancelling ? t("headingStopping") : t("headingImporting") : unfinished ? t("headingStopped") : errors ? t("headingCompletedErrors") : t("headingCompleted"); const issues = verification ? verification.furniDataMissing + verification.furniDataConflicts : 0; return (

{heading}

{imported} {" "} {t("imported")} · 0 && "text-[var(--admin-error)]", )} > {errors} {" "} {t("failed")} {unfinished > 0 && ( <> · {unfinished} {done ? t("notCompleted") : t("remaining")} )} {!done && startTime ? ( <> · {formatDuration(elapsed)} {rate && ( {rate} )} ) : null}

{done ? ( <> {!!failedClassnames?.length && onRetry && ( )} ) : ( )}
{done && verification && (

{issues ? t("verificationIssues", { count: issues }) : t("verificationDone")}

)}
{t("viewItemDetails", { count: total })}
{entries.length > VISIBLE_ITEMS && (

{t("showingItems", { shown: VISIBLE_ITEMS, total: entries.length, })}

)} {entries.slice(0, VISIBLE_ITEMS).map((item) => (
{item.status === "done" ? ( ) : item.status === "failed" ? ( ) : done || item.status === "pending" ? ( ) : ( )} {item.classname} {done && item.status !== "done" && item.status !== "failed" ? t("statusNotCompleted") : t(STATUS_KEYS[item.status])} {item.message && ( {item.message} )} {[...new Set(item.warnings ?? [])].map((warning) => ( {warning} ))}
))}
); }); function NodeChip({ count, label }: { count: number; label: string }) { if (!count) return null; return ( {count} {label} ); }