feat(admin): centralize subscriptions under /admin?tab=subscriptions

Replaced the dedicated /admin/subscriptions page with a redirect to the central admin panel’s subscriptions tab. Removed session checks, imports, and the AdminSubscriptionDashboard since routing and access are now handled centrally. This streamlines admin UX and keeps access control unified.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-09 01:50:50 -03:00
1 parent d8e7d5d7e6
commit 62ab5b2608
19 files changed
+3091 -357

No files matched your search

@@ -0,0 +1,990 @@
"use client";
import { useEffect, useMemo, useState, useCallback, useRef } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
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";
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"];
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<string, any>;
fields?: Record<string, string | boolean | null>;
}
interface ItemCreationModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
mode: "create" | "edit";
item?: {
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<string, any>;
};
categories: string[];
serverOptions: string[];
onSubmit: (payload: ItemFormPayload) => Promise<void>;
submitting?: boolean;
}
type AnalysisResult = {
table: string;
columns: Record<string, unknown>;
} | 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;
}
export function ItemCreationModal({
open,
onOpenChange,
mode,
item,
categories,
serverOptions,
onSubmit,
submitting,
}: ItemCreationModalProps) {
const isEdit = mode === "edit";
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<AnalysisResult>(null);
const [analysisLoading, setAnalysisLoading] = useState(false);
const [analysisError, setAnalysisError] = useState<string | null>(null);
const [submitStatus, setSubmitStatus] = useState<string | null>(null);
const allowAiTools = !isEdit;
const [categoryTemplates, setCategoryTemplates] = useState<CategoryTemplate[]>([]);
const [categoryLoading, setCategoryLoading] = useState(false);
const [categoryError, setCategoryError] = useState<string | null>(null);
const [dynamicValues, setDynamicValues] = useState<Record<string, string | boolean | null>>({});
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<string | null>(null);
const [newFieldName, setNewFieldName] = useState("");
const [newFieldType, setNewFieldType] = useState<CategoryColumn["type"]>("TEXT");
const [newFieldNotNull, setNewFieldNotNull] = useState(false);
const [newFieldError, setNewFieldError] = useState<string | null>(null);
const fetchCategories = useCallback(async () => {
setCategoryLoading(true);
setCategoryError(null);
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();
setCategoryTemplates(Array.isArray(data?.categories) ? data.categories : []);
} catch (error) {
console.error(error);
setCategoryError(error instanceof Error ? error.message : "Erro ao carregar categorias");
} finally {
setCategoryLoading(false);
}
}, []);
const dynamicInitializedRef = useRef(false);
const lastCategoryRef = useRef<string | null>(null);
useEffect(() => {
if (!open) return;
fetchCategories();
}, [open, fetchCategories]);
useEffect(() => {
if (!open) {
setSubmitStatus(null);
setAnalysis(null);
setAnalysisError(null);
dynamicInitializedRef.current = false;
lastCategoryRef.current = null;
return;
}
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));
setSubmitStatus(null);
} else {
const defaultCategory = categoryTemplates[0]?.name ?? categories[0] ?? "";
setName("");
setCategory(defaultCategory.toLowerCase());
setRarity("");
setImageUrl("");
setImageLocalPath("");
setDescription("");
setLevel("");
setWeight("");
setSlots("");
setSkill("");
setServersCsv("");
setTagsCsv("");
setSubmitStatus(null);
}
}, [open, isEdit, item, categories, categoryTemplates]);
const getTemplateForCategory = useCallback(
(categoryName: string | null | undefined) =>
categoryTemplates.find((template) => template.name === (categoryName ?? "").toLowerCase()),
[categoryTemplates]
);
const buildDynamicInitialValues = useCallback(
(template: CategoryTemplate | undefined, metadata?: Record<string, unknown>) => {
if (!template) return {} as Record<string, string | boolean | null>;
const values: Record<string, string | boolean | null> = {};
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;
},
[]
);
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 handleSubmit = 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<string, unknown> = {};
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.");
if (!isEdit) {
setName("");
setCategory(categories[0] ?? "");
setRarity("");
setImageUrl("");
setImageLocalPath("");
setDescription("");
setLevel("");
setWeight("");
setSlots("");
setSkill("");
setServersCsv("");
setTagsCsv("");
setAnalysis(null);
}
} catch (error) {
console.error(error);
setSubmitStatus(
error instanceof Error ? error.message : "Erro ao salvar. Tente novamente."
);
}
};
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");
}
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");
}
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],
);
useEffect(() => {
if (!open) 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<string, unknown> | undefined) : undefined;
if (!dynamicInitializedRef.current || lastCategoryRef.current !== template.name) {
setDynamicValues(buildDynamicInitialValues(template, metadataSource));
dynamicInitializedRef.current = true;
lastCategoryRef.current = template.name;
}
}, [open, category, categoryTemplates, getTemplateForCategory, buildDynamicInitialValues, isEdit, item]);
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl w-full h-[90vh] overflow-hidden">
<div className="flex h-full flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>{isEdit ? "Editar item" : "Novo item"}</DialogTitle>
<DialogDescription>
Preencha os dados do item unificado. Utilize a IA para gerar um JSON de referência, se desejar.
</DialogDescription>
</DialogHeader>
<div className="flex-1 min-h-0 overflow-y-auto pr-2">
<div className="grid gap-6 lg:grid-cols-[1.15fr,0.85fr]">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="item-name">Nome</Label>
<Input
id="item-name"
placeholder="Ex.: Final Fate"
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label>Categoria</Label>
<div className="flex flex-wrap items-center gap-2">
<Select
value={category}
onValueChange={handleCategoryChange}
disabled={categoryLoading || isEdit}
>
<SelectTrigger className="min-w-[200px]">
<SelectValue
placeholder={categoryLoading ? "Carregando..." : "Selecione a categoria"}
/>
</SelectTrigger>
<SelectContent>
{categoryTemplates.map((template) => (
<SelectItem key={template.name} value={template.name}>
{template.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setShowNewCategoryModal(true);
setNewCategoryError(null);
setNewCategoryName("");
setNewCategoryFields([]);
}}
disabled={categoryLoading}
>
Nova categoria
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setShowNewFieldModal(true);
setNewFieldError(null);
setNewFieldName("");
setNewFieldType("TEXT");
setNewFieldNotNull(false);
}}
disabled={!category || categoryLoading}
>
+ campo
</Button>
</div>
{categoryError && (
<p className="text-xs text-destructive">{categoryError}</p>
)}
{isEdit && (
<p className="text-xs text-muted-foreground">
A categoria existente do item não pode ser alterada neste fluxo.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="item-rarity">Raridade</Label>
<Input
id="item-rarity"
placeholder="Ex.: legendary"
value={rarity}
onChange={(event) => setRarity(event.target.value)}
/>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="item-image-url">Imagem (URL)</Label>
<Input
id="item-image-url"
placeholder="https://..."
value={imageUrl}
onChange={(event) => setImageUrl(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="item-image-local">Imagem local (caminho)</Label>
<Input
id="item-image-local"
placeholder="Opcional"
value={imageLocalPath}
onChange={(event) => setImageLocalPath(event.target.value)}
/>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="item-level">Level requerido</Label>
<Input
id="item-level"
placeholder="Ex.: 250"
value={level}
onChange={(event) => setLevel(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="item-weight">Peso</Label>
<Input
id="item-weight"
placeholder="Ex.: 45"
value={weight}
onChange={(event) => setWeight(event.target.value)}
/>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="item-slots">Slots</Label>
<Input
id="item-slots"
placeholder="Ex.: 2"
value={slots}
onChange={(event) => setSlots(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="item-skill">Skill requerido</Label>
<Input
id="item-skill"
placeholder="Ex.: Sword 115"
value={skill}
onChange={(event) => setSkill(event.target.value)}
/>
</div>
</div>
{dynamicColumns.length > 0 && (
<div className="space-y-4">
<div>
<Label className="text-sm font-semibold">Campos específicos da categoria</Label>
<p className="text-xs text-muted-foreground">
Defina valores adicionais conforme o modelo da categoria selecionada.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{dynamicColumns.map((column) => {
const value = dynamicValues[column.name];
if (column.type === "BOOLEAN") {
return (
<div key={column.name} className="flex items-center gap-2">
<Checkbox
id={`dynamic-${column.name}`}
checked={Boolean(value)}
onCheckedChange={(checked) =>
handleDynamicValueChange(column, checked === true)
}
/>
<Label htmlFor={`dynamic-${column.name}`} className="capitalize">
{column.name.replace(/_/g, " ")}
</Label>
</div>
);
}
const inputType = column.type === "TEXT" ? "text" : "number";
const step = column.type === "REAL" ? "0.01" : "1";
return (
<div key={column.name} className="space-y-2">
<Label htmlFor={`dynamic-${column.name}`} className="capitalize">
{column.name.replace(/_/g, " ")}
</Label>
<Input
id={`dynamic-${column.name}`}
type={inputType}
step={column.type === "TEXT" ? undefined : step}
value={
typeof value === "string"
? value
: value === null || value === undefined
? ""
: String(value)
}
onChange={(event) =>
handleDynamicValueChange(column, event.target.value)
}
/>
</div>
);
})}
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="item-servers">Servidores (separados por vírgula)</Label>
<Input
id="item-servers"
placeholder="Ex.: Ferobra, Boneka"
value={serversCsv}
onChange={(event) => setServersCsv(event.target.value)}
/>
{serverSuggestions && (
<p className="text-[11px] text-muted-foreground">
Sugestões cadastradas: {serverSuggestions}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="item-tags">Tags adicionais</Label>
<Input
id="item-tags"
placeholder="Ex.: endgame, two-handed"
value={tagsCsv}
onChange={(event) => setTagsCsv(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="item-description">Descrição detalhada</Label>
<Textarea
id="item-description"
placeholder="Cole aqui a descrição oficial do item..."
rows={6}
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
<div className="flex flex-wrap gap-2 pt-2">
{allowAiTools && (
<Button type="button" variant="outline" onClick={handleAnalyze} disabled={analysisLoading}>
{analysisLoading ? "Analisando..." : "Analisar com IA"}
</Button>
)}
<Button type="button" onClick={handleSubmit} disabled={submitting}>
{submitting ? "Salvando..." : isEdit ? "Salvar alterações" : "Criar item"}
</Button>
</div>
{submitStatus && <p className="text-xs text-muted-foreground">{submitStatus}</p>}
</div>
<div className="space-y-4">
{allowAiTools && (
<Card>
<CardHeader>
<CardTitle className="text-sm">Resultado da IA</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-xs text-muted-foreground">
O serviço Gemini deve retornar exclusivamente um JSON válido que descreve a tabela destino e os campos correspondentes.
</p>
{analysisError && (
<div className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{analysisError}
</div>
)}
<div className="max-h-64 overflow-auto rounded-md border bg-muted/40 p-3 text-[11px]">
<pre className="whitespace-pre-wrap">
{analysis ? JSON.stringify(analysis, null, 2) : "// Aguarde a análise para visualizar o JSON"}
</pre>
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle className="text-sm">Resumo</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-xs text-muted-foreground">
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="uppercase">
{category || "Categoria"}
</Badge>
{rarity && (
<Badge variant="outline" className="uppercase">
{rarity}
</Badge>
)}
</div>
<p>
Utilize os campos para definir servidores específicos. Todos os itens sem servidores marcados serão considerados globais.
</p>
</CardContent>
</Card>
</div>
</div>
</div>
<div className="shrink-0 pt-4">
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
Fechar
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={showNewCategoryModal} onOpenChange={setShowNewCategoryModal}>
<DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Nova categoria</DialogTitle>
<DialogDescription>
Defina nome e campos adicionais para criar uma nova categoria de itens.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-category-name">Nome da categoria</Label>
<Input
id="new-category-name"
placeholder="Ex.: vips"
value={newCategoryName}
onChange={(event) => setNewCategoryName(event.target.value)}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Campos iniciais (opcional)</Label>
<Button type="button" variant="outline" size="sm" onClick={handleAddCategoryField}>
Adicionar campo
</Button>
</div>
{newCategoryFields.length === 0 ? (
<p className="text-xs text-muted-foreground">Nenhum campo adicional configurado.</p>
) : (
<div className="space-y-3">
{newCategoryFields.map((field) => (
<div key={field.id} className="grid gap-2 rounded-md border p-3">
<div className="space-y-1">
<Label htmlFor={`field-name-${field.id}`}>Nome</Label>
<Input
id={`field-name-${field.id}`}
value={field.name}
onChange={(event) =>
handleUpdateCategoryField(field.id, "name", event.target.value)
}
placeholder="Ex.: bonus_magico"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label>Tipo</Label>
<Select
value={field.type}
onValueChange={(value) => handleUpdateCategoryField(field.id, "type", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="TEXT">Texto</SelectItem>
<SelectItem value="INTEGER">Inteiro</SelectItem>
<SelectItem value="REAL">Decimal</SelectItem>
<SelectItem value="BOOLEAN">Booleano</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Checkbox
id={`field-required-${field.id}`}
checked={field.notNull}
onCheckedChange={(checked) =>
handleUpdateCategoryField(field.id, "notNull", checked === true)
}
/>
<Label htmlFor={`field-required-${field.id}`}>Obrigatório</Label>
</div>
</div>
<div className="flex justify-end">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveCategoryField(field.id)}
>
Remover
</Button>
</div>
</div>
))}
</div>
)}
</div>
{newCategoryError && <p className="text-xs text-destructive">{newCategoryError}</p>}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNewCategoryModal(false)}>
Cancelar
</Button>
<Button onClick={handleCreateCategory}>Criar categoria</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showNewFieldModal} onOpenChange={setShowNewFieldModal}>
<DialogContent className="max-w-md max-h-[70vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Novo campo para categoria</DialogTitle>
<DialogDescription>
Adicione um novo campo à categoria selecionada. A alteração é permanente no schema.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Categoria</Label>
<Input value={category} readOnly className="bg-muted/40" />
</div>
<div className="space-y-2">
<Label htmlFor="new-field-name">Nome do campo</Label>
<Input
id="new-field-name"
value={newFieldName}
onChange={(event) => setNewFieldName(event.target.value)}
placeholder="Ex.: bonus_magico"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label>Tipo</Label>
<Select value={newFieldType} onValueChange={(value) => setNewFieldType(value as CategoryColumn["type"])}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="TEXT">Texto</SelectItem>
<SelectItem value="INTEGER">Inteiro</SelectItem>
<SelectItem value="REAL">Decimal</SelectItem>
<SelectItem value="BOOLEAN">Booleano</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="new-field-required"
checked={newFieldNotNull}
onCheckedChange={(checked) => setNewFieldNotNull(checked === true)}
/>
<Label htmlFor="new-field-required">Obrigatório</Label>
</div>
</div>
{newFieldError && <p className="text-xs text-destructive">{newFieldError}</p>}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNewFieldModal(false)}>
Cancelar
</Button>
<Button onClick={handleAddFieldToCategory}>Adicionar campo</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}