"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< number, { base: FurniRow; count: number; ids: number[] } >(); 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}
); }