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>
129 lines
3.3 KiB
TypeScript
129 lines
3.3 KiB
TypeScript
'use client';
|
||
|
||
import * as React from 'react';
|
||
import { format } from 'date-fns';
|
||
import { ptBR } from 'date-fns/locale';
|
||
import { CalendarIcon } from 'lucide-react';
|
||
import type { DateRange } from 'react-day-picker';
|
||
|
||
import { cn } from '@/lib/utils';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Calendar } from '@/components/ui/calendar';
|
||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||
|
||
interface DatePickerProps {
|
||
value?: Date;
|
||
onChange: (date: Date | undefined) => void;
|
||
placeholder?: string;
|
||
disabled?: boolean;
|
||
className?: string;
|
||
id?: string;
|
||
'aria-invalid'?: boolean;
|
||
'aria-describedby'?: string;
|
||
}
|
||
|
||
/** Seleção de data única — usada em formulários (ex: vencimento de parcela). */
|
||
function DatePicker({
|
||
value,
|
||
onChange,
|
||
placeholder = 'Selecionar data',
|
||
disabled,
|
||
className,
|
||
id,
|
||
...aria
|
||
}: DatePickerProps) {
|
||
const [open, setOpen] = React.useState(false);
|
||
|
||
return (
|
||
<Popover open={open} onOpenChange={setOpen}>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
id={id}
|
||
variant="outline"
|
||
disabled={disabled}
|
||
className={cn(
|
||
'w-full justify-start font-normal',
|
||
!value && 'text-muted-foreground',
|
||
className,
|
||
)}
|
||
{...aria}
|
||
>
|
||
<CalendarIcon />
|
||
{value ? format(value, "dd 'de' MMMM 'de' yyyy", { locale: ptBR }) : placeholder}
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0" align="start">
|
||
<Calendar
|
||
mode="single"
|
||
selected={value}
|
||
onSelect={(date) => {
|
||
onChange(date);
|
||
setOpen(false);
|
||
}}
|
||
locale={ptBR}
|
||
autoFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
}
|
||
|
||
interface DateRangePickerProps {
|
||
value?: DateRange;
|
||
onChange: (range: DateRange | undefined) => void;
|
||
placeholder?: string;
|
||
disabled?: boolean;
|
||
className?: string;
|
||
id?: string;
|
||
}
|
||
|
||
/** Seleção de período — usada nos filtros de dashboard/listagens (seção 7 do schema). */
|
||
function DateRangePicker({
|
||
value,
|
||
onChange,
|
||
placeholder = 'Selecionar período',
|
||
disabled,
|
||
className,
|
||
id,
|
||
}: DateRangePickerProps) {
|
||
const [open, setOpen] = React.useState(false);
|
||
|
||
const label = React.useMemo(() => {
|
||
if (!value?.from) return placeholder;
|
||
if (!value.to) return format(value.from, 'dd/MM/yyyy', { locale: ptBR });
|
||
return `${format(value.from, 'dd/MM/yyyy', { locale: ptBR })} – ${format(value.to, 'dd/MM/yyyy', { locale: ptBR })}`;
|
||
}, [value, placeholder]);
|
||
|
||
return (
|
||
<Popover open={open} onOpenChange={setOpen}>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
id={id}
|
||
variant="outline"
|
||
disabled={disabled}
|
||
className={cn(
|
||
'w-full justify-start font-normal',
|
||
!value?.from && 'text-muted-foreground',
|
||
className,
|
||
)}
|
||
>
|
||
<CalendarIcon />
|
||
{label}
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0" align="start">
|
||
<Calendar
|
||
mode="range"
|
||
selected={value}
|
||
onSelect={onChange}
|
||
locale={ptBR}
|
||
numberOfMonths={2}
|
||
autoFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
);
|
||
}
|
||
|
||
export { DatePicker, DateRangePicker };
|