"use client"; import { ArrowRight, Eye, Folder, FolderTree, Home, Loader2, Plus, RefreshCw, Search, Wand2, X, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { organizeImportFurni } from "@/actions/catalog"; import { IconPicker } from "@/components/admin/catalog/icon-picker"; import { CatalogIcon, LAYOUT_COLORS } from "@/components/admin/catalog-tree"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { CATALOG_LAYOUTS, type CatalogLayout, isValidCatalogLayout, LAYOUT_DESCRIPTIONS, } from "@/lib/catalog-layouts"; // import { CATALOG_LAYOUTS } from "@/lib/catalog-layouts"; import { getLocalIconUrl } from "@/lib/furni/classname"; import { cn } from "@/lib/utils"; interface OrganizeGroupOverride { caption?: string; icon?: number; layout?: CatalogLayout; } interface RootPage { id: number; caption: string; } interface OrganizeImportGroup { name: string; icon: number; layout: CatalogLayout | null; total: number; moved: number; added: number; thumbs: { itemName: string; publicName: string; spriteId: number; alreadyPlaced: boolean; sourcePageCaption: string | null; }[]; items: { itemId: number; catalogItemId: number | null; alreadyPlaced: boolean; }[]; } interface OrganizeImportsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export function OrganizeImportsDialog({ open, onOpenChange, }: OrganizeImportsDialogProps) { const t = useTranslations("pages.admin.catalog.workspace.organizeImports"); const router = useRouter(); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(false); const [groups, setGroups] = useState([]); const [rootPages, setRootPages] = useState([]); const [importRootPageId, setImportRootPageId] = useState(null); const [importRootCaption, setImportRootCaption] = useState( null, ); const [parentId, setParentId] = useState(-1); const [moveMode, setMoveMode] = useState(false); const [destinationPageId, setDestinationPageId] = useState( null, ); const [destinationCaption, setDestinationCaption] = useState( null, ); const [destQuery, setDestQuery] = useState(""); const [destDebounced, setDestDebounced] = useState(""); const [destResults, setDestResults] = useState([]); const [destSearchLoading, setDestSearchLoading] = useState(false); const [approved, setApproved] = useState>({}); const [overrides, setOverrides] = useState< Record >({}); const [previewGroup, setPreviewGroup] = useState( null, ); const [isCreating, setIsCreating] = useState(false); const [freeMode, setFreeMode] = useState(true); const [credits, setCredits] = useState(0); const [points, setPoints] = useState(0); const [pointsType, setPointsType] = useState(0); const [groupSearchQuery, setGroupSearchQuery] = useState(""); const [onlyUnplaced, setOnlyUnplaced] = useState(false); const [batchLayout, setBatchLayout] = useState("default_3x3"); const [batchPrefix, setBatchPrefix] = useState(""); const load = useCallback(async () => { setLoading(true); setLoadError(false); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 45000); try { const [data, tree] = await Promise.all([ fetch("/api/admin/import/organize", { signal: controller.signal }).then( (r) => r.json(), ), fetch("/api/admin/catalog/tree?parentId=-1", { signal: controller.signal, }).then((r) => r.json()), ]); setGroups(Array.isArray(data?.groups) ? data.groups : []); setImportRootPageId(data?.importRootPageId ?? null); setImportRootCaption(data?.importRootCaption ?? null); const pages = Array.isArray(tree?.pages) ? tree.pages : []; setRootPages( pages.map((p: { id: number; caption: string }) => ({ id: Number(p.id), caption: String(p.caption ?? p.id), })), ); } catch (error) { console.error("[OrganizeImports] load failed:", error); setLoadError(true); setGroups([]); } finally { clearTimeout(timeout); setLoading(false); } }, []); useEffect(() => { if (!open) return; setGroups([]); setApproved({}); setOverrides({}); setParentId(-1); setMoveMode(false); setDestinationPageId(null); setDestinationCaption(null); setDestQuery(""); setDestDebounced(""); setDestResults([]); setFreeMode(true); setCredits(0); setPoints(0); setPointsType(0); void load(); }, [open, load]); useEffect(() => { const t = setTimeout(() => setDestDebounced(destQuery.trim()), 300); return () => clearTimeout(t); }, [destQuery]); useEffect(() => { if (!moveMode || destDebounced.length < 2) { if (!moveMode) setDestResults([]); return; } setDestSearchLoading(true); const ac = new AbortController(); fetch( `/api/admin/catalog/tree?search=${encodeURIComponent(destDebounced)}`, { signal: ac.signal, }, ) .then((r) => r.json()) .then((data) => { const pages = Array.isArray(data?.pages) ? data.pages : []; setDestResults( pages .filter((p: { id: number }) => Number(p.id) > 0) .map((p: { id: number; caption: string }) => ({ id: Number(p.id), caption: String(p.caption ?? p.id), })), ); }) .catch((error) => console.error("[OrganizeImports] page search failed:", error), ) .finally(() => setDestSearchLoading(false)); return () => ac.abort(); }, [moveMode, destDebounced]); const allApproved = groups.length > 0 && groups.every((g) => approved[g.name]); const totalSelected = useMemo( () => groups.reduce( (sum, g) => sum + (approved[g.name] ? g.items.length : 0), 0, ), [groups, approved], ); // One approved group becomes exactly one page, so the "Create … pages" // label must count groups, not the furni items inside them. const selectedGroupsCount = useMemo( () => groups.filter((g) => approved[g.name]).length, [groups, approved], ); const parentOptions = useMemo(() => { const options: RootPage[] = []; for (const p of rootPages) { if (importRootPageId != null && p.id === importRootPageId) continue; options.push(p); } return options; }, [rootPages, importRootPageId]); const toggleAll = () => { const next = !allApproved; setApproved(Object.fromEntries(groups.map((g) => [g.name, next]))); }; const updateGroup = (name: string, patch: OrganizeGroupOverride) => { setOverrides((prev) => ({ ...prev, [name]: { ...prev[name], ...patch } })); }; const applyBatchLayout = (layout: CatalogLayout) => { setOverrides((prev) => { const next = { ...prev }; for (const g of groups) { if (approved[g.name] !== false) { next[g.name] = { ...next[g.name], layout }; } } return next; }); }; const applyBatchPrefix = () => { if (!batchPrefix.trim()) return; setOverrides((prev) => { const next = { ...prev }; for (const g of groups) { if (approved[g.name] !== false) { const currentCaption = next[g.name]?.caption ?? g.name; if (!currentCaption.startsWith(batchPrefix)) { next[g.name] = { ...next[g.name], caption: `${batchPrefix}${currentCaption}`, }; } } } return next; }); }; const resetAllOverrides = () => { setOverrides({}); setApproved(Object.fromEntries(groups.map((g) => [g.name, true]))); }; const filteredGroups = useMemo(() => { const q = groupSearchQuery.trim().toLowerCase(); return groups.filter((g) => { if (onlyUnplaced && g.added === 0 && g.moved === 0) return false; if (!q) return true; if (g.name.toLowerCase().includes(q)) return true; const caption = overrides[g.name]?.caption?.toLowerCase(); if (caption?.includes(q)) return true; return g.thumbs.some((t) => t.itemName.toLowerCase().includes(q)); }); }, [groups, groupSearchQuery, onlyUnplaced, overrides]); const handleCreate = async () => { if (isCreating) return; if (totalSelected > 1000) { toast.error(t("errorMaxItems")); return; } if (moveMode && destinationPageId == null) { toast.error(t("chooseDestination")); return; } const payload = groups .filter((g) => approved[g.name]) .map((g) => { const o = overrides[g.name] ?? {}; return { caption: moveMode ? (destinationCaption ?? "Imported") : (o.caption ?? g.name).trim(), pageLayout: o.layout ?? g.layout ?? "default_3x3", iconImage: o.icon ?? g.icon, destinationPageId: moveMode ? destinationPageId : null, items: g.items.map((item) => ({ itemId: item.itemId, catalogItemId: item.catalogItemId, credits: freeMode ? 0 : credits, points: freeMode ? 0 : points, pointsType: freeMode ? 0 : pointsType, amount: 1, })), }; }); // Frontend validation for group count const MAX_GROUPS = 50; if (payload.length > MAX_GROUPS) { toast.error(t("errorMaxGroups", { count: MAX_GROUPS })); return; } if (payload.length === 0 || payload.some((g) => !g.caption)) { toast.error(t("errorNoName")); return; } setIsCreating(true); try { const result = await organizeImportFurni({ parentId, groups: payload }); if (!result.ok) throw new Error(result.error ?? "Failed"); const created = result.data?.created ?? []; if (moveMode) { const moved = created.reduce((sum, c) => sum + c.moved, 0); const added = created.reduce((sum, c) => sum + c.added, 0); toast.success( t("successMove", { moved, added, destination: destinationCaption ?? "page", }), ); } else { const moved = created.reduce((sum, c) => sum + c.moved, 0); const added = created.reduce((sum, c) => sum + c.added, 0); toast.success( t("successCreate", { count: created.length, moved, added, }), ); } onOpenChange(false); router.refresh(); } catch (error) { console.error("[OrganizeImports] create failed:", error); toast.error(t("errorFailed")); } finally { setIsCreating(false); } }; return ( <> {t("title")} {totalSelected > 0 && !loading && ( {t("itemsBadge", { count: totalSelected })} )}
{moveMode ? t("tipMove") : t("tipCreate")}

{t("limitHint")}

{/* Price (new offers only) */} {!loading && groups.length > 0 && (
{t("price")} {!freeMode && ( <> {t("pointsType")} )}

{t("newPriceHint")}

)} {/* Mode: create new pages vs move into an existing page */} {!loading && groups.length > 0 && (
{t("mode")}
)} {/* Destination page (move mode) */} {moveMode && !loading && groups.length > 0 && (
{t("destination")} {destinationPageId != null ? (
{destinationCaption} {t("destinationChosen")}
) : (
setDestQuery(e.target.value)} /> {destSearchLoading && ( )}
)} {destinationPageId == null && destDebounced.length >= 2 && (
{destResults.length === 0 && !destSearchLoading ? (

{t("noDestResults")}

) : ( destResults.map((p) => ( )) )}
)}

{t("destinationHint")}

)} {/* Parent */} {!moveMode && !loading && groups.length > 0 && (
{t("parent")}
)} {/* Loading / empty / groups */} {loading ? (
{t("loading")}
) : groups.length === 0 ? (
{loadError ? t("loadError") : t("empty")}
) : ( <>
setGroupSearchQuery(e.target.value)} />
{/* Batch Actions Bar */}
{t("batchLabel")}
setBatchPrefix(e.target.value)} /> {Object.keys(overrides).length > 0 && ( )}
{filteredGroups.map((group) => { const o = overrides[group.name] ?? {}; const caption = o.caption ?? group.name; const icon = o.icon ?? group.icon; const layout = o.layout ?? group.layout ?? "default_3x3"; const isApproved = approved[group.name] !== false; const moved = group.moved; const added = group.added; return ( ); })} )}
{/* Visual Preview Sub-Dialog */} !open && setPreviewGroup(null)} > {previewGroup?.name} {previewGroup && (
{t("layout")}:{" "} {previewGroup.layout ?? "default_3x3"}
{previewGroup.total} items
{previewGroup.layout && LAYOUT_DESCRIPTIONS[previewGroup.layout] && (

{LAYOUT_DESCRIPTIONS[previewGroup.layout]}

)}
{previewGroup.thumbs.map((thumb) => (
{thumb.itemName}
))}
)}
); } function FurniThumb({ item, size = 24, }: { item: { itemName: string }; size?: number; }) { const primary = getLocalIconUrl(item.itemName); return (
{/* eslint-disable-next-line @next/next/no-img-element */} {item.itemName} { const img = e.target as HTMLImageElement; img.style.opacity = "0"; }} loading="lazy" />
); }