mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE): Add advanced filtering capabilities to master data tables
This commit is contained in:
@@ -1,28 +1,46 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
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';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import KandangTableSkeleton from '@/components/pages/master-data/kandang/skeleton/KandangTableSkeleton';
|
import KandangTableSkeleton from '@/components/pages/master-data/kandang/skeleton/KandangTableSkeleton';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
|
import { UserApi } from '@/services/api/user';
|
||||||
|
import { User } from '@/types/api/api-general';
|
||||||
import { formatNumber } from '@/lib/helper';
|
import { 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 { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import {
|
||||||
|
KandangFilterSchema,
|
||||||
|
KandangFilterType,
|
||||||
|
} from '@/components/pages/master-data/kandang/filter/KandangFilter';
|
||||||
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -115,14 +133,111 @@ const KandangsTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
|
locationFilter: '',
|
||||||
|
picFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
locationFilter: 'location_id',
|
||||||
|
picFilter: 'pic_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<KandangFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
location_id: null,
|
||||||
|
pic_id: null,
|
||||||
|
},
|
||||||
|
validationSchema: KandangFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('locationFilter', values.location_id || '');
|
||||||
|
updateFilter('picFilter', values.pic_id || '');
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('locationFilter', '');
|
||||||
|
updateFilter('picFilter', '');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== LOCATION OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
} = useSelect<Location>(
|
||||||
|
filterModal.open ? LocationApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== PIC OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setPicInputValue,
|
||||||
|
options: picOptions,
|
||||||
|
isLoadingOptions: isLoadingPicOptions,
|
||||||
|
loadMore: loadMorePics,
|
||||||
|
} = useSelect<User>(
|
||||||
|
filterModal.open ? UserApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HANDLERS =====
|
||||||
|
const handleFilterLocationChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const location = val as OptionType | null;
|
||||||
|
const locationId = location?.value ? String(location.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterPicChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const pic = val as OptionType | null;
|
||||||
|
const picId = pic?.value ? String(pic.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('pic_id', picId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const locationIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.location_id) return null;
|
||||||
|
return (
|
||||||
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.location_id, locationOptions]);
|
||||||
|
|
||||||
|
const picIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.pic_id) return null;
|
||||||
|
return (
|
||||||
|
picOptions.find((opt) => String(opt.value) === formik.values.pic_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.pic_id, picOptions]);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -146,7 +261,7 @@ const KandangsTable = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableState('kandangs-table', pathname);
|
setTableState('kandangs-table', pathname);
|
||||||
}, [pathname]);
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
setSearchValue(e.target.value);
|
||||||
@@ -247,7 +362,7 @@ const KandangsTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search and Filter */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
@@ -268,6 +383,13 @@ const KandangsTable = () => {
|
|||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -326,6 +448,81 @@ const KandangsTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationIdValue}
|
||||||
|
onChange={handleFilterLocationChange}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
label='PIC'
|
||||||
|
placeholder='Pilih PIC'
|
||||||
|
options={picOptions}
|
||||||
|
value={picIdValue}
|
||||||
|
onChange={handleFilterPicChange}
|
||||||
|
onInputChange={setPicInputValue}
|
||||||
|
isLoading={isLoadingPicOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMorePics}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,42 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
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';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import LocationTableSkeleton from '@/components/pages/master-data/location/skeleton/LocationTableSkeleton';
|
import LocationTableSkeleton from '@/components/pages/master-data/location/skeleton/LocationTableSkeleton';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
||||||
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 { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import {
|
||||||
|
LocationFilterSchema,
|
||||||
|
LocationFilterType,
|
||||||
|
} from '@/components/pages/master-data/location/filter/LocationFilter';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -114,15 +129,73 @@ const LocationsTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
|
areaFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
areaFilter: 'area_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<LocationFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
area_id: null,
|
||||||
|
},
|
||||||
|
validationSchema: LocationFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('areaFilter', '');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== AREA OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
|
} = useSelect<Area>(
|
||||||
|
filterModal.open ? AreaApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HANDLERS =====
|
||||||
|
const handleFilterAreaChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const area = val as OptionType | null;
|
||||||
|
const areaId = area?.value ? String(area.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('area_id', areaId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const areaIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.area_id) return null;
|
||||||
|
return (
|
||||||
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.area_id, areaOptions]);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: locations,
|
data: locations,
|
||||||
@@ -145,7 +218,9 @@ const LocationsTable = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableState('locations-table', pathname);
|
setTableState('locations-table', pathname);
|
||||||
}, [pathname]);
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
setSearchValue(e.target.value);
|
||||||
@@ -241,7 +316,7 @@ const LocationsTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search and Filter */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
@@ -262,6 +337,13 @@ const LocationsTable = () => {
|
|||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -320,6 +402,68 @@ const LocationsTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
options={areaOptions}
|
||||||
|
value={areaIdValue}
|
||||||
|
onChange={handleFilterAreaChange}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
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';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import ProductTableSkeleton from '@/components/pages/master-data/product/skeleton/ProductTableSkeleton';
|
import ProductTableSkeleton from '@/components/pages/master-data/product/skeleton/ProductTableSkeleton';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProductApi } from '@/services/api/master-data';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
|
import { ProductApi, ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { formatCurrency } from '@/lib/helper';
|
import { formatCurrency } 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 {
|
||||||
|
ProductFilterSchema,
|
||||||
|
ProductFilterType,
|
||||||
|
} from '@/components/pages/master-data/product/filter/ProductFilter';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -116,13 +131,74 @@ const ProductsTable = () => {
|
|||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
|
productCategoryFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
productCategoryFilter: 'product_category_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<ProductFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
product_category_id: null,
|
||||||
|
},
|
||||||
|
validationSchema: ProductFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('productCategoryFilter', values.product_category_id || '');
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('productCategoryFilter', '');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== PRODUCT CATEGORY OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setProductCategoryInputValue,
|
||||||
|
options: productCategoryOptions,
|
||||||
|
isLoadingOptions: isLoadingProductCategoryOptions,
|
||||||
|
loadMore: loadMoreProductCategories,
|
||||||
|
} = useSelect<ProductCategory>(
|
||||||
|
filterModal.open ? ProductCategoryApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HANDLERS =====
|
||||||
|
const handleFilterProductCategoryChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const category = val as OptionType | null;
|
||||||
|
const categoryId = category?.value ? String(category.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('product_category_id', categoryId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const productCategoryIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.product_category_id) return null;
|
||||||
|
return (
|
||||||
|
productCategoryOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.product_category_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.product_category_id, productCategoryOptions]);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateFilter('search', searchValue);
|
updateFilter('search', searchValue);
|
||||||
}, [searchValue, updateFilter]);
|
}, [searchValue, updateFilter]);
|
||||||
@@ -292,7 +368,7 @@ const ProductsTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search and Filter */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
@@ -313,6 +389,13 @@ const ProductsTable = () => {
|
|||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -371,6 +454,68 @@ const ProductsTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori Produk'
|
||||||
|
placeholder='Pilih Kategori Produk'
|
||||||
|
options={productCategoryOptions}
|
||||||
|
value={productCategoryIdValue}
|
||||||
|
onChange={handleFilterProductCategoryChange}
|
||||||
|
onInputChange={setProductCategoryInputValue}
|
||||||
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreProductCategories}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
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';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import ProductionStandardTableSkeleton from '@/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton';
|
import ProductionStandardTableSkeleton from '@/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
import { ProductionStandard } from '@/types/api/master-data/production-standard';
|
import { ProductionStandard } from '@/types/api/master-data/production-standard';
|
||||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { usePathname } from 'next/navigation';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import {
|
||||||
|
ProductionStandardFilterSchema,
|
||||||
|
ProductionStandardFilterType,
|
||||||
|
} from '@/components/pages/master-data/production-standard/filter/ProductionStandardFilter';
|
||||||
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -104,9 +113,81 @@ const ProductionStandardTable = () => {
|
|||||||
const { setTableState } = useUiStore();
|
const { setTableState } = useUiStore();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
projectCategoryFilter: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
projectCategoryFilter: 'project_category',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<ProductionStandardFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
project_category: null,
|
||||||
|
},
|
||||||
|
validationSchema: ProductionStandardFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('projectCategoryFilter', values.project_category || '');
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('projectCategoryFilter', '');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
||||||
|
const projectCategoryOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'GROWING', label: 'Growing' },
|
||||||
|
{ value: 'LAYING', label: 'Laying' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HANDLERS =====
|
||||||
|
const handleFilterProjectCategoryChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const option = val as OptionType | null;
|
||||||
|
const category = option?.value ? String(option.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('project_category', category);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const projectCategoryValue = useMemo(() => {
|
||||||
|
if (!formik.values.project_category) return null;
|
||||||
|
return (
|
||||||
|
projectCategoryOptions.find(
|
||||||
|
(opt) => opt.value === formik.values.project_category
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.project_category, projectCategoryOptions]);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableState('production-standard-table', pathname);
|
setTableState('production-standard-table', pathname);
|
||||||
}, [pathname]);
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
@@ -115,7 +196,7 @@ const ProductionStandardTable = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
mutate: refreshProductionStandards,
|
mutate: refreshProductionStandards,
|
||||||
} = useSWR(
|
} = useSWR(
|
||||||
`${ProductionStandardApi.basePath}`,
|
`${ProductionStandardApi.basePath}${getTableFilterQueryString()}`,
|
||||||
ProductionStandardApi.getAllFetcher
|
ProductionStandardApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -149,7 +230,10 @@ const ProductionStandardTable = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) =>
|
||||||
|
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||||
|
props.row.index +
|
||||||
|
1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
@@ -185,7 +269,7 @@ const ProductionStandardTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[deleteModal]
|
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -206,6 +290,16 @@ const ProductionStandardTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filter */}
|
||||||
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Section */}
|
{/* Table Section */}
|
||||||
@@ -233,6 +327,11 @@ const ProductionStandardTable = () => {
|
|||||||
<Table<ProductionStandard>
|
<Table<ProductionStandard>
|
||||||
data={productionStandards.data}
|
data={productionStandards.data}
|
||||||
columns={productionStandardColumns}
|
columns={productionStandardColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={productionStandards?.meta?.page ?? 0}
|
||||||
|
totalItems={productionStandards?.meta?.total_results ?? 0}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={setSorting}
|
||||||
@@ -259,6 +358,65 @@ const ProductionStandardTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={projectCategoryOptions}
|
||||||
|
value={projectCategoryValue}
|
||||||
|
onChange={handleFilterProjectCategoryChange}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,42 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
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';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import SupplierTableSkeleton from '@/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton';
|
import SupplierTableSkeleton from '@/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
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 { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import {
|
||||||
|
SupplierFilterSchema,
|
||||||
|
SupplierFilterType,
|
||||||
|
} from '@/components/pages/master-data/supplier/filter/SupplierFilter';
|
||||||
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -114,21 +129,126 @@ const SuppliersTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
|
categoryFilter: '',
|
||||||
|
flagFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
categoryFilter: 'category_id',
|
||||||
|
flagFilter: 'flag',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<SupplierFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
category_id: null,
|
||||||
|
flag: false,
|
||||||
|
},
|
||||||
|
validationSchema: SupplierFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('categoryFilter', values.category_id || '');
|
||||||
|
updateFilter(
|
||||||
|
'flagFilter',
|
||||||
|
values.flag === true ? 'EKSPEDISI' : values.flag === false ? '' : ''
|
||||||
|
);
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('categoryFilter', '');
|
||||||
|
updateFilter('flagFilter', '');
|
||||||
|
formik.setFieldValue('flag', false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
|
||||||
|
const categoryOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'SAPRONAK', label: 'SAPRONAK' },
|
||||||
|
{ value: 'BOP', label: 'BOP' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FLAG OPTIONS (EKSPEDISI) =====
|
||||||
|
const flagOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'true', label: 'Ya' },
|
||||||
|
{ value: 'false', label: 'Tidak' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HANDLERS =====
|
||||||
|
const handleFilterCategoryChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const option = val as OptionType | null;
|
||||||
|
const categoryId = option?.value ? String(option.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('category_id', categoryId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterFlagChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const option = val as OptionType | null;
|
||||||
|
const boolValue =
|
||||||
|
option?.value === 'true'
|
||||||
|
? true
|
||||||
|
: option?.value === 'false'
|
||||||
|
? false
|
||||||
|
: null;
|
||||||
|
|
||||||
|
formik.setFieldValue('flag', boolValue);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const categoryIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.category_id) return null;
|
||||||
|
return (
|
||||||
|
categoryOptions.find((opt) => opt.value === formik.values.category_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.category_id, categoryOptions]);
|
||||||
|
|
||||||
|
const flagValue = useMemo(() => {
|
||||||
|
if (formik.values.flag === null) return null;
|
||||||
|
return (
|
||||||
|
flagOptions.find((opt) => opt.value === String(formik.values.flag)) ||
|
||||||
|
flagOptions[1]
|
||||||
|
);
|
||||||
|
}, [formik.values.flag, flagOptions]);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filterModal.open) {
|
||||||
|
const flagBoolValue =
|
||||||
|
tableFilterState.flagFilter === 'EKSPEDISI' ? true : false;
|
||||||
|
formik.setFieldValue('flag', flagBoolValue);
|
||||||
|
}
|
||||||
|
}, [filterModal.open, tableFilterState.flagFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateFilter('search', searchValue);
|
updateFilter('search', searchValue);
|
||||||
}, [searchValue, updateFilter]);
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableState('suppliers-table', pathname);
|
setTableState('suppliers-table', pathname);
|
||||||
}, [pathname]);
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
@@ -261,7 +381,7 @@ const SuppliersTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search and Filter */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
@@ -282,6 +402,13 @@ const SuppliersTable = () => {
|
|||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -340,6 +467,74 @@ const SuppliersTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={categoryOptions}
|
||||||
|
value={categoryIdValue}
|
||||||
|
onChange={handleFilterCategoryChange}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Flag Ekspedisi'
|
||||||
|
placeholder='Pilih'
|
||||||
|
options={flagOptions}
|
||||||
|
value={flagValue}
|
||||||
|
onChange={handleFilterFlagChange}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,44 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
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';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
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 PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import WarehouseTableSkeleton from '@/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton';
|
import WarehouseTableSkeleton from '@/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton';
|
||||||
|
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
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, AreaApi } from '@/services/api/master-data';
|
||||||
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 { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import {
|
||||||
|
WarehouseFilterSchema,
|
||||||
|
WarehouseFilterType,
|
||||||
|
} from '@/components/pages/master-data/warehouse/filter/WarehouseFilter';
|
||||||
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -114,21 +131,131 @@ const WarehousesTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
|
areaFilter: '',
|
||||||
|
activeProjectFlockFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
areaFilter: 'area_id',
|
||||||
|
activeProjectFlockFilter: 'active_project_flock',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<WarehouseFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
area_id: null,
|
||||||
|
active_project_flock: false,
|
||||||
|
},
|
||||||
|
validationSchema: WarehouseFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
|
updateFilter(
|
||||||
|
'activeProjectFlockFilter',
|
||||||
|
values.active_project_flock === true ? 'true' : ''
|
||||||
|
);
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('areaFilter', '');
|
||||||
|
updateFilter('activeProjectFlockFilter', '');
|
||||||
|
formik.setFieldValue('active_project_flock', false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== AREA OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
|
} = useSelect<Area>(
|
||||||
|
filterModal.open ? AreaApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== ACTIVE PROJECT FLOCK OPTIONS =====
|
||||||
|
const activeProjectFlockOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'true', label: 'Kandang Aktif' },
|
||||||
|
{ value: 'false', label: 'Semua Kandang' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HANDLERS =====
|
||||||
|
const handleFilterAreaChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const area = val as OptionType | null;
|
||||||
|
const areaId = area?.value ? String(area.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('area_id', areaId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterActiveProjectFlockChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const option = val as OptionType | null;
|
||||||
|
const boolValue =
|
||||||
|
option?.value === 'true'
|
||||||
|
? true
|
||||||
|
: option?.value === 'false'
|
||||||
|
? false
|
||||||
|
: null;
|
||||||
|
|
||||||
|
formik.setFieldValue('active_project_flock', boolValue);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const areaIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.area_id) return null;
|
||||||
|
return (
|
||||||
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.area_id, areaOptions]);
|
||||||
|
|
||||||
|
const activeProjectFlockValue = useMemo(() => {
|
||||||
|
if (formik.values.active_project_flock === null) return null;
|
||||||
|
return (
|
||||||
|
activeProjectFlockOptions.find(
|
||||||
|
(opt) => opt.value === String(formik.values.active_project_flock)
|
||||||
|
) || activeProjectFlockOptions[1]
|
||||||
|
);
|
||||||
|
}, [formik.values.active_project_flock, activeProjectFlockOptions]);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filterModal.open) {
|
||||||
|
const activeProjectFlockValue =
|
||||||
|
tableFilterState.activeProjectFlockFilter === 'true' ? true : false; // Default ke false (Semua Kandang)
|
||||||
|
formik.setFieldValue('active_project_flock', activeProjectFlockValue);
|
||||||
|
}
|
||||||
|
}, [filterModal.open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateFilter('search', searchValue);
|
updateFilter('search', searchValue);
|
||||||
}, [searchValue, updateFilter]);
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableState('warehouses-table', pathname);
|
setTableState('warehouses-table', pathname);
|
||||||
}, [pathname]);
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
@@ -264,7 +391,7 @@ const WarehousesTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search and Filter */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
@@ -285,6 +412,13 @@ const WarehousesTable = () => {
|
|||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -344,6 +478,77 @@ const WarehousesTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
options={areaOptions}
|
||||||
|
value={areaIdValue}
|
||||||
|
onChange={handleFilterAreaChange}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Filter Berdasarkan'
|
||||||
|
placeholder='Pilih'
|
||||||
|
options={activeProjectFlockOptions}
|
||||||
|
value={activeProjectFlockValue}
|
||||||
|
onChange={handleFilterActiveProjectFlockChange}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user