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'>
<RequirePermission permissions='lti.master.area.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/area/detail/?areaId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.area.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.area.detail'>
Edit <Button
</Button> href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.area.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='text-error hover:text-inherit' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.area.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.area.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,65 +129,12 @@ 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,95 +154,114 @@ 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedArea(props.row.original);
deleteModal.openModal();
};
// track sorting return (
useEffect(() => { <RowOptionsMenu
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
if (!isNameSorted) { deleteClickHandler={deleteClickHandler}
updateFilter('nameSort', ''); />
} else { );
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); },
} },
}, [sorting, updateFilter]); ],
[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.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='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
className='w-full sm:w-fit' >
> <Icon icon='heroicons:plus' width={20} height={20} />
<Icon icon='ic:round-plus' width={24} height={24} /> Add Area
Tambah </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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Area> {/* Table Section */}
data={isResponseSuccess(areas) ? areas?.data : []} <div className='flex flex-col mb-4'>
columns={areasColumns} <Table<Area>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(areas) ? areas?.data : []}
page={isResponseSuccess(areas) ? areas?.meta?.page : 0} columns={areasColumns}
totalItems={isResponseSuccess(areas) ? areas?.meta?.total_results : 0} pageSize={tableFilterState.pageSize}
onPageChange={setPage} page={isResponseSuccess(areas) ? areas?.meta?.page : 0}
isLoading={isLoading} totalItems={
sorting={sorting} isResponseSuccess(areas) ? areas?.meta?.total_results : 0
setSorting={setSorting} }
className={{ onPageChange={setPage}
containerClassName: cn({ onPageSizeChange={setPageSize}
'mb-20': isResponseSuccess(areas) && areas?.data?.length === 0, isLoading={isLoading}
}), sorting={sorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', setSorting={setSorting}
tableClassName: 'font-inter w-full table-auto min-h-full!', className={{
headerRowClassName: 'border-b border-b-gray-200', containerClassName: cn('p-3 mb-0', {
headerColumnClassName: 'w-full': isResponseSuccess(areas) && areas?.data?.length === 0,
'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', headerColumnClassName: 'text-nowrap',
bodyColumnClassName: }}
'px-6 py-3 last:flex last:flex-row last:justify-end', />
}} </div>
/>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -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'>
<RequirePermission permissions='lti.master.banks.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.banks.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.banks.detail'>
Edit <Button
</Button> href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.banks.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.banks.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.banks.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,78 +129,12 @@ 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>[] = [ };
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'alias',
header: 'Alias',
},
{
accessorKey: 'account_number',
header: 'No. Rekening',
},
{
accessorKey: 'owner',
header: 'Pemilik',
cell: (props) => (props.getValue() ? props.getValue() : '-'),
},
{
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 = () => {
setSelectedBank(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);
@@ -192,93 +154,127 @@ const BanksTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const banksColumns: ColumnDef<Bank>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'alias',
header: 'Alias',
},
{
accessorKey: 'account_number',
header: 'No. Rekening',
},
{
accessorKey: 'owner',
header: 'Pemilik',
cell: (props) => props.getValue() || '-',
},
{
header: 'Aksi',
cell: (props: CellContext<Bank, 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedBank(props.row.original);
deleteModal.openModal();
};
// track sorting return (
useEffect(() => { <RowOptionsMenu
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
if (!isNameSorted) { deleteClickHandler={deleteClickHandler}
updateFilter('nameSort', ''); />
} else { );
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); },
} },
}, [sorting]); ],
[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 */}
<RequirePermission permissions='lti.master.banks.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.banks.create'>
href='/master-data/bank/add' <Button
variant='outline' href='/master-data/bank/add'
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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Bank> {/* Table Section */}
data={isResponseSuccess(banks) ? banks?.data : []} <div className='flex flex-col mb-4'>
columns={banksColumns} <Table<Bank>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(banks) ? banks?.data : []}
page={isResponseSuccess(banks) ? banks?.meta?.page : 0} columns={banksColumns}
totalItems={isResponseSuccess(banks) ? banks?.meta?.total_results : 0} pageSize={tableFilterState.pageSize}
onPageChange={setPage} page={isResponseSuccess(banks) ? banks?.meta?.page : 0}
isLoading={isLoading} totalItems={
sorting={sorting} isResponseSuccess(banks) ? banks?.meta?.total_results : 0
setSorting={setSorting} }
className={{ onPageChange={setPage}
containerClassName: cn({ onPageSizeChange={setPageSize}
'mb-20': isResponseSuccess(banks) && banks?.data?.length === 0, isLoading={isLoading}
}), sorting={sorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', setSorting={setSorting}
tableClassName: 'font-inter w-full table-auto min-h-full!', className={{
headerRowClassName: 'border-b border-b-gray-200', containerClassName: cn('p-3 mb-0', {
headerColumnClassName: 'w-full': isResponseSuccess(banks) && banks?.data?.length === 0,
'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', headerColumnClassName: 'text-nowrap',
bodyColumnClassName: }}
'px-6 py-3 last:flex last:flex-row last:justify-end', />
}} </div>
/>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -1,77 +1,102 @@
'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'>
<RequirePermission permissions='lti.master.customer.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission> <PopoverContent
<RequirePermission permissions='lti.master.customer.update'> id={popoverId}
<Button anchorName={popoverAnchorName}
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`} position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
variant='ghost' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
color='warning' >
className='justify-start text-sm' <div className='flex flex-col bg-base-100 rounded-xl'>
> <RequirePermission permissions='lti.master.customer.detail'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Button
Edit href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
</Button> variant='ghost'
</RequirePermission> color='none'
<RequirePermission permissions='lti.master.customer.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.customer.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.customer.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,81 +157,132 @@ 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 */}
<RequirePermission permissions='lti.master.customer.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.customer.create'>
href='/master-data/customer/add' <Button
variant='outline' href='/master-data/customer/add'
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>
<DebouncedTextInput
name='search'
placeholder='Cari Kandang'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div> </div>
<div className='flex flex-row justify-end'> {/* Search */}
<SelectInput <div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
label='Baris' <DebouncedTextInput
options={ROWS_OPTIONS} name='search'
value={{ placeholder='Cari Customer'
label: String(tableFilterState.pageSize), value={tableFilterState.search ?? ''}
value: tableFilterState.pageSize, onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Customer> {/* Table Section */}
data={isResponseSuccess(customers) ? customers?.data : []} <div className='flex flex-col mb-4'>
columns={customersColumns} <Table<Customer>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(customers) ? customers?.data : []}
page={isResponseSuccess(customers) ? customers?.meta?.page : 0} columns={customersColumns}
totalItems={ pageSize={tableFilterState.pageSize}
isResponseSuccess(customers) ? customers?.meta?.total_results : 0 page={isResponseSuccess(customers) ? customers?.meta?.page : 0}
} totalItems={
onPageChange={setPage} isResponseSuccess(customers) ? customers?.meta?.total_results : 0
isLoading={isLoading} }
className={{ onPageChange={setPage}
containerClassName: cn({ onPageSizeChange={setPageSize}
'mb-20': isLoading={isLoading}
isResponseSuccess(customers) && customers?.data?.length === 0, sorting={sorting}
}), setSorting={setSorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', className={{
tableClassName: 'font-inter w-full table-auto min-h-full!', containerClassName: cn('p-3 mb-0', {
headerRowClassName: 'border-b border-b-gray-200', 'w-full':
headerColumnClassName: isResponseSuccess(customers) && customers?.data?.length === 0,
'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', headerColumnClassName: 'text-nowrap',
bodyColumnClassName: }}
'px-6 py-3 last:flex last:flex-row last:justify-end', />
}} </div>
/>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -1,87 +1,102 @@
'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'>
<RequirePermission permissions='lti.master.flocks.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon <Icon icon='material-symbols:more-vert' width={16} height={16} />
icon='mdi:eye-outline' </PopoverButton>
width={16}
height={16} <PopoverContent
className='justify-start text-sm' id={popoverId}
/> anchorName={popoverAnchorName}
Detail position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
</Button> className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
</RequirePermission> >
<RequirePermission permissions='lti.master.flocks.update'> <div className='flex flex-col bg-base-100 rounded-xl'>
<Button <RequirePermission permissions='lti.master.flocks.detail'>
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`} <Button
variant='ghost' href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
color='warning' variant='ghost'
className='justify-start text-sm' color='none'
> className='p-3 justify-start text-sm font-semibold w-full'
<Icon onClick={closePopover}
icon='material-symbols:edit-outline' >
width={16} <Icon icon='heroicons:eye' width={20} height={20} />
height={16} Detail
className='justify-start text-sm' </Button>
/> </RequirePermission>
Edit <RequirePermission permissions='lti.master.flocks.update'>
</Button> <Button
</RequirePermission> href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
<RequirePermission permissions='lti.master.flocks.delete'> variant='ghost'
<Button color='none'
onClick={deleteClickHandler} className='p-3 justify-start text-sm font-semibold w-full'
variant='ghost' onClick={closePopover}
color='error' >
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' <Icon icon='heroicons:pencil-square' width={20} height={20} />
> Edit
<Icon </Button>
icon='material-symbols:delete-outline-rounded' </RequirePermission>
width={16} <RequirePermission permissions='lti.master.flocks.delete'>
height={16} <Button
className='justify-start text-sm' onClick={() => {
/> deleteClickHandler();
Delete closePopover();
</Button> }}
</RequirePermission> variant='ghost'
</RowOptionsMenuWrapper> color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,85 +155,128 @@ 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 */}
<RequirePermission permissions='lti.master.flocks.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.flocks.create'>
href='/master-data/flock/add' <Button
variant='outline' href='/master-data/flock/add'
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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Flock> {/* Table Section */}
data={isResponseSuccess(flocks) ? flocks?.data : []} <div className='flex flex-col mb-4'>
columns={flocksColumns} <Table<Flock>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(flocks) ? flocks?.data : []}
page={isResponseSuccess(flocks) ? flocks?.meta?.page : 0} columns={flocksColumns}
totalItems={ pageSize={tableFilterState.pageSize}
isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0 page={isResponseSuccess(flocks) ? flocks?.meta?.page : 0}
} totalItems={
onPageChange={setPage} isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0
isLoading={isLoading} }
className={{ onPageChange={setPage}
containerClassName: cn({ onPageSizeChange={setPageSize}
'mb-20': isResponseSuccess(flocks) && flocks?.data?.length === 0, isLoading={isLoading}
}), sorting={sorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', setSorting={setSorting}
tableClassName: 'font-inter w-full table-auto min-h-full!', className={{
headerRowClassName: 'border-b border-b-gray-200', containerClassName: cn('p-3 mb-0', {
headerColumnClassName: 'w-full':
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', isResponseSuccess(flocks) && flocks?.data?.length === 0,
bodyRowClassName: 'border-b border-b-gray-200', }),
bodyColumnClassName: headerColumnClassName: 'text-nowrap',
'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'>
<RequirePermission permissions='lti.master.kandangs.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.kandangs.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.kandangs.detail'>
Edit <Button
</Button> href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.kandangs.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.kandangs.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.kandangs.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,82 +129,14 @@ 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,114 +158,128 @@ const KandangsTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const kandangsColumns: ColumnDef<Kandang>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
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;
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedKandang(props.row.original);
deleteModal.openModal();
};
const updateSortingFilter = useCallback( return (
( <RowOptionsMenu
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>, props={props}
sortFilter: ColumnSort | undefined popoverPosition={isLast2Rows ? 'top' : 'bottom'}
) => { deleteClickHandler={deleteClickHandler}
if (!sortFilter) { />
updateFilter(sortName, ''); );
} else { },
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); },
} ],
}, [tableFilterState.pageSize, tableFilterState.page, deleteModal]
[updateFilter]
); );
// track sorting
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);
updateSortingFilter('locationSort', locationSortFilter);
updateSortingFilter('picSort', picSortFilter);
}, [sorting, updateSortingFilter]);
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='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
className='w-full sm:w-fit' >
> <Icon icon='heroicons:plus' width={20} height={20} />
<Icon icon='ic:round-plus' width={24} height={24} /> Add Kandang
Tambah </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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Kandang> {/* Table Section */}
data={isResponseSuccess(kandangs) ? kandangs?.data : []} <div className='flex flex-col mb-4'>
columns={kandangsColumns} <Table<Kandang>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(kandangs) ? kandangs?.data : []}
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0} columns={kandangsColumns}
totalItems={ pageSize={tableFilterState.pageSize}
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0 page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
} totalItems={
onPageChange={setPage} isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
isLoading={isLoading} }
sorting={sorting} onPageChange={setPage}
setSorting={setSorting} onPageSizeChange={setPageSize}
className={{ isLoading={isLoading}
containerClassName: cn({ sorting={sorting}
'mb-20': setSorting={setSorting}
isResponseSuccess(kandangs) && kandangs?.data?.length === 0, className={{
}), containerClassName: cn('p-3 mb-0', {
tableWrapperClassName: 'overflow-x-auto min-h-full!', 'w-full':
tableClassName: 'font-inter w-full table-auto min-h-full!', isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
headerRowClassName: 'border-b border-b-gray-200', }),
headerColumnClassName: headerColumnClassName: 'text-nowrap',
'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: </div>
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -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'>
<RequirePermission permissions='lti.master.locations.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/location/detail/?locationId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.locations.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.locations.detail'>
Edit <Button
</Button> href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.locations.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.locations.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.locations.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,76 +129,14 @@ 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,114 +158,123 @@ const LocationsTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const locationsColumns: ColumnDef<Location>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
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;
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedLocation(props.row.original);
deleteModal.openModal();
};
const updateSortingFilter = useCallback( return (
( <RowOptionsMenu
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>, props={props}
sortFilter: ColumnSort | undefined popoverPosition={isLast2Rows ? 'top' : 'bottom'}
) => { deleteClickHandler={deleteClickHandler}
if (!sortFilter) { />
updateFilter(sortName, ''); );
} else { },
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); },
} ],
}, [tableFilterState.pageSize, tableFilterState.page, deleteModal]
[updateFilter]
); );
// track sorting
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);
updateSortingFilter('addressSort', addressSortFilter);
updateSortingFilter('areaSort', areaSortFilter);
}, [sorting, updateSortingFilter]);
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='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
className='w-full sm:w-fit' >
> <Icon icon='heroicons:plus' width={20} height={20} />
<Icon icon='ic:round-plus' width={24} height={24} /> Add Location
Tambah </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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Location> {/* Table Section */}
data={isResponseSuccess(locations) ? locations?.data : []} <div className='flex flex-col mb-4'>
columns={locationsColumns} <Table<Location>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(locations) ? locations?.data : []}
page={isResponseSuccess(locations) ? locations?.meta?.page : 0} columns={locationsColumns}
totalItems={ pageSize={tableFilterState.pageSize}
isResponseSuccess(locations) ? locations?.meta?.total_results : 0 page={isResponseSuccess(locations) ? locations?.meta?.page : 0}
} totalItems={
onPageChange={setPage} isResponseSuccess(locations) ? locations?.meta?.total_results : 0
isLoading={isLoading} }
sorting={sorting} onPageChange={setPage}
setSorting={setSorting} onPageSizeChange={setPageSize}
className={{ isLoading={isLoading}
containerClassName: cn({ sorting={sorting}
'mb-20': setSorting={setSorting}
isResponseSuccess(locations) && locations?.data?.length === 0, className={{
}), containerClassName: cn('p-3 mb-0', {
tableWrapperClassName: 'overflow-x-auto min-h-full!', 'w-full':
tableClassName: 'font-inter w-full table-auto min-h-full!', isResponseSuccess(locations) && locations?.data?.length === 0,
headerRowClassName: 'border-b border-b-gray-200', }),
headerColumnClassName: headerColumnClassName: 'text-nowrap',
'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: </div>
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -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'>
<RequirePermission permissions='lti.master.product_categories.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.product_categories.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.product_categories.detail'>
Edit <Button
</Button> href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.product_categories.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.product_categories.update'>
icon='mdi:delete-outline' <Button
width={16} href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.product_categories.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,71 +139,15 @@ 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedProductCategory(props.row.original);
deleteModal.openModal();
};
return (
<RowOptionsMenu
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,91 +237,86 @@ 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 */}
<RequirePermission permissions='lti.master.product_categories.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.product_categories.create'>
href='/master-data/product-category/add' <Button
variant='outline' href='/master-data/product-category/add'
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
</div> icon='heroicons:magnifying-glass'
<div className='flex flex-row justify-end'> width={20}
<SelectInput height={20}
label='Baris' />
options={ROWS_OPTIONS} }
value={{ className={{
label: String(tableFilterState.pageSize), wrapper: 'w-full min-w-24 max-w-3xs',
value: tableFilterState.pageSize, inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<ProductCategory>
data={ {/* Table Section */}
isResponseSuccess(productCategories) ? productCategories?.data : [] <div className='flex flex-col mb-4'>
} <Table<ProductCategory>
columns={productCategoryColumns} data={
pageSize={tableFilterState.pageSize} isResponseSuccess(productCategories)
page={ ? productCategories?.data
isResponseSuccess(productCategories) : []
? productCategories?.meta?.page }
: 0 columns={productCategoryColumns}
} pageSize={tableFilterState.pageSize}
totalItems={ page={
isResponseSuccess(productCategories) isResponseSuccess(productCategories)
? productCategories?.meta?.total_results ? productCategories?.meta?.page
: 0 : 0
} }
onPageChange={setPage} totalItems={
isLoading={isLoading} isResponseSuccess(productCategories)
sorting={sorting} ? productCategories?.meta?.total_results
setSorting={setSorting} : 0
className={{ }
containerClassName: cn({ onPageChange={setPage}
'mb-20': onPageSizeChange={setPageSize}
isResponseSuccess(productCategories) && isLoading={isLoading}
productCategories?.data?.length === 0, sorting={sorting}
}), setSorting={setSorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', className={{
tableClassName: 'font-inter w-full table-auto min-h-full!', containerClassName: cn('p-3 mb-0', {
headerRowClassName: 'border-b border-b-gray-200', 'w-full':
headerColumnClassName: isResponseSuccess(productCategories) &&
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', productCategories?.data?.length === 0,
bodyRowClassName: 'border-b border-b-gray-200', }),
bodyColumnClassName: headerColumnClassName: 'text-nowrap',
'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'>
<RequirePermission permissions='lti.master.production_standards.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.production_standards.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.production_standards.detail'>
Edit <Button
</Button> href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.production_standards.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='text-error hover:text-inherit' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.production_standards.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.production_standards.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,112 +136,107 @@ const ProductionStandardTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const productionStandardColumns: ColumnDef<ProductionStandard>[] = useMemo(
() => [
{
header: 'No',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorFn: (row) => row.project_category ?? '-',
header: 'Kategori',
},
{
header: 'Aksi',
cell: (props: CellContext<ProductionStandard, 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 = () => {
setSelectedProductionStandard(props.row.original);
deleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
deleteClickHandler={deleteClickHandler}
/>
);
},
},
],
[deleteModal]
);
return ( return (
<> <>
<div className='flex flex-col gap-6 p-6'> <div className='w-full'>
<div className='flex flex-row gap-6 justify-start'> {/* Header Section */}
<RequirePermission permissions='lti.master.production_standards.create'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<Button {/* Action Buttons */}
href='/master-data/production-standard/add' <div className='w-fit flex flex-row gap-3 flex-wrap'>
variant='outline' <RequirePermission permissions='lti.master.production_standards.create'>
> <Button
<Icon icon='mdi:plus' /> Tambah href='/master-data/production-standard/add'
</Button> color='primary'
</RequirePermission> 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>
</div>
</div> </div>
<RequirePermission permissions='lti.master.production_standards.list'>
{/* Table Section */}
<div className='flex flex-col mb-4'>
<Table<ProductionStandard> <Table<ProductionStandard>
data={ data={
isResponseSuccess(productionStandards) isResponseSuccess(productionStandards)
? productionStandards.data ? productionStandards.data
: [] : []
} }
columns={[ columns={productionStandardColumns}
{ isLoading={isLoading}
header: 'No', sorting={sorting}
accessorFn: (row, index) => index + 1, setSorting={setSorting}
},
{
header: 'Nama',
accessorKey: 'name',
},
{
header: 'Kategori',
accessorFn: (row) => row.project_category,
},
{
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 = () => {
setSelectedProductionStandard(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>
)}
</>
);
},
},
]}
className={{ className={{
headerColumnClassName: cn( containerClassName: cn('p-3 mb-0', {
TABLE_DEFAULT_STYLING.headerColumnClassName, 'w-full':
'last:flex last:flex-row last:justify-end' isResponseSuccess(productionStandards) &&
), productionStandards?.data?.length === 0,
bodyColumnClassName: cn( }),
TABLE_DEFAULT_STYLING.bodyColumnClassName, headerColumnClassName: 'text-nowrap',
'last:flex last:flex-row last:justify-end'
),
}} }}
/> />
</RequirePermission> </div>
</div> </div>
<RequirePermission permissions='lti.master.production_standards.delete'>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin menghapus data Production Standard ini (${selectedProductionStandard?.name})?`} text={`Apakah anda yakin ingin menghapus data Production Standard ini (${selectedProductionStandard?.name})?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
primaryButton={{ primaryButton={{
text: 'Ya', text: 'Ya',
color: 'error', color: 'error',
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
/> />
</RequirePermission>
</> </>
); );
}; };
@@ -1,87 +1,102 @@
'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'>
<RequirePermission permissions='lti.master.suppliers.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon <Icon icon='material-symbols:more-vert' width={16} height={16} />
icon='mdi:eye-outline' </PopoverButton>
width={16}
height={16} <PopoverContent
className='justify-start text-sm' id={popoverId}
/> anchorName={popoverAnchorName}
Detail position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
</Button> className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
</RequirePermission> >
<RequirePermission permissions='lti.master.suppliers.update'> <div className='flex flex-col bg-base-100 rounded-xl'>
<Button <RequirePermission permissions='lti.master.suppliers.detail'>
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`} <Button
variant='ghost' href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
color='warning' variant='ghost'
className='justify-start text-sm' color='none'
> className='p-3 justify-start text-sm font-semibold w-full'
<Icon onClick={closePopover}
icon='material-symbols:edit-outline' >
width={16} <Icon icon='heroicons:eye' width={20} height={20} />
height={16} Detail
className='justify-start text-sm' </Button>
/> </RequirePermission>
Edit <RequirePermission permissions='lti.master.suppliers.update'>
</Button> <Button
</RequirePermission> href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
<RequirePermission permissions='lti.master.suppliers.delete'> variant='ghost'
<Button color='none'
onClick={deleteClickHandler} className='p-3 justify-start text-sm font-semibold w-full'
variant='ghost' onClick={closePopover}
color='error' >
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' <Icon icon='heroicons:pencil-square' width={20} height={20} />
> Edit
<Icon </Button>
icon='material-symbols:delete-outline-rounded' </RequirePermission>
width={16} <RequirePermission permissions='lti.master.suppliers.delete'>
height={16} <Button
className='justify-start text-sm' onClick={() => {
/> deleteClickHandler();
Delete closePopover();
</Button> }}
</RequirePermission> variant='ghost'
</RowOptionsMenuWrapper> color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,97 +128,16 @@ 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);
{ };
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'alias',
header: 'Alias',
},
{
accessorKey: 'pic',
header: 'Nama PIC',
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'type',
header: 'Tipe',
},
{
accessorKey: 'phone',
header: 'No. Telp',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'address',
header: 'Alamat',
},
{
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 = () => {
setSelectedSupplier(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptions
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptions
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
// Handler
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -221,82 +157,146 @@ const SuppliersTable = () => {
toast.success('Successfully delete Supplier!'); toast.success('Successfully delete Supplier!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value); const suppliersColumns: ColumnDef<Supplier>[] = 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: 'alias',
header: 'Alias',
},
{
accessorKey: 'pic',
header: 'Nama PIC',
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'type',
header: 'Tipe',
},
{
accessorKey: 'phone',
header: 'No. Telp',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'address',
header: 'Alamat',
},
{
header: 'Aksi',
cell: (props: CellContext<Supplier, 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 = () => {
setSelectedSupplier(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 */}
<RequirePermission permissions='lti.master.suppliers.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.suppliers.create'>
href='/master-data/supplier/add' <Button
variant='outline' href='/master-data/supplier/add'
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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Supplier> {/* Table Section */}
data={isResponseSuccess(suppliers) ? suppliers?.data : []} <div className='flex flex-col mb-4'>
columns={suppliersColumns} <Table<Supplier>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(suppliers) ? suppliers?.data : []}
page={isResponseSuccess(suppliers) ? suppliers?.meta?.page : 0} columns={suppliersColumns}
totalItems={ pageSize={tableFilterState.pageSize}
isResponseSuccess(suppliers) ? suppliers?.meta?.total_results : 0 page={isResponseSuccess(suppliers) ? suppliers?.meta?.page : 0}
} totalItems={
onPageChange={setPage} isResponseSuccess(suppliers) ? suppliers?.meta?.total_results : 0
isLoading={isLoading} }
className={{ onPageChange={setPage}
containerClassName: cn({ onPageSizeChange={setPageSize}
'mb-20': isLoading={isLoading}
isResponseSuccess(suppliers) && suppliers?.data?.length === 0, sorting={sorting}
}), setSorting={setSorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', className={{
tableClassName: 'font-inter w-full table-auto min-h-full!', containerClassName: cn('p-3 mb-0', {
headerRowClassName: 'border-b border-b-gray-200', 'w-full':
headerColumnClassName: isResponseSuccess(suppliers) && suppliers?.data?.length === 0,
'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', headerColumnClassName: 'text-nowrap',
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'
+172 -178
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'>
<RequirePermission permissions='lti.master.uoms.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.uoms.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.uoms.detail'>
Edit <Button
</Button> href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.uoms.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.uoms.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.uoms.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,65 +129,12 @@ 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,93 +154,112 @@ 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedUom(props.row.original);
deleteModal.openModal();
};
// track sorting return (
useEffect(() => { <RowOptionsMenu
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); props={props}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
if (!isNameSorted) { deleteClickHandler={deleteClickHandler}
updateFilter('nameSort', ''); />
} else { );
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); },
} },
}, [sorting, updateFilter]); ],
[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 */}
<RequirePermission permissions='lti.master.uoms.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.uoms.create'>
href='/master-data/uom/add' <Button
variant='outline' href='/master-data/uom/add'
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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Uom> {/* Table Section */}
data={isResponseSuccess(uoms) ? uoms?.data : []} <div className='flex flex-col mb-4'>
columns={uomsColumns} <Table<Uom>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(uoms) ? uoms?.data : []}
page={isResponseSuccess(uoms) ? uoms?.meta?.page : 0} columns={uomsColumns}
totalItems={isResponseSuccess(uoms) ? uoms?.meta?.total_results : 0} pageSize={tableFilterState.pageSize}
onPageChange={setPage} page={isResponseSuccess(uoms) ? uoms?.meta?.page : 0}
isLoading={isLoading} totalItems={isResponseSuccess(uoms) ? uoms?.meta?.total_results : 0}
sorting={sorting} onPageChange={setPage}
setSorting={setSorting} onPageSizeChange={setPageSize}
className={{ isLoading={isLoading}
containerClassName: cn({ sorting={sorting}
'mb-20': isResponseSuccess(uoms) && uoms?.data?.length === 0, setSorting={setSorting}
}), className={{
tableWrapperClassName: 'overflow-x-auto min-h-full!', containerClassName: cn('p-3 mb-0', {
tableClassName: 'font-inter w-full table-auto min-h-full!', 'w-full': isResponseSuccess(uoms) && uoms?.data?.length === 0,
headerRowClassName: 'border-b border-b-gray-200', }),
headerColumnClassName: headerColumnClassName: 'text-nowrap',
'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: </div>
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
</div> </div>
<ConfirmationModal <ConfirmationModal
@@ -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'>
<RequirePermission permissions='lti.master.warehouses.detail'> <PopoverButton
<Button tabIndex={0}
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`} variant='ghost'
variant='ghost' color='none'
color='primary' popoverTarget={popoverId}
className='justify-start text-sm' anchorName={popoverAnchorName}
> >
<Icon icon='mdi:eye-outline' width={16} height={16} /> <Icon icon='material-symbols:more-vert' width={16} height={16} />
Detail </PopoverButton>
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.warehouses.update'> <PopoverContent
<Button id={popoverId}
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`} anchorName={popoverAnchorName}
variant='ghost' position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
color='warning' className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
className='justify-start text-sm' >
> <div className='flex flex-col bg-base-100 rounded-xl'>
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <RequirePermission permissions='lti.master.warehouses.detail'>
Edit <Button
</Button> href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
</RequirePermission> variant='ghost'
color='none'
<RequirePermission permissions='lti.master.warehouses.delete'> className='p-3 justify-start text-sm font-semibold w-full'
<Button onClick={closePopover}
onClick={deleteClickHandler} >
variant='ghost' <Icon icon='heroicons:eye' width={20} height={20} />
color='error' Detail
className='text-error hover:text-inherit' </Button>
> </RequirePermission>
<Icon <RequirePermission permissions='lti.master.warehouses.update'>
icon='material-symbols:delete-outline-rounded' <Button
width={16} href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
height={16} variant='ghost'
className='justify-start text-sm' color='none'
/> className='p-3 justify-start text-sm font-semibold w-full'
Delete onClick={closePopover}
</Button> >
</RequirePermission> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</RowOptionsMenuWrapper> Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.warehouses.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</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,101 +129,14 @@ 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,118 +158,149 @@ const WarehousesTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const warehousesColumns: ColumnDef<Warehouse>[] = useMemo(
updateFilter('search', e.target.value); () => [
}; {
header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
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;
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const newVal = val as OptionType;
setPageSize(newVal.value as number); const deleteClickHandler = () => {
}; setSelectedWarehouse(props.row.original);
deleteModal.openModal();
};
const updateSortingFilter = useCallback( return (
( <RowOptionsMenu
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>, props={props}
sortFilter: ColumnSort | undefined popoverPosition={isLast2Rows ? 'top' : 'bottom'}
) => { deleteClickHandler={deleteClickHandler}
if (!sortFilter) { />
updateFilter(sortName, ''); );
} else { },
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); },
} ],
}, [tableFilterState.pageSize, tableFilterState.page, deleteModal]
[updateFilter]
); );
// track sorting
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);
updateSortingFilter('typeSort', typeSortFilter);
updateSortingFilter('areaSort', areaSortFilter);
updateSortingFilter('locationSort', locationSortFilter);
updateSortingFilter('kandangSort', kandangSortFilter);
}, [sorting, updateSortingFilter]);
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 */}
<RequirePermission permissions='lti.master.warehouses.create'> <div className='w-fit flex flex-row gap-3 flex-wrap'>
<Button <RequirePermission permissions='lti.master.warehouses.create'>
href='/master-data/warehouse/add' <Button
variant='outline' href='/master-data/warehouse/add'
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
</div> icon='heroicons:magnifying-glass'
width={20}
<div className='flex flex-row justify-end'> height={20}
<SelectInput />
label='Baris' }
options={ROWS_OPTIONS} className={{
value={{ wrapper: 'w-full min-w-24 max-w-3xs',
label: String(tableFilterState.pageSize), inputWrapper: 'rounded-xl! shadow-button-soft',
value: tableFilterState.pageSize, input:
'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/> />
</div> </div>
</div> </div>
<Table<Warehouse> {/* Table Section */}
data={isResponseSuccess(warehouses) ? warehouses?.data : []} <div className='flex flex-col mb-4'>
columns={warehousesColumns} <Table<Warehouse>
pageSize={tableFilterState.pageSize} data={isResponseSuccess(warehouses) ? warehouses?.data : []}
page={isResponseSuccess(warehouses) ? warehouses?.meta?.page : 0} columns={warehousesColumns}
totalItems={ pageSize={tableFilterState.pageSize}
isResponseSuccess(warehouses) ? warehouses?.meta?.total_results : 0 page={isResponseSuccess(warehouses) ? warehouses?.meta?.page : 0}
} totalItems={
onPageChange={setPage} isResponseSuccess(warehouses)
isLoading={isLoading} ? warehouses?.meta?.total_results
sorting={sorting} : 0
setSorting={setSorting} }
className={{ onPageChange={setPage}
containerClassName: cn({ onPageSizeChange={setPageSize}
'mb-20': isLoading={isLoading}
isResponseSuccess(warehouses) && warehouses?.data?.length === 0, sorting={sorting}
}), setSorting={setSorting}
tableWrapperClassName: 'overflow-x-auto min-h-full!', className={{
tableClassName: 'font-inter w-full table-auto min-h-full!', containerClassName: cn('p-3 mb-0', {
headerRowClassName: 'border-b border-b-gray-200', 'w-full':
headerColumnClassName: isResponseSuccess(warehouses) &&
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', warehouses?.data?.length === 0,
bodyRowClassName: 'border-b border-b-gray-200', }),
bodyColumnClassName: headerColumnClassName: 'text-nowrap',
'px-6 py-3 last:flex last:flex-row last:justify-end', }}
}} />
/> </div>
</div> </div>
<ConfirmationModal <ConfirmationModal