feat(FE-64): refactor MovementTable with new TableToolbar and TableRowSizeSelector components

This commit is contained in:
rstubryan
2025-10-14 09:26:21 +07:00
parent a4ff4f7b2a
commit 44e07ddc50
4 changed files with 249 additions and 208 deletions
@@ -1,23 +1,22 @@
'use client';
import { useState, useMemo } from 'react';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { cn } from '@/lib/helper';
import { ROWS_OPTIONS } from '@/config/constant';
import { Movement } from '@/types/api/inventory/movement';
import { BaseMetadata } from '@/types/api/api-general';
import { TableToolbar } from '@/components/table/TableToolbar';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { TableRowOptions } from '@/components/table/TableRowOptions';
import { OptionType } from '@/components/input/SelectInput';
import Button from '@/components/Button';
import { cn } from '@/lib/helper';
// Dummy data
const baseMetadata: BaseMetadata = {
const baseMetadata = {
created_user: {
id: 1,
id_user: 1,
@@ -339,77 +338,64 @@ const dummyMovements: Movement[] = [
},
];
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Movement, unknown>;
deleteClickHandler: () => void;
}) => (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<Button
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/inventory/movement/detail/edit/?movementId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</div>
);
const MovementTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedMovement, setSelectedMovement] = useState<
Movement | undefined
>(undefined);
const [, setSelectedMovement] = useState<Movement | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const deleteModal = useModal();
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setPage(1);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
setTimeout(() => {
setIsDeleteLoading(false);
deleteModal.closeModal();
}, 1000);
};
const paginatedData = useMemo(() => {
const start = (page - 1) * pageSize;
return dummyMovements.slice(start, start + pageSize);
}, [page, pageSize]);
const movementsColumns: ColumnDef<Movement>[] = [
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: '/inventory/movement/add',
label: 'Tambah Movement',
}}
search={{
value: search,
onChange: searchChangeHandler,
placeholder: 'Cari Movement',
}}
/>
<TableRowSizeSelector
value={pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
</div>
<Table
data={paginatedData}
columns={[
{
header: '#',
cell: (props) => pageSize * (page - 1) + props.row.index + 1,
@@ -427,7 +413,8 @@ const MovementTable = () => {
{
accessorKey: 'product',
header: 'Nama Produk',
cell: (props) => props.row.original.product.map((p) => p.product.name),
cell: (props) =>
props.row.original.product.map((p) => p.product.name),
},
{
accessorKey: 'alasan_transfer',
@@ -440,103 +427,25 @@ const MovementTable = () => {
props.row.original.ekspedisi.map((e) => e.biaya_ekspedisi),
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
id: 'actions',
cell: (props) => (
<div className='dropdown dropdown-end'>
<Button tabIndex={0} variant='ghost' className='px-2'>
<Icon icon='carbon:overflow-menu-vertical' />
</Button>
<TableRowOptions
type='dropdown'
recordId={props.row.original.id}
basePath='/inventory/movement'
onDelete={() => {
setSelectedMovement(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const deleteModal = useModal();
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
setTimeout(() => {
setIsDeleteLoading(false);
deleteModal.closeModal();
}, 1000);
};
const searchChangeHandler: React.ChangeEventHandler<HTMLInputElement> = (
e
) => {
setSearch(e.target.value);
setPage(1);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
};
return (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='flex flex-row'>
<Button href='/inventory/movement/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Movement
</Button>
</div>
<DebouncedTextInput
name='search'
placeholder='Cari Movement'
value={search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<div className='flex flex-row justify-end'>
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(pageSize),
value: pageSize,
}}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/>
</div>
</div>
<Table<Movement>
data={paginatedData}
columns={movementsColumns}
),
},
]}
pageSize={pageSize}
page={page}
totalItems={dummyMovements.length}
@@ -558,11 +467,11 @@ const MovementTable = () => {
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Movement ini (ID: ${selectedMovement?.id})?`}
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -573,7 +482,7 @@ const MovementTable = () => {
onClick: confirmationModalDeleteClickHandler,
}}
/>
</>
</div>
);
};
+62
View File
@@ -0,0 +1,62 @@
import { Icon } from '@iconify/react';
import Button from '../Button';
import { cn } from '@/lib/helper';
interface TableRowOptionsProps {
type?: 'dropdown' | 'collapse';
recordId: string | number;
basePath: string;
onDelete?: () => void;
}
export const TableRowOptions = ({
type = 'dropdown',
recordId,
basePath,
onDelete,
}: TableRowOptionsProps) => (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<Button
href={`${basePath}/detail/?id=${recordId}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`${basePath}/detail/edit/?id=${recordId}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
{onDelete && (
<Button
onClick={onDelete}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
/>
Delete
</Button>
)}
</div>
);
@@ -0,0 +1,33 @@
import SelectInput from '../input/SelectInput';
export interface OptionType {
label: string;
value: string | number;
}
interface TableRowSizeSelectorProps {
value: number;
onChange: (val: OptionType | OptionType[] | null) => void;
options: OptionType[];
}
export const TableRowSizeSelector = ({
value,
onChange,
options,
}: TableRowSizeSelectorProps) => {
return (
<div className='flex flex-row justify-end'>
<SelectInput
label='Baris'
options={options}
value={{
label: String(value),
value: value,
}}
onChange={onChange}
className={{ wrapper: 'max-w-28' }}
/>
</div>
);
};
+37
View File
@@ -0,0 +1,37 @@
import { Icon } from '@iconify/react';
import Button from '../Button';
import DebouncedTextInput from '../input/DebouncedTextInput';
interface TableToolbarProps {
addButton?: {
href: string;
label: string;
};
search: {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
};
}
export const TableToolbar = ({ addButton, search }: TableToolbarProps) => {
return (
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
{addButton && (
<div className='flex flex-row'>
<Button href={addButton.href} color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
{addButton.label}
</Button>
</div>
)}
<DebouncedTextInput
name='search'
placeholder={search.placeholder || 'Cari...'}
value={search.value}
onChange={search.onChange}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
);
};