2025-03-28 00:19:01 +07:00
|
|
|
import {
|
2025-03-28 18:17:13 +07:00
|
|
|
PaginationState,
|
2025-12-05 11:55:28 +07:00
|
|
|
SortingState,
|
2025-03-28 00:19:01 +07:00
|
|
|
useReactTable,
|
|
|
|
|
getCoreRowModel,
|
|
|
|
|
getPaginationRowModel,
|
2025-12-05 11:55:28 +07:00
|
|
|
getSortedRowModel,
|
|
|
|
|
getFilteredRowModel,
|
2025-03-28 18:17:13 +07:00
|
|
|
flexRender,
|
|
|
|
|
ColumnDef,
|
2025-03-28 21:46:10 +07:00
|
|
|
Table,
|
2025-08-20 21:58:28 +07:00
|
|
|
RowData,
|
2025-12-05 11:55:28 +07:00
|
|
|
TableOptions,
|
2025-03-28 00:19:01 +07:00
|
|
|
} from '@tanstack/react-table';
|
2025-03-28 18:50:17 +07:00
|
|
|
import { Pagination } from '../../molecules';
|
|
|
|
|
|
2025-03-28 18:17:13 +07:00
|
|
|
import React from 'react';
|
2025-08-20 21:58:28 +07:00
|
|
|
import { cn } from '@imphnen-frontend-service/utils';
|
2025-03-27 15:17:04 +07:00
|
|
|
|
2025-12-05 11:55:28 +07:00
|
|
|
interface DataTableProps<T extends RowData> {
|
|
|
|
|
table?: Table<T>;
|
|
|
|
|
data?: T[];
|
|
|
|
|
columns?: ColumnDef<T, unknown>[];
|
2025-03-28 18:17:13 +07:00
|
|
|
pageSize?: number;
|
2025-12-05 11:55:28 +07:00
|
|
|
className?: string;
|
2025-03-27 15:17:04 +07:00
|
|
|
}
|
|
|
|
|
|
2025-08-20 21:58:28 +07:00
|
|
|
export const DataTable = <T extends RowData>({
|
2025-12-05 11:55:28 +07:00
|
|
|
table,
|
|
|
|
|
data = [],
|
|
|
|
|
columns = [],
|
2025-03-28 18:17:13 +07:00
|
|
|
pageSize = 9,
|
2025-12-05 11:55:28 +07:00
|
|
|
className,
|
2025-03-28 18:17:13 +07:00
|
|
|
}: DataTableProps<T>) => {
|
|
|
|
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
|
|
|
pageIndex: 0,
|
|
|
|
|
pageSize,
|
|
|
|
|
});
|
2025-12-05 11:55:28 +07:00
|
|
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
2025-03-28 18:17:13 +07:00
|
|
|
|
2025-12-05 11:55:28 +07:00
|
|
|
// Update pagination state when pageSize prop changes
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
setPagination((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
pageSize,
|
|
|
|
|
}));
|
|
|
|
|
}, [pageSize]);
|
|
|
|
|
|
|
|
|
|
// Reset pagination when data changes to prevent out-of-bounds errors
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (data.length > 0) {
|
|
|
|
|
setPagination((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
pageIndex: 0, // Reset to first page when data changes
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}, [data.length]);
|
|
|
|
|
|
|
|
|
|
// Memoize data and columns to prevent unnecessary re-renders
|
|
|
|
|
const memoizedData = React.useMemo(() => data, [data]);
|
|
|
|
|
const memoizedColumns = React.useMemo(() => columns, [columns]);
|
|
|
|
|
|
|
|
|
|
// Memoize table configuration to prevent recreation on every render
|
|
|
|
|
const tableConfig = React.useMemo(() => {
|
|
|
|
|
const config: TableOptions<T> = {
|
|
|
|
|
data: memoizedData,
|
|
|
|
|
columns: memoizedColumns,
|
|
|
|
|
state: {
|
|
|
|
|
pagination,
|
|
|
|
|
sorting,
|
|
|
|
|
},
|
|
|
|
|
onPaginationChange: setPagination,
|
|
|
|
|
onSortingChange: setSorting,
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
|
|
|
getSortedRowModel: getSortedRowModel(),
|
|
|
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}, [memoizedData, memoizedColumns, pagination, sorting]);
|
|
|
|
|
|
|
|
|
|
// Prefer external table instance if provided; otherwise create an internal one
|
|
|
|
|
const internalTable = useReactTable(tableConfig);
|
|
|
|
|
const t = table ?? internalTable;
|
|
|
|
|
|
|
|
|
|
// Handle empty data state
|
|
|
|
|
const isEmpty = t.getRowModel().rows.length === 0;
|
2025-03-28 18:17:13 +07:00
|
|
|
|
2025-03-27 15:17:04 +07:00
|
|
|
return (
|
2025-12-05 11:55:28 +07:00
|
|
|
<div className={cn('flex flex-col gap-8', className)}>
|
2025-03-28 18:50:17 +07:00
|
|
|
<div className="w-full overflow-x-auto">
|
|
|
|
|
<table className="w-full min-w-full text-base">
|
2025-04-05 06:05:50 +07:00
|
|
|
<thead className="bg-primary-50 mb-3 text-left text-nowrap">
|
2025-12-05 11:55:28 +07:00
|
|
|
{t.getHeaderGroups().map((headerGroup) => (
|
2025-03-28 18:50:17 +07:00
|
|
|
<tr key={headerGroup.id}>
|
|
|
|
|
{headerGroup.headers.map((header) => (
|
|
|
|
|
<th
|
|
|
|
|
key={header.id}
|
2025-12-05 11:55:28 +07:00
|
|
|
onClick={
|
|
|
|
|
header.column.getCanSort()
|
|
|
|
|
? header.column.getToggleSortingHandler()
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
className={cn(
|
|
|
|
|
'py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg',
|
|
|
|
|
header.column.getCanSort() &&
|
|
|
|
|
'cursor-pointer select-none hover:bg-primary-100 transition-colors',
|
|
|
|
|
header?.column?.columnDef?.meta?.headerClassName
|
|
|
|
|
)}
|
2025-03-28 18:50:17 +07:00
|
|
|
>
|
|
|
|
|
{header.isPlaceholder
|
|
|
|
|
? null
|
|
|
|
|
: flexRender(
|
|
|
|
|
header.column.columnDef.header,
|
|
|
|
|
header.getContext()
|
|
|
|
|
)}
|
2025-12-05 11:55:28 +07:00
|
|
|
{header.column.getCanSort() && (
|
|
|
|
|
<span className="ml-2 text-xs text-gray-500">
|
|
|
|
|
{header.column.getIsSorted() === 'asc' && '▲'}
|
|
|
|
|
{header.column.getIsSorted() === 'desc' && '▼'}
|
|
|
|
|
{!header.column.getIsSorted() && <span>⇅</span>}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2025-03-28 18:50:17 +07:00
|
|
|
</th>
|
|
|
|
|
))}
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</thead>
|
2025-03-28 22:41:55 +07:00
|
|
|
<tbody>
|
2025-12-05 11:55:28 +07:00
|
|
|
{isEmpty ? (
|
|
|
|
|
<tr>
|
|
|
|
|
<td
|
|
|
|
|
colSpan={t.getAllColumns().length}
|
|
|
|
|
className="py-8 px-5 text-center text-neutral-500"
|
|
|
|
|
>
|
|
|
|
|
No data available
|
|
|
|
|
</td>
|
2025-03-28 18:50:17 +07:00
|
|
|
</tr>
|
2025-12-05 11:55:28 +07:00
|
|
|
) : (
|
|
|
|
|
t.getRowModel().rows.map((row, rowIndex) => (
|
|
|
|
|
<tr
|
|
|
|
|
key={row.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
'hover:bg-primary-50 transition-colors',
|
|
|
|
|
rowIndex % 2 === 0 ? 'bg-white' : 'bg-primary-100'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{row.getVisibleCells().map((cell) => (
|
|
|
|
|
<td
|
|
|
|
|
key={cell.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
'py-3 px-5 first:rounded-l-lg last:rounded-r-lg',
|
|
|
|
|
cell?.column?.columnDef?.meta?.cellClassName
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{flexRender(
|
|
|
|
|
cell.column.columnDef.cell,
|
|
|
|
|
cell.getContext()
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
))}
|
|
|
|
|
</tr>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2025-03-28 18:50:17 +07:00
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2025-12-05 11:55:28 +07:00
|
|
|
<Pagination table={t} />
|
2025-03-27 15:17:04 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-27 16:29:09 +07:00
|
|
|
export default DataTable;
|