"use client"; import { useEffect, useMemo, useState, useCallback, useRef } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; interface CategoryColumn { name: string; type: "TEXT" | "INTEGER" | "REAL" | "BOOLEAN"; notNull?: boolean; editable?: boolean; } interface CategoryTemplate { name: string; columns: CategoryColumn[]; imagePath: string; } const BASE_COLUMN_NAMES = ["id", "nome", "image_url", "image_local_path"]; const categoriesCache: { data: CategoryTemplate[] | null; pending: Promise | null; } = { data: null, pending: null, }; function generateTempId() { return Math.random().toString(36).slice(2, 10); } export interface ItemFormPayload { id?: number; name: string; category: string; rarity?: string | null; imageUrl?: string | null; imageLocalPath?: string | null; level?: number | null; weight?: number | null; slots?: number | null; skill?: string | null; description?: string | null; servers: string[]; tags: string[]; metadata?: Record; fields?: Record; } interface ItemReference { item_id: number; item_name: string; category: string; rarity: string | null; image_url: string | null; image_local_path: string | null; level: number | null; weight: number | null; slots: number | null; skill: string | null; description?: string | null; servers: string[]; tags: string[]; metadata: Record; } type AnalysisResult = | { table: string; columns: Record; } | null; function parseCsv(input: string): string[] { return input .split(",") .map((value) => value.trim()) .filter(Boolean); } function serializeCsv(list: string[]): string { if (!Array.isArray(list) || list.length === 0) return ""; return list.join(", "); } function parseNumberInput(value: string): number | null { if (!value) return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } interface HeaderSlotProps { title: string; description: string; } interface FooterSlotProps { isEdit: boolean; submitting?: boolean; submitLabel: string; onCancel?: () => void; onSubmitClick: () => void; } type PrefillPayload = Partial<{ name: string; category: string; rarity: string | null; imageUrl: string | null; imageLocalPath: string | null; level: number | string | null; weight: number | string | null; slots: number | string | null; skill: string | null; servers: string[] | string; tags: string[] | string; description: string | null; metadata: Record; }>; export interface AdminItemFormProps { mode: "create" | "edit"; item?: ItemReference; categories: string[]; serverOptions: string[]; submitting?: boolean; onSubmit: (payload: ItemFormPayload) => Promise; onCancel?: () => void; onSuccess?: () => void; isActive?: boolean; allowAiTools?: boolean; prefillData?: PrefillPayload | null; className?: string; contentClassName?: string; headerSlot?: (props: HeaderSlotProps) => React.ReactNode; footerSlot?: (props: FooterSlotProps) => React.ReactNode; } export function AdminItemForm({ mode, item, categories, serverOptions, submitting, onSubmit, onCancel, onSuccess, isActive = true, allowAiTools: allowAiToolsProp, prefillData, className, contentClassName, headerSlot, footerSlot, }: AdminItemFormProps) { const isEdit = mode === "edit"; const allowAiTools = allowAiToolsProp ?? !isEdit; const [name, setName] = useState(""); const [category, setCategory] = useState(""); const [rarity, setRarity] = useState(""); const [imageUrl, setImageUrl] = useState(""); const [imageLocalPath, setImageLocalPath] = useState(""); const [description, setDescription] = useState(""); const [level, setLevel] = useState(""); const [weight, setWeight] = useState(""); const [slots, setSlots] = useState(""); const [skill, setSkill] = useState(""); const [serversCsv, setServersCsv] = useState(""); const [tagsCsv, setTagsCsv] = useState(""); const [analysis, setAnalysis] = useState(null); const [analysisLoading, setAnalysisLoading] = useState(false); const [analysisError, setAnalysisError] = useState(null); const [submitStatus, setSubmitStatus] = useState(null); const [categoryTemplates, setCategoryTemplates] = useState([]); const [categoryLoading, setCategoryLoading] = useState(false); const [categoryError, setCategoryError] = useState(null); const [dynamicValues, setDynamicValues] = useState>({}); const [prefilledMetadata, setPrefilledMetadata] = useState>({}); const [showNewCategoryModal, setShowNewCategoryModal] = useState(false); const [showNewFieldModal, setShowNewFieldModal] = useState(false); const [newCategoryName, setNewCategoryName] = useState(""); const [newCategoryFields, setNewCategoryFields] = useState< Array<{ id: string; name: string; type: CategoryColumn["type"]; notNull: boolean }> >([]); const [newCategoryError, setNewCategoryError] = useState(null); const [newFieldName, setNewFieldName] = useState(""); const [newFieldType, setNewFieldType] = useState("TEXT"); const [newFieldNotNull, setNewFieldNotNull] = useState(false); const [newFieldError, setNewFieldError] = useState(null); const dynamicInitializedRef = useRef(false); const lastCategoryRef = useRef(null); const [pendingPrefill, setPendingPrefill] = useState(null); const resetFormState = useCallback( (templateList: CategoryTemplate[]) => { if (isEdit && item) { setName(item.item_name); setCategory(item.category.toLowerCase()); setRarity(item.rarity ?? ""); setImageUrl(item.image_url ?? ""); setImageLocalPath(item.image_local_path ?? ""); setDescription(item.description ?? ""); setLevel(item.level !== null && item.level !== undefined ? String(item.level) : ""); setWeight(item.weight !== null && item.weight !== undefined ? String(item.weight) : ""); setSlots(item.slots !== null && item.slots !== undefined ? String(item.slots) : ""); setSkill(item.skill ?? ""); setServersCsv(serializeCsv(item.servers)); setTagsCsv(serializeCsv(item.tags)); } else { const defaultCategory = templateList[0]?.name ?? categories[0] ?? ""; setName(""); setCategory(defaultCategory.toLowerCase()); setRarity(""); setImageUrl(""); setImageLocalPath(""); setDescription(""); setLevel(""); setWeight(""); setSlots(""); setSkill(""); setServersCsv(""); setTagsCsv(""); } setSubmitStatus(null); setAnalysis(null); setAnalysisError(null); setCategoryError(null); setNewCategoryError(null); setNewFieldError(null); setNewCategoryFields([]); dynamicInitializedRef.current = false; lastCategoryRef.current = null; setPrefilledMetadata({}); }, [isEdit, item, categories], ); const fetchCategories = useCallback(async () => { setCategoryError(null); if (categoriesCache.data) { setCategoryTemplates(categoriesCache.data); resetFormState(categoriesCache.data); return; } if (categoriesCache.pending) { setCategoryLoading(true); try { const cached = await categoriesCache.pending; if (cached) { setCategoryTemplates(cached); resetFormState(cached); } } catch (error) { console.error(error); setCategoryError(error instanceof Error ? error.message : "Erro ao carregar categorias"); } finally { setCategoryLoading(false); } return; } setCategoryLoading(true); const pendingPromise = (async () => { try { const response = await fetch("/api/admin/categories", { credentials: "include" }); if (!response.ok) { throw new Error("Falha ao carregar categorias"); } const data = await response.json(); const templates = Array.isArray(data?.categories) ? data.categories : []; categoriesCache.data = templates; setCategoryTemplates(templates); resetFormState(templates); return templates; } catch (error) { console.error(error); setCategoryError(error instanceof Error ? error.message : "Erro ao carregar categorias"); throw error; } finally { categoriesCache.pending = null; setCategoryLoading(false); } })(); categoriesCache.pending = pendingPromise; await pendingPromise; }, [resetFormState]); useEffect(() => { if (!isActive) { resetFormState(categoryTemplates); return; } fetchCategories(); }, [isActive, fetchCategories, categoryTemplates, resetFormState]); useEffect(() => { if (!isActive) { resetFormState(categoryTemplates); return; } if (categoryTemplates.length === 0) { resetFormState(categoryTemplates); } }, [isActive, categoryTemplates, resetFormState]); const getTemplateForCategory = useCallback( (categoryName: string | null | undefined) => categoryTemplates.find((template) => template.name === (categoryName ?? "").toLowerCase()), [categoryTemplates], ); const buildDynamicInitialValues = useCallback( (template: CategoryTemplate | undefined, metadata?: Record) => { if (!template) return {} as Record; const values: Record = {}; template.columns .filter((column) => !BASE_COLUMN_NAMES.includes(column.name) && column.editable !== false) .forEach((column) => { const sourceValue = metadata ? metadata[column.name] : undefined; if (sourceValue === undefined || sourceValue === null) { values[column.name] = column.type === "BOOLEAN" ? false : ""; return; } if (column.type === "BOOLEAN") { values[column.name] = Boolean(Number(sourceValue)); } else if (column.type === "INTEGER" || column.type === "REAL") { values[column.name] = String(sourceValue); } else { values[column.name] = String(sourceValue); } }); return values; }, [], ); useEffect(() => { if (!isActive) return; const effectiveCategory = ( category || item?.category || categoryTemplates[0]?.name || "" ).toLowerCase(); if (!effectiveCategory) return; const template = getTemplateForCategory(effectiveCategory); if (!template) return; if (!category) { setCategory(template.name); } const metadataSource = isEdit ? (item?.metadata as Record | undefined) : undefined; if (!dynamicInitializedRef.current || lastCategoryRef.current !== template.name) { setDynamicValues(buildDynamicInitialValues(template, metadataSource)); dynamicInitializedRef.current = true; lastCategoryRef.current = template.name; } }, [ isActive, category, item, categoryTemplates, getTemplateForCategory, buildDynamicInitialValues, isEdit, ]); const handleAnalyze = async () => { if (!allowAiTools) return; if (!name.trim() || !description.trim()) { setAnalysisError("Informe nome e descrição para gerar o JSON."); return; } setAnalysisError(null); setAnalysis(null); setAnalysisLoading(true); try { const res = await fetch("/api/admin/items/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim(), description: description.trim(), scope: serverOptions.length > 0 ? "server" : "global", server: serverOptions[0] ?? null, }), }); if (!res.ok) { throw new Error("Falha ao comunicar com o serviço de IA."); } const data = await res.json(); setAnalysis(data.structure ?? null); } catch (error) { console.error(error); setAnalysisError( error instanceof Error ? error.message : "Erro desconhecido ao gerar JSON.", ); } finally { setAnalysisLoading(false); } }; const handleDynamicValueChange = useCallback( (column: CategoryColumn, value: string | boolean | null) => { setDynamicValues((prev) => ({ ...prev, [column.name]: value, })); }, [], ); const handleCategoryChange = useCallback((value: string) => { dynamicInitializedRef.current = false; lastCategoryRef.current = null; setCategory(value.toLowerCase()); }, []); const handleAddCategoryField = useCallback(() => { setNewCategoryFields((prev) => [ ...prev, { id: generateTempId(), name: "", type: "TEXT", notNull: false }, ]); }, []); const handleUpdateCategoryField = useCallback( (id: string, key: "name" | "type" | "notNull", value: string | boolean) => { setNewCategoryFields((prev) => prev.map((field) => field.id === id ? { ...field, [key]: key === "notNull" ? Boolean(value) : value, } : field, ), ); }, [], ); const handleRemoveCategoryField = useCallback((id: string) => { setNewCategoryFields((prev) => prev.filter((field) => field.id !== id)); }, []); const handleCreateCategory = useCallback(async () => { if (!newCategoryName.trim()) { setNewCategoryError("Informe o nome da categoria"); return; } try { setNewCategoryError(null); const payload = { name: newCategoryName.trim().toLowerCase(), columns: newCategoryFields .filter((field) => field.name.trim()) .map((field) => ({ name: field.name.trim().toLowerCase(), type: field.type, notNull: field.notNull, })), }; const res = await fetch("/api/admin/categories", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload), }); if (!res.ok) { const error = await res.json().catch(() => null); throw new Error(error?.error || "Falha ao criar categoria"); } categoriesCache.data = null; await fetchCategories(); handleCategoryChange(payload.name); setShowNewCategoryModal(false); setNewCategoryFields([]); } catch (error) { console.error(error); setNewCategoryError( error instanceof Error ? error.message : "Erro ao criar categoria", ); } }, [ newCategoryName, newCategoryFields, fetchCategories, handleCategoryChange, ]); const handleAddFieldToCategory = useCallback(async () => { if (!category) { setNewFieldError("Selecione uma categoria"); return; } if (!newFieldName.trim()) { setNewFieldError("Informe o nome do campo"); return; } try { setNewFieldError(null); const payload = { name: newFieldName.trim().toLowerCase(), type: newFieldType, notNull: newFieldNotNull, }; const res = await fetch(`/api/admin/categories/${category}/fields`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload), }); if (!res.ok) { const error = await res.json().catch(() => null); throw new Error(error?.error || "Falha ao adicionar campo"); } categoriesCache.data = null; await fetchCategories(); dynamicInitializedRef.current = false; lastCategoryRef.current = null; setShowNewFieldModal(false); } catch (error) { console.error(error); setNewFieldError( error instanceof Error ? error.message : "Erro ao adicionar campo", ); } }, [category, newFieldName, newFieldType, newFieldNotNull, fetchCategories]); const serverSuggestions = useMemo(() => { if (serverOptions.length === 0) return ""; return serverOptions.join(", "); }, [serverOptions]); const activeTemplate = useMemo(() => getTemplateForCategory(category), [category, getTemplateForCategory]); const dynamicColumns = useMemo( () => activeTemplate?.columns.filter( (column) => !BASE_COLUMN_NAMES.includes(column.name) && column.editable !== false, ) ?? [], [activeTemplate], ); const submitLabel = isEdit ? "Salvar alterações" : "Criar item"; const handlePrimarySubmit = useCallback(async () => { if (!name.trim()) { setSubmitStatus("Informe o nome do item."); return; } if (!category.trim()) { setSubmitStatus("Informe a categoria do item."); return; } setSubmitStatus(null); try { const metadataPayload: Record = { ...prefilledMetadata }; if (allowAiTools && analysis) { metadataPayload.aiStructure = analysis; } for (const [key, value] of Object.entries(dynamicValues)) { metadataPayload[key] = value; } await onSubmit({ id: item?.item_id, name: name.trim(), category: category.trim().toLowerCase(), rarity: rarity.trim() || null, imageUrl: imageUrl.trim() || null, imageLocalPath: imageLocalPath.trim() || null, description: description.trim() || null, level: parseNumberInput(level), weight: parseNumberInput(weight), slots: parseNumberInput(slots), skill: skill.trim() || null, servers: parseCsv(serversCsv), tags: parseCsv(tagsCsv), metadata: metadataPayload, fields: dynamicValues, }); setSubmitStatus(isEdit ? "Alterações salvas com sucesso." : "Item criado com sucesso."); onSuccess?.(); if (!isEdit) { resetFormState(categoryTemplates); } } catch (error) { console.error(error); setSubmitStatus(error instanceof Error ? error.message : "Erro ao salvar. Tente novamente."); } }, [ name, category, rarity, imageUrl, imageLocalPath, description, level, weight, slots, skill, serversCsv, tagsCsv, dynamicValues, allowAiTools, analysis, onSubmit, item, isEdit, onSuccess, categoryTemplates, resetFormState, prefilledMetadata, ]); const title = isEdit ? "Editar item" : "Novo item"; const descriptionText = allowAiTools ? "Preencha os dados do item unificado. Utilize a IA para gerar um JSON de referência, se desejar." : "Preencha os dados do item unificado. Ajuste as sugestões geradas pela IA acima para concluir o cadastro."; const handleFormSubmit = (event: React.FormEvent) => { event.preventDefault(); void handlePrimarySubmit(); }; const formClassName = className ?? "flex h-full flex-col"; const bodyClassName = contentClassName ?? "flex-1 min-h-0 overflow-y-auto px-6 py-4"; const applyPrefill = useCallback( (prefill: PrefillPayload) => { if (prefill.name !== undefined) setName(prefill.name ?? ""); if (prefill.description !== undefined) setDescription(prefill.description ?? ""); if (prefill.rarity !== undefined) setRarity(prefill.rarity ?? ""); if (prefill.imageUrl !== undefined) setImageUrl(prefill.imageUrl ?? ""); if (prefill.imageLocalPath !== undefined) setImageLocalPath(prefill.imageLocalPath ?? ""); if (prefill.level !== undefined) setLevel(prefill.level ? String(prefill.level) : ""); if (prefill.weight !== undefined) setWeight(prefill.weight ? String(prefill.weight) : ""); if (prefill.slots !== undefined) setSlots(prefill.slots ? String(prefill.slots) : ""); if (prefill.skill !== undefined) setSkill(prefill.skill ?? ""); if (prefill.servers !== undefined) { const serversValue = Array.isArray(prefill.servers) ? prefill.servers.join(", ") : prefill.servers ?? ""; setServersCsv(serversValue); } if (prefill.tags !== undefined) { const tagsValue = Array.isArray(prefill.tags) ? prefill.tags.join(", ") : prefill.tags ?? ""; setTagsCsv(tagsValue); } let targetCategory = category; const metadataCategory = typeof prefill.metadata?.category === "string" ? prefill.metadata.category.toLowerCase() : null; if (prefill.category) { targetCategory = prefill.category.toLowerCase(); setCategory(targetCategory); dynamicInitializedRef.current = false; lastCategoryRef.current = null; } else if (metadataCategory) { targetCategory = metadataCategory; setCategory(targetCategory); dynamicInitializedRef.current = false; lastCategoryRef.current = null; } if (prefill.metadata && typeof prefill.metadata === "object") { setPrefilledMetadata(prefill.metadata); const template = getTemplateForCategory(targetCategory); if (template) { setDynamicValues(buildDynamicInitialValues(template, prefill.metadata)); } } }, [ buildDynamicInitialValues, category, getTemplateForCategory, ], ); useEffect(() => { if (prefillData) { setPendingPrefill(prefillData); } }, [prefillData]); useEffect(() => { if (!pendingPrefill) return; if (categoryTemplates.length === 0) return; applyPrefill(pendingPrefill); setPendingPrefill(null); }, [applyPrefill, pendingPrefill, categoryTemplates]); return ( <>
{headerSlot?.({ title, description: descriptionText }) ?? (

{title}

{descriptionText}

)}
setName(event.target.value)} />
{categoryError &&

{categoryError}

} {isEdit && (

A categoria existente do item não pode ser alterada neste fluxo.

)}
setRarity(event.target.value)} />
setImageUrl(event.target.value)} />
setImageLocalPath(event.target.value)} />
setLevel(event.target.value)} />
setWeight(event.target.value)} />
setSlots(event.target.value)} />
setSkill(event.target.value)} />
{dynamicColumns.length > 0 && (

Defina valores adicionais conforme o modelo da categoria selecionada.

{dynamicColumns.map((column) => { const value = dynamicValues[column.name]; if (column.type === "BOOLEAN") { return (
handleDynamicValueChange(column, checked === true) } />
); } const inputType = column.type === "TEXT" ? "text" : "number"; const step = column.type === "REAL" ? "0.01" : "1"; return (
handleDynamicValueChange(column, event.target.value) } />
); })}
)}
setServersCsv(event.target.value)} /> {serverSuggestions && (

Sugestões cadastradas: {serverSuggestions}

)}
setTagsCsv(event.target.value)} />