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
+109
View File
@@ -0,0 +1,109 @@
'use client';
import * as React from 'react';
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
export interface ComboboxOption {
value: string;
label: string;
}
interface ComboboxProps {
options: ComboboxOption[];
value?: string;
onChange: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
emptyText?: string;
disabled?: boolean;
loading?: boolean;
className?: string;
id?: string;
'aria-invalid'?: boolean;
'aria-describedby'?: string;
}
/**
* `Select` com busca, usado nas listas longas do sistema (cliente,
* produto/serviço) — seção 7 do schema de frontend. Composição de
* Command + Popover, não do `Select` nativo do Radix.
*/
function Combobox({
options,
value,
onChange,
placeholder = 'Selecionar…',
searchPlaceholder = 'Buscar…',
emptyText = 'Nenhum resultado encontrado.',
disabled,
loading,
className,
id,
...aria
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const selected = options.find((option) => option.value === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id={id}
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled || loading}
loading={loading}
className={cn(
'w-full justify-between font-normal',
!selected && 'text-muted-foreground',
className,
)}
{...aria}
>
{selected ? selected.label : placeholder}
<ChevronsUpDownIcon className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-(--radix-popover-trigger-width) p-0" align="start">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{emptyText}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => {
onChange(option.value);
setOpen(false);
}}
>
<CheckIcon
className={cn('mr-2', option.value === value ? 'opacity-100' : 'opacity-0')}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
export { Combobox };