feat: improve custom fields admin and date filters

This commit is contained in:
Esdras Renan 2025-11-15 01:51:55 -03:00
parent 11a4b903c4
commit b721348e19
14 changed files with 491 additions and 205 deletions

View file

@ -0,0 +1,25 @@
import { FieldsManager } from "@/components/admin/fields/fields-manager"
import { AppShell } from "@/components/app-shell"
import { SiteHeader } from "@/components/site-header"
import { requireAuthenticatedSession } from "@/lib/auth-server"
export const dynamic = "force-dynamic"
export default async function AdminCustomFieldsPage() {
await requireAuthenticatedSession()
return (
<AppShell
header={
<SiteHeader
title="Campos personalizados"
lead="Crie campos adicionais para admissão, desligamento e chamados gerais mantendo a consistência visual do painel."
/>
}
>
<div className="mx-auto w-full max-w-6xl space-y-8 px-6 pb-12 lg:px-8">
<FieldsManager />
</div>
</AppShell>
)
}

View file

@ -1,5 +1,4 @@
import { CategoriesManager } from "@/components/admin/categories/categories-manager"
import { FieldsManager } from "@/components/admin/fields/fields-manager"
import { TicketFormTemplatesManager } from "@/components/admin/fields/ticket-form-templates-manager"
import { AppShell } from "@/components/app-shell"
import { SiteHeader } from "@/components/site-header"
@ -11,15 +10,14 @@ export default function AdminFieldsPage() {
<AppShell
header={
<SiteHeader
title="Categorias e campos personalizados"
lead="Administre as categorias primárias/secundárias e os campos adicionais aplicados aos tickets."
title="Categorias e formulários"
lead="Administre categorias, subcategorias e templates utilizados para classificar tickets."
/>
}
>
<div className="mx-auto w-full max-w-6xl space-y-8 px-6 lg:px-8">
<CategoriesManager />
<TicketFormTemplatesManager />
<FieldsManager />
</div>
</AppShell>
)

View file

