'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 { 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, DialogTrigger, } 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 [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() }, }, ) } // ── 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 if (!confirm(`Delete ${selected.size} selected item(s)?`)) 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) { if (!confirm('Delete this catalog item?')) 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 (
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}c )} {item.costPoints > 0 && ( {item.costPoints} {pts.label}{' '} )} {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

)}
) }