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