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:
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { productSchema, type ProductFormValues } from '@/lib/validators/products';
|
||||
import type { ItemTipo } from '@/lib/api/products';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { FormField } from '@/components/ui/form-field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
interface ProductFormProps {
|
||||
defaultValues: ProductFormValues;
|
||||
onSubmit: (values: ProductFormValues) => void;
|
||||
onCancel: () => void;
|
||||
submitting: boolean;
|
||||
submitLabel: string;
|
||||
}
|
||||
|
||||
/** Campos de produto/serviço, compartilhado entre `/app/produtos/novo` e `/app/produtos/:id` (Etapa 16). */
|
||||
export function ProductForm({ defaultValues, onSubmit, onCancel, submitting, submitLabel }: ProductFormProps) {
|
||||
const form = useForm<ProductFormValues>({ resolver: zodResolver(productSchema), defaultValues });
|
||||
|
||||
return (
|
||||
<form className="flex max-w-2xl flex-col gap-6" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dados do item</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[160px_1fr]">
|
||||
<FormField label="Tipo" id="tipo" error={form.formState.errors.tipo?.message} required>
|
||||
<Select
|
||||
value={form.watch('tipo')}
|
||||
onValueChange={(value) => form.setValue('tipo', value as ItemTipo, { shouldValidate: true })}
|
||||
>
|
||||
<SelectTrigger id="tipo" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="produto">Produto</SelectItem>
|
||||
<SelectItem value="servico">Serviço</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormField>
|
||||
<FormField label="Nome" error={form.formState.errors.nome?.message} required>
|
||||
<Input {...form.register('nome')} />
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Descrição" error={form.formState.errors.descricao?.message}>
|
||||
<Textarea rows={2} {...form.register('descricao')} />
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField label="SKU" helpText="Opcional — precisa ser único no seu catálogo" error={form.formState.errors.sku?.message}>
|
||||
<Input {...form.register('sku')} />
|
||||
</FormField>
|
||||
<FormField label="Unidade" helpText="Ex: un, kg, hora" error={form.formState.errors.unidade?.message}>
|
||||
<Input {...form.register('unidade')} />
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField label="Valor de venda (R$)" error={form.formState.errors.valor_venda?.message} required>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
{...form.register('valor_venda', { valueAsNumber: true })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Valor de custo (R$)" helpText="Opcional" error={form.formState.errors.valor_custo?.message}>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
{...form.register('valor_custo', {
|
||||
setValueAs: (v) => (v === '' ? undefined : Number(v)),
|
||||
})}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Recorrente" id="recorrente" error={form.formState.errors.recorrente?.message}>
|
||||
<Select
|
||||
value={form.watch('recorrente') ? 'true' : 'false'}
|
||||
onValueChange={(value) => form.setValue('recorrente', value === 'true', { shouldValidate: true })}
|
||||
>
|
||||
<SelectTrigger id="recorrente" className="w-full sm:w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="false">Não</SelectItem>
|
||||
<SelectItem value="true">Sim — cobrança recorrente</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormField>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="submit" loading={submitting}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user