feat: improve custom fields admin and date filters
This commit is contained in:
parent
11a4b903c4
commit
b721348e19
14 changed files with 491 additions and 205 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue