Files
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

87 lines
2.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react';
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
interface DataPaginationProps {
/** 1-indexado, compatível com `page`/`limit`/`meta.total` da API (Etapas 1–8 do backend). */
page: number;
limit: number;
total: number;
onPageChange: (page: number) => void;
className?: string;
}
function getPageNumbers(page: number, totalPages: number): (number | 'ellipsis')[] {
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
const pages = new Set([1, totalPages, page, page - 1, page + 1]);
const sorted = [...pages].filter((p) => p >= 1 && p <= totalPages).sort((a, b) => a - b);
const result: (number | 'ellipsis')[] = [];
sorted.forEach((p, i) => {
if (i > 0 && p - sorted[i - 1] > 1) result.push('ellipsis');
result.push(p);
});
return result;
}
/** Controles de página compatíveis com os parâmetros de paginação do backend (Etapa 12). */
function DataPagination({ page, limit, total, onPageChange, className }: DataPaginationProps) {
const totalPages = Math.max(1, Math.ceil(total / limit));
const firstItem = total === 0 ? 0 : (page - 1) * limit + 1;
const lastItem = Math.min(page * limit, total);
return (
<nav
aria-label="Paginação"
className={cn('flex flex-col items-center justify-between gap-3 sm:flex-row', className)}
>
<p className="text-sm text-muted-foreground">
{total === 0 ? 'Nenhum resultado' : `Mostrando ${firstItem}–${lastItem} de ${total}`}
</p>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
aria-label="Página anterior"
disabled={page <= 1}
onClick={() => onPageChange(page - 1)}
>
<ChevronLeftIcon />
</Button>
{getPageNumbers(page, totalPages).map((p, index) =>
p === 'ellipsis' ? (
<span key={`ellipsis-${index}`} className="flex size-11 items-center justify-center">
<MoreHorizontalIcon className="size-4 text-muted-foreground" aria-hidden />
</span>
) : (
<Button
key={p}
variant={p === page ? 'outline' : 'ghost'}
size="icon"
aria-label={`Página ${p}`}
aria-current={p === page ? 'page' : undefined}
onClick={() => onPageChange(p)}
>
{p}
</Button>
),
)}
<Button
variant="ghost"
size="icon"
aria-label="Próxima página"
disabled={page >= totalPages}
onClick={() => onPageChange(page + 1)}
>
<ChevronRightIcon />
</Button>
</div>
</nav>
);
}
export { DataPagination };