Uecko_ERP/packages/rdx-ui/src/components/datatable/data-table.tsx

309 lines
10 KiB
TypeScript
Raw Normal View History

2025-10-16 11:18:55 +00:00
"use client"
import {
ColumnDef,
ColumnFiltersState,
ColumnSizingState,
2025-10-16 17:59:13 +00:00
Row,
2025-10-16 11:18:55 +00:00
SortingState,
2025-10-16 17:59:13 +00:00
Table,
TableMeta,
2025-10-16 11:18:55 +00:00
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from "@tanstack/react-table"
import * as React from "react"
import {
2025-10-16 17:59:13 +00:00
Button,
2025-10-16 11:18:55 +00:00
Dialog,
DialogContent,
2025-10-16 17:59:13 +00:00
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
TableBody,
2025-10-16 11:18:55 +00:00
TableCell,
2025-10-16 17:59:13 +00:00
Table as TableComp,
TableFooter,
2025-10-16 11:18:55 +00:00
TableHead,
TableHeader,
2025-10-16 17:59:13 +00:00
TableRow
2025-10-16 11:18:55 +00:00
} from '@repo/shadcn-ui/components'
import { DataTablePagination } from './data-table-pagination.tsx'
import { DataTableToolbar } from "./data-table-toolbar.tsx"
import { useTranslation } from "../../locales/i18n.ts"
2025-10-16 17:59:13 +00:00
export type DataTableOps<TData> = {
onAdd?: (table: Table<TData>) => void;
}
export type DataTableRowOps<TData> = {
duplicate?(index: number, table: Table<TData>): void;
remove?(index: number, table: Table<TData>): void;
move?(from: number, to: number, table: Table<TData>): void;
canMoveUp?(index: number, table: Table<TData>): boolean;
canMoveDown?(index: number, lastIndex: number, table: Table<TData>): boolean;
};
2025-10-16 11:18:55 +00:00
2025-10-16 17:59:13 +00:00
export type DataTableBulkRowOps<TData> = {
duplicateSelected?: (indexes: number[], table: Table<TData>) => void;
removeSelected?: (indexes: number[], table: Table<TData>) => void;
moveSelectedUp?: (indexes: number[], table: Table<TData>) => void;
moveSelectedDown?: (indexes: number[], table: Table<TData>) => void;
2025-10-16 11:18:55 +00:00
};
2025-10-16 17:59:13 +00:00
export type DataTableMeta<TData> = TableMeta<TData> & {
2025-10-18 19:57:52 +00:00
totalItems?: number; // para paginación server-side
readOnly?: boolean;
2025-10-16 17:59:13 +00:00
tableOps?: DataTableOps<TData>
rowOps?: DataTableRowOps<TData>
bulkOps?: DataTableBulkRowOps<TData>
}
export interface DataTableProps<TData, TValue> {
2025-10-16 11:18:55 +00:00
columns: ColumnDef<TData, TValue>[]
2025-10-16 17:59:13 +00:00
data: TData[]
meta?: DataTableMeta<TData>
2025-10-16 11:18:55 +00:00
2025-10-16 17:59:13 +00:00
// Configuración
readOnly?: boolean
enablePagination?: boolean
pageSize?: number
enableRowSelection?: boolean
EditorComponent?: React.ComponentType<{ row: TData; index: number; onClose: () => void }>
2025-10-16 11:18:55 +00:00
2025-10-18 19:57:52 +00:00
getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
// Soporte para paginación server-side
manualPagination?: boolean;
pageIndex?: number; // 0-based
totalItems?: number;
onPageChange?: (pageIndex: number) => void;
onPageSizeChange?: (pageSize: number) => void;
// Acción al hacer click en una fila
onRowClick?: (row: TData, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
2025-10-16 11:18:55 +00:00
}
export function DataTable<TData, TValue>({
columns,
data,
meta,
2025-10-18 19:57:52 +00:00
2025-10-16 17:59:13 +00:00
readOnly = false,
enablePagination = true,
2025-10-18 22:31:30 +00:00
pageSize = 10,
2025-10-16 17:59:13 +00:00
enableRowSelection = false,
EditorComponent,
2025-10-18 19:57:52 +00:00
getRowId,
manualPagination,
pageIndex = 0,
totalItems,
onPageChange,
onPageSizeChange,
onRowClick,
2025-10-16 11:18:55 +00:00
}: DataTableProps<TData, TValue>) {
const { t } = useTranslation();
2025-10-18 19:57:52 +00:00
const [rowSelection, setRowSelection] = React.useState({});
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [colSizes, setColSizes] = React.useState<ColumnSizingState>({});
const [editIndex, setEditIndex] = React.useState<number | null>(null);
// Configuración TanStack
2025-10-16 11:18:55 +00:00
const table = useReactTable({
data,
columns,
columnResizeMode: "onChange",
onColumnSizingChange: setColSizes,
2025-10-18 19:57:52 +00:00
meta: { ...meta, totalItems, readOnly },
getRowId:
getRowId ??
((originalRow: TData, i: number) =>
typeof (originalRow as any).id !== "undefined"
? String((originalRow as any).id)
: String(i)),
2025-10-16 11:18:55 +00:00
state: {
columnSizing: colSizes,
sorting,
columnVisibility,
rowSelection,
columnFilters,
2025-10-18 22:31:30 +00:00
pagination: { pageIndex, pageSize },
2025-10-16 11:18:55 +00:00
},
2025-10-18 19:57:52 +00:00
manualPagination,
pageCount: manualPagination
? Math.ceil((totalItems ?? data.length) / (pageSize ?? 25))
: undefined,
2025-10-18 22:31:30 +00:00
// Propagar cambios al padre
2025-10-18 19:57:52 +00:00
onPaginationChange: (updater) => {
2025-10-18 22:31:30 +00:00
const next = typeof updater === "function"
? updater({ pageIndex, pageSize })
: updater;
if (typeof next.pageIndex === "number") onPageChange?.(next.pageIndex);
if (typeof next.pageSize === "number") onPageSizeChange?.(next.pageSize);
2025-10-16 17:59:13 +00:00
},
2025-10-18 19:57:52 +00:00
2025-10-16 11:18:55 +00:00
enableRowSelection,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
2025-10-18 19:57:52 +00:00
2025-10-16 17:59:13 +00:00
getCoreRowModel: getCoreRowModel(),
2025-10-16 11:18:55 +00:00
getFilteredRowModel: getFilteredRowModel(),
2025-10-18 19:57:52 +00:00
getPaginationRowModel: manualPagination ? undefined : getPaginationRowModel(),
2025-10-16 11:18:55 +00:00
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
2025-10-16 17:59:13 +00:00
const handleCloseEditor = React.useCallback(() => setEditIndex(null), [])
2025-10-18 19:57:52 +00:00
// Render principal
2025-10-16 11:18:55 +00:00
return (
2025-10-18 22:31:30 +00:00
<div
2025-10-19 19:04:16 +00:00
className="transition-[max-height] duration-300 ease-in-out"
2025-10-18 22:31:30 +00:00
>
<div className="flex flex-col gap-0">
<DataTableToolbar table={table} showViewOptions={!readOnly} />
<div className="overflow-hidden rounded-md border">
<TableComp className="w-full text-sm">
{/* CABECERA */}
2025-10-19 19:04:16 +00:00
<TableHeader className="sticky top-0 z-10 bg-muted">
2025-10-18 22:31:30 +00:00
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}>
{hg.headers.map((h) => {
const w = h.getSize();
const minW = h.column.columnDef.minSize;
const maxW = h.column.columnDef.maxSize;
2025-10-16 11:18:55 +00:00
return (
2025-10-18 22:31:30 +00:00
<TableHead
key={h.id}
colSpan={h.colSpan}
2025-10-16 11:18:55 +00:00
style={{
width: w ? `${w}px` : undefined,
minWidth: typeof minW === "number" ? `${minW}px` : undefined,
maxWidth: typeof maxW === "number" ? `${maxW}px` : undefined,
}}
>
2025-10-18 22:31:30 +00:00
<div className={"text-xs text-muted-foreground text-nowrap cursor-default"}>
{h.isPlaceholder
? null
: flexRender(h.column.columnDef.header, h.getContext())}
</div>
</TableHead>
2025-10-16 11:18:55 +00:00
);
})}
</TableRow>
2025-10-18 22:31:30 +00:00
))}
</TableHeader>
{/* CUERPO */}
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row, rowIndex) => (
<TableRow
key={row.id}
role="button"
tabIndex={0}
data-state={row.getIsSelected() && "selected"}
className={`group bg-background ${readOnly ? "cursor-default" : "cursor-pointer"}`}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any);
}}
2025-10-18 23:05:02 +00:00
onClick={(e) => onRowClick?.(row.original, rowIndex, e)}
2025-10-18 22:31:30 +00:00
onDoubleClick={
!readOnly && !onRowClick ? () => setEditIndex(rowIndex) : undefined
} >
{row.getVisibleCells().map((cell) => {
const w = cell.column.getSize();
const minW = cell.column.columnDef.minSize;
const maxW = cell.column.columnDef.maxSize;
return (
<TableCell
key={cell.id}
className="align-top"
style={{
width: w ? `${w}px` : undefined,
minWidth: typeof minW === "number" ? `${minW}px` : undefined,
maxWidth: typeof maxW === "number" ? `${maxW}px` : undefined,
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground">
{t("components.datatable.empty")}
</TableCell>
</TableRow>
)}
</TableBody>
{/* Paginación */}
{enablePagination && (
<TableFooter>
<TableRow>
<TableCell colSpan={100}>
<DataTablePagination table={table} onPageChange={onPageChange} onPageSizeChange={onPageSizeChange} />
</TableCell>
</TableRow>
</TableFooter>)
}
</TableComp>
</div>
{/* Editor modal */}
{EditorComponent && editIndex !== null && (
<Dialog open onOpenChange={handleCloseEditor}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{t("components.datatable.editor.title")}</DialogTitle>
<DialogDescription>{t("components.datatable.editor.subtitle")}</DialogDescription>
</DialogHeader>
<div className="mt-4">
<EditorComponent
row={data[editIndex]}
index={editIndex}
onClose={handleCloseEditor}
/>
</div>
<DialogFooter>
<Button type="button" variant="secondary" onClick={handleCloseEditor}>
{t("common.close")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
2025-10-16 17:59:13 +00:00
</div>
2025-10-16 11:18:55 +00:00
</div>
2025-10-16 17:59:13 +00:00
)
2025-10-16 11:18:55 +00:00
}