refactor(FE): Refactor master-data pages to simplify component rendering

This commit is contained in:
rstubryan
2026-03-02 11:17:54 +07:00
parent 7db6ae4077
commit aadf10b8b9
25 changed files with 2118 additions and 2269 deletions
+1 -5
View File
@@ -1,11 +1,7 @@
import AreasTable from '@/components/pages/master-data/area/AreasTable'; import AreasTable from '@/components/pages/master-data/area/AreasTable';
const Nonstock = () => { const Nonstock = () => {
return ( return <AreasTable />;
<section className='w-full p-4'>
<AreasTable />
</section>
);
}; };
export default Nonstock; export default Nonstock;
+1 -5
View File
@@ -1,11 +1,7 @@
import BanksTable from '@/components/pages/master-data/bank/BanksTable'; import BanksTable from '@/components/pages/master-data/bank/BanksTable';
const Bank = () => { const Bank = () => {
return ( return <BanksTable />;
<section className='w-full p-4'>
<BanksTable />
</section>
);
}; };
export default Bank; export default Bank;
+1 -5
View File
@@ -1,11 +1,7 @@
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable'; import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
const Customer = () => { const Customer = () => {
return ( return <CustomersTable />;
<section className='w-full p-4'>
<CustomersTable />
</section>
);
}; };
export default Customer; export default Customer;
+1 -5
View File
@@ -1,11 +1,7 @@
import FlockTable from '@/components/pages/master-data/flock/FlocksTable'; import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
const Flock = () => { const Flock = () => {
return ( return <FlockTable />;
<section className='w-full p-4'>
<FlockTable />
</section>
);
}; };
export default Flock; export default Flock;
+1 -5
View File
@@ -1,11 +1,7 @@
import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable'; import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable';
const Nonstock = () => { const Nonstock = () => {
return ( return <KandangsTable />;
<section className='w-full p-4'>
<KandangsTable />
</section>
);
}; };
export default Nonstock; export default Nonstock;
+1 -5
View File
@@ -1,11 +1,7 @@
import LocationsTable from '@/components/pages/master-data/location/LocationsTable'; import LocationsTable from '@/components/pages/master-data/location/LocationsTable';
const Nonstock = () => { const Nonstock = () => {
return ( return <LocationsTable />;
<section className='w-full p-4'>
<LocationsTable />
</section>
);
}; };
export default Nonstock; export default Nonstock;
+1 -5
View File
@@ -1,11 +1,7 @@
import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable'; import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable';
const Nonstock = () => { const Nonstock = () => {
return ( return <NonstocksTable />;
<section className='w-full p-4'>
<NonstocksTable />
</section>
);
}; };
export default Nonstock; export default Nonstock;
@@ -1,11 +1,7 @@
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable'; import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
const ProductCategory = () => { const ProductCategory = () => {
return ( return <ProductCategoryTable />;
<section className='w-full p-4'>
<ProductCategoryTable />
</section>
);
}; };
export default ProductCategory; export default ProductCategory;
@@ -1,11 +1,7 @@
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable'; import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
const ProductionStandardPage = () => { const ProductionStandardPage = () => {
return ( return <ProductionStandardTable />;
<div className='w-full'>
<ProductionStandardTable />
</div>
);
}; };
export default ProductionStandardPage; export default ProductionStandardPage;
+1 -5
View File
@@ -1,11 +1,7 @@
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable'; import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
const Supplier = () => { const Supplier = () => {
return ( return <SuppliersTable />;
<section className='w-full p-4'>
<SuppliersTable />
</section>
);
}; };
export default Supplier; export default Supplier;
+1 -5
View File
@@ -1,11 +1,7 @@
import UomsTable from '@/components/pages/master-data/uom/UomsTable'; import UomsTable from '@/components/pages/master-data/uom/UomsTable';
const Nonstock = () => { const Nonstock = () => {
return ( return <UomsTable />;
<section className='w-full p-4'>
<UomsTable />
</section>
);
}; };
export default Nonstock; export default Nonstock;
+1 -5
View File
@@ -1,11 +1,7 @@
import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable'; import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable';
const Warehouse = () => { const Warehouse = () => {
return ( return <WarehousesTable />;
<section className='w-full p-4'>
<WarehousesTable />
</section>
);
}; };
export default Warehouse; export default Warehouse;
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -11,71 +11,92 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Area } from '@/types/api/master-data/area'; import { Area } from '@/types/api/master-data/area';
import { AreaApi } from '@/services/api/master-data'; import { AreaApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Area, unknown>; props: CellContext<Area, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `area#${props.row.original.id}`;
const popoverAnchorName = `--anchor-area#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.area.detail'> <RequirePermission permissions='lti.master.area.detail'>
<Button <Button
href={`/master-data/area/detail/?areaId=${props.row.original.id}`} href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.area.update'> <RequirePermission permissions='lti.master.area.update'>
<Button <Button
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`} href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.area.delete'> <RequirePermission permissions='lti.master.area.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='text-error hover:text-inherit' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -87,10 +108,17 @@ const AreasTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '' }, initial: {
paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: areas, data: areas,
isLoading, isLoading,
@@ -101,66 +129,13 @@ const AreasTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined); const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
const areasColumns: ColumnDef<Area>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
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 = () => {
setSelectedArea(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -179,96 +154,115 @@ const AreasTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const areasColumns: ColumnDef<Area>[] = useMemo(
updateFilter('search', e.target.value); () => [
{
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
header: 'Aksi',
cell: (props: CellContext<Area, unknown>) => {
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 = () => {
setSelectedArea(props.row.original);
deleteModal.openModal();
}; };
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { return (
const newVal = val as OptionType; <RowOptionsMenu
props={props}
setPageSize(newVal.value as number); popoverPosition={isLast2Rows ? 'top' : 'bottom'}
}; deleteClickHandler={deleteClickHandler}
/>
// track sorting );
useEffect(() => { },
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); },
],
if (!isNameSorted) { [tableFilterState.pageSize, tableFilterState.page, deleteModal]
updateFilter('nameSort', ''); );
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-full flex flex-row'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.area.create'> <RequirePermission permissions='lti.master.area.create'>
<Button <Button
href='/master-data/area/add' href='/master-data/area/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Area
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
</div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Area' placeholder='Cari Area'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Area> <Table<Area>
data={isResponseSuccess(areas) ? areas?.data : []} data={isResponseSuccess(areas) ? areas?.data : []}
columns={areasColumns} columns={areasColumns}
pageSize={tableFilterState.pageSize} pageSize={tableFilterState.pageSize}
page={isResponseSuccess(areas) ? areas?.meta?.page : 0} page={isResponseSuccess(areas) ? areas?.meta?.page : 0}
totalItems={isResponseSuccess(areas) ? areas?.meta?.total_results : 0} totalItems={
isResponseSuccess(areas) ? areas?.meta?.total_results : 0
}
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': isResponseSuccess(areas) && areas?.data?.length === 0, 'w-full': isResponseSuccess(areas) && areas?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -11,71 +11,92 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Bank } from '@/types/api/master-data/bank'; import { Bank } from '@/types/api/master-data/bank';
import { BankApi } from '@/services/api/master-data'; import { BankApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Bank, unknown>; props: CellContext<Bank, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `bank#${props.row.original.id}`;
const popoverAnchorName = `--anchor-bank#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.banks.detail'> <RequirePermission permissions='lti.master.banks.detail'>
<Button <Button
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`} href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.banks.update'> <RequirePermission permissions='lti.master.banks.update'>
<Button <Button
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`} href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.banks.delete'> <RequirePermission permissions='lti.master.banks.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -87,10 +108,17 @@ const BanksTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '' }, initial: {
paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: banks, data: banks,
isLoading, isLoading,
@@ -101,15 +129,35 @@ const BanksTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined); const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const banksColumns: ColumnDef<Bank>[] = [ const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteResponse = await BankApi.delete(selectedBank?.id as number);
if (isResponseError(deleteResponse)) {
toast.error(deleteResponse.message);
setIsDeleteLoading(false);
return;
}
refreshBanks();
deleteModal.closeModal();
toast.success('Successfully delete Bank!');
setIsDeleteLoading(false);
};
const banksColumns: ColumnDef<Bank>[] = useMemo(
() => [
{ {
header: '#', header: 'No',
cell: (props) => cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) + tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index + props.row.index +
@@ -130,12 +178,13 @@ const BanksTable = () => {
{ {
accessorKey: 'owner', accessorKey: 'owner',
header: 'Pemilik', header: 'Pemilik',
cell: (props) => (props.getValue() ? props.getValue() : '-'), cell: (props) => props.getValue() || '-',
}, },
{ {
header: 'Aksi', header: 'Aksi',
cell: (props) => { cell: (props: CellContext<Bank, unknown>) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length; const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows; const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex = const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1; currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
@@ -148,138 +197,85 @@ const BanksTable = () => {
}; };
return ( return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu <RowOptionsMenu
type='dropdown'
props={props} props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
/> />
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
); );
}, },
}, },
]; ],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
const confirmationModalDeleteClickHandler = async () => { );
setIsDeleteLoading(true);
const deleteResponse = await BankApi.delete(selectedBank?.id as number);
if (isResponseError(deleteResponse)) {
toast.error(deleteResponse.message);
setIsDeleteLoading(false);
return;
}
refreshBanks();
deleteModal.closeModal();
toast.success('Successfully delete Bank!');
setIsDeleteLoading(false);
};
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting]);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.banks.create'> <RequirePermission permissions='lti.master.banks.create'>
<Button <Button
href='/master-data/bank/add' href='/master-data/bank/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Bank
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Bank' placeholder='Cari Bank'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Bank> <Table<Bank>
data={isResponseSuccess(banks) ? banks?.data : []} data={isResponseSuccess(banks) ? banks?.data : []}
columns={banksColumns} columns={banksColumns}
pageSize={tableFilterState.pageSize} pageSize={tableFilterState.pageSize}
page={isResponseSuccess(banks) ? banks?.meta?.page : 0} page={isResponseSuccess(banks) ? banks?.meta?.page : 0}
totalItems={isResponseSuccess(banks) ? banks?.meta?.total_results : 0} totalItems={
isResponseSuccess(banks) ? banks?.meta?.total_results : 0
}
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': isResponseSuccess(banks) && banks?.data?.length === 0, 'w-full': isResponseSuccess(banks) && banks?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -1,46 +1,70 @@
'use client'; 'use client';
import Button from '@/components/Button'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { ROWS_OPTIONS } from '@/config/constant'; import PopoverButton from '@/components/popover/PopoverButton';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import PopoverContent from '@/components/popover/PopoverContent';
import { cn } from '@/lib/helper';
import { CustomerApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Customer } from '@/types/api/master-data/customer'; import { Customer } from '@/types/api/master-data/customer';
import { Icon } from '@iconify/react'; import { CustomerApi } from '@/services/api/master-data';
import { CellContext, ColumnDef } from '@tanstack/react-table'; import { cn } from '@/lib/helper';
import { useState } from 'react'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import toast from 'react-hot-toast'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import useSWR from 'swr';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Customer, unknown>; props: CellContext<Customer, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `customer#${props.row.original.id}`;
const popoverAnchorName = `--anchor-customer#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.customer.detail'> <RequirePermission permissions='lti.master.customer.detail'>
<Button <Button
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`} href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
@@ -48,30 +72,31 @@ const RowOptionsMenu = ({
<Button <Button
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`} href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.customer.delete'> <RequirePermission permissions='lti.master.customer.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -83,16 +108,17 @@ const CustomersTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '', picSort: '' }, initial: {
search: '',
},
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name',
picSort: 'sort_pic',
}, },
}); });
// Fetch Data const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: customers, data: customers,
isLoading, isLoading,
@@ -102,87 +128,16 @@ const CustomersTable = () => {
CustomerApi.getAllFetcher CustomerApi.getAllFetcher
); );
// State
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedCustomer, setSelectedCustomer] = useState< const [selectedCustomer, setSelectedCustomer] = useState<
Customer | undefined Customer | undefined
>(undefined); >(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// Columns Definition const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
const customersColumns: ColumnDef<Customer>[] = [ updateFilter('search', e.target.value);
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'pic',
header: 'PIC',
cell: (props) => props.row.original.pic.name,
},
{
accessorKey: 'type',
header: 'Type',
cell: (props) => props.row.original.type,
},
{
accessorKey: 'phone',
header: 'Phone',
},
{
accessorKey: 'email',
header: 'Email',
},
{
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 = () => {
setSelectedCustomer(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
// Handler
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -202,56 +157,110 @@ const CustomersTable = () => {
toast.success('Successfully delete Customer!'); toast.success('Successfully delete Customer!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value); const customersColumns: ColumnDef<Customer>[] = useMemo(
}; () => [
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { {
const newVal = val as OptionType; header: 'No',
setPageSize(newVal.value as number); cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorFn: (row) => row.pic?.name ?? '-',
header: 'PIC',
},
{
accessorKey: 'type',
header: 'Type',
},
{
accessorKey: 'phone',
header: 'Phone',
},
{
accessorKey: 'email',
header: 'Email',
},
{
header: 'Aksi',
cell: (props: CellContext<Customer, unknown>) => {
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 = () => {
setSelectedCustomer(props.row.original);
deleteModal.openModal();
}; };
return (
<RowOptionsMenu
props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.customer.create'> <RequirePermission permissions='lti.master.customer.create'>
<Button <Button
href='/master-data/customer/add' href='/master-data/customer/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Customer
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Kandang' placeholder='Cari Customer'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Customer> <Table<Customer>
data={isResponseSuccess(customers) ? customers?.data : []} data={isResponseSuccess(customers) ? customers?.data : []}
columns={customersColumns} columns={customersColumns}
@@ -261,23 +270,20 @@ const CustomersTable = () => {
isResponseSuccess(customers) ? customers?.meta?.total_results : 0 isResponseSuccess(customers) ? customers?.meta?.total_results : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': 'w-full':
isResponseSuccess(customers) && customers?.data?.length === 0, isResponseSuccess(customers) && customers?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -1,51 +1,70 @@
'use client'; 'use client';
import { CellContext, ColumnDef } from '@tanstack/react-table'; import { ChangeEventHandler, useMemo, useState } from 'react';
import { Flock } from '@/types/api/master-data/flock';
import { cn } from '@/lib/helper';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { FlockApi } from '@/services/api/master-data'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { useModal } from '@/components/Modal';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant';
import Table from '@/components/Table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
const RowsOptions = ({ import { Icon } from '@iconify/react';
type = 'dropdown', 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 RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Flock } from '@/types/api/master-data/flock';
import { FlockApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Flock, unknown>; props: CellContext<Flock, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `flock#${props.row.original.id}`;
const popoverAnchorName = `--anchor-flock#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.flocks.detail'> <RequirePermission permissions='lti.master.flocks.detail'>
<Button <Button
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`} href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon <Icon icon='heroicons:eye' width={20} height={20} />
icon='mdi:eye-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
@@ -53,35 +72,31 @@ const RowsOptions = ({
<Button <Button
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`} href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon <Icon icon='heroicons:pencil-square' width={20} height={20} />
icon='material-symbols:edit-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.flocks.delete'> <RequirePermission permissions='lti.master.flocks.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -93,15 +108,17 @@ const FlockTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '' }, initial: {
search: '',
},
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name',
}, },
}); });
// Fetch Data const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: flocks, data: flocks,
isLoading, isLoading,
@@ -111,74 +128,16 @@ const FlockTable = () => {
FlockApi.getAllFetcher FlockApi.getAllFetcher
); );
// State
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedFlock, setSelectedFlock] = useState<Flock | undefined>( const [selectedFlock, setSelectedFlock] = useState<Flock | undefined>(
undefined undefined
); );
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// Columns Definition const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
const flocksColumns: ColumnDef<Flock>[] = [ updateFilter('search', e.target.value);
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'created_at',
header: 'Dibuat pada',
cell: (props) =>
new Date(props.row.original.created_at).toLocaleDateString(),
},
{
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 = () => {
setSelectedFlock(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowsOptions
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowsOptions
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
// Handler
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -196,56 +155,100 @@ const FlockTable = () => {
toast.success('Successfully delete Flock!'); toast.success('Successfully delete Flock!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value); const flocksColumns: ColumnDef<Flock>[] = useMemo(
}; () => [
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { {
const newVal = val as OptionType; header: 'No',
setPageSize(newVal.value as number); cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'created_at',
header: 'Dibuat pada',
cell: (props) =>
new Date(props.row.original.created_at).toLocaleDateString('id-ID'),
},
{
header: 'Aksi',
cell: (props: CellContext<Flock, unknown>) => {
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 = () => {
setSelectedFlock(props.row.original);
deleteModal.openModal();
}; };
return (
<RowOptionsMenu
props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.flocks.create'> <RequirePermission permissions='lti.master.flocks.create'>
<Button <Button
href='/master-data/flock/add' href='/master-data/flock/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Flock
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Flock' placeholder='Cari Flock'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Flock> <Table<Flock>
data={isResponseSuccess(flocks) ? flocks?.data : []} data={isResponseSuccess(flocks) ? flocks?.data : []}
columns={flocksColumns} columns={flocksColumns}
@@ -255,26 +258,25 @@ const FlockTable = () => {
isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0 isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': isResponseSuccess(flocks) && flocks?.data?.length === 0, 'w-full':
isResponseSuccess(flocks) && flocks?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin menghapus data Supplier ini (${selectedFlock?.name})?`} text={`Apakah anda yakin ingin menghapus data Flock ini (${selectedFlock?.name})?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -1,13 +1,8 @@
'use client'; 'use client';
import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
CellContext,
ColumnDef,
ColumnSort,
SortingState,
} from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -16,71 +11,92 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { KandangApi } from '@/services/api/master-data'; import { KandangApi } from '@/services/api/master-data';
import { cn, formatNumber } from '@/lib/helper'; import { cn, formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Kandang, unknown>; props: CellContext<Kandang, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `kandang#${props.row.original.id}`;
const popoverAnchorName = `--anchor-kandang#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.kandangs.detail'> <RequirePermission permissions='lti.master.kandangs.detail'>
<Button <Button
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`} href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.kandangs.update'> <RequirePermission permissions='lti.master.kandangs.update'>
<Button <Button
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`} href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.kandangs.delete'> <RequirePermission permissions='lti.master.kandangs.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -94,21 +110,15 @@ const KandangsTable = () => {
} = useTableFilter({ } = useTableFilter({
initial: { initial: {
search: '', search: '',
nameSort: '',
locationSort: '',
capacitySort: '',
picSort: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name',
locationSort: 'sort_location',
capacitySort: 'sort_capacity',
picSort: ' sort_pic',
}, },
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: kandangs, data: kandangs,
isLoading, isLoading,
@@ -119,83 +129,15 @@ const KandangsTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedKandang, setSelectedKandang] = useState<Kandang | undefined>( const [selectedKandang, setSelectedKandang] = useState<Kandang | undefined>(
undefined undefined
); );
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
const kandangsColumns: ColumnDef<Kandang>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'location',
header: 'Lokasi',
cell: (props) => props.row.original.location.name,
},
{
accessorKey: 'capacity',
header: 'Kapasitas',
cell: (props) => formatNumber(props.row.original.capacity ?? 0),
},
{
accessorKey: 'pic',
header: 'PIC',
cell: (props) => props.row.original.pic.name,
},
{
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 = () => {
setSelectedKandang(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -216,87 +158,106 @@ const KandangsTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const kandangsColumns: ColumnDef<Kandang>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { cell: (props) =>
const newVal = val as OptionType; tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
setPageSize(newVal.value as number); 1,
};
const updateSortingFilter = useCallback(
(
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>,
sortFilter: ColumnSort | undefined
) => {
if (!sortFilter) {
updateFilter(sortName, '');
} else {
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
}
}, },
[updateFilter] {
); accessorKey: 'name',
header: 'Nama',
},
{
accessorFn: (row) => row.location?.name ?? '-',
header: 'Lokasi',
},
{
accessorKey: 'capacity',
header: 'Kapasitas',
cell: (props) => formatNumber(props.row.original.capacity ?? 0),
},
{
accessorFn: (row) => row.pic?.name ?? '-',
header: 'PIC',
},
{
header: 'Aksi',
cell: (props: CellContext<Kandang, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
// track sorting const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
useEffect(() => {
const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name');
const locationSortFilter = sorting.find(
(sortItem) => sortItem.id === 'location'
);
const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic');
updateSortingFilter('nameSort', nameSortFilter); const deleteClickHandler = () => {
updateSortingFilter('locationSort', locationSortFilter); setSelectedKandang(props.row.original);
updateSortingFilter('picSort', picSortFilter); deleteModal.openModal();
}, [sorting, updateSortingFilter]); };
return (
<RowOptionsMenu
props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-full flex flex-row'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.kandangs.create'> <RequirePermission permissions='lti.master.kandangs.create'>
<Button <Button
href='/master-data/kandang/add' href='/master-data/kandang/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Kandang
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
</div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Kandang' placeholder='Cari Kandang'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Kandang> <Table<Kandang>
data={isResponseSuccess(kandangs) ? kandangs?.data : []} data={isResponseSuccess(kandangs) ? kandangs?.data : []}
columns={kandangsColumns} columns={kandangsColumns}
@@ -306,25 +267,20 @@ const KandangsTable = () => {
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0 isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': 'w-full':
isResponseSuccess(kandangs) && kandangs?.data?.length === 0, isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -1,13 +1,8 @@
'use client'; 'use client';
import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
CellContext,
ColumnDef,
ColumnSort,
SortingState,
} from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -16,71 +11,92 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Location } from '@/types/api/master-data/location'; import { Location } from '@/types/api/master-data/location';
import { LocationApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Location, unknown>; props: CellContext<Location, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `location#${props.row.original.id}`;
const popoverAnchorName = `--anchor-location#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.locations.detail'> <RequirePermission permissions='lti.master.locations.detail'>
<Button <Button
href={`/master-data/location/detail/?locationId=${props.row.original.id}`} href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.locations.update'> <RequirePermission permissions='lti.master.locations.update'>
<Button <Button
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`} href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.locations.delete'> <RequirePermission permissions='lti.master.locations.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -92,16 +108,17 @@ const LocationsTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '', addressSort: '', areaSort: '' }, initial: {
search: '',
},
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name',
addressSort: 'sort_address',
areaSort: ' sort_area',
}, },
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: locations, data: locations,
isLoading, isLoading,
@@ -112,77 +129,15 @@ const LocationsTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedLocation, setSelectedLocation] = useState< const [selectedLocation, setSelectedLocation] = useState<
Location | undefined Location | undefined
>(undefined); >(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
const locationsColumns: ColumnDef<Location>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'address',
header: 'Alamat',
},
{
accessorKey: 'area',
header: 'Area',
cell: (props) => props.row.original.area.name,
},
{
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 = () => {
setSelectedLocation(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -203,87 +158,101 @@ const LocationsTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const locationsColumns: ColumnDef<Location>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { cell: (props) =>
const newVal = val as OptionType; tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
setPageSize(newVal.value as number); 1,
};
const updateSortingFilter = useCallback(
(
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>,
sortFilter: ColumnSort | undefined
) => {
if (!sortFilter) {
updateFilter(sortName, '');
} else {
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
}
}, },
[updateFilter] {
); accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'address',
header: 'Alamat',
},
{
accessorFn: (row) => row.area?.name ?? '-',
header: 'Area',
},
{
header: 'Aksi',
cell: (props: CellContext<Location, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
// track sorting const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
useEffect(() => {
const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name');
const addressSortFilter = sorting.find(
(sortItem) => sortItem.id === 'address'
);
const areaSortFilter = sorting.find((sortItem) => sortItem.id === 'area');
updateSortingFilter('nameSort', nameSortFilter); const deleteClickHandler = () => {
updateSortingFilter('addressSort', addressSortFilter); setSelectedLocation(props.row.original);
updateSortingFilter('areaSort', areaSortFilter); deleteModal.openModal();
}, [sorting, updateSortingFilter]); };
return (
<RowOptionsMenu
props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-full flex flex-row'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.locations.create'> <RequirePermission permissions='lti.master.locations.create'>
<Button <Button
href='/master-data/location/add' href='/master-data/location/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Location
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
</div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Location' placeholder='Cari Location'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Location> <Table<Location>
data={isResponseSuccess(locations) ? locations?.data : []} data={isResponseSuccess(locations) ? locations?.data : []}
columns={locationsColumns} columns={locationsColumns}
@@ -293,25 +262,20 @@ const LocationsTable = () => {
isResponseSuccess(locations) ? locations?.meta?.total_results : 0 isResponseSuccess(locations) ? locations?.meta?.total_results : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': 'w-full':
isResponseSuccess(locations) && locations?.data?.length === 0, isResponseSuccess(locations) && locations?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -227,7 +227,7 @@ const NonstocksTable = () => {
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='heroicons:plus' width={20} height={20} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Nonstock Add Nonstock
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
@@ -1,6 +1,12 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'; import {
ChangeEventHandler,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -11,11 +17,9 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data';
@@ -23,60 +27,83 @@ import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<ProductCategory, unknown>; props: CellContext<ProductCategory, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `product-category#${props.row.original.id}`;
const popoverAnchorName = `--anchor-product-category#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.product_categories.detail'> <RequirePermission permissions='lti.master.product_categories.detail'>
<Button <Button
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`} href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.product_categories.update'> <RequirePermission permissions='lti.master.product_categories.update'>
<Button <Button
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`} href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.product_categories.delete'> <RequirePermission permissions='lti.master.product_categories.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='mdi:delete-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -91,10 +118,17 @@ const ProductCategoryTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: searchValue, nameSort: '' }, initial: {
paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, search: searchValue,
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: productCategories, data: productCategories,
isLoading, isLoading,
@@ -105,72 +139,16 @@ const ProductCategoryTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedProductCategory, setSelectedProductCategory] = useState< const [selectedProductCategory, setSelectedProductCategory] = useState<
ProductCategory | undefined ProductCategory | undefined
>(undefined); >(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
const productCategoryColumns: ColumnDef<ProductCategory>[] = [ updateFilter('search', e.target.value);
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'code',
header: 'Code',
},
{
accessorKey: 'name',
header: 'Nama',
},
{
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 = () => {
setSelectedProductCategory(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -191,15 +169,51 @@ const ProductCategoryTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const productCategoryColumns: ColumnDef<ProductCategory>[] = useMemo(
setSearchValue(e.target.value); () => [
updateFilter('search', e.target.value); {
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'code',
header: 'Code',
},
{
accessorKey: 'name',
header: 'Nama',
},
{
header: 'Aksi',
cell: (props: CellContext<ProductCategory, unknown>) => {
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 = () => {
setSelectedProductCategory(props.row.original);
deleteModal.openModal();
}; };
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { return (
const newVal = val as OptionType; <RowOptionsMenu
setPageSize(newVal.value as number); props={props}
}; popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
useEffect(() => { useEffect(() => {
// Store current path on mount // Store current path on mount
@@ -223,57 +237,56 @@ const ProductCategoryTable = () => {
}; };
}, [resetSearchValue]); }, [resetSearchValue]);
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.product_categories.create'> <RequirePermission permissions='lti.master.product_categories.create'>
<Button <Button
href='/master-data/product-category/add' href='/master-data/product-category/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Product Category
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Product Category' placeholder='Cari Product Category'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
<div className='flex flex-row justify-end'> className={{
<SelectInput wrapper: 'w-full min-w-24 max-w-3xs',
label='Baris' inputWrapper: 'rounded-xl! shadow-button-soft',
options={ROWS_OPTIONS} input:
value={{ 'placeholder:font-semibold placeholder:text-base-content/50',
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<ProductCategory> <Table<ProductCategory>
data={ data={
isResponseSuccess(productCategories) ? productCategories?.data : [] isResponseSuccess(productCategories)
? productCategories?.data
: []
} }
columns={productCategoryColumns} columns={productCategoryColumns}
pageSize={tableFilterState.pageSize} pageSize={tableFilterState.pageSize}
@@ -288,26 +301,22 @@ const ProductCategoryTable = () => {
: 0 : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': 'w-full':
isResponseSuccess(productCategories) && isResponseSuccess(productCategories) &&
productCategories?.data?.length === 0, productCategories?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
@@ -272,7 +272,7 @@ const ProductsTable = () => {
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='heroicons:plus' width={20} height={20} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Produk Add Product
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
@@ -1,92 +1,121 @@
'use client'; 'use client';
import Button from '@/components/Button'; import { useMemo, useState } from 'react';
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { ProductionStandard } from '@/types/api/master-data/production-standard';
import { Icon } from '@iconify/react';
import useSWR from 'swr'; import useSWR from 'swr';
import { ProductionStandardApi } from '@/services/api/master-data'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { CellContext } from '@tanstack/react-table';
import { useModal } from '@/components/Modal';
import { useState } from 'react';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { ProductionStandard } from '@/types/api/master-data/production-standard';
import { ProductionStandardApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<ProductionStandard, unknown>; props: CellContext<ProductionStandard, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `production-standard#${props.row.original.id}`;
const popoverAnchorName = `--anchor-production-standard#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.production_standards.detail'> <RequirePermission permissions='lti.master.production_standards.detail'>
<Button <Button
href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`} href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.production_standards.update'> <RequirePermission permissions='lti.master.production_standards.update'>
<Button <Button
href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`} href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.production_standards.delete'> <RequirePermission permissions='lti.master.production_standards.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='text-error hover:text-inherit' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
const ProductionStandardTable = () => { const ProductionStandardTable = () => {
const deleteModal = useModal(); const [sorting, setSorting] = useState<SortingState>([]);
const {
data: productionStandards,
isLoading,
mutate: refreshProductionStandards,
} = useSWR(
`${ProductionStandardApi.basePath}`,
ProductionStandardApi.getAllFetcher
);
const deleteModal = useModal();
const [selectedProductionStandard, setSelectedProductionStandard] = useState< const [selectedProductionStandard, setSelectedProductionStandard] = useState<
ProductionStandard | undefined ProductionStandard | undefined
>(undefined); >(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { data: productionStandards, mutate: refreshProductionStandards } =
useSWR(
`${ProductionStandardApi.basePath}`,
ProductionStandardApi.getAllFetcher
);
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -107,51 +136,30 @@ const ProductionStandardTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
return ( const productionStandardColumns: ColumnDef<ProductionStandard>[] = useMemo(
<> () => [
<div className='flex flex-col gap-6 p-6'>
<div className='flex flex-row gap-6 justify-start'>
<RequirePermission permissions='lti.master.production_standards.create'>
<Button
href='/master-data/production-standard/add'
variant='outline'
>
<Icon icon='mdi:plus' /> Tambah
</Button>
</RequirePermission>
</div>
<RequirePermission permissions='lti.master.production_standards.list'>
<Table<ProductionStandard>
data={
isResponseSuccess(productionStandards)
? productionStandards.data
: []
}
columns={[
{ {
header: 'No', header: 'No',
accessorFn: (row, index) => index + 1, cell: (props) => props.row.index + 1,
}, },
{ {
header: 'Nama',
accessorKey: 'name', accessorKey: 'name',
header: 'Nama',
}, },
{ {
accessorFn: (row) => row.project_category ?? '-',
header: 'Kategori', header: 'Kategori',
accessorFn: (row) => row.project_category,
}, },
{ {
header: 'Aksi', header: 'Aksi',
cell: (props) => { cell: (props: CellContext<ProductionStandard, unknown>) => {
const currentPageSize = const currentPageSize =
props.table.getPaginationRowModel().rows.length; props.table.getPaginationRowModel().rows.length;
const currentPageRows = const currentPageRows = props.table.getPaginationRowModel().flatRows;
props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex = const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1; currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => { const deleteClickHandler = () => {
setSelectedProductionStandard(props.row.original); setSelectedProductionStandard(props.row.original);
@@ -159,45 +167,62 @@ const ProductionStandardTable = () => {
}; };
return ( return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu <RowOptionsMenu
type='dropdown'
props={props} props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
/> />
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
); );
}, },
}, },
]} ],
className={{ [deleteModal]
headerColumnClassName: cn( );
TABLE_DEFAULT_STYLING.headerColumnClassName,
'last:flex last:flex-row last:justify-end' return (
), <>
bodyColumnClassName: cn( <div className='w-full'>
TABLE_DEFAULT_STYLING.bodyColumnClassName, {/* Header Section */}
'last:flex last:flex-row last:justify-end' <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
), {/* Action Buttons */}
}} <div className='w-fit flex flex-row gap-3 flex-wrap'>
/> <RequirePermission permissions='lti.master.production_standards.create'>
<Button
href='/master-data/production-standard/add'
color='primary'
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
>
<Icon icon='heroicons:plus' width={20} height={20} />
Add Standard Production
</Button>
</RequirePermission> </RequirePermission>
</div> </div>
<RequirePermission permissions='lti.master.production_standards.delete'> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<ProductionStandard>
data={
isResponseSuccess(productionStandards)
? productionStandards.data
: []
}
columns={productionStandardColumns}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn('p-3 mb-0', {
'w-full':
isResponseSuccess(productionStandards) &&
productionStandards?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap',
}}
/>
</div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
@@ -212,7 +237,6 @@ const ProductionStandardTable = () => {
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
/> />
</RequirePermission>
</> </>
); );
}; };
@@ -1,51 +1,70 @@
'use client'; 'use client';
import Button from '@/components/Button'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { ROWS_OPTIONS } from '@/config/constant'; import PopoverButton from '@/components/popover/PopoverButton';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import PopoverContent from '@/components/popover/PopoverContent';
import { cn } from '@/lib/helper';
import { SupplierApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Supplier } from '@/types/api/master-data/supplier';
import { Icon } from '@iconify/react';
import { CellContext, ColumnDef } from '@tanstack/react-table';
import { useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const RowOptions = ({ import { Supplier } from '@/types/api/master-data/supplier';
type = 'dropdown', import { SupplierApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Supplier, unknown>; props: CellContext<Supplier, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `supplier#${props.row.original.id}`;
const popoverAnchorName = `--anchor-supplier#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.suppliers.detail'> <RequirePermission permissions='lti.master.suppliers.detail'>
<Button <Button
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`} href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon <Icon icon='heroicons:eye' width={20} height={20} />
icon='mdi:eye-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
@@ -53,35 +72,31 @@ const RowOptions = ({
<Button <Button
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`} href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon <Icon icon='heroicons:pencil-square' width={20} height={20} />
icon='material-symbols:edit-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.suppliers.delete'> <RequirePermission permissions='lti.master.suppliers.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -93,15 +108,17 @@ const SuppliersTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '' }, initial: {
search: '',
},
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name',
}, },
}); });
// Fetch Data const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: suppliers, data: suppliers,
isLoading, isLoading,
@@ -111,17 +128,40 @@ const SuppliersTable = () => {
SupplierApi.getAllFetcher SupplierApi.getAllFetcher
); );
// State
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedSupplier, setSelectedSupplier] = useState< const [selectedSupplier, setSelectedSupplier] = useState<
Supplier | undefined Supplier | undefined
>(undefined); >(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// Columns Definition const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
const suppliersColumns: ColumnDef<Supplier>[] = [ updateFilter('search', e.target.value);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteResponse = await SupplierApi.delete(
selectedSupplier?.id as number
);
if (isResponseError(deleteResponse)) {
toast.error(deleteResponse.message);
setIsDeleteLoading(false);
return;
}
refreshSuppliers();
deleteModal.closeModal();
toast.success('Successfully delete Supplier!');
setIsDeleteLoading(false);
};
const suppliersColumns: ColumnDef<Supplier>[] = useMemo(
() => [
{ {
header: '#', header: 'No',
cell: (props) => cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) + tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index + props.row.index +
@@ -161,8 +201,9 @@ const SuppliersTable = () => {
}, },
{ {
header: 'Aksi', header: 'Aksi',
cell: (props) => { cell: (props: CellContext<Supplier, unknown>) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length; const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows; const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex = const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1; currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
@@ -175,102 +216,63 @@ const SuppliersTable = () => {
}; };
return ( return (
<> <RowOptionsMenu
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptions
type='dropdown'
props={props} props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
/> />
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptions
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
); );
}, },
}, },
]; ],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
// Handler
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteResponse = await SupplierApi.delete(
selectedSupplier?.id as number
); );
if (isResponseError(deleteResponse)) {
toast.error(deleteResponse.message);
setIsDeleteLoading(false);
return;
}
refreshSuppliers();
deleteModal.closeModal();
toast.success('Successfully delete Supplier!');
setIsDeleteLoading(false);
};
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.suppliers.create'> <RequirePermission permissions='lti.master.suppliers.create'>
<Button <Button
href='/master-data/supplier/add' href='/master-data/supplier/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Supplier
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Supplier' placeholder='Cari Supplier'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Supplier> <Table<Supplier>
data={isResponseSuccess(suppliers) ? suppliers?.data : []} data={isResponseSuccess(suppliers) ? suppliers?.data : []}
columns={suppliersColumns} columns={suppliersColumns}
@@ -280,23 +282,21 @@ const SuppliersTable = () => {
isResponseSuccess(suppliers) ? suppliers?.meta?.total_results : 0 isResponseSuccess(suppliers) ? suppliers?.meta?.total_results : 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': 'w-full':
isResponseSuccess(suppliers) && suppliers?.data?.length === 0, isResponseSuccess(suppliers) && suppliers?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
+127 -133
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -11,71 +11,92 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Uom } from '@/types/api/master-data/uom'; import { Uom } from '@/types/api/master-data/uom';
import { UomApi } from '@/services/api/master-data'; import { UomApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Uom, unknown>; props: CellContext<Uom, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `uom#${props.row.original.id}`;
const popoverAnchorName = `--anchor-uom#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.uoms.detail'> <RequirePermission permissions='lti.master.uoms.detail'>
<Button <Button
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`} href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.uoms.update'> <RequirePermission permissions='lti.master.uoms.update'>
<Button <Button
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`} href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.uoms.delete'> <RequirePermission permissions='lti.master.uoms.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -87,10 +108,17 @@ const UomsTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '', nameSort: '' }, initial: {
paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: uoms, data: uoms,
isLoading, isLoading,
@@ -101,66 +129,13 @@ const UomsTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedUom, setSelectedUom] = useState<Uom | undefined>(undefined); const [selectedUom, setSelectedUom] = useState<Uom | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
const uomsColumns: ColumnDef<Uom>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
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 = () => {
setSelectedUom(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -179,69 +154,93 @@ const UomsTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const uomsColumns: ColumnDef<Uom>[] = useMemo(
updateFilter('search', e.target.value); () => [
{
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
header: 'Aksi',
cell: (props: CellContext<Uom, unknown>) => {
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 = () => {
setSelectedUom(props.row.original);
deleteModal.openModal();
}; };
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { return (
const newVal = val as OptionType; <RowOptionsMenu
props={props}
setPageSize(newVal.value as number); popoverPosition={isLast2Rows ? 'top' : 'bottom'}
}; deleteClickHandler={deleteClickHandler}
/>
// track sorting );
useEffect(() => { },
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); },
],
if (!isNameSorted) { [tableFilterState.pageSize, tableFilterState.page, deleteModal]
updateFilter('nameSort', ''); );
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.uoms.create'> <RequirePermission permissions='lti.master.uoms.create'>
<Button <Button
href='/master-data/uom/add' href='/master-data/uom/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add UOM
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari UOM' placeholder='Cari UOM'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Uom> <Table<Uom>
data={isResponseSuccess(uoms) ? uoms?.data : []} data={isResponseSuccess(uoms) ? uoms?.data : []}
columns={uomsColumns} columns={uomsColumns}
@@ -249,24 +248,19 @@ const UomsTable = () => {
page={isResponseSuccess(uoms) ? uoms?.meta?.page : 0} page={isResponseSuccess(uoms) ? uoms?.meta?.page : 0}
totalItems={isResponseSuccess(uoms) ? uoms?.meta?.total_results : 0} totalItems={isResponseSuccess(uoms) ? uoms?.meta?.total_results : 0}
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': isResponseSuccess(uoms) && uoms?.data?.length === 0, 'w-full': isResponseSuccess(uoms) && uoms?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -1,13 +1,8 @@
'use client'; 'use client';
import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
CellContext,
ColumnDef,
ColumnSort,
SortingState,
} from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -16,71 +11,92 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { WarehouseApi } from '@/services/api/master-data'; import { WarehouseApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', popoverPosition = 'bottom',
props, props,
deleteClickHandler, deleteClickHandler,
}: { }: {
type: 'dropdown' | 'collapse'; popoverPosition: 'bottom' | 'top';
props: CellContext<Warehouse, unknown>; props: CellContext<Warehouse, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `warehouse#${props.row.original.id}`;
const popoverAnchorName = `--anchor-warehouse#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return ( return (
<RowOptionsMenuWrapper type={type}> <div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.master.warehouses.detail'> <RequirePermission permissions='lti.master.warehouses.detail'>
<Button <Button
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`} href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='heroicons:eye' width={20} height={20} />
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.warehouses.update'> <RequirePermission permissions='lti.master.warehouses.update'>
<Button <Button
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`} href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='none'
className='justify-start text-sm' className='p-3 justify-start text-sm font-semibold w-full'
onClick={closePopover}
> >
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.master.warehouses.delete'> <RequirePermission permissions='lti.master.warehouses.delete'>
<Button <Button
onClick={deleteClickHandler} onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost' variant='ghost'
color='error' color='none'
className='text-error hover:text-inherit' className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
> >
<Icon <Icon icon='heroicons:trash' width={20} height={20} />
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
</RowOptionsMenuWrapper> </div>
</PopoverContent>
</div>
); );
}; };
@@ -94,23 +110,15 @@ const WarehousesTable = () => {
} = useTableFilter({ } = useTableFilter({
initial: { initial: {
search: '', search: '',
nameSort: '',
typeSort: '',
areaSort: '',
locationSort: '',
kandangSort: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
nameSort: 'sort_name',
typeSort: 'sort_type',
areaSort: ' sort_area',
locationSort: ' sort_location',
kandangSort: ' sort_kandang',
}, },
}); });
const [sorting, setSorting] = useState<SortingState>([]);
const { const {
data: warehouses, data: warehouses,
isLoading, isLoading,
@@ -121,102 +129,15 @@ const WarehousesTable = () => {
); );
const deleteModal = useModal(); const deleteModal = useModal();
const [selectedWarehouse, setSelectedWarehouse] = useState< const [selectedWarehouse, setSelectedWarehouse] = useState<
Warehouse | undefined Warehouse | undefined
>(undefined); >(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]); const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
const warehousesColumns: ColumnDef<Warehouse>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'type',
header: 'Tipe',
},
{
accessorKey: 'area',
header: 'Area',
cell: (props) => props.row.original.area.name,
},
{
accessorKey: 'location',
header: 'Lokasi',
cell: (props) => {
if (
props.row.original.type === 'LOKASI' ||
props.row.original.type === 'KANDANG'
) {
return props.row.original.location.name;
} else {
return '-';
}
},
},
{
accessorKey: 'kandang',
header: 'Kandang',
cell: (props) => {
if (props.row.original.type === 'KANDANG') {
return props.row.original.kandang.name;
} else {
return '-';
}
},
},
{
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 = () => {
setSelectedWarehouse(props.row.original);
deleteModal.openModal();
}; };
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -237,119 +158,150 @@ const WarehousesTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const warehousesColumns: ColumnDef<Warehouse>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { cell: (props) =>
const newVal = val as OptionType; tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
setPageSize(newVal.value as number); 1,
};
const updateSortingFilter = useCallback(
(
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>,
sortFilter: ColumnSort | undefined
) => {
if (!sortFilter) {
updateFilter(sortName, '');
} else {
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
}
}, },
[updateFilter] {
); accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'type',
header: 'Tipe',
},
{
accessorFn: (row) => row.area?.name ?? '-',
header: 'Area',
},
{
accessorKey: 'location',
header: 'Lokasi',
cell: (props) => {
if (
props.row.original.type === 'LOKASI' ||
props.row.original.type === 'KANDANG'
) {
return props.row.original.location?.name ?? '-';
}
return '-';
},
},
{
accessorKey: 'kandang',
header: 'Kandang',
cell: (props) => {
if (props.row.original.type === 'KANDANG') {
return props.row.original.kandang?.name ?? '-';
}
return '-';
},
},
{
header: 'Aksi',
cell: (props: CellContext<Warehouse, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
// track sorting const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
useEffect(() => {
const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name');
const typeSortFilter = sorting.find((sortItem) => sortItem.id === 'type');
const areaSortFilter = sorting.find((sortItem) => sortItem.id === 'area');
const locationSortFilter = sorting.find(
(sortItem) => sortItem.id === 'location'
);
const kandangSortFilter = sorting.find(
(sortItem) => sortItem.id === 'kandang'
);
updateSortingFilter('nameSort', nameSortFilter); const deleteClickHandler = () => {
updateSortingFilter('typeSort', typeSortFilter); setSelectedWarehouse(props.row.original);
updateSortingFilter('areaSort', areaSortFilter); deleteModal.openModal();
updateSortingFilter('locationSort', locationSortFilter); };
updateSortingFilter('kandangSort', kandangSortFilter);
}, [sorting, updateSortingFilter]); return (
<RowOptionsMenu
props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
return ( return (
<> <>
<div className='w-full p-0 sm:p-4'> <div className='w-full'>
<div className='flex flex-col gap-2 mb-4'> {/* Header Section */}
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-full flex flex-row'> {/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.master.warehouses.create'> <RequirePermission permissions='lti.master.warehouses.create'>
<Button <Button
href='/master-data/warehouse/add' href='/master-data/warehouse/add'
variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Warehouse
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Cari Warehouse' placeholder='Cari Warehouse'
value={tableFilterState.search} value={tableFilterState.search ?? ''}
onChange={searchChangeHandler} onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }} startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/> />
</div> }
className={{
<div className='flex flex-row justify-end'> wrapper: 'w-full min-w-24 max-w-3xs',
<SelectInput inputWrapper: 'rounded-xl! shadow-button-soft',
label='Baris' input:
options={ROWS_OPTIONS} 'placeholder:font-semibold placeholder:text-base-content/50',
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<Warehouse> <Table<Warehouse>
data={isResponseSuccess(warehouses) ? warehouses?.data : []} data={isResponseSuccess(warehouses) ? warehouses?.data : []}
columns={warehousesColumns} columns={warehousesColumns}
pageSize={tableFilterState.pageSize} pageSize={tableFilterState.pageSize}
page={isResponseSuccess(warehouses) ? warehouses?.meta?.page : 0} page={isResponseSuccess(warehouses) ? warehouses?.meta?.page : 0}
totalItems={ totalItems={
isResponseSuccess(warehouses) ? warehouses?.meta?.total_results : 0 isResponseSuccess(warehouses)
? warehouses?.meta?.total_results
: 0
} }
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading} isLoading={isLoading}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3 mb-0', {
'mb-20': 'w-full':
isResponseSuccess(warehouses) && warehouses?.data?.length === 0, isResponseSuccess(warehouses) &&
warehouses?.data?.length === 0,
}), }),
tableWrapperClassName: 'overflow-x-auto min-h-full!', headerColumnClassName: 'text-nowrap',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
/> />
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}