"use client" import { useMemo, useState } from "react" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" import { IconAdjustments, IconForms, IconListDetails, IconTypography } from "@tabler/icons-react" // @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { useAuth } from "@/lib/auth-client" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" type FieldOption = { value: string; label: string } type Field = { id: string key: string label: string description: string type: "text" | "number" | "select" | "date" | "boolean" required: boolean options: FieldOption[] order: number } const TYPE_LABELS: Record = { text: "Texto", number: "Número", select: "Seleção", date: "Data", boolean: "Verdadeiro/Falso", } export function FieldsManager() { const { session, convexUserId } = useAuth() const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID const fields = useQuery( api.fields.list, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) as Field[] | undefined const createField = useMutation(api.fields.create) const updateField = useMutation(api.fields.update) const removeField = useMutation(api.fields.remove) const reorderFields = useMutation(api.fields.reorder) const [label, setLabel] = useState("") const [description, setDescription] = useState("") const [type, setType] = useState("text") const [required, setRequired] = useState(false) const [options, setOptions] = useState([]) const [saving, setSaving] = useState(false) const [editingField, setEditingField] = useState(null) const totals = useMemo(() => { if (!fields) return { total: 0, required: 0, select: 0 } return { total: fields.length, required: fields.filter((field) => field.required).length, select: fields.filter((field) => field.type === "select").length, } }, [fields]) const resetForm = () => { setLabel("") setDescription("") setType("text") setRequired(false) setOptions([]) } const normalizeOptions = (source: FieldOption[]) => source .map((option) => ({ label: option.label.trim(), value: option.value.trim() || option.label.trim().toLowerCase().replace(/\s+/g, "_"), })) .filter((option) => option.label.length > 0) const handleCreate = async (event: React.FormEvent) => { event.preventDefault() if (!label.trim()) { toast.error("Informe o rótulo do campo") return } if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } const preparedOptions = type === "select" ? normalizeOptions(options) : undefined setSaving(true) toast.loading("Criando campo...", { id: "field" }) try { await createField({ tenantId, actorId: convexUserId as Id<"users">, label: label.trim(), description: description.trim() || undefined, type, required, options: preparedOptions, }) toast.success("Campo criado", { id: "field" }) resetForm() } catch (error) { console.error(error) toast.error("Não foi possível criar o campo", { id: "field" }) } finally { setSaving(false) } } const handleRemove = async (field: Field) => { const confirmed = window.confirm(`Excluir o campo ${field.label}?`) if (!confirmed) return if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } toast.loading("Removendo campo...", { id: `field-remove-${field.id}` }) try { await removeField({ tenantId, fieldId: field.id as Id<"ticketFields">, actorId: convexUserId as Id<"users">, }) toast.success("Campo removido", { id: `field-remove-${field.id}` }) } catch (error) { console.error(error) toast.error("Não foi possível remover o campo", { id: `field-remove-${field.id}` }) } } const openEdit = (field: Field) => { setEditingField(field) setLabel(field.label) setDescription(field.description) setType(field.type) setRequired(field.required) setOptions(field.options) } const handleUpdate = async () => { if (!editingField) return if (!label.trim()) { toast.error("Informe o rótulo do campo") return } if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } const preparedOptions = type === "select" ? normalizeOptions(options) : undefined setSaving(true) toast.loading("Atualizando campo...", { id: "field-edit" }) try { await updateField({ tenantId, fieldId: editingField.id as Id<"ticketFields">, actorId: convexUserId as Id<"users">, label: label.trim(), description: description.trim() || undefined, type, required, options: preparedOptions, }) toast.success("Campo atualizado", { id: "field-edit" }) setEditingField(null) resetForm() } catch (error) { console.error(error) toast.error("Não foi possível atualizar o campo", { id: "field-edit" }) } finally { setSaving(false) } } const moveField = async (field: Field, direction: "up" | "down") => { if (!fields) return if (!convexUserId) { toast.error("Sessão não sincronizada com o Convex") return } const index = fields.findIndex((item) => item.id === field.id) const targetIndex = direction === "up" ? index - 1 : index + 1 if (targetIndex < 0 || targetIndex >= fields.length) return const reordered = [...fields] const [removed] = reordered.splice(index, 1) reordered.splice(targetIndex, 0, removed) try { await reorderFields({ tenantId, actorId: convexUserId as Id<"users">, orderedIds: reordered.map((item) => item.id as Id<"ticketFields">), }) toast.success("Ordem atualizada") } catch (error) { console.error(error) toast.error("Não foi possível reordenar os campos") } } const addOption = () => { setOptions((current) => [...current, { label: "", value: "" }]) } const updateOption = (index: number, key: keyof FieldOption, value: string) => { setOptions((current) => { const copy = [...current] copy[index] = { ...copy[index], [key]: value } return copy }) } const removeOption = (index: number) => { setOptions((current) => current.filter((_, optIndex) => optIndex !== index)) } return (
Campos personalizados Metadados adicionais disponíveis nos tickets. {fields ? totals.total : } Campos obrigatórios Informações exigidas na abertura. {fields ? totals.required : } Campos de seleção Usados para listas e múltipla escolha. {fields ? totals.select : }
Novo campo Capture informações específicas do seu fluxo de atendimento.
setLabel(event.target.value)} required />
setRequired(Boolean(value))} />