feat: link tickets in comments and align admin sidebars

This commit is contained in:
Esdras Renan 2025-10-23 00:46:50 -03:00
parent c35eb673d3
commit b0f57009ac
15 changed files with 1606 additions and 424 deletions

View file

@ -91,6 +91,8 @@ const ROLE_LABEL: Record<AdminAccount["role"], string> = {
COLLABORATOR: "Colaborador",
}
const NO_CONTACT_VALUE = "__none__"
function createId(prefix: string) {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return `${prefix}-${crypto.randomUUID()}`
@ -612,16 +614,16 @@ function CompanySectionSheet({ editor, onClose, onUpdated }: CompanySectionSheet
return (
<Sheet open={open} onOpenChange={(value) => (!value ? onClose() : null)}>
<SheetContent className="flex w-full max-w-4xl flex-col gap-0 p-0">
<SheetHeader className="border-b border-border/60 px-6 py-4">
<SheetTitle className="text-base font-semibold">
<SheetContent className="flex w-full max-w-none flex-col gap-0 bg-background p-0 sm:max-w-[48rem] lg:max-w-[60rem] xl:max-w-[68rem]">
<SheetHeader className="border-b border-border/60 px-10 py-7">
<SheetTitle className="text-xl font-semibold">
{editor?.section === "contacts" ? "Contatos da empresa" : null}
{editor?.section === "locations" ? "Localizações e unidades" : null}
{editor?.section === "contracts" ? "Contratos ativos" : null}
</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto px-6 py-6">{content}</div>
<SheetFooter className="border-t border-border/60 px-6 py-4">
<div className="flex-1 overflow-y-auto px-10 py-8">{content}</div>
<SheetFooter className="border-t border-border/60 px-10 py-5">
<div className="flex w-full items-center justify-end">
{isSubmitting ? (
<span className="text-sm text-muted-foreground">Salvando...</span>
@ -674,11 +676,11 @@ function ContactsEditor({
return (
<FormProvider {...form}>
<form onSubmit={submit} className="space-y-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-foreground">Contatos estratégicos</p>
<p className="text-xs text-muted-foreground">
<form onSubmit={submit} className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1.5">
<p className="text-base font-semibold text-foreground">Contatos estratégicos</p>
<p className="text-sm text-muted-foreground">
Cadastre responsáveis por aprovação, faturamento e comunicação.
</p>
</div>
@ -686,6 +688,7 @@ function ContactsEditor({
type="button"
variant="outline"
size="sm"
className="self-start sm:self-auto"
onClick={() =>
fieldArray.append({
id: createId("contact"),
@ -703,18 +706,18 @@ function ContactsEditor({
})
}
>
<IconPlus className="mr-1 size-3.5" />
<IconPlus className="mr-2 size-4" />
Novo contato
</Button>
</div>
<div className="space-y-4">
<div className="space-y-6">
{fieldArray.fields.map((field, index) => {
const errors = form.formState.errors.contacts?.[index]
return (
<Card key={field.id}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
<CardTitle className="text-base font-semibold">Contato #{index + 1}</CardTitle>
<Card key={field.id} className="border border-border/60 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-lg font-semibold">Contato #{index + 1}</CardTitle>
<Button
type="button"
variant="ghost"
@ -725,29 +728,39 @@ function ContactsEditor({
<IconTrash className="size-4" />
</Button>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<div>
<Label>Nome completo</Label>
<CardContent className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Nome completo
</Label>
<Input {...form.register(`contacts.${index}.fullName` as const)} />
<FieldError message={errors?.fullName?.message} />
</div>
<div>
<Label>E-mail</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
E-mail
</Label>
<Input {...form.register(`contacts.${index}.email` as const)} />
<FieldError message={errors?.email?.message} />
</div>
<div>
<Label>Telefone</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Telefone
</Label>
<Input {...form.register(`contacts.${index}.phone` as const)} placeholder="(11) 99999-0000" />
<FieldError message={errors?.phone?.message as string | undefined} />
</div>
<div>
<Label>WhatsApp</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
WhatsApp
</Label>
<Input {...form.register(`contacts.${index}.whatsapp` as const)} placeholder="(11) 99999-0000" />
<FieldError message={errors?.whatsapp?.message as string | undefined} />
</div>
<div>
<Label>Função</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Função
</Label>
<Controller
name={`contacts.${index}.role` as const}
control={form.control}
@ -767,12 +780,16 @@ function ContactsEditor({
)}
/>
</div>
<div>
<Label>Cargo interno</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Cargo interno
</Label>
<Input {...form.register(`contacts.${index}.title` as const)} placeholder="ex.: Coordenador TI" />
</div>
<div className="md:col-span-2">
<Label>Preferências de contato</Label>
<div className="md:col-span-2 space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Preferências de contato
</Label>
<Controller
name={`contacts.${index}.preference` as const}
control={form.control}
@ -785,26 +802,28 @@ function ContactsEditor({
)}
/>
</div>
<div className="flex items-center gap-3">
<div className="md:col-span-2 flex items-center gap-3 rounded-lg border border-border/60 bg-muted/30 px-4 py-3">
<Checkbox
checked={form.watch(`contacts.${index}.canAuthorizeTickets` as const)}
onCheckedChange={(checked) =>
form.setValue(`contacts.${index}.canAuthorizeTickets` as const, Boolean(checked))
}
/>
<span className="text-sm text-muted-foreground">Pode autorizar tickets</span>
<span className="text-sm font-medium text-neutral-700">Pode autorizar tickets</span>
</div>
<div className="flex items-center gap-3">
<div className="md:col-span-2 flex items-center gap-3 rounded-lg border border-border/60 bg-muted/30 px-4 py-3">
<Checkbox
checked={form.watch(`contacts.${index}.canApproveCosts` as const)}
onCheckedChange={(checked) =>
form.setValue(`contacts.${index}.canApproveCosts` as const, Boolean(checked))
}
/>
<span className="text-sm text-muted-foreground">Pode aprovar custos</span>
<span className="text-sm font-medium text-neutral-700">Pode aprovar custos</span>
</div>
<div className="md:col-span-2">
<Label>Anotações</Label>
<div className="md:col-span-2 space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Anotações
</Label>
<Textarea {...form.register(`contacts.${index}.notes` as const)} />
</div>
</CardContent>
@ -813,11 +832,11 @@ function ContactsEditor({
})}
</div>
<div className="flex justify-end gap-3">
<Button type="button" variant="ghost" onClick={onCancel}>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
<Button type="button" variant="ghost" className="whitespace-nowrap sm:w-auto" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
<Button type="submit" className="whitespace-nowrap sm:w-auto">
<IconPencil className="mr-2 size-4" />
Salvar contatos
</Button>
@ -862,18 +881,19 @@ function LocationsEditor({
return (
<FormProvider {...form}>
<form onSubmit={submit} className="space-y-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-foreground">Localizações</p>
<p className="text-xs text-muted-foreground">
Define unidades críticas, data centers e filiais para atendimento dedicado.
<form onSubmit={submit} className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1.5">
<p className="text-base font-semibold text-foreground">Localizações</p>
<p className="text-sm text-muted-foreground">
Defina unidades críticas, data centers e filiais para atendimento dedicado.
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
className="self-start sm:self-auto"
onClick={() =>
fieldArray.append({
id: createId("location"),
@ -886,18 +906,18 @@ function LocationsEditor({
})
}
>
<IconPlus className="mr-1 size-3.5" />
<IconPlus className="mr-2 size-4" />
Nova unidade
</Button>
</div>
<div className="space-y-4">
<div className="space-y-6">
{fieldArray.fields.map((field, index) => {
const errors = form.formState.errors.locations?.[index]
return (
<Card key={field.id}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
<CardTitle className="text-base font-semibold">Unidade #{index + 1}</CardTitle>
<Card key={field.id} className="border border-border/60 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-lg font-semibold">Unidade #{index + 1}</CardTitle>
<Button
type="button"
variant="ghost"
@ -908,14 +928,14 @@ function LocationsEditor({
<IconTrash className="size-4" />
</Button>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<div>
<Label>Nome</Label>
<CardContent className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Nome</Label>
<Input {...form.register(`locations.${index}.name` as const)} />
<FieldError message={errors?.name?.message} />
</div>
<div>
<Label>Tipo</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Tipo</Label>
<Controller
name={`locations.${index}.type` as const}
control={form.control}
@ -935,18 +955,25 @@ function LocationsEditor({
)}
/>
</div>
<div>
<Label>Contato responsável</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Contato responsável
</Label>
<Controller
name={`locations.${index}.responsibleContactId` as const}
control={form.control}
render={({ field }) => (
<Select value={field.value ?? ""} onValueChange={(value) => field.onChange(value || null)}>
<Select
value={field.value ?? NO_CONTACT_VALUE}
onValueChange={(value) =>
field.onChange(value === NO_CONTACT_VALUE ? null : value)
}
>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">Nenhum</SelectItem>
<SelectItem value={NO_CONTACT_VALUE}>Nenhum</SelectItem>
{contacts.map((contact) => (
<SelectItem key={contact.id} value={contact.id}>
{contact.fullName}
@ -957,8 +984,10 @@ function LocationsEditor({
)}
/>
</div>
<div>
<Label>Notas</Label>
<div className="md:col-span-2 space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Notas
</Label>
<Textarea {...form.register(`locations.${index}.notes` as const)} />
</div>
</CardContent>
@ -967,11 +996,11 @@ function LocationsEditor({
})}
</div>
<div className="flex justify-end gap-3">
<Button type="button" variant="ghost" onClick={onCancel}>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
<Button type="button" variant="ghost" className="whitespace-nowrap sm:w-auto" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
<Button type="submit" className="whitespace-nowrap sm:w-auto">
<IconPencil className="mr-2 size-4" />
Salvar localizações
</Button>
@ -1019,11 +1048,11 @@ function ContractsEditor({
return (
<FormProvider {...form}>
<form onSubmit={submit} className="space-y-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-foreground">Contratos</p>
<p className="text-xs text-muted-foreground">
<form onSubmit={submit} className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1.5">
<p className="text-base font-semibold text-foreground">Contratos</p>
<p className="text-sm text-muted-foreground">
Registre vigência, escopo e condições para este cliente.
</p>
</div>
@ -1031,6 +1060,7 @@ function ContractsEditor({
type="button"
variant="outline"
size="sm"
className="self-start sm:self-auto"
onClick={() =>
fieldArray.append({
id: createId("contract"),
@ -1047,18 +1077,18 @@ function ContractsEditor({
})
}
>
<IconPlus className="mr-1 size-3.5" />
<IconPlus className="mr-2 size-4" />
Novo contrato
</Button>
</div>
<div className="space-y-4">
<div className="space-y-6">
{fieldArray.fields.map((field, index) => {
const errors = form.formState.errors.contracts?.[index]
return (
<Card key={field.id}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
<CardTitle className="text-base font-semibold">Contrato #{index + 1}</CardTitle>
<Card key={field.id} className="border border-border/60 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-lg font-semibold">Contrato #{index + 1}</CardTitle>
<Button
type="button"
variant="ghost"
@ -1069,9 +1099,11 @@ function ContractsEditor({
<IconTrash className="size-4" />
</Button>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<div>
<Label>Tipo</Label>
<CardContent className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Tipo
</Label>
<Controller
name={`contracts.${index}.contractType` as const}
control={form.control}
@ -1091,34 +1123,46 @@ function ContractsEditor({
)}
/>
</div>
<div>
<Label>SKU/plano</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
SKU/plano
</Label>
<Input {...form.register(`contracts.${index}.planSku` as const)} />
</div>
<div>
<Label>Início</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Início
</Label>
<Input type="date" {...form.register(`contracts.${index}.startDate` as const)} />
<FieldError message={errors?.startDate?.message as string | undefined} />
</div>
<div>
<Label>Fim</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Fim
</Label>
<Input type="date" {...form.register(`contracts.${index}.endDate` as const)} />
<FieldError message={errors?.endDate?.message as string | undefined} />
</div>
<div>
<Label>Renovação</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Renovação
</Label>
<Input type="date" {...form.register(`contracts.${index}.renewalDate` as const)} />
</div>
<div>
<Label>Valor (R$)</Label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Valor (R$)
</Label>
<Input
type="number"
step="0.01"
{...form.register(`contracts.${index}.price` as const, { valueAsNumber: true })}
/>
</div>
<div className="md:col-span-2">
<Label>Escopo</Label>
<div className="md:col-span-2 space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Escopo
</Label>
<Controller
name={`contracts.${index}.scope` as const}
control={form.control}
@ -1138,8 +1182,10 @@ function ContractsEditor({
)}
/>
</div>
<div className="md:col-span-2">
<Label>Observações</Label>
<div className="md:col-span-2 space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Observações
</Label>
<Textarea {...form.register(`contracts.${index}.notes` as const)} />
</div>
</CardContent>
@ -1148,11 +1194,11 @@ function ContractsEditor({
})}
</div>
<div className="flex justify-end gap-3">
<Button type="button" variant="ghost" onClick={onCancel}>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
<Button type="button" variant="ghost" className="whitespace-nowrap sm:w-auto" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
<Button type="submit" className="whitespace-nowrap sm:w-auto">
<IconPencil className="mr-2 size-4" />
Salvar contratos
</Button>