Files
SeConectai_Vendas/frontend/app/dev/componentes/showcase-client.tsx
T
tiagoandClaude Sonnet 5 323912748f 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>
2026-08-16 09:41:13 -03:00

409 lines
14 KiB
TypeScript

'use client';
import * as React from 'react';
import type { DateRange } from 'react-day-picker';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { FormField } from '@/components/ui/form-field';
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from '@/components/ui/input-group';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Combobox } from '@/components/ui/combobox';
import { DatePicker, DateRangePicker } from '@/components/ui/date-picker';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { STATUS_PILL_MAP, StatusPill, type BusinessStatus } from '@/components/ui/status-pill';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Skeleton, SkeletonCard, SkeletonText } from '@/components/ui/skeleton';
import { EmptyState } from '@/components/ui/empty-state';
import { DataPagination } from '@/components/ui/data-pagination';
import { ResponsiveDialog } from '@/components/ui/responsive-dialog';
import { DataTable, type DataTableColumn, type DataTableSort } from '@/components/ui/data-table';
import { toast } from '@/lib/toast';
interface DemoCliente {
id: string;
nome: string;
documento: string;
telefone: string;
cidade: string;
status: BusinessStatus;
valor: number;
}
const DEMO_CLIENTES: DemoCliente[] = [
{
id: '1',
nome: 'Ateliê Boa Vista Ltda',
documento: '12.345.678/0001-90',
telefone: '(11) 98765-4321',
cidade: 'São Paulo/SP',
status: 'ativo',
valor: 4230.5,
},
{
id: '2',
nome: 'Comércio Silva & Filhos',
documento: '98.765.432/0001-10',
telefone: '(21) 91234-5678',
cidade: 'Rio de Janeiro/RJ',
status: 'inadimplente',
valor: 1890,
},
{
id: '3',
nome: 'Padaria Pão Dourado',
documento: '11.222.333/0001-44',
telefone: '(31) 99887-6655',
cidade: 'Belo Horizonte/MG',
status: 'ativo',
valor: 750.9,
},
{
id: '4',
nome: 'Distribuidora Nordeste',
documento: '55.666.777/0001-88',
telefone: '(85) 98111-2233',
cidade: 'Fortaleza/CE',
status: 'trial',
valor: 0,
},
{
id: '5',
nome: 'Oficina Central Auto Peças',
documento: '22.333.444/0001-55',
telefone: '(41) 97654-3210',
cidade: 'Curitiba/PR',
status: 'suspenso',
valor: 320,
},
];
const CLIENTE_COLUMNS: DataTableColumn<DemoCliente>[] = [
{ id: 'nome', header: 'Cliente', primary: true, sortable: true, cell: (row) => row.nome },
{ id: 'documento', header: 'Documento', cell: (row) => row.documento },
{ id: 'telefone', header: 'Telefone', cell: (row) => row.telefone },
{ id: 'cidade', header: 'Cidade', cell: (row) => row.cidade },
{
id: 'valor',
header: 'Valor em aberto',
sortable: true,
className: 'text-right',
cell: (row) => row.valor.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
},
{
id: 'status',
header: 'Status',
primary: true,
cell: (row) => <StatusPill status={row.status} />,
},
];
const PRODUTO_OPTIONS = [
{ value: 'p1', label: 'Consultoria mensal' },
{ value: 'p2', label: 'Licença de software' },
{ value: 'p3', label: 'Suporte técnico avançado' },
{ value: 'p4', label: 'Treinamento presencial' },
];
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="flex flex-col gap-4 border-t border-border pt-8 first:border-0 first:pt-0">
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
{children}
</section>
);
}
export function ComponentesShowcase() {
const [tableLoading, setTableLoading] = React.useState(false);
const [tableEmpty, setTableEmpty] = React.useState(false);
const [sort, setSort] = React.useState<DataTableSort | undefined>();
const [page, setPage] = React.useState(1);
const [dialogOpen, setDialogOpen] = React.useState(false);
const [comboboxValue, setComboboxValue] = React.useState<string>();
const [date, setDate] = React.useState<Date>();
const [range, setRange] = React.useState<DateRange>();
const [paginationDemoPage, setPaginationDemoPage] = React.useState(1);
const sortedData = React.useMemo(() => {
if (!sort) return DEMO_CLIENTES;
const factor = sort.direction === 'asc' ? 1 : -1;
return [...DEMO_CLIENTES].sort((a, b) => {
const av = a[sort.columnId as keyof DemoCliente];
const bv = b[sort.columnId as keyof DemoCliente];
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * factor;
return String(av).localeCompare(String(bv)) * factor;
});
}, [sort]);
return (
<main className="mx-auto flex max-w-5xl flex-col gap-10 p-6 pb-24 sm:p-10">
<header className="flex flex-col gap-1">
<p className="text-sm text-muted-foreground">/dev/componentes — Etapa 12</p>
<h1 className="text-2xl font-semibold text-foreground">
Design system — SeConectai Vendas
</h1>
<p className="text-sm text-muted-foreground">
Rota só de desenvolvimento (404 em produção). Redimensione a janela para ver o
comportamento responsivo.
</p>
</header>
<Section title="Button">
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button size="sm">Small (denso, ex: linha de tabela)</Button>
<Button size="md">Medium (padrão, 44px)</Button>
<Button size="lg">Large</Button>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button loading>Salvando…</Button>
<Button disabled>Desabilitado</Button>
</div>
</Section>
<Section title="Input / Textarea / FormField">
<div className="grid max-w-md gap-4">
<FormField label="Nome fantasia" helpText="Como o cliente é conhecido no dia a dia.">
<Input placeholder="Ex: Padaria Pão Dourado" />
</FormField>
<FormField label="E-mail" error="E-mail inválido" required>
<Input type="email" defaultValue="nao-e-um-email" />
</FormField>
<FormField label="Valor de venda">
<InputGroup>
<InputGroupAddon>
<InputGroupText>R$</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="0,00" inputMode="decimal" />
</InputGroup>
</FormField>
<FormField label="Observações">
<Textarea placeholder="Notas internas sobre o cliente…" />
</FormField>
</div>
</Section>
<Section title="Select / Combobox (busca)">
<div className="grid max-w-md gap-4">
<FormField label="Papel do usuário">
<Select defaultValue="tenant_admin">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tenant_admin">Administrador</SelectItem>
<SelectItem value="vendedor">Vendedor</SelectItem>
<SelectItem value="financeiro">Financeiro</SelectItem>
</SelectContent>
</Select>
</FormField>
<FormField label="Produto/serviço" helpText="Busca por nome ou SKU.">
<Combobox
options={PRODUTO_OPTIONS}
value={comboboxValue}
onChange={setComboboxValue}
placeholder="Selecionar produto…"
searchPlaceholder="Buscar produto…"
/>
</FormField>
</div>
</Section>
<Section title="DatePicker">
<div className="grid max-w-md gap-4 sm:grid-cols-2">
<FormField label="Data de vencimento">
<DatePicker value={date} onChange={setDate} />
</FormField>
<FormField label="Período">
<DateRangePicker value={range} onChange={setRange} />
</FormField>
</div>
</Section>
<Section title="Table (responsiva — vira Cards abaixo de 768px)">
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setTableLoading((v) => !v)}>
{tableLoading ? 'Parar loading' : 'Simular loading'}
</Button>
<Button variant="outline" size="sm" onClick={() => setTableEmpty((v) => !v)}>
{tableEmpty ? 'Mostrar dados' : 'Simular vazio'}
</Button>
</div>
<DataTable
columns={CLIENTE_COLUMNS}
data={tableEmpty ? [] : sortedData}
keyField={(row) => row.id}
loading={tableLoading}
sort={sort}
onSortChange={setSort}
emptyTitle="Nenhum cliente cadastrado ainda"
emptyDescription="Cadastre o primeiro cliente para começar a vender."
emptyAction={<Button size="sm">Cadastrar cliente</Button>}
rowActions={() => (
<Button variant="ghost" size="sm">
Ver
</Button>
)}
pagination={{ page, limit: 5, total: 23, onPageChange: setPage }}
/>
</Section>
<Section title="Pagination (isolada)">
<DataPagination
page={paginationDemoPage}
limit={10}
total={97}
onPageChange={setPaginationDemoPage}
/>
</Section>
<Section title="Modal / Sheet responsivo">
<p className="text-sm text-muted-foreground">
Vira modal centralizado no desktop e bottom sheet no mobile (mesmo breakpoint da Table).
</p>
<Button className="w-fit" onClick={() => setDialogOpen(true)}>
Abrir exemplo
</Button>
<ResponsiveDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
title="Cadastro rápido de cliente"
description="Usado no fluxo de nova venda quando o cliente ainda não existe."
footer={
<>
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Cancelar
</Button>
<Button onClick={() => setDialogOpen(false)}>Salvar</Button>
</>
}
>
<div className="flex flex-col gap-4 px-4 pb-4 sm:px-0 sm:pb-0">
<FormField label="Nome">
<Input placeholder="Nome do cliente" />
</FormField>
<FormField label="Telefone/WhatsApp">
<Input placeholder="(00) 00000-0000" />
</FormField>
</div>
</ResponsiveDialog>
</Section>
<Section title="Toast">
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => toast.success('Cliente salvo com sucesso.')}
>
Sucesso
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.error('Não foi possível salvar. Tente novamente.')}
>
Erro
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.warning('Limite do plano quase atingido.')}
>
Aviso
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.info('Sessão renovada automaticamente.')}
>
Info
</Button>
</div>
</Section>
<Section title="Badge / StatusPill">
<div className="flex flex-wrap gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
<div className="flex flex-wrap gap-2">
{(Object.keys(STATUS_PILL_MAP) as BusinessStatus[]).map((status) => (
<StatusPill key={status} status={status} />
))}
</div>
</Section>
<Section title="Card">
<Card className="max-w-sm">
<CardHeader>
<CardTitle>Total recebido no mês</CardTitle>
<CardDescription>Atualizado há 2 minutos</CardDescription>
</CardHeader>
<CardContent>
<p className="text-2xl font-semibold tabular-nums text-foreground">R$ 18.420,00</p>
</CardContent>
</Card>
</Section>
<Section title="EmptyState">
<EmptyState
title="Nenhuma venda registrada"
description="As vendas confirmadas aparecerão aqui."
action={<Button size="sm">Nova venda</Button>}
/>
</Section>
<Section title="Skeleton">
<div className="flex max-w-md flex-col gap-3">
<SkeletonText />
<SkeletonText className="w-2/3" />
<SkeletonCard />
<Skeleton className="size-11 rounded-full" />
</div>
</Section>
<Section title="Tabs">
<Tabs defaultValue="itens" className="max-w-md">
<TabsList>
<TabsTrigger value="itens">Itens</TabsTrigger>
<TabsTrigger value="parcelas">Parcelas</TabsTrigger>
<TabsTrigger value="historico">Histórico</TabsTrigger>
</TabsList>
<TabsContent value="itens" className="text-sm text-muted-foreground">
Lista de itens da venda.
</TabsContent>
<TabsContent value="parcelas" className="text-sm text-muted-foreground">
Lista de parcelas geradas.
</TabsContent>
<TabsContent value="historico" className="text-sm text-muted-foreground">
Histórico de mudanças de status.
</TabsContent>
</Tabs>
</Section>
</main>
);
}