"use client"; import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; import { Pencil, Plus, RefreshCw } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ItemCreationModal, type ItemFormPayload, } from "@/components/admin/items/ItemCreationModal"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export interface AdminItemRecord { item_id: number; source_table_id: number; source_table_name: string; item_name: string; category: string; image_local_path: string | null; image_url: string | null; rarity: string | null; level: number | null; weight: number | null; slots: number | null; skill: string | null; metadata: Record; created_at: string | Date; updated_at: string | Date; servers: string[]; tags: string[]; description?: string | null; } export interface AdminItemsResponse { items: AdminItemRecord[]; total: number; page: number; pageSize: number; servers: string[]; categories: string[]; } interface AdminItemManagerProps { initialData: AdminItemsResponse; locale: string; } export function AdminItemManager({ initialData, locale, }: AdminItemManagerProps) { const router = useRouter(); const [data, setData] = useState(initialData); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [apiStatus, setApiStatus] = useState<{ code: number | null; ok: boolean | null; durationMs: number | null; checkedAt: string | null; }>({ code: null, ok: null, durationMs: null, checkedAt: null }); const [serverFilter, setServerFilter] = useState("all"); const [search, setSearch] = useState(""); const [categoryFilter, setCategoryFilter] = useState("all"); const [rarityFilter, setRarityFilter] = useState("all"); const [page, setPage] = useState(initialData.page ?? 1); const pageSize = initialData.pageSize ?? 50; const [modalOpen, setModalOpen] = useState(false); const [selectedItem, setSelectedItem] = useState( null, ); const [isSubmitting, setIsSubmitting] = useState(false); const [activeServers, setActiveServers] = useState([]); const [_serversLoading, setServersLoading] = useState(false); const searchDebounce = useRef(undefined); function isAdminItemsResponse(val: any): val is AdminItemsResponse { return ( val && Array.isArray(val.items) && typeof val.total === "number" && typeof val.page === "number" && typeof val.pageSize === "number" && Array.isArray(val.servers) && Array.isArray(val.categories) ); } // Carrega servidores ativos independentemente dos itens cadastrados useEffect(() => { let cancelled = false; async function loadServers() { try { setServersLoading(true); const res = await fetch("/api/servers", { credentials: "include", headers: { Accept: "application/json" }, cache: "no-store", }); if (!res.ok) { console.warn( "[AdminItemManager] falha ao buscar servidores ativos:", res.status, ); return; } const json = await res.json(); const names: string[] = Array.isArray(json?.servers) ? json.servers .map((s: any) => typeof s?.serverName === "string" ? s.serverName : null, ) .filter((n: string | null): n is string => Boolean(n)) : []; if (!cancelled) { setActiveServers(names); } } catch (e) { console.warn("[AdminItemManager] erro ao buscar /api/servers:", e); } finally { if (!cancelled) setServersLoading(false); } } loadServers(); return () => { cancelled = true; }; }, []); const fetchItems = useCallback(async () => { setLoading(true); setError(null); const params = new URLSearchParams(); if (search.trim()) params.set("q", search.trim()); if (serverFilter !== "all") params.set("server", serverFilter); if (categoryFilter !== "all") params.set("category", categoryFilter); if (rarityFilter !== "all") params.set("rarity", rarityFilter); params.set("page", String(page)); params.set("pageSize", String(pageSize)); const url = `/api/admin/items?${params.toString()}`; const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), 15000); const start = performance.now(); console.group("[AdminItemManager] fetchItems"); console.log("URL:", url); console.log("QueryParams:", Object.fromEntries(params.entries())); try { const res = await fetch(url, { credentials: "include", headers: { Accept: "application/json" }, cache: "no-store", signal: controller.signal, }); const durationMs = Math.round(performance.now() - start); setApiStatus({ code: res.status, ok: res.ok, durationMs, checkedAt: new Date().toISOString(), }); console.log( "Status:", res.status, res.statusText, "Duration:", `${durationMs}ms`, ); console.log("Headers:", { "content-type": res.headers.get("content-type"), "x-powered-by": res.headers.get("x-powered-by"), }); if (!res.ok) { let errorPayload: any = null; try { errorPayload = await res.json(); } catch { // pode não ser JSON } const baseMsg = res.status === 401 ? "Sessão expirada ou não autenticado. Faça login e tente novamente." : res.status === 403 ? "Acesso restrito ao administrador." : "Não foi possível carregar os itens."; const apiMsg = errorPayload?.error || errorPayload?.message || null; const composed = apiMsg ? `${baseMsg} (${apiMsg})` : `${baseMsg} (HTTP ${res.status})`; console.error("Erro na API:", composed, "Payload:", errorPayload); throw new Error(composed); } const json = await res.json(); if (!isAdminItemsResponse(json)) { console.error("Resposta inválida da API:", json); throw new Error("Resposta inválida da API de itens."); } setData(json); console.log("Itens carregados:", { total: json.total, page: json.page, pageSize: json.pageSize, }); } catch (err: any) { const durationMs = Math.round(performance.now() - start); setApiStatus((prev) => ({ code: prev.code, ok: false, durationMs, checkedAt: new Date().toISOString(), })); if (err?.name === "AbortError") { console.error("Requisição abortada por timeout"); setError("Tempo de resposta excedido. Tente novamente."); } else { console.error("Falha ao carregar itens:", err); setError( err instanceof Error ? err.message : "Falha ao carregar itens.", ); } } finally { console.groupEnd(); window.clearTimeout(timeoutId); setLoading(false); } }, [ search, serverFilter, categoryFilter, rarityFilter, page, pageSize, isAdminItemsResponse, ]); useEffect(() => { if (searchDebounce.current !== undefined) { window.clearTimeout(searchDebounce.current); } searchDebounce.current = window.setTimeout(() => { fetchItems(); }, 350); return () => { if (searchDebounce.current !== undefined) { window.clearTimeout(searchDebounce.current); searchDebounce.current = undefined; } }; }, [fetchItems]); const rarityOptions = useMemo(() => { const set = new Set(); data.items.forEach((item) => { if (item.rarity) { set.add(item.rarity); } }); return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }), ); }, [data.items]); const openEditModal = (item: AdminItemRecord) => { setSelectedItem(item); setModalOpen(true); }; const handleSubmit = async (payload: ItemFormPayload) => { setIsSubmitting(true); try { const method = "PUT"; const body: Record = { id: payload.id, name: payload.name, category: payload.category, rarity: payload.rarity, imageUrl: payload.imageUrl, imageLocalPath: payload.imageLocalPath, level: payload.level, weight: payload.weight, slots: payload.slots, skill: payload.skill, description: payload.description, servers: payload.servers, tags: payload.tags, metadata: payload.metadata, fields: payload.fields, }; const res = await fetch("/api/admin/items", { method, credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { const error = await res .json() .catch(() => ({ error: "Erro desconhecido" })); throw new Error( error?.error || error?.message || "Falha ao salvar item", ); } await fetchItems(); setModalOpen(false); } catch (err) { console.error(err); setError(err instanceof Error ? err.message : "Erro ao salvar item."); } finally { setIsSubmitting(false); } }; const itemsForDisplay = data.items; const serverTabs = ["all", ...data.servers]; return (
Todos os itens cadastrados Visualize e edite os registros unificados importados do database do Tibia Wiki. Itens específicos de servidores podem ser marcados através das tags.
setSearch(event.target.value)} />
{loading ? "Carregando..." : `${data.total} itens encontrados`} {apiStatus.code !== null && ( API: {apiStatus.code} {apiStatus.ok ? "OK" : "Erro"} {apiStatus.durationMs !== null ? ` • ${apiStatus.durationMs}ms` : ""} )}
{error && ( {error} )} {loading ? (
) : ( { setServerFilter(value); setPage(1); }} className="space-y-4" > {serverTabs.map((server) => ( {server === "all" ? "Todos" : server} ))} {serverTabs.map((server) => (
{itemsForDisplay.length === 0 ? ( ) : ( itemsForDisplay.map((item) => ( )) )}
Imagem Nome Categoria Raridade Origem Servidores Atualizado em Ações
Nenhum item encontrado com os filtros atuais.
{item.image_url ? (
{item.item_name}
) : ( Sem imagem )}
{item.item_name}
#{item.item_id}
{item.category} {item.rarity ? ( {item.rarity} ) : ( - )}
{item.source_table_name}
ID origem: {item.source_table_id}
{item.servers.length > 0 ? (
{item.servers.map((serverTag) => ( {serverTag} ))}
) : ( Global )}
{format( new Date(item.updated_at), "dd/MM/yyyy HH:mm", { locale: ptBR }, )}
Página {page} • Total de{" "} {Math.max(1, Math.ceil(data.total / pageSize))}
))}
)}
); }