'use client' import { Armchair, ArrowDown, ArrowLeft, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, Frame, Layers, Pencil, RefreshCw, Search, Trash2, Users, UserX, } from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' import { bulkDeleteRoomItems, deleteRoomItem, roomRconAction } from '@/actions/rooms' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useServerAction } from '@/hooks/use-server-action' import { EditItemDialog } from './edit-item-dialog' interface FurniRow { id: number baseId: number name: string itemName: string type: string x: number y: number z: number rot: number wallPos: string userId: number ownerUsername: string | null extraData: string } interface Props { room: { id: number name: string ownerUsername: string } items?: FurniRow[] canEdit: boolean } const PER_PAGE = 50 type SortKey = 'id' | 'name' | 'type' | 'pos' | 'owner' type SortDir = 'asc' | 'desc' type View = 'flat' | 'grouped' function iconUrl(itemName: string) { if (!itemName) return null const base = itemName.split('*')[0] return `/swf/dcr/hof_furni/icons/${base}_icon.png` } function isWallType(t: string) { return t === 'i' } export function FurniClient({ room, items = [], canEdit }: Props) { const { run, isPending } = useServerAction() const [search, setSearch] = useState('') const [page, setPage] = useState(1) const [view, setView] = useState('flat') const [typeFilter, setTypeFilter] = useState<'all' | 'floor' | 'wall'>('all') const [ownerFilter, setOwnerFilter] = useState('all') const [sortKey, setSortKey] = useState('id') const [sortDir, setSortDir] = useState('desc') const [selected, setSelected] = useState>(new Set()) const [editingItem, setEditingItem] = useState(null) // ── Stats ────────────────────────────────────────────────── const stats = useMemo(() => { const floor = items.filter((i) => !isWallType(i.type)).length const wall = items.filter((i) => isWallType(i.type)).length const uniqueBases = new Set(items.map((i) => i.baseId)).size const owners = new Set(items.map((i) => i.userId).filter((u) => u > 0)).size return { total: items.length, floor, wall, uniqueBases, owners } }, [items]) // ── Owner options for filter ─────────────────────────────── const ownerOptions = useMemo(() => { const map = new Map() for (const i of items) { const key = i.ownerUsername ?? `#${i.userId}` map.set(key, (map.get(key) ?? 0) + 1) } return Array.from(map.entries()).sort((a, b) => b[1] - a[1]) }, [items]) // ── Filtering ────────────────────────────────────────────── const filtered = useMemo(() => { const q = search.trim().toLowerCase() return items.filter((i) => { if (typeFilter === 'floor' && isWallType(i.type)) return false if (typeFilter === 'wall' && !isWallType(i.type)) return false if (ownerFilter !== 'all') { const key = i.ownerUsername ?? `#${i.userId}` if (key !== ownerFilter) return false } if (q) { return ( i.name.toLowerCase().includes(q) || i.itemName.toLowerCase().includes(q) || String(i.baseId).includes(q) || String(i.id).includes(q) ) } return true }) }, [items, search, typeFilter, ownerFilter]) // ── Sorting ──────────────────────────────────────────────── const sorted = useMemo(() => { const arr = [...filtered] arr.sort((a, b) => { let cmp = 0 switch (sortKey) { case 'id': cmp = a.id - b.id break case 'name': cmp = a.name.localeCompare(b.name) break case 'type': cmp = a.type.localeCompare(b.type) break case 'pos': cmp = a.x - b.x || a.y - b.y break case 'owner': cmp = (a.ownerUsername ?? '').localeCompare(b.ownerUsername ?? '') break } return sortDir === 'asc' ? cmp : -cmp }) return arr }, [filtered, sortKey, sortDir]) // ── Grouped view ─────────────────────────────────────────── const grouped = useMemo(() => { const map = new Map() for (const i of filtered) { const existing = map.get(i.baseId) if (existing) { existing.count++ existing.ids.push(i.id) } else { map.set(i.baseId, { base: i, count: 1, ids: [i.id] }) } } return Array.from(map.values()).sort((a, b) => b.count - a.count) }, [filtered]) // ── Pagination (flat view only) ───────────────────────────── const lastPage = Math.max(1, Math.ceil(sorted.length / PER_PAGE)) const currentPage = Math.min(page, lastPage) const pageRows = sorted.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE) // ── Handlers ─────────────────────────────────────────────── function toggleSort(key: SortKey) { if (sortKey === key) { setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) } else { setSortKey(key) setSortDir('asc') } } function SortIcon({ k }: { k: SortKey }) { if (sortKey !== k) return return sortDir === 'asc' ? : } function togglePageSelection(checked: boolean) { const next = new Set(selected) for (const r of pageRows) { if (checked) next.add(r.id) else next.delete(r.id) } setSelected(next) } function toggleRowSelection(id: number, checked: boolean) { const next = new Set(selected) if (checked) next.add(id) else next.delete(id) setSelected(next) } function handleRcon(action: 'reload' | 'kick') { if (action === 'kick' && !confirm('Kick all users from this room?')) return run(() => roomRconAction({ roomId: room.id, action }), { successMessage: action === 'reload' ? 'Room reloaded' : 'All users kicked', }) } function handleDelete(itemId: number, name: string) { if (!confirm(`Delete "${name}" (#${itemId}) from this room?`)) return run(() => deleteRoomItem({ roomId: room.id, itemId }), { successMessage: 'Item deleted' }) } function handleBulkDelete() { if (selected.size === 0) return if (!confirm(`Delete ${selected.size} selected item${selected.size > 1 ? 's' : ''}?`)) return run(() => bulkDeleteRoomItems({ roomId: room.id, itemIds: Array.from(selected) }), { successMessage: `${selected.size} item(s) deleted`, onSuccess: () => setSelected(new Set()), }) } function handleDeleteGroup(_baseId: number, ids: number[], name: string) { if (!confirm(`Delete all ${ids.length} "${name}" from this room?`)) return run(() => bulkDeleteRoomItems({ roomId: room.id, itemIds: ids }), { successMessage: `${ids.length} item(s) deleted`, }) } const allPageSelected = pageRows.length > 0 && pageRows.every((r) => selected.has(r.id)) return (
{/* ── Header ─────────────────────────────────────────── */}

