173 lines
6.0 KiB
TypeScript
173 lines
6.0 KiB
TypeScript
import {
|
|
Pagination,
|
|
PaginationContent,
|
|
PaginationItem,
|
|
PaginationLink,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@repo/shadcn-ui/components";
|
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
import type { Table } from "@tanstack/react-table";
|
|
import {
|
|
ChevronLeftIcon,
|
|
ChevronRightIcon,
|
|
ChevronsLeftIcon,
|
|
ChevronsRightIcon,
|
|
} from "lucide-react";
|
|
|
|
import { useTranslation } from "../../locales/i18n.ts";
|
|
|
|
import type { DataTableMeta } from "./data-table.tsx";
|
|
|
|
interface DataTablePaginationProps<TData> {
|
|
table: Table<TData>;
|
|
onPageChange?: (pageIndex: number) => void;
|
|
onPageSizeChange?: (pageSize: number) => void;
|
|
|
|
className?: string;
|
|
}
|
|
|
|
export function DataTablePagination<TData>({
|
|
table,
|
|
onPageChange,
|
|
onPageSizeChange,
|
|
className,
|
|
}: DataTablePaginationProps<TData>) {
|
|
const { t } = useTranslation();
|
|
|
|
const { pageIndex: rawIndex, pageSize: rawSize } = table.getState().pagination;
|
|
const meta = table.options.meta as DataTableMeta<TData>;
|
|
const totalRows = meta?.totalItems ?? table.getFilteredRowModel().rows.length;
|
|
|
|
// Normalización segura
|
|
const pageIndex = Number.isFinite(rawIndex) && rawIndex >= 0 ? rawIndex : 0;
|
|
const pageSize = Number.isFinite(rawSize) && rawSize > 0 ? rawSize : 10;
|
|
|
|
const pageCount = table.getPageCount() || Math.max(1, Math.ceil((totalRows || 0) / pageSize));
|
|
const hasSelected = table.getFilteredSelectedRowModel().rows.length > 0;
|
|
|
|
// Rango visible (1-based en UI)
|
|
const start = totalRows > 0 ? pageIndex * pageSize + 1 : 0;
|
|
const end = totalRows > 0 ? Math.min(start + pageSize - 1, totalRows) : 0;
|
|
|
|
// Handlers de navegación controlada
|
|
const notify = (next: Partial<{ pageIndex: number; pageSize: number }>) =>
|
|
table.options.onPaginationChange?.({ pageIndex, pageSize, ...next });
|
|
|
|
const gotoPage = (index: number) =>
|
|
onPageChange ? onPageChange(index) : notify({ pageIndex: index });
|
|
const handlePageSizeChange = (size: string) =>
|
|
onPageSizeChange ? onPageSizeChange(Number(size)) : notify({ pageSize: Number(size) });
|
|
|
|
const gotoPreviousPage = () => gotoPage(pageIndex - 1);
|
|
const gotoNextPage = () => gotoPage(pageIndex + 1);
|
|
const gotoFirstPage = () => gotoPage(0);
|
|
const gotoLastPage = () => gotoPage(pageCount - 1);
|
|
|
|
return (
|
|
<div className={cn("flex items-center justify-between", className)}>
|
|
{/* Información izquierda */}
|
|
<div className="flex flex-col sm:flex-row items-center gap-4 flex-1 text-sm text-muted-foreground">
|
|
<span aria-live="polite">
|
|
{t("components.datatable.pagination.showing_range", { start, end, total: totalRows })}
|
|
</span>
|
|
|
|
{hasSelected && (
|
|
<span aria-live="polite">
|
|
{t("components.datatable.pagination.rows_selected", {
|
|
count: table.getFilteredSelectedRowModel().rows.length,
|
|
total: table.getFilteredRowModel().rows.length,
|
|
})}
|
|
</span>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span>{t("components.datatable.pagination.rows_per_page")}</span>
|
|
<Select onValueChange={handlePageSizeChange} value={String(pageSize)}>
|
|
<SelectTrigger className="w-20 h-8 bg-white border-gray-200">
|
|
<SelectValue placeholder={String(pageSize)} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{[10, 20, 25, 30, 40, 50].map((size) => (
|
|
<SelectItem key={size} value={String(size)}>
|
|
{size}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Controles derecha */}
|
|
<div className="flex items-center gap-2">
|
|
<Pagination>
|
|
<PaginationContent>
|
|
<PaginationItem>
|
|
<PaginationLink
|
|
aria-label={t("components.datatable.pagination.goto_first_page")}
|
|
className="px-2.5 cursor-pointer"
|
|
isActive={pageIndex > 0}
|
|
onClick={() => {
|
|
if (pageIndex > 0) gotoFirstPage();
|
|
}}
|
|
size="sm"
|
|
>
|
|
<ChevronsLeftIcon className="size-4" />
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
|
|
<PaginationItem>
|
|
<PaginationLink
|
|
aria-label={t("components.datatable.pagination.goto_previous_page")}
|
|
className="px-2.5 cursor-pointer"
|
|
isActive={pageIndex > 0}
|
|
onClick={() => {
|
|
if (pageIndex > 0) gotoPreviousPage();
|
|
}}
|
|
size="sm"
|
|
>
|
|
<ChevronLeftIcon className="size-4" />
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
|
|
<span aria-live="polite" className="text-sm text-muted-foreground px-2">
|
|
{t("components.datatable.pagination.page_of", { page: pageIndex + 1, of: pageCount })}
|
|
</span>
|
|
|
|
<PaginationItem>
|
|
<PaginationLink
|
|
aria-label={t("components.datatable.pagination.goto_next_page")}
|
|
className="px-2.5 cursor-pointer"
|
|
isActive={pageIndex < pageCount - 1}
|
|
onClick={() => {
|
|
if (pageIndex < pageCount - 1) gotoNextPage();
|
|
}}
|
|
size="sm"
|
|
>
|
|
<ChevronRightIcon className="size-4" />
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
|
|
<PaginationItem>
|
|
<PaginationLink
|
|
aria-label={t("components.datatable.pagination.goto_last_page")}
|
|
className="px-2.5 cursor-pointer"
|
|
isActive={pageIndex < pageCount - 1}
|
|
onClick={() => {
|
|
if (pageIndex < pageCount - 1) gotoLastPage();
|
|
}}
|
|
size="sm"
|
|
>
|
|
<ChevronsRightIcon className="size-4" />
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
</PaginationContent>
|
|
</Pagination>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|