Studio: add clone-all-per-hotel, per-item status detail, retry, ETA, and auto-translate
- Add 'Clone all missing' button for clone sources with confirmation dialog - Show per-item status badges: nitro, furnidata, catalog entry - Add status filters: missing furnidata, missing catalog entry - Add ETA and percentage to batch progress bar - Add retry button for failed items after batch import - Auto-translate all languages after clone-all completes - Fix statusFilter 'all' bug that incorrectly filtered imported items - Bulk-load FurnitureData + catalog references for O(1) per-item checks
This commit is contained in:
1 parent
1610aa1008
commit
1374bd332f
4 files changed
+580
-47
No files matched your search
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Ban, Check, Loader2, ShieldCheck, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -32,6 +33,13 @@ function SummaryRow({
|
||||
);
|
||||
}
|
||||
|
||||
function formatEta(ms: number): string {
|
||||
if (ms < 60_000) return `${Math.ceil(ms / 1000)}s`;
|
||||
const min = Math.floor(ms / 60_000);
|
||||
const sec = Math.ceil((ms % 60_000) / 1000);
|
||||
return `${min}m ${sec}s`;
|
||||
}
|
||||
|
||||
export function BatchProgress({
|
||||
progress,
|
||||
done,
|
||||
@@ -39,8 +47,11 @@ export function BatchProgress({
|
||||
failed,
|
||||
cancelling,
|
||||
verification,
|
||||
startTime,
|
||||
failedClassnames,
|
||||
onCancel,
|
||||
onDismiss,
|
||||
onRetry,
|
||||
}: {
|
||||
progress: Map<string, BatchItemStatus>;
|
||||
done: boolean;
|
||||
@@ -48,8 +59,11 @@ export function BatchProgress({
|
||||
failed: number;
|
||||
cancelling?: boolean;
|
||||
verification?: VerificationSummary | null;
|
||||
startTime?: number;
|
||||
failedClassnames?: string[];
|
||||
onCancel?: () => void;
|
||||
onDismiss?: () => void;
|
||||
onRetry?: (classnames: string[]) => void;
|
||||
}) {
|
||||
const entries = [...progress.entries()];
|
||||
const total = entries.length;
|
||||
@@ -58,6 +72,27 @@ export function BatchProgress({
|
||||
).length;
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
|
||||
// Live ETA calculation
|
||||
const [eta, setEta] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (done || !startTime || completed === 0) {
|
||||
setEta(null);
|
||||
return;
|
||||
}
|
||||
const elapsed = Date.now() - startTime;
|
||||
const avgPerItem = elapsed / completed;
|
||||
const remaining = (total - completed) * avgPerItem;
|
||||
setEta(formatEta(remaining));
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const avgPerItem = elapsed / completed;
|
||||
const remaining = (total - completed) * avgPerItem;
|
||||
setEta(formatEta(remaining));
|
||||
}, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [done, startTime, completed, total]);
|
||||
|
||||
return (
|
||||
<div className="border-t border-[var(--admin-border)] p-3">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
@@ -81,6 +116,11 @@ export function BatchProgress({
|
||||
<span className="text-xs font-mono text-[var(--admin-text-muted)]">
|
||||
{completed}/{total}
|
||||
</span>
|
||||
{eta && (
|
||||
<span className="text-xs font-mono text-[var(--admin-accent)]">
|
||||
~{eta}
|
||||
</span>
|
||||
)}
|
||||
<Loader2
|
||||
size={14}
|
||||
className="animate-spin text-[var(--admin-accent)]"
|
||||
@@ -90,11 +130,16 @@ export function BatchProgress({
|
||||
</div>
|
||||
|
||||
{!done && (
|
||||
<div className="mb-2 h-1.5 overflow-hidden rounded-full bg-[var(--admin-accent)]/10">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--admin-accent)] transition-all duration-300"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--admin-accent)]/10">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--admin-accent)] transition-all duration-300"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-mono font-bold text-[var(--admin-text)]">
|
||||
{percent}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -165,10 +210,22 @@ export function BatchProgress({
|
||||
|
||||
<div className="mt-2 flex justify-end gap-2">
|
||||
{done ? (
|
||||
<Button size="sm" variant="secondary" onClick={onDismiss}>
|
||||
<Check size={14} />
|
||||
Dismiss
|
||||
</Button>
|
||||
<>
|
||||
{failedClassnames && failedClassnames.length > 0 && onRetry && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onRetry(failedClassnames)}
|
||||
>
|
||||
<Loader2 size={14} />
|
||||
Retry {failedClassnames.length} failed
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="secondary" onClick={onDismiss}>
|
||||
<Check size={14} />
|
||||
Dismiss
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
Reference in new issue
Block a user