{room.name}

Room #{room.id} · Owner: {room.ownerUsername}

{canEdit && (
)}
{/* ── Stats Cards ────────────────────────────────────── */}
} label="Total" value={stats.total} /> } label="Floor" value={stats.floor} /> } label="Wall" value={stats.wall} /> } label="Unique furni" value={stats.uniqueBases} /> } label="Owners" value={stats.owners} />
{/* ── Toolbar ────────────────────────────────────────── */}
{ setSearch(e.target.value) setPage(1) }} className="pl-8" />
setView(v as View)} className="ml-auto"> Flat Grouped
{/* Bulk action bar */} {view === 'flat' && selected.size > 0 && canEdit && (
{selected.size} selected
)} {/* ── Flat Table ─────────────────────────────────── */} {view === 'flat' ? ( <>
{canEdit && ( togglePageSelection(c === true)} /> )} Rot {canEdit && Actions} {pageRows.length === 0 ? ( {items.length === 0 ? (

This room has no furniture.

) : (

No items match your filters.

)}
) : ( pageRows.map((item) => { const icon = iconUrl(item.itemName) const wall = isWallType(item.type) const isSelected = selected.has(item.id) return ( {canEdit && ( toggleRowSelection(item.id, c === true)} /> )} {icon ? ( // eslint-disable-next-line @next/next/no-img-element {item.name} { ;(e.target as HTMLImageElement).style.visibility = 'hidden' }} /> ) : (
)}
{item.name}
{item.itemName || `base #${item.baseId}`}
{wall ? 'Wall' : item.type === 's' ? 'Floor' : item.type} {wall ? item.wallPos || '-' : `${item.x},${item.y},${item.z.toFixed(1)}`} {item.rot} {item.ownerUsername ? ( {item.ownerUsername} ) : ( #{item.userId} )} #{item.id} {canEdit && (
)} ) }) )}
{lastPage > 1 && (
Showing {(currentPage - 1) * PER_PAGE + 1}- {Math.min(currentPage * PER_PAGE, sorted.length)} of {sorted.length}
{currentPage} / {lastPage}
)} ) : ( /* ── Grouped View ──────────────────────────────── */
Furniture Type Count {canEdit && Action} {grouped.length === 0 ? ( No items. ) : ( grouped.map((g) => { const icon = iconUrl(g.base.itemName) const wall = isWallType(g.base.type) return ( {icon ? ( // eslint-disable-next-line @next/next/no-img-element {g.base.name} { ;(e.target as HTMLImageElement).style.visibility = 'hidden' }} /> ) : (
)}
{g.base.name}
{g.base.itemName || `base #${g.base.baseId}`}
{wall ? 'Wall' : g.base.type === 's' ? 'Floor' : g.base.type} {g.count} {canEdit && ( )} ) }) )}
)}
!o && setEditingItem(null)} />
) } function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: number }) { return (
{label}
{value.toLocaleString()}
{icon}
) }