@ -4,6 +4,7 @@ import { useMemo, useState } from "react"
import { useMutation, useQuery } from "convex/react"
import { toast } from "sonner"
import { IconAdjustments, IconForms, IconListDetails, IconTypography } from "@tabler/icons-react"
import { ArrowDown, ArrowUp } from "lucide-react"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { useAuth } from "@/lib/auth-client"
@ -113,6 +114,7 @@ export function FieldsManager() {
const [editingField, setEditingField] = useState<Field | null>(null)
const [editingScope, setEditingScope] = useState<string>("all")
const [editingCompanySelection, setEditingCompanySelection] = useState<string>("all")
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const totals = useMemo(() => {
if (!fields) return { total: 0, required: 0, select: 0 }
@ -134,6 +136,11 @@ export function FieldsManager() {
setEditingCompanySelection("all")
}
const closeCreateDialog = () => {
setIsCreateDialogOpen(false)
resetForm()
}
const normalizeOptions = (source: FieldOption[]) =>
source
.map((option) => ({
@ -170,7 +177,7 @@ export function FieldsManager() {
companyId: companyIdValue,
})
toast.success("Campo criado", { id: "field" })
resetForm()
closeCreateDialog()
} catch (error) {
console.error(error)
toast.error("Não foi possível criar o campo", { id: "field" })
@ -336,128 +343,16 @@ export function FieldsManager() {
</CardTitle>
<CardDescription>Capture informações específicas do seu fluxo de atendimento.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleCreate} className="grid gap-4 lg:grid-cols-[minmax(0,280px)_minmax(0,1fr)]">
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="field-label">Rótulo</Label>
<Input
id="field-label"
placeholder="Ex.: Número do contrato"
value={label}
onChange={(event) => setLabel(event.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label>Tipo de dado</Label>
<Select value={type} onValueChange={(value) => setType(value as Field["type"])}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">Texto curto</SelectItem>
<SelectItem value="number">Número</SelectItem>
<SelectItem value="select">Seleção</SelectItem>
<SelectItem value="date">Data</SelectItem>
<SelectItem value="boolean">Verdadeiro/Falso</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Checkbox id="field-required" checked={required} onCheckedChange={(value) => setRequired(Boolean(value))} />
<Label htmlFor="field-required" className="text-sm font-normal text-neutral-600">
Campo obrigatório na abertura
</Label>
</div>
<div className="space-y-2">
<Label>Aplicar em</Label>
<Select value={scopeSelection} onValueChange={setScopeSelection}>
<SelectTrigger>
<SelectValue placeholder="Todos os formulários" />
</SelectTrigger>
<SelectContent>
{scopeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Empresa (opcional)</Label>
<SearchableCombobox
value={companySelection}
onValueChange={(value) => setCompanySelection(value ?? "all")}
options={companyComboboxOptions}
placeholder="Todas as empresas"
renderValue={(option) =>
option ? (
<span className="truncate">{option.label}</span>
) : (
<span className="text-muted-foreground">Todas as empresas</span>
)
}
/>
<p className="text-xs text-neutral-500">
Selecione uma empresa para tornar este campo exclusivo dela. Sem seleção, o campo aparecerá em todos os tickets.
</p>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="field-description">Descrição</Label>
<textarea
id="field-description"
className="min-h-[96px] w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-neutral-700 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-900/10"
placeholder="Como este campo será utilizado"
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
{type === "select" ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Opções</Label>
<Button type="button" variant="outline" size="sm" onClick={addOption}>
Adicionar opção
</Button>
</div>
{options.length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-4 text-sm text-neutral-500">
Adicione pelo menos uma opção para este campo de seleção.
</p>
) : (
<div className="space-y-3">
{options.map((option, index) => (
<div key={index} className="grid gap-3 rounded-lg border border-slate-200 p-3 md:grid-cols-[minmax(0,1fr)_minmax(0,200px)_auto]">
<Input
placeholder="Rótulo"
value={option.label}
onChange={(event) => updateOption(index, "label", event.target.value)}
/>
<Input
placeholder="Valor"
value={option.value}
onChange={(event) => updateOption(index, "value", event.target.value)}
/>
<Button variant="ghost" type="button" onClick={() => removeOption(index)}>
Remover
</Button>
</div>
))}
</div>
)}
</div>
) : null}
<div className="flex justify-end">
<Button type="submit" disabled={saving}>
Criar campo
</Button>
</div>
</div>
</form>
<CardContent className="flex justify-between gap-4">
<p className="text-sm text-neutral-600">Abra o formulário avançado para definir escopo, empresa e opções.</p>
<Button
onClick={() => {
resetForm()
setIsCreateDialogOpen(true)
}}
>
Configurar campo
</Button>
</CardContent>
</Card>
@ -521,26 +416,28 @@ export function FieldsManager() {
Excluir
</Button>
</div>
<div className="flex gap-2 text-xs text-neutral-500">
<div className="flex gap-1 pt-4">
<Button
type="button"
size="sm"
size="icon"
variant="ghost"
className="px-2"
className="size-8"
disabled={index === 0}
onClick={() => moveField(field, "up")}
>
Subir
<ArrowUp className="size-4" />
<span className="sr-only">Subir</span>
</Button>
<Button
type="button"
size="sm"
size="icon"
variant="ghost"
className="px-2"
className="size-8"
disabled={index === fields.length - 1}
onClick={() => moveField(field, "down")}
>
Descer
<ArrowDown className="size-4" />
<span className="sr-only">Descer</span>
</Button>
</div>
</div>
@ -564,6 +461,144 @@ export function FieldsManager() {
)}
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={(open) => {
if (!open) {
closeCreateDialog()
} else {
setIsCreateDialogOpen(true)
}
}}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Novo campo personalizado</DialogTitle>
</DialogHeader>
<form onSubmit={handleCreate} className="grid gap-4 py-2 lg:grid-cols-[minmax(0,280px)_minmax(0,1fr)]">
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="field-label">Rótulo</Label>
<Input
id="field-label"
placeholder="Ex.: Número do contrato"
value={label}
onChange={(event) => setLabel(event.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label>Tipo de dado</Label>
<Select value={type} onValueChange={(value) => setType(value as Field["type"])}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">Texto curto</SelectItem>
<SelectItem value="number">Número</SelectItem>
<SelectItem value="select">Seleção</SelectItem>
<SelectItem value="date">Data</SelectItem>
<SelectItem value="boolean">Verdadeiro/Falso</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Checkbox id="field-required" checked={required} onCheckedChange={(value) => setRequired(Boolean(value))} />
<Label htmlFor="field-required" className="text-sm font-normal text-neutral-600">
Campo obrigatório na abertura
</Label>
</div>
<div className="space-y-2">
<Label>Aplicar em</Label>
<Select value={scopeSelection} onValueChange={setScopeSelection}>
<SelectTrigger>
<SelectValue placeholder="Todos os formulários" />
</SelectTrigger>
<SelectContent>
{scopeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Empresa (opcional)</Label>
<SearchableCombobox
value={companySelection}
onValueChange={(value) => setCompanySelection(value ?? "all")}
options={companyComboboxOptions}
placeholder="Todas as empresas"
renderValue={(option) =>
option ? (
<span className="truncate">{option.label}</span>
) : (
<span className="text-muted-foreground">Todas as empresas</span>
)
}
/>
<p className="text-xs text-neutral-500">
Sem seleção o campo aparecerá para todos os tickets.
</p>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="field-description">Descrição</Label>
<textarea
id="field-description"
className="min-h-[96px] w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-neutral-700 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-900/10"
placeholder="Como este campo será utilizado"
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
{type === "select" ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Opções</Label>
<Button type="button" variant="outline" size="sm" onClick={addOption}>
Adicionar opção
</Button>
</div>
{options.length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-4 text-sm text-neutral-500">
Adicione pelo menos uma opção para este campo de seleção.
</p>
) : (
<div className="space-y-3">
{options.map((option, index) => (
<div key={index} className="grid gap-3 rounded-lg border border-slate-200 p-3 md:grid-cols-[minmax(0,1fr)_minmax(0,200px)_auto]">
<Input
placeholder="Rótulo"
value={option.label}
onChange={(event) => updateOption(index, "label", event.target.value)}
/>
<Input
placeholder="Valor"
value={option.value}
onChange={(event) => updateOption(index, "value", event.target.value)}
/>
<Button variant="ghost" type="button" onClick={() => removeOption(index)}>
Remover
</Button>
</div>
))}
</div>
)}
</div>
) : null}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={closeCreateDialog} disabled={saving}>
Cancelar
</Button>
<Button type="submit" disabled={saving}>
{saving ? "Salvando..." : "Criar campo"}
</Button>
</div>
</div>
</form>
</DialogContent>
</Dialog>
<Dialog open={Boolean(editingField)} onOpenChange={(value) => (!value ? setEditingField(null) : null)}>
<DialogContent className="max-w-3xl">
<DialogHeader>

View file

@ -5,6 +5,7 @@ import {
AlertTriangle,
Building,
Building2,
ClipboardList,
CalendarDays,
ChevronDown,
Clock4,
@ -115,6 +116,7 @@ const navigation: NavigationGroup[] = [
{ title: "Equipe", url: "/admin", icon: LayoutDashboard, requiredRole: "admin", exact: true },
{ title: "Empresas", url: "/admin/companies", icon: Building, requiredRole: "admin" },
{ title: "Usuários", url: "/admin/users", icon: Users, requiredRole: "admin" },
{ title: "Campos personalizados", url: "/admin/custom-fields", icon: ClipboardList, requiredRole: "admin" },
{ title: "Templates de comentários", url: "/settings/templates", icon: LayoutTemplate, requiredRole: "admin" },
{ title: "Templates de relatórios", url: "/admin/report-templates", icon: LayoutTemplate, requiredRole: "admin" },
],

View file

@ -1,7 +1,8 @@
"use client"
import { useMemo } from "react"
import { useEffect, useMemo, useState } from "react"
import type { DateRange } from "react-day-picker"
import { IconEraser } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
@ -18,6 +19,7 @@ type DateRangeButtonProps = {
to: string | null
onChange: (next: DateRangeValue) => void
className?: string
clearLabel?: string
}
function strToDate(value?: string | null): Date | undefined {
@ -39,7 +41,8 @@ function formatPtBR(value?: Date): string {
return value ? value.toLocaleDateString("pt-BR") : ""
}
export function DateRangeButton({ from, to, onChange, className }: DateRangeButtonProps) {
export function DateRangeButton({ from, to, onChange, className, clearLabel = "Limpar período" }: DateRangeButtonProps) {
const [open, setOpen] = useState(false)
const range: DateRange | undefined = useMemo(
() => ({
from: strToDate(from),
@ -47,35 +50,54 @@ export function DateRangeButton({ from, to, onChange, className }: DateRangeButt
}),
[from, to],
)
const [draftRange, setDraftRange] = useState<DateRange | undefined>(range)
const label =
range?.from && range?.to
? `${formatPtBR(range.from)} - ${formatPtBR(range.to)}`
: "Período"
useEffect(() => {
if (!open) {
setDraftRange(range)
}
}, [open, range])
const displayRange = open ? draftRange ?? range : range
const label = (() => {
if (displayRange?.from && displayRange?.to) {
return `${formatPtBR(displayRange.from)} - ${formatPtBR(displayRange.to)}`
}
if (displayRange?.from && !displayRange?.to) {
return `${formatPtBR(displayRange.from)} - …`
}
return "Período"
})()
const handleSelect = (next?: DateRange) => {
if (!next?.from && !next?.to) {
onChange({ from: null, to: null })
setDraftRange(undefined)
return
}
if (next?.from && !next?.to) {
const single = dateToStr(next.from)
if (from && to && from === to && single === from) {
onChange({ from: null, to: null })
return
}
onChange({ from: single, to: single })
return
}
setDraftRange(next)
const nextFrom = dateToStr(next?.from) ?? null
const nextTo = dateToStr(next?.to) ?? nextFrom
onChange({ from: nextFrom, to: nextTo })
if (next?.from && next?.to) {
const nextFrom = dateToStr(next.from)
const nextTo = dateToStr(next.to) ?? nextFrom
onChange({ from: nextFrom, to: nextTo })
setOpen(false)
}
}
return (
<Popover>
<Popover
open={open}
onOpenChange={(next) => {
setOpen(next)
if (!next) {
setDraftRange(range)
} else {
setDraftRange(range)
}
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
@ -95,8 +117,25 @@ export function DateRangeButton({ from, to, onChange, className }: DateRangeButt
fixedWeeks
showOutsideDays
/>
<div className="flex items-center justify-center gap-3 border-t border-border/70 bg-slate-50/80 px-3 py-2">
<button
type="button"
onClick={() => {
onChange({ from: null, to: null })
setDraftRange(undefined)
setOpen(false)
}}
className="inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm font-medium text-neutral-600 transition hover:text-neutral-900 disabled:opacity-40"
disabled={!from && !to}
>
<IconEraser className="size-4" />
<span>{clearLabel}</span>
</button>
{!draftRange?.to && draftRange?.from ? (
<span className="text-xs text-neutral-500">Selecione a data final</span>
) : null}
</div>
</PopoverContent>
</Popover>
)
}

View file

@ -55,16 +55,20 @@ export function PortalTicketForm() {
) as TicketFormDefinition[] | undefined
const forms = useMemo<TicketFormDefinition[]>(() => {
const base: TicketFormDefinition = {
const fallback: TicketFormDefinition = {
key: "default",
label: "Chamado",
description: "Formulário básico para solicitações gerais.",
fields: [],
}
if (Array.isArray(formsRemote) && formsRemote.length > 0) {
return [base, ...formsRemote]
if (!formsRemote || formsRemote.length === 0) {
return [fallback]
}
return [base]
const hasDefault = formsRemote.some((form) => form.key === fallback.key)
if (hasDefault) {
return formsRemote
}
return [fallback, ...formsRemote]
}, [formsRemote])
const [selectedFormKey, setSelectedFormKey] = useState<string>("default")
@ -100,10 +104,10 @@ export function PortalTicketForm() {
const customFieldsInvalid = useMemo(
() =>
selectedFormKey !== "default" && selectedForm?.fields?.length
selectedForm?.fields?.length
? hasMissingRequiredCustomFields(selectedForm.fields, customFieldValues)
: false,
[selectedFormKey, selectedForm, customFieldValues]
[selectedForm, customFieldValues]
)
const handleFormSelection = (value: string) => {
@ -171,7 +175,7 @@ export function PortalTicketForm() {
}
let customFieldsPayload: Array<{ fieldId: Id<"ticketFields">; value: unknown }> = []
if (selectedFormKey !== "default" && selectedForm?.fields?.length) {
if (selectedForm?.fields?.length) {
const normalized = normalizeCustomFieldInputs(selectedForm.fields, customFieldValues)
if (!normalized.ok) {
toast.error(normalized.message, { id: "portal-new-ticket" })
@ -322,7 +326,7 @@ export function PortalTicketForm() {
subcategoryLabel="Subcategoria *"
secondaryEmptyLabel="Selecione uma categoria"
/>
{selectedFormKey !== "default" && selectedForm.fields.length > 0 ? (
{selectedForm.fields.length > 0 ? (
<div className="grid gap-3 rounded-xl border border-slate-200 bg-white px-4 py-4 sm:grid-cols-2">
<p className="text-sm font-semibold text-neutral-800 sm:col-span-2">Informações adicionais</p>
{selectedForm.fields.map((field) => {

View file

@ -283,8 +283,8 @@ export function MachineCategoryReport() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-3 md:grid-cols-3">
<div className="space-y-1">
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-3">
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
Máquina
</span>
@ -296,7 +296,7 @@ export function MachineCategoryReport() {
triggerClassName="h-10 w-full rounded-2xl border border-slate-300 bg-white px-3 text-sm font-medium text-neutral-800"
/>
</div>
<div className="space-y-1">
<div className="space-y-3">
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
Usuário (solicitante)
</span>

View file

@ -119,7 +119,7 @@ export function ReportsFilterToolbar({
<ToggleGroupItem value="365d" className="hidden min-w-[96px] justify-center px-4 lg:inline-flex">
12 meses
</ToggleGroupItem>
<ToggleGroupItem value="all" className="hidden min-w-[96px] justify-center px-4 lg:inline-flex">
<ToggleGroupItem value="all" className="hidden min-w-[120px] justify-center px-5 lg:inline-flex">
Todo histórico
</ToggleGroupItem>
</>

View file

@ -4,7 +4,7 @@ import { useMemo, useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import { Settings2, Share2, ShieldCheck, UserCog, UserPlus, Users2, Layers3, MessageSquareText, BellRing } from "lucide-react"
import { Settings2, Share2, ShieldCheck, UserCog, UserPlus, Users2, Layers3, MessageSquareText, BellRing, ClipboardList } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@ -12,7 +12,6 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Separator } from "@/components/ui/separator"
import { useAuth, signOut } from "@/lib/auth-client"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { FieldsManager } from "@/components/admin/fields/fields-manager"
import type { LucideIcon } from "lucide-react"
@ -36,6 +35,14 @@ const ROLE_LABELS: Record<string, string> = {
}
const SETTINGS_ACTIONS: SettingsAction[] = [
{
title: "Campos personalizados",
description: "Configure campos extras por formulário e empresa para enriquecer os tickets.",
href: "/admin/custom-fields",
cta: "Abrir campos",
requiredRole: "admin",
icon: ClipboardList,
},
{
title: "Times & papéis",
description: "Controle quem pode atuar nas filas e atribua permissões refinadas por equipe.",
@ -53,10 +60,10 @@ const SETTINGS_ACTIONS: SettingsAction[] = [
icon: Share2,
},
{
title: "Campos e categorias",
description: "Ajuste categorias, subcategorias e campos personalizados para qualificar tickets.",
title: "Categorias e formulários",
description: "Mantenha categorias padronizadas e templates de formulário alinhados à operação.",
href: "/admin/fields",
cta: "Editar estrutura",
cta: "Gerenciar categorias",
requiredRole: "admin",
icon: Layers3,
},
@ -271,17 +278,7 @@ export function SettingsContent() {
})}
</div>
</section>
{isStaff ? (
<section id="custom-fields" className="space-y-4">
<div>
<h2 className="text-base font-semibold text-neutral-900">Campos personalizados</h2>
<p className="text-sm text-neutral-600">
Ajuste os campos de admissão, desligamento e demais metadados diretamente pelo painel administrativo.
</p>
</div>
<FieldsManager />
</section>
) : null}
</div>
)
}

View file

@ -216,16 +216,20 @@ export function NewTicketDialog({
) as TicketFormDefinition[] | undefined
const forms = useMemo<TicketFormDefinition[]>(() => {
const base: TicketFormDefinition = {
const fallback: TicketFormDefinition = {
key: "default",
label: "Chamado",
description: "Formulário básico para abertura de chamados gerais.",
fields: [],
}
if (Array.isArray(formsRemote) && formsRemote.length > 0) {
return [base, ...formsRemote]
if (!formsRemote || formsRemote.length === 0) {
return [fallback]
}
return [base]
const hasDefault = formsRemote.some((form) => form.key === fallback.key)
if (hasDefault) {
return formsRemote
}
return [fallback, ...formsRemote]
}, [formsRemote])
const [selectedFormKey, setSelectedFormKey] = useState<string>("default")
@ -531,7 +535,7 @@ export function NewTicketDialog({
}
let customFieldsPayload: Array<{ fieldId: Id<"ticketFields">; value: unknown }> = []
if (selectedFormKey !== "default" && selectedForm?.fields?.length) {
if (selectedForm?.fields?.length) {
const normalized = normalizeCustomFieldInputs(selectedForm.fields, customFieldValues)
if (!normalized.ok) {
toast.error(normalized.message, { id: "new-ticket" })
@ -1036,7 +1040,7 @@ export function NewTicketDialog({
</div>
</div>
{selectedFormKey !== "default" && selectedForm.fields.length > 0 ? (
{selectedForm.fields.length > 0 ? (
<div className="grid gap-4 rounded-xl border border-slate-200 bg-white px-4 py-4 sm:grid-cols-2 lg:col-span-2">
<p className="text-sm font-semibold text-neutral-800 sm:col-span-2">Informações adicionais</p>
{selectedForm.fields.map((field) => {

View file

@ -256,7 +256,11 @@ export function TicketCustomFieldsSection({ ticket, variant = "card", className
if (!formsRemote || formsRemote.length === 0) {
return [DEFAULT_FORM]
}
return formsRemote
const hasDefault = formsRemote.some((form) => form.key === DEFAULT_FORM.key)
if (hasDefault) {
return formsRemote
}
return [DEFAULT_FORM, ...formsRemote]
}, [formsRemote])
const selectedForm = useMemo<TicketFormDefinition>(() => {

View file

@ -102,7 +102,7 @@ export function SearchableCombobox({
aria-controls={listId}
disabled={disabled}
className={cn(
"flex h-full w-full items-center justify-between gap-2 rounded-full border border-input bg-background px-3 text-sm font-semibold text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60",
"flex h-11 w-full items-center justify-between gap-2 rounded-full border border-input bg-background px-3 text-sm font-semibold text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60",
className,
triggerClassName,
)}