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