refactor(FE-361): Refactor table and pagination components

This commit is contained in:
rstubryan
2025-12-09 18:04:33 +07:00
parent b039ec832b
commit 8f5dd1851a
3 changed files with 560 additions and 510 deletions
+302 -212
View File
@@ -1,7 +1,9 @@
'use client';
import { ReactNode } from 'react';
import { ChangeEventHandler, ReactNode } from 'react';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import { cn } from '@/lib/helper';
@@ -17,16 +19,18 @@ const PaginationButton = ({
disabled?: boolean;
onClick?: () => void;
}) => (
<button
className={cn(
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square',
'disabled:text-gray-700 disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-gray-50 disabled:active:translate-y-0'
)}
<Button
variant='ghost'
color='none'
disabled={disabled}
onClick={onClick}
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
'disabled:text-primary disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-primary/10 disabled:active:translate-y-0'
)}
>
{content}
</button>
</Button>
);
const EtcPaginationButton = ({
@@ -48,7 +52,7 @@ const EtcPaginationButton = ({
tabIndex={0}
role='button'
className={cn(
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square'
)}
>
...
@@ -57,7 +61,7 @@ const EtcPaginationButton = ({
<div className='dropdown-content'>
<ul
tabIndex={0}
className='menu bg-base-100 rounded-lg z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
className='menu bg-base-100 rounded-lg! z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
>
{pages.map((pageNumber) => (
<li key={pageNumber}>
@@ -76,7 +80,7 @@ const EtcPaginationButton = ({
<button
disabled
className={cn(
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square'
)}
>
...
@@ -90,16 +94,20 @@ const Pagination = ({
currentPage = 1,
totalItems = 0,
itemsPerPage = 10,
rowOptions = [10, 20, 50, 100],
onPageChange,
onPrevPage = () => {},
onNextPage = () => {},
onRowChange,
}: {
currentPage: number;
totalItems: number;
itemsPerPage: number;
rowOptions?: number[];
onPageChange: (pageNumber: number) => void;
onPrevPage: () => void;
onNextPage: () => void;
onRowChange?: (row: number) => void;
}) => {
const totalPages =
Math.ceil(totalItems / itemsPerPage) === 0
@@ -107,30 +115,139 @@ const Pagination = ({
: Math.ceil(totalItems / itemsPerPage);
const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber);
const firstPageClickHandler = () => onPageChange(1);
const lastPageClickHandler = () => onPageChange(totalPages);
const rowChangeHandler: ChangeEventHandler<HTMLSelectElement> = (e) => {
onRowChange?.(Number(e.target.value));
};
const DisplayedRowCountSelect = () => (
<div className='flex flex-row items-center gap-4'>
<span className='text-sm font-medium text-base-content/50'>Showing</span>
<select
defaultValue={itemsPerPage}
onChange={rowChangeHandler}
className='select select-xs w-fit pl-3 pr-7 text-base-content/50'
>
{rowOptions.map((rowOption, rowOptionIdx) => (
<option
key={rowOptionIdx}
value={rowOption}
className='text-base-content active:text-neutral-content'
>
{rowOption} Per page
</option>
))}
</select>
</div>
);
const GoToFirstPageButton = () => (
<Button
disabled={currentPage === 1}
onClick={firstPageClickHandler}
variant='ghost'
color='none'
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-double-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const PrevPageButton = () => (
<Button
disabled={currentPage === 1}
onClick={onPrevPage}
variant='ghost'
color='none'
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const GoToLastPageButton = () => (
<Button
variant='ghost'
color='none'
disabled={currentPage === totalPages}
onClick={lastPageClickHandler}
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-double-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const NextPageButton = () => (
<Button
variant='ghost'
color='none'
disabled={currentPage === totalPages}
onClick={onNextPage}
className={cn(
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='heroicons:chevron-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</Button>
);
const PageInfo = () => (
<span className='text-nowrap text-sm font-medium text-base-content/50'>
Page {currentPage} of {totalPages}
</span>
);
return (
<div>
<div className='join w-full justify-between items-center gap-3'>
<button
disabled={currentPage === 1}
onClick={onPrevPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='uil:arrow-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>{' '}
Previous
</button>
<div className='@container'>
<div className='flex flex-row justify-center items-center'>
<div className='hidden @md:block'>
<DisplayedRowCountSelect />
</div>
{totalPages <= 7 && (
<div className='join-item join gap-0.5'>
{range(1, totalPages).map((pageNumber) => (
<div className='join w-full justify-end @md:justify-center items-center gap-0.5'>
<div className='hidden @md:block'>
<GoToFirstPageButton />
</div>
<div className='hidden @md:block'>
<PrevPageButton />
</div>
{totalPages <= 7 &&
range(1, totalPages).map((pageNumber) => (
<PaginationButton
key={pageNumber}
content={pageNumber}
@@ -138,195 +255,168 @@ const Pagination = ({
onClick={() => pageChangeHandler(pageNumber)}
/>
))}
</div>
)}
{totalPages > 7 && (
<div className='join-item join gap-0.5'>
<PaginationButton
content={1}
disabled={currentPage === 1}
onClick={() => pageChangeHandler(1)}
/>
{totalPages >= 2 &&
(currentPage <= 3 || currentPage >= totalPages - 2) && (
<PaginationButton
content={2}
disabled={currentPage === 2}
onClick={() => pageChangeHandler(2)}
/>
)}
{totalPages >= 2 &&
currentPage > 3 &&
currentPage < totalPages - 2 && (
<EtcPaginationButton
startPage={2}
endPage={currentPage - 2}
onPageItemClick={pageChangeHandler}
/>
)}
{totalPages >= 3 &&
(currentPage <= 4 || currentPage >= totalPages - 2) &&
currentPage !== totalPages - 2 && (
<PaginationButton
content={3}
disabled={currentPage === 3}
onClick={() => pageChangeHandler(3)}
/>
)}
{totalPages >= 7 &&
(currentPage <= 2 || currentPage >= totalPages - 2) && (
<EtcPaginationButton
startPage={
currentPage <= 2
? currentPage + 2
: currentPage === totalPages - 2
? 3
: currentPage >= totalPages - 1
? 4
: 1
}
endPage={
currentPage <= 2 || currentPage >= totalPages - 1
? totalPages - 3
: currentPage === totalPages - 2
? totalPages - 4
: 2
}
onPageItemClick={pageChangeHandler}
/>
)}
{totalPages >= 3 &&
currentPage > 4 &&
currentPage < totalPages - 1 && (
<PaginationButton
content={currentPage - 1}
onClick={() => pageChangeHandler(currentPage - 1)}
/>
)}
{totalPages >= 7 &&
currentPage > 3 &&
currentPage < totalPages - 2 && (
<PaginationButton content={currentPage} disabled />
)}
{totalPages >= 5 &&
currentPage > 2 &&
currentPage < totalPages - 2 && (
<PaginationButton
content={currentPage + 1}
onClick={() => pageChangeHandler(currentPage + 1)}
/>
)}
{totalPages >= 5 &&
(currentPage <= 2 || currentPage >= totalPages - 2) && (
<PaginationButton
content={totalPages - 2}
disabled={currentPage === totalPages - 2}
onClick={() => pageChangeHandler(totalPages - 2)}
/>
)}
{totalPages >= 6 &&
currentPage > 2 &&
currentPage < totalPages - 3 && (
<EtcPaginationButton
startPage={
currentPage <= 3
? currentPage + 2
: currentPage >= 4
? currentPage + 2
: 1
}
endPage={
currentPage <= 3
? totalPages - 2
: currentPage >= 4
? totalPages - 1
: 0
}
onPageItemClick={pageChangeHandler}
/>
)}
{totalPages >= 6 &&
(currentPage <= 3 || currentPage >= totalPages - 3) && (
<PaginationButton
content={totalPages - 1}
disabled={currentPage === totalPages - 1}
onClick={() => pageChangeHandler(totalPages - 1)}
/>
)}
{totalPages >= 7 && (
{totalPages > 7 && (
<>
<PaginationButton
content={totalPages}
disabled={currentPage === totalPages}
onClick={() => pageChangeHandler(totalPages)}
content={1}
disabled={currentPage === 1}
onClick={() => pageChangeHandler(1)}
/>
)}
</div>
)}
<button
disabled={currentPage === totalPages}
onClick={onNextPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
{totalPages >= 2 &&
(currentPage <= 3 || currentPage >= totalPages - 2) && (
<PaginationButton
content={2}
disabled={currentPage === 2}
onClick={() => pageChangeHandler(2)}
/>
)}
{totalPages >= 2 &&
currentPage > 3 &&
currentPage < totalPages - 2 && (
<EtcPaginationButton
startPage={2}
endPage={currentPage - 2}
onPageItemClick={pageChangeHandler}
/>
)}
{totalPages >= 3 &&
(currentPage <= 4 || currentPage >= totalPages - 2) &&
currentPage !== totalPages - 2 && (
<PaginationButton
content={3}
disabled={currentPage === 3}
onClick={() => pageChangeHandler(3)}
/>
)}
{totalPages >= 7 &&
(currentPage <= 2 || currentPage >= totalPages - 2) && (
<EtcPaginationButton
startPage={
currentPage <= 2
? currentPage + 2
: currentPage === totalPages - 2
? 3
: currentPage >= totalPages - 1
? 4
: 1
}
endPage={
currentPage <= 2 || currentPage >= totalPages - 1
? totalPages - 3
: currentPage === totalPages - 2
? totalPages - 4
: 2
}
onPageItemClick={pageChangeHandler}
/>
)}
{totalPages >= 3 &&
currentPage > 4 &&
currentPage < totalPages - 1 && (
<PaginationButton
content={currentPage - 1}
onClick={() => pageChangeHandler(currentPage - 1)}
/>
)}
{totalPages >= 7 &&
currentPage > 3 &&
currentPage < totalPages - 2 && (
<PaginationButton content={currentPage} disabled />
)}
{totalPages >= 5 &&
currentPage > 2 &&
currentPage < totalPages - 2 && (
<PaginationButton
content={currentPage + 1}
onClick={() => pageChangeHandler(currentPage + 1)}
/>
)}
{totalPages >= 5 &&
(currentPage <= 2 || currentPage >= totalPages - 2) && (
<PaginationButton
content={totalPages - 2}
disabled={currentPage === totalPages - 2}
onClick={() => pageChangeHandler(totalPages - 2)}
/>
)}
{totalPages >= 6 &&
currentPage > 2 &&
currentPage < totalPages - 3 && (
<EtcPaginationButton
startPage={
currentPage <= 3
? currentPage + 2
: currentPage >= 4
? currentPage + 2
: 1
}
endPage={
currentPage <= 3
? totalPages - 2
: currentPage >= 4
? totalPages - 1
: 0
}
onPageItemClick={pageChangeHandler}
/>
)}
{totalPages >= 6 &&
(currentPage <= 3 || currentPage >= totalPages - 3) && (
<PaginationButton
content={totalPages - 1}
disabled={currentPage === totalPages - 1}
onClick={() => pageChangeHandler(totalPages - 1)}
/>
)}
{totalPages >= 7 && (
<PaginationButton
content={totalPages}
disabled={currentPage === totalPages}
onClick={() => pageChangeHandler(totalPages)}
/>
)}
</>
)}
>
Next{' '}
<Icon
icon='uil:arrow-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</button>
<div className='hidden @md:block'>
<NextPageButton />
</div>
<div className='hidden @md:block'>
<GoToLastPageButton />
</div>
</div>
<div className='hidden @md:block'>
<PageInfo />
</div>
</div>
<div className='flex gap-2 mt-2 sm:hidden'>
<button
disabled={currentPage === 1}
onClick={onPrevPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='uil:arrow-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>{' '}
Previous
</button>
<div className='flex @md:hidden flex-col justify-center items-end gap-2'>
<div className='flex flex-row items-center gap-0.5'>
<GoToFirstPageButton />
<PrevPageButton />
<NextPageButton />
<GoToLastPageButton />
</div>
<button
disabled={currentPage === totalPages}
onClick={onNextPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
Next{' '}
<Icon
icon='uil:arrow-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</button>
<div className='flex flex-row items-center gap-4'>
<DisplayedRowCountSelect />
<PageInfo />
</div>
</div>
</div>
);
+139 -99
View File
@@ -14,6 +14,7 @@ import {
SortingState,
OnChangeFn,
Row,
HeaderContext,
} from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react';
@@ -41,6 +42,7 @@ export interface TableProps<TData extends object> {
data: TData[];
columns: ColumnDef<TData, unknown>[];
pageSize?: number;
onPageSizeChange?: (pageSize: number) => void;
totalItems?: number;
page?: number;
onPageChange?: (page: number) => void;
@@ -56,8 +58,8 @@ export interface TableProps<TData extends object> {
setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
renderFooter?: boolean;
footerContent?: ReactNode;
footerData?: TData[];
withCheckbox?: boolean;
rowOptions?: number[];
}
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -70,31 +72,35 @@ const emptyContentDefaultValue = (
</div>
);
const TABLE_DEFAULT_STYLING = {
containerClassName: 'w-full mb-20',
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: 'px-4 py-3 text-base-content/50',
tableBodyClassName: '',
bodyRowClassName: 'border-t border-t-base-content/10',
bodyColumnClassName: 'px-4 py-3 text-base-content',
paginationClassName: '',
tableFooterClassName: 'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
};
const Table = <TData extends object>({
data = [],
columns = [],
pageSize = 10,
onPageSizeChange,
totalItems,
page,
onPageChange,
isLoading = false,
fuzzySearchValue,
onFuzzySearchValueChange,
className = {
containerClassName: '',
tableWrapperClassName: '',
tableClassName: '',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: '',
tableBodyClassName: '',
bodyRowClassName: '',
bodyColumnClassName: '',
tableFooterClassName: '',
footerRowClassName: '',
footerColumnClassName: '',
paginationClassName: '',
},
className = TABLE_DEFAULT_STYLING,
emptyContent = emptyContentDefaultValue,
sorting,
setSorting,
@@ -103,14 +109,19 @@ const Table = <TData extends object>({
setRowSelection,
enableRowSelection,
renderFooter = false,
footerContent,
footerData = [],
withCheckbox = false,
rowOptions = [10, 20, 50, 100],
}: TableProps<TData>) => {
const isServerSideTable =
totalItems !== undefined &&
page !== undefined &&
onPageChange !== undefined;
const tableClassNames = {
...TABLE_DEFAULT_STYLING,
...className,
};
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: pageSize,
@@ -172,14 +183,6 @@ const Table = <TData extends object>({
const table = useReactTable(tableOptions);
const { setPageSize } = table;
const footerTableOptions: TableOptions<TData> = {
columns,
data: footerData,
getCoreRowModel: getCoreRowModel(),
};
const footerTable = useReactTable(footerTableOptions);
const prevPageClickHandler = () => {
table.previousPage();
@@ -211,68 +214,104 @@ const Table = <TData extends object>({
}, [pageSize, setPageSize]);
return (
<div className={className.containerClassName}>
<div className={className.tableWrapperClassName}>
<table className={className.tableClassName}>
<thead className={className.tableHeaderClassName}>
<div className={tableClassNames.containerClassName}>
<div className={tableClassNames.tableWrapperClassName}>
<table className={tableClassNames.tableClassName}>
<thead className={tableClassNames.tableHeaderClassName}>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className={className.headerRowClassName}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
className.headerColumnClassName
)}
>
<div className='flex items-center gap-1'>
{flexRender(
header.column.columnDef.header,
header.getContext()
<tr
key={headerGroup.id}
className={tableClassNames.headerRowClassName}
>
{headerGroup.headers.map((header) => {
const columnRelativeDepth =
header.depth - header.column.depth;
if (
!header.isPlaceholder &&
columnRelativeDepth > 1 &&
header.id === header.column.id
) {
return null;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan = leafs[leafs.length - 1].depth - header.depth;
}
return (
<th
key={header.id}
colSpan={header.colSpan}
rowSpan={rowSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
{
'first:w-9 first:pr-0': withCheckbox,
},
tableClassNames.headerColumnClassName
)}
>
<div
className={cn('flex items-center gap-1', {
'justify-center relative after:content-[""] after:absolute after:left-0 after:right-0 after:bottom-0 after:h-px after:bg-base-content/10 after:translate-y-4':
header.colSpan > 1,
})}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<div className='flex items-center'>
<Icon
icon='lucide:arrow-up'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='lucide:arrow-down'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
))}
{header.column.getCanSort() && (
<div className='w-4 h-4 relative flex flex-col items-center'>
<Icon
icon='heroicons:chevron-up-16-solid'
width={18}
height={18}
className={cn(
'absolute -top-1',
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='heroicons:chevron-down-16-solid'
width={18}
height={18}
className={cn(
'absolute -bottom-1.5',
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
);
})}
</tr>
))}
</thead>
<tbody className={className.tableBodyClassName}>
<tbody className={tableClassNames.tableBodyClassName}>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className={className.bodyRowClassName}>
<tr key={row.id} className={tableClassNames.bodyRowClassName}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className={className.bodyColumnClassName}>
<td
key={cell.id}
className={cn(
{ 'first:w-9 first:pr-0': withCheckbox },
tableClassNames.bodyColumnClassName
)}
>
{!isLoading &&
flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -283,24 +322,23 @@ const Table = <TData extends object>({
))}
</tbody>
<tfoot className={cn(className.tableFooterClassName)}>
{renderFooter &&
(footerData && footerData.length > 0
? footerTable.getRowModel().rows.map((row) => (
<tr key={row.id} className={className.footerRowClassName}>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className={className.footerColumnClassName}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))
: footerContent)}
{renderFooter && (
<tr className={className.footerRowClassName}>
{table.getAllLeafColumns().map((column) => (
<td
key={column.id}
className={className.footerColumnClassName}
>
{column.columnDef.footer &&
flexRender(column.columnDef.footer, {
column,
header: column.columnDef,
table,
} as HeaderContext<TData, unknown>)}
</td>
))}
</tr>
)}
</tfoot>
</table>
</div>
@@ -310,7 +348,7 @@ const Table = <TData extends object>({
emptyContent}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
<div className={cn('mt-5', className.paginationClassName)}>
<div className={cn('mt-5', tableClassNames.paginationClassName)}>
<Pagination
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
itemsPerPage={table.getState().pagination.pageSize}
@@ -322,6 +360,8 @@ const Table = <TData extends object>({
onPrevPage={prevPageClickHandler}
onNextPage={nextPageClickHandler}
onPageChange={pageChangeHandler}
rowOptions={rowOptions}
onRowChange={onPageSizeChange}
/>
</div>
)}
@@ -164,200 +164,144 @@ const PurchasesPerSupplierTab = () => {
// TODO START
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tableColumns: ColumnDef<any>[] = [
{
header: 'No',
accessorKey: 'no',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) {
return (
<div className='font-semibold text-gray-900'>
{props.row.original.no}
</div>
);
}
return props.row.index + 1;
const getTableColumns = (totals: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tableColumns: ColumnDef<any>[] = [
{
id: 'no',
header: 'No',
cell: (props) => props.row.index + 1,
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
},
},
{
header: 'Tanggal Terima',
accessorKey: 'received_date',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) {
return (
<div className='font-semibold text-gray-900'>
{props.row.original.received_date}
</div>
);
}
return formatDate(props.row.original.received_date, 'DD MMM YYYY');
{
id: 'received_date',
header: 'Tanggal Terima',
accessorKey: 'received_date',
cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'),
},
},
{
header: 'Tanggal PO',
accessorKey: 'po_date',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
return formatDate(props.row.original.po_date, 'DD MMM YYYY');
{
id: 'po_date',
header: 'Tanggal PO',
accessorKey: 'po_date',
cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'),
},
},
{
header: 'No. Referensi',
accessorKey: 'po_number',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
return props.row.original.po_number;
{
id: 'po_number',
header: 'No. Referensi',
accessorKey: 'po_number',
cell: (props) => props.getValue() || '-',
},
},
{
header: 'Nama Produk',
accessorKey: 'product_name',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
return props.row.original.product_name;
{
id: 'product_name',
header: 'Nama Produk',
accessorKey: 'product_name',
cell: (props) => props.getValue() || '-',
},
},
{
header: 'Tujuan',
accessorKey: 'destination_warehouse',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
return props.row.original.destination_warehouse;
{
id: 'destination_warehouse',
header: 'Tujuan',
accessorKey: 'destination_warehouse',
cell: (props) => props.getValue() || '-',
},
},
{
header: 'QTY',
accessorKey: 'qty',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
}
>
{value.toLocaleString()}
{
id: 'qty',
header: 'QTY',
accessorKey: 'qty',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{value.toLocaleString()}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{totals.totalQty.toLocaleString()}
</div>
);
),
},
},
{
header: 'Harga Beli (Rp)',
accessorKey: 'price',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
if (isFooter) {
return (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(value)}
</div>
);
}
return (
<div className='text-right'>
{formatCurrency(props.row.original.price)}
{
id: 'price',
header: 'Harga Beli (Rp)',
accessorKey: 'price',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalPrice)}
</div>
);
),
},
},
{
header: 'Value Harga Beli (Rp)',
accessorKey: 'purchase_amount',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
}
>
{formatCurrency(value)}
{
id: 'purchase_amount',
header: 'Value Harga Beli (Rp)',
accessorKey: 'purchase_amount',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalPurchaseAmount)}
</div>
);
),
},
},
{
header: 'Transport (Rp)',
accessorKey: 'transport',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
if (isFooter) {
return (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(value)}
</div>
);
}
return (
<div className='text-right'>
{formatCurrency(props.row.original.transport)}
{
id: 'transport',
header: 'Transport (Rp)',
accessorKey: 'transport',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalTransport)}
</div>
);
),
},
},
{
header: 'Value Transport (Rp)',
accessorKey: 'value_transport',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
}
>
{formatCurrency(value)}
{
id: 'value_transport',
header: 'Value Transport (Rp)',
accessorKey: 'value_transport',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalValueTransport)}
</div>
);
),
},
},
{
header: 'Jumlah (Rp)',
accessorKey: 'total',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
}
>
{formatCurrency(value)}
{
id: 'total',
header: 'Jumlah (Rp)',
accessorKey: 'total',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalJumlah)}
</div>
);
),
},
},
{
header: 'Ekspedisi',
accessorKey: 'expedition_vendor_name',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
return props.row.original.expedition_vendor_name;
{
id: 'expedition_vendor_name',
header: 'Ekspedisi',
accessorKey: 'expedition_vendor_name',
cell: (props) => props.getValue() || '-',
},
},
{
header: 'Surat Jalan',
accessorKey: 'travel_number',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
return props.row.original.travel_number;
{
id: 'travel_number',
header: 'Surat Jalan',
accessorKey: 'travel_number',
cell: (props) => props.getValue() || '-',
},
},
];
];
return tableColumns;
};
return (
<>
@@ -451,31 +395,8 @@ const PurchasesPerSupplierTab = () => {
totalJumlah,
};
const footerData =
supplier.items.length > 0
? [
{
id: -999,
no: 'Total',
received_date: '',
po_date: null,
po_number: null,
product_name: null,
destination_warehouse: null,
qty: totals.totalQty,
price: totals.totalPrice,
purchase_amount: totals.totalPurchaseAmount,
transport: totals.totalTransport,
value_transport: totals.totalValueTransport,
total: totals.totalJumlah,
expedition_vendor_name: null,
travel_number: null,
_isFooter: true,
},
]
: [];
const totalPurchase = totals.totalJumlah;
const tableColumns = getTableColumns(totals);
return (
<Card
@@ -489,7 +410,6 @@ const PurchasesPerSupplierTab = () => {
data={supplier.items}
columns={tableColumns}
pageSize={10}
footerData={footerData}
renderFooter={supplier.items.length > 0}
className={{
tableWrapperClassName: 'overflow-x-auto mt-4',