Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)

Adiciona o frontend Next.js em frontend/ (setup, design system,
autenticação/tema, layout/navegação, área master, área tenant completa:
clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas,
equipe e perfil). Estende o backend de forma aditiva e autorizada:
cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe +
edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo
e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 09:41:13 -03:00
co-authored by Claude Sonnet 5
parent f9738e54da
commit 323912748f
179 changed files with 29312 additions and 22 deletions
@@ -0,0 +1,92 @@
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createCustomer, type Customer } from '@/lib/api/customers';
import { customerSchema, type CustomerFormValues } from '@/lib/validators/customers';
import { ApiError } from '@/lib/api/client';
import { toast } from '@/lib/toast';
import { Button } from '@/components/ui/button';
import { FormField } from '@/components/ui/form-field';
import { Input } from '@/components/ui/input';
import { ResponsiveDialog } from '@/components/ui/responsive-dialog';
interface CustomerQuickCreateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onCreated: (customer: Customer) => void;
}
/**
* Cadastro rápido de cliente sem sair do fluxo de nova venda (Etapa 17,
* critério de aceite "não perder o que já foi preenchido na venda") — só
* os campos mínimos; o resto pode ser completado depois em
* `/app/clientes/:id`.
*/
export function CustomerQuickCreateDialog({ open, onOpenChange, onCreated }: CustomerQuickCreateDialogProps) {
const queryClient = useQueryClient();
const form = useForm<CustomerFormValues>({
resolver: zodResolver(customerSchema),
defaultValues: { razao_social: '', documento: '', email: '', telefone_whatsapp: '' },
});
const mutation = useMutation({
mutationFn: (values: CustomerFormValues) =>
createCustomer({
razao_social: values.razao_social,
documento: values.documento || undefined,
email: values.email || undefined,
telefone_whatsapp: values.telefone_whatsapp || undefined,
}),
onSuccess: (customer) => {
queryClient.invalidateQueries({ queryKey: ['tenant', 'customers'] });
toast.success('Cliente cadastrado.');
form.reset({ razao_social: '', documento: '', email: '', telefone_whatsapp: '' });
onCreated(customer);
onOpenChange(false);
},
onError: (err) => {
toast.error(err instanceof ApiError ? err.message : 'Não foi possível cadastrar o cliente.');
},
});
return (
<ResponsiveDialog
open={open}
onOpenChange={onOpenChange}
title="Cadastro rápido de cliente"
description="Só o essencial — complete o resto depois no cadastro do cliente."
footer={
<>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancelar
</Button>
<Button loading={mutation.isPending} onClick={form.handleSubmit((values) => mutation.mutate(values))}>
Cadastrar e usar
</Button>
</>
}
>
<form
className="flex flex-col gap-4 px-4 pb-4 sm:px-0 sm:pb-0"
onSubmit={form.handleSubmit((values) => mutation.mutate(values))}
>
<FormField label="Razão social" error={form.formState.errors.razao_social?.message} required>
<Input {...form.register('razao_social')} />
</FormField>
<FormField label="Documento (CPF/CNPJ)" error={form.formState.errors.documento?.message}>
<Input {...form.register('documento')} />
</FormField>
<FormField label="Telefone/WhatsApp" error={form.formState.errors.telefone_whatsapp?.message}>
<Input {...form.register('telefone_whatsapp')} />
</FormField>
<FormField label="E-mail" error={form.formState.errors.email?.message}>
<Input type="email" {...form.register('email')} />
</FormField>
</form>
</ResponsiveDialog>
);
}