"use client"; import { closestCenter, DndContext, type DragEndEvent, DragOverlay, type DragStartEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ArrowRightLeft, Copy, DollarSign, GripVertical, LayoutGrid, Loader2, MoveRight, Pencil, Plus, RotateCcw, Rows3, Save, Search, Trash2, X, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { createCatalogItem, deleteCatalogItems, moveCatalogItems, reorderCatalogItems, updateCatalogItem, } from "@/actions/catalog-items"; import { BulkImportItems } from "@/components/admin/catalog/bulk-import-items"; import { ItemsShopPreview } from "@/components/admin/catalog/items-shop-preview"; import { QuickAddFurni } from "@/components/admin/catalog/quick-add-furni"; import { useConfirmDialog } from "@/components/admin/confirm-dialog"; import { CurrencyIcon, currencyKindFromPointsType, } from "@/components/shared/currency-icon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useServerAction } from "@/hooks/use-server-action"; import { getSuggestions, recordMove } from "@/lib/move-suggestions"; import { cn } from "@/lib/utils"; import { InteractionTypeCombobox, MovePageCombobox, ToggleField, } from "./field-helpers"; import { FurniIcon } from "./furni-icon"; import { SongPicker } from "./song-picker"; import type { BaseItemData, CatalogItemData, InlineEdit } from "./types"; import { DEFAULT_NEW_ITEM, getPointsLabel, POINTS_TYPES } from "./types"; export type { BaseItemData, CatalogItemData }; // ── Sortable row wrapper (dnd-kit) ───────────────────────────── function SortableRow({ id, className, children, }: { id: number; className?: string; children: (props: { dragHandleProps: React.HTMLAttributes; }) => React.ReactNode; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id, }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.35 : 1, }; const dragHandleProps = { ...attributes, ...listeners, } as unknown as React.HTMLAttributes; return ( } style={style} className={className} > {children({ dragHandleProps })} ); } interface CatalogItemsTableProps { items: CatalogItemData[]; pageId: number; pageLayout: string; canEdit: boolean; interactionTypes: string[]; furniRevisionMap: Record; allPages: { id: number; caption: string }[]; } export function CatalogItemsTable({ items, pageId, pageLayout, canEdit, interactionTypes, furniRevisionMap, allPages, }: CatalogItemsTableProps) { const router = useRouter(); const { run, isPending } = useServerAction(); const { confirm, dialog: confirmDialog } = useConfirmDialog(); const [addOpen, setAddOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [editingBase, setEditingBase] = useState(null); const [savingItemId, setSavingItemId] = useState(null); const [newItem, setNewItem] = useState({ ...DEFAULT_NEW_ITEM }); // ── View mode (table / grid) ──────────────────────────────────── const [viewMode, setViewMode] = useState<"table" | "grid">("table"); // ── Search / Filter ───────────────────────────────────────────── const [searchQuery, setSearchQuery] = useState(""); const filteredItems = useMemo(() => { if (!searchQuery.trim()) return items; const q = searchQuery.toLowerCase(); return items.filter( (item) => item.catalogName.toLowerCase().includes(q) || item.baseItemName.toLowerCase().includes(q) || item.baseName.toLowerCase().includes(q) || item.itemIds.includes(q) || String(item.id).includes(q), ); }, [items, searchQuery]); // ── Move items dialog ─────────────────────────────────────────── const [moveOpen, setMoveOpen] = useState(false); const [moveTargetPageId, setMoveTargetPageId] = useState(null); // ── Bulk price dialog ─────────────────────────────────────────── const [bulkPriceOpen, setBulkPriceOpen] = useState(false); const [bulkPriceField, setBulkPriceField] = useState< "costCredits" | "costPoints" >("costCredits"); const [bulkPriceMode, setBulkPriceMode] = useState<"set" | "add" | "percent">( "set", ); const [bulkPriceValue, setBulkPriceValue] = useState(0); // ── Drag & Drop (dnd-kit) ─────────────────────────────────────── const [activeDragId, setActiveDragId] = useState(null); const [itemOrder, setItemOrder] = useState(items.map((i) => i.id)); const dndSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), ); const orderedFilteredItems = useMemo(() => { if (searchQuery.trim()) return filteredItems; const orderMap = new Map(itemOrder.map((id, idx) => [id, idx])); return [...filteredItems].sort( (a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0), ); }, [filteredItems, itemOrder, searchQuery]); // ── Selection ──────────────────────────────────────────────────── const [selected, setSelected] = useState>(new Set()); const toggleOne = useCallback((id: number) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const toggleAll = useCallback(() => { setSelected((prev) => prev.size === filteredItems.length ? new Set() : new Set(filteredItems.map((i) => i.id)), ); }, [filteredItems]); // ── Inline edits ───────────────────────────────────────────────── const [inlineEdits, setInlineEdits] = useState>( Object.fromEntries( items.map((item) => [ item.id, { costCredits: item.costCredits, costPoints: item.costPoints, pointsType: item.pointsType, amount: item.amount, }, ]), ), ); function updateInline( id: number, key: keyof InlineEdit, value: string | number, ) { setInlineEdits((prev) => ({ ...prev, [id]: { ...prev[id], [key]: value }, })); } const isItemModified = useCallback( (item: CatalogItemData) => { const edit = inlineEdits[item.id]; if (!edit) return false; return ( edit.costCredits !== item.costCredits || edit.costPoints !== item.costPoints || edit.pointsType !== item.pointsType || edit.amount !== item.amount ); }, [inlineEdits], ); const modifiedItems = useMemo( () => items.filter(isItemModified), [items, isItemModified], ); const hasOrderChanges = useMemo(() => { const originalOrder = items.map((i) => i.id); return itemOrder.some((id, idx) => id !== originalOrder[idx]); }, [items, itemOrder]); const hasUnsavedChanges = modifiedItems.length > 0 || hasOrderChanges; // Warn before navigating away with unsaved changes useEffect(() => { if (!hasUnsavedChanges) return; function onBeforeUnload(e: BeforeUnloadEvent) { e.preventDefault(); } window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, [hasUnsavedChanges]); // Ctrl+S / Cmd+S to save modified items useEffect(() => { if (!canEdit) return; function onKeyDown(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault(); if (modifiedItems.length > 0) handleSaveAllModified(); else if (hasOrderChanges) handleSaveOrder(); } } document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }); function resetInlineEdit(id: number) { const item = items.find((i) => i.id === id); if (!item) return; setInlineEdits((prev) => ({ ...prev, [id]: { costCredits: item.costCredits, costPoints: item.costPoints, pointsType: item.pointsType, amount: item.amount, }, })); } function resetAllEdits() { setInlineEdits( Object.fromEntries( items.map((item) => [ item.id, { costCredits: item.costCredits, costPoints: item.costPoints, pointsType: item.pointsType, amount: item.amount, }, ]), ), ); } // ── Open edit dialog ─────────────────────────────────────────── function openEditDialog(item: CatalogItemData) { setEditingItem({ ...item }); setEditingBase(item.baseItem ? { ...item.baseItem } : null); } // ── Save single item inline ────────────────────────────────────── async function handleSaveOne(item: CatalogItemData) { const edit = inlineEdits[item.id]; if (!edit) return; setSavingItemId(item.id); run( () => updateCatalogItem({ id: item.id, catalogFields: { catalogName: item.catalogName, itemIds: item.itemIds, costCredits: edit.costCredits, costPoints: edit.costPoints, pointsType: edit.pointsType, amount: edit.amount, limitedSells: item.limitedSells, limitedStack: item.limitedStack, orderNumber: item.orderNumber, offerId: item.offerId, haveOffer: item.haveOffer, clubOnly: item.clubOnly, extradata: item.extradata, }, }), { successMessage: `Item #${item.id} saved.`, errorMessage: `Failed to save item #${item.id}.`, onSuccess: () => { setSavingItemId(null); router.refresh(); }, onError: () => setSavingItemId(null), }, ); } // ── Save all modified items ────────────────────────────────────── async function handleSaveAllModified() { if (modifiedItems.length === 0) return; run( async () => { let savedCount = 0; let failedCount = 0; for (const item of modifiedItems) { const edit = inlineEdits[item.id]; if (!edit) continue; const result = await updateCatalogItem({ id: item.id, catalogFields: { catalogName: item.catalogName, itemIds: item.itemIds, costCredits: edit.costCredits, costPoints: edit.costPoints, pointsType: edit.pointsType, amount: edit.amount, limitedSells: item.limitedSells, limitedStack: item.limitedStack, orderNumber: item.orderNumber, offerId: item.offerId, haveOffer: item.haveOffer, clubOnly: item.clubOnly, extradata: item.extradata, }, }); if (result.ok) savedCount++; else failedCount++; } if (failedCount > 0) { return { ok: false as const, error: `Saved ${savedCount}, failed ${failedCount}.`, }; } return { ok: true as const, data: { savedCount } }; }, { successMessage: `Saved ${modifiedItems.length} item(s).`, errorMessage: "Failed to save items.", }, ); } // ── Bulk delete selected ───────────────────────────────────────── async function handleBulkDelete() { if (selected.size === 0) return; const ok = await confirm({ title: "Delete items", description: `Delete ${selected.size} selected item(s)?`, confirmLabel: "Delete", }); if (!ok) return; run(() => deleteCatalogItems({ ids: [...selected] }), { successMessage: `Deleted ${selected.size} item(s).`, errorMessage: "Failed to delete items.", onSuccess: () => { setSelected(new Set()); router.refresh(); }, }); } // ── Move selected items to another page ───────────────────────── async function handleMoveItems() { if (selected.size === 0 || !moveTargetPageId) return; // Record move patterns for all selected items for (const id of selected) { const item = items.find((i) => i.id === id); if (item) { recordMove({ interactionType: item.baseItem?.interactionType, itemName: item.baseItemName, targetPageId: moveTargetPageId, }); } } run( () => moveCatalogItems({ ids: [...selected], targetPageId: moveTargetPageId, }), { successMessage: `Moved ${selected.size} item(s) to page #${moveTargetPageId}.`, errorMessage: "Failed to move items.", onSuccess: () => { setSelected(new Set()); setMoveOpen(false); setMoveTargetPageId(null); router.refresh(); }, }, ); } // ── Apply bulk price update to inline edits ──────────────────── function applyBulkPrice() { const targetIds = selected.size > 0 ? selected : new Set(filteredItems.map((i) => i.id)); setInlineEdits((prev) => { const next = { ...prev }; for (const id of targetIds) { const edit = next[id]; if (!edit) continue; const currentVal = edit[bulkPriceField]; let newVal: number; if (bulkPriceMode === "set") { newVal = bulkPriceValue; } else if (bulkPriceMode === "add") { newVal = currentVal + bulkPriceValue; } else { newVal = Math.round(currentVal * (1 + bulkPriceValue / 100)); } next[id] = { ...edit, [bulkPriceField]: Math.max(0, newVal) }; } return next; }); setBulkPriceOpen(false); toast.success( `Price update applied to ${targetIds.size} item(s). Save to persist.`, ); } // ── Drag & Drop handlers (dnd-kit) ────────────────────────────── const handleDragStart = useCallback((event: DragStartEvent) => { setActiveDragId(Number(event.active.id)); }, []); const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event; setActiveDragId(null); if (!over || active.id === over.id) return; setItemOrder((prev) => { const fromIdx = prev.indexOf(Number(active.id)); const toIdx = prev.indexOf(Number(over.id)); if (fromIdx === -1 || toIdx === -1) return prev; return arrayMove(prev, fromIdx, toIdx); }); }, []); const handleDragCancel = useCallback(() => { setActiveDragId(null); }, []); async function handleSaveOrder() { const orders = itemOrder.map((id, idx) => ({ id, orderNumber: idx + 1 })); run(() => reorderCatalogItems({ orders }), { successMessage: "Item order saved.", errorMessage: "Failed to save order.", }); } function resetOrder() { setItemOrder(items.map((i) => i.id)); } // Helper to get furni revision info for an item function getFurniInfo(item: CatalogItemData) { const firstBaseId = item.itemIds .split(";") .map(Number) .find((n) => n > 0); if (!firstBaseId) return { classname: item.baseItemName, revision: 0 }; const furni = furniRevisionMap[firstBaseId]; return furni ? { classname: furni.classname || item.baseItemName, revision: furni.revision, } : { classname: item.baseItemName, revision: 0 }; } // ── Original CRUD handlers ─────────────────────────────────────── async function handleAddItem() { run( () => createCatalogItem({ ...newItem, pageId, haveOffer: newItem.haveOffer as "0" | "1", clubOnly: newItem.clubOnly as "0" | "1", }), { successMessage: "Item added.", errorMessage: "Failed to add item", onSuccess: () => { setAddOpen(false); setNewItem({ ...DEFAULT_NEW_ITEM }); router.refresh(); }, }, ); } async function handleDeleteItem(id: number) { const ok = await confirm({ title: "Delete item", description: "Delete this catalog item?", confirmLabel: "Delete", }); if (!ok) return; run(() => deleteCatalogItems({ ids: [id] }), { successMessage: "Item deleted.", errorMessage: "Failed to delete item.", }); } // ── Move single item to another page ───────────────────────── const [moveOneId, setMoveOneId] = useState(null); const [moveOneTarget, setMoveOneTarget] = useState(null); // Get the item being moved for suggestions const moveOneItem = moveOneId ? items.find((i) => i.id === moveOneId) : null; const moveOneSuggestions = useMemo(() => { if (!moveOneItem) return []; return getSuggestions({ interactionType: moveOneItem.baseItem?.interactionType, itemName: moveOneItem.baseItemName, }); }, [moveOneItem]); async function handleMoveSingleItem() { if (!moveOneId || !moveOneTarget) return; // Record for learning before moving if (moveOneItem) { recordMove({ interactionType: moveOneItem.baseItem?.interactionType, itemName: moveOneItem.baseItemName, targetPageId: moveOneTarget, }); } run( () => moveCatalogItems({ ids: [moveOneId], targetPageId: moveOneTarget }), { successMessage: `Item #${moveOneId} moved to page #${moveOneTarget}.`, errorMessage: "Failed to move item.", onSuccess: () => { setMoveOneId(null); setMoveOneTarget(null); router.refresh(); }, }, ); } async function handleDuplicateItem(item: CatalogItemData) { run( () => createCatalogItem({ pageId, itemIds: item.itemIds, catalogName: item.catalogName, costCredits: item.costCredits, costPoints: item.costPoints, pointsType: item.pointsType, amount: item.amount, limitedSells: 0, limitedStack: item.limitedStack, orderNumber: item.orderNumber + 1, offerId: item.offerId, songId: item.songId, haveOffer: item.haveOffer as "0" | "1", clubOnly: item.clubOnly as "0" | "1", extradata: item.extradata, }), { successMessage: `Item #${item.id} duplicated.`, errorMessage: "Failed to duplicate item.", }, ); } async function handleUpdateItem() { if (!editingItem) return; run( () => updateCatalogItem({ id: editingItem.id, catalogFields: { catalogName: editingItem.catalogName, itemIds: editingItem.itemIds, costCredits: editingItem.costCredits, costPoints: editingItem.costPoints, pointsType: editingItem.pointsType, amount: editingItem.amount, limitedSells: editingItem.limitedSells, limitedStack: editingItem.limitedStack, orderNumber: editingItem.orderNumber, offerId: editingItem.offerId, songId: editingItem.songId, haveOffer: editingItem.haveOffer, clubOnly: editingItem.clubOnly, extradata: editingItem.extradata, }, ...(editingBase ? { baseItem: { id: editingBase.id, fields: editingBase as unknown as Record, }, } : {}), }), { successMessage: `Item #${editingItem.id} updated.`, errorMessage: "Failed to update item", onSuccess: () => { setEditingItem(null); setEditingBase(null); router.refresh(); }, }, ); } // ── Add Item Form Fields (for "Add" dialog) ─────────────────── function AddItemFormFields({ data, onChange, }: { data: typeof DEFAULT_NEW_ITEM; onChange: (key: string, value: string | number) => void; }) { return (
onChange("catalogName", e.target.value)} />
onChange("itemIds", e.target.value)} />
onChange("costCredits", parseInt(e.target.value, 10) || 0) } />
onChange("costPoints", parseInt(e.target.value, 10) || 0) } />
onChange("amount", parseInt(e.target.value, 10) || 1) } />
onChange("orderNumber", parseInt(e.target.value, 10) || 0) } />
onChange("offerId", parseInt(e.target.value, 10)) } />
onChange("limitedSells", parseInt(e.target.value, 10) || 0) } />
onChange("limitedStack", parseInt(e.target.value, 10) || 0) } />
onChange("extradata", e.target.value)} placeholder="Optional extra data" />
); } return ( <> {confirmDialog}
Items ({items.length}) {searchQuery && filteredItems.length !== items.length && ( ({filteredItems.length} shown) )} {selected.size > 0 && (

{selected.size} selected

)}
{/* View toggle */}
{canEdit && ( <> {selected.size > 0 && ( <> )} {hasOrderChanges && ( <> )} {modifiedItems.length > 0 && ( <> )} router.refresh()} /> Add Catalog Item setNewItem((p) => ({ ...p, [key]: value })) } /> )}
{/* Unsaved changes banner */} {hasUnsavedChanges && (
You have unsaved changes ( {modifiedItems.length > 0 ? `${modifiedItems.length} item(s) modified` : ""} {modifiedItems.length > 0 && hasOrderChanges ? ", " : ""} {hasOrderChanges ? "order changed" : ""}). Press{" "} Ctrl+S {" "} to save.
)} {/* Search bar */} {items.length > 0 && (
setSearchQuery(e.target.value)} className="pl-9 h-9" /> {searchQuery && ( )}
)}
{items.length === 0 ? (

No items in this page.

{canEdit && ( )}
) : filteredItems.length === 0 ? (

No items match "{searchQuery}".

) : viewMode === "grid" ? ( openEditDialog(item) : undefined} /> ) : (
{canEdit && } {canEdit && ( 0 && selected.size === filteredItems.length } onCheckedChange={toggleAll} aria-label="Select all" /> )} ID Name Item IDs Item Name Price Amt Status {canEdit && ( Actions )} i.id)} strategy={verticalListSortingStrategy} > {orderedFilteredItems.map((item) => { const pts = getPointsLabel(item.pointsType); const edit = inlineEdits[item.id]; const modified = isItemModified(item); const isSaving = savingItemId === item.id; const furniInfo = getFurniInfo(item); const sortableDisabled = !canEdit || !!searchQuery; return ( {({ dragHandleProps }) => ( <> {canEdit && ( {!sortableDisabled ? (
) : (
)} )} {canEdit && ( toggleOne(item.id)} aria-label={`Select item ${item.id}`} /> )} {item.id} {item.catalogName || ( unnamed )} {item.itemIds} {item.baseItemName || "-"} {canEdit ? (
updateInline( item.id, "costCredits", parseInt(e.target.value, 10) || 0, ) } className="h-8 text-xs w-16" title="Credits" /> c updateInline( item.id, "costPoints", parseInt(e.target.value, 10) || 0, ) } className="h-8 text-xs w-16" title="Points" /> {pts.label.charAt(0)}
) : (
{item.costCredits > 0 && ( {item.costCredits} )} {item.costPoints > 0 && ( {item.costPoints} )} {item.costCredits === 0 && item.costPoints === 0 && ( Free )}
)}
{canEdit ? ( updateInline( item.id, "amount", parseInt(e.target.value, 10) || 1, ) } className="h-8 text-xs w-16" /> ) : ( {item.amount} )}
{item.haveOffer === "1" ? "Offer" : "No Offer"} {item.limitedStack > 0 && ( LTD {item.limitedSells}/ {item.limitedStack} )} {item.clubOnly === "1" && ( HC )}
{canEdit && (
{modified && ( <> )}
)} )} ); })}
{activeDragId !== null && (() => { const dragged = items.find((i) => i.id === activeDragId); if (!dragged) return null; const info = getFurniInfo(dragged); return (

{dragged.catalogName || dragged.baseName || `#${dragged.id}`}

#{dragged.id}

); })()}
)} {/* ── Edit Item Dialog (tabbed: Catalog Settings + Furniture Settings) ── */} {editingItem && ( { if (!open) { setEditingItem(null); setEditingBase(null); } }} > Edit Item #{editingItem.id} {/* Read-only identity fields */}
{editingItem.catalogName || ( unnamed )}
{editingItem.itemIds}
Catalog Settings Furniture Settings {!editingBase && ( (N/A) )} {/* ── Catalog Settings Tab ── */}
setEditingItem( (p) => p && { ...p, costCredits: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingItem( (p) => p && { ...p, costPoints: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingItem( (p) => p && { ...p, amount: parseInt(e.target.value, 10) || 1, }, ) } />
setEditingItem( (p) => p && { ...p, orderNumber: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingItem( (p) => p && { ...p, offerId: parseInt(e.target.value, 10), }, ) } />
setEditingItem( (p) => p && { ...p, limitedSells: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingItem( (p) => p && { ...p, limitedStack: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingItem( (p) => p && { ...p, extradata: e.target.value }, ) } placeholder="Optional extra data" />
setEditingItem((p) => p && { ...p, songId: id }) } />

Select a soundtrack to play when this item is used in-game. Uploaded from /admin/sounds.

{/* ── Furniture Settings Tab (items_base) ── */} {editingBase && ( {/* Identity */}

Identity

{editingBase.id}
setEditingBase( (p) => p && { ...p, spriteId: parseInt(e.target.value, 10) || 0, }, ) } />
{/* Dimensions */}

Dimensions

setEditingBase( (p) => p && { ...p, width: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingBase( (p) => p && { ...p, length: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingBase( (p) => p && { ...p, stackHeight: parseFloat(e.target.value) || 0, }, ) } />
{/* Interaction */}

Interaction

setEditingBase( (p) => p && { ...p, interactionType: v }, ) } types={interactionTypes} />
setEditingBase( (p) => p && { ...p, interactionModesCount: parseInt(e.target.value, 10) || 0, }, ) } />
{/* Permissions */}

Permissions

setEditingBase( (p) => p && { ...p, allowStack: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowSit: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowLay: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowWalk: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowGift: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowTrade: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowRecycle: v ? 1 : 0 }, ) } /> setEditingBase( (p) => p && { ...p, allowMarketplaceSell: v ? 1 : 0, }, ) } /> setEditingBase( (p) => p && { ...p, allowInventoryStack: v ? 1 : 0 }, ) } />
{/* Extra */}

Extra

setEditingBase( (p) => p && { ...p, vendingIds: e.target.value }, ) } placeholder="0" />
setEditingBase( (p) => p && { ...p, customparams: e.target.value, }, ) } placeholder="" />
setEditingBase( (p) => p && { ...p, effectIdMale: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingBase( (p) => p && { ...p, effectIdFemale: parseInt(e.target.value, 10) || 0, }, ) } />
setEditingBase( (p) => p && { ...p, clothingOnWalk: e.target.value, }, ) } placeholder="" />
)}
{/* Action buttons */}
)} {/* ── Move Items Dialog ── */} { if (!open) { setMoveOpen(false); setMoveTargetPageId(null); } }} > Move {selected.size} item(s) to another page
{/* ── Move Single Item Dialog ── */} { if (!open) { setMoveOneId(null); setMoveOneTarget(null); } }} > Move Item to another page
{/* Item info */} {moveOneItem && (

{moveOneItem.catalogName || moveOneItem.baseName}

#{moveOneItem.id} · {moveOneItem.baseItemName || "—"} {moveOneItem.baseItem?.interactionType && ` · ${moveOneItem.baseItem.interactionType}`}

)} {moveOneSuggestions.length > 0 && (

Suggestions based on previous moves for similar items.

)}
{/* ── Bulk Price Dialog ── */} Bulk Price Update

{selected.size > 0 ? `Apply to ${selected.size} selected item(s).` : `Apply to all ${filteredItems.length} visible item(s).`}

setBulkPriceValue(parseFloat(e.target.value) || 0) } placeholder={ bulkPriceMode === "percent" ? "e.g. 10 for +10%" : "e.g. 5" } /> {bulkPriceMode === "percent" && (

Use negative values to decrease (e.g. -20 for -20%)

)} {bulkPriceMode === "add" && (

Use negative values to subtract

)}
); }