mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
refactor(FE-361): Refactor table and pagination components
This commit is contained in:
+167
-77
@@ -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
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user