From 5e5400f56b835675904c2b296e835458e93b29d3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 3 Mar 2026 14:03:29 +0700 Subject: [PATCH] feat(FE): Add advanced filtering capabilities to master data tables --- .../master-data/kandang/KandangsTable.tsx | 211 ++++++++++++++++- .../master-data/location/LocationsTable.tsx | 160 ++++++++++++- .../master-data/product/ProductTable.tsx | 153 +++++++++++- .../ProductionStandardTable.tsx | 172 +++++++++++++- .../master-data/supplier/SupplierTable.tsx | 207 ++++++++++++++++- .../master-data/warehouse/WarehousesTable.tsx | 219 +++++++++++++++++- 6 files changed, 1083 insertions(+), 39 deletions(-) diff --git a/src/components/pages/master-data/kandang/KandangsTable.tsx b/src/components/pages/master-data/kandang/KandangsTable.tsx index 90d00b29..4e54c728 100644 --- a/src/components/pages/master-data/kandang/KandangsTable.tsx +++ b/src/components/pages/master-data/kandang/KandangsTable.tsx @@ -1,28 +1,46 @@ '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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; +import Modal, { 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 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 { 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 { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { usePathname } from 'next/navigation'; 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 = ({ popoverPosition = 'bottom', @@ -115,14 +133,111 @@ const KandangsTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: searchValue, + search: '', + locationFilter: '', + picFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', + locationFilter: 'location_id', + picFilter: 'pic_id', }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + 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( + filterModal.open ? LocationApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== PIC OPTIONS ===== + const { + setInputValue: setPicInputValue, + options: picOptions, + isLoadingOptions: isLoadingPicOptions, + loadMore: loadMorePics, + } = useSelect( + 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([]); const { @@ -146,7 +261,7 @@ const KandangsTable = () => { useEffect(() => { setTableState('kandangs-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); @@ -247,7 +362,7 @@ const KandangsTable = () => { - {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -326,6 +448,81 @@ const KandangsTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/location/LocationsTable.tsx b/src/components/pages/master-data/location/LocationsTable.tsx index 82352dc8..d2d14417 100644 --- a/src/components/pages/master-data/location/LocationsTable.tsx +++ b/src/components/pages/master-data/location/LocationsTable.tsx @@ -1,27 +1,42 @@ '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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; +import Modal, { 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 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 { 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 { useTableFilter } from '@/services/hooks/useTableFilter'; -import { usePathname } from 'next/navigation'; import { useUiStore } from '@/stores/ui/ui.store'; +import { + LocationFilterSchema, + LocationFilterType, +} from '@/components/pages/master-data/location/filter/LocationFilter'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -114,15 +129,73 @@ const LocationsTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: searchValue, + search: '', + areaFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', + areaFilter: 'area_id', }, }); - const [sorting, setSorting] = useState([]); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + 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( + 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 { data: locations, @@ -145,7 +218,9 @@ const LocationsTable = () => { useEffect(() => { setTableState('locations-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); + + const [sorting, setSorting] = useState([]); const searchChangeHandler: ChangeEventHandler = (e) => { setSearchValue(e.target.value); @@ -241,7 +316,7 @@ const LocationsTable = () => { - {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -320,6 +402,68 @@ const LocationsTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index d16bffe2..3e522407 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -1,28 +1,43 @@ '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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; +import Modal, { 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 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 { 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 { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useUiStore } from '@/stores/ui/ui.store'; +import { + ProductFilterSchema, + ProductFilterType, +} from '@/components/pages/master-data/product/filter/ProductFilter'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -116,13 +131,74 @@ const ProductsTable = () => { } = useTableFilter({ initial: { search: '', + productCategoryFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', + productCategoryFilter: 'product_category_id', }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + 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( + 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(() => { updateFilter('search', searchValue); }, [searchValue, updateFilter]); @@ -292,7 +368,7 @@ const ProductsTable = () => { - {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -371,6 +454,68 @@ const ProductsTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx index 2e788ed3..d843a929 100644 --- a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -1,25 +1,34 @@ '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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; +import Modal, { 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 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 { ProductionStandardApi } from '@/services/api/master-data'; 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 { + ProductionStandardFilterSchema, + ProductionStandardFilterType, +} from '@/components/pages/master-data/production-standard/filter/ProductionStandardFilter'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -104,9 +113,81 @@ const ProductionStandardTable = () => { const { setTableState } = useUiStore(); 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({ + 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(() => { setTableState('production-standard-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const [sorting, setSorting] = useState([]); @@ -115,7 +196,7 @@ const ProductionStandardTable = () => { isLoading, mutate: refreshProductionStandards, } = useSWR( - `${ProductionStandardApi.basePath}`, + `${ProductionStandardApi.basePath}${getTableFilterQueryString()}`, ProductionStandardApi.getAllFetcher ); @@ -149,7 +230,10 @@ const ProductionStandardTable = () => { () => [ { header: 'No', - cell: (props) => props.row.index + 1, + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, }, { accessorKey: 'name', @@ -185,7 +269,7 @@ const ProductionStandardTable = () => { }, }, ], - [deleteModal] + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); return ( @@ -206,6 +290,16 @@ const ProductionStandardTable = () => { + + {/* Filter */} +
+ +
{/* Table Section */} @@ -233,6 +327,11 @@ const ProductionStandardTable = () => { data={productionStandards.data} columns={productionStandardColumns} + pageSize={tableFilterState.pageSize} + page={productionStandards?.meta?.page ?? 0} + totalItems={productionStandards?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} isLoading={false} sorting={sorting} setSorting={setSorting} @@ -259,6 +358,65 @@ const ProductionStandardTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/supplier/SupplierTable.tsx b/src/components/pages/master-data/supplier/SupplierTable.tsx index b7fd571a..4d9d97bc 100644 --- a/src/components/pages/master-data/supplier/SupplierTable.tsx +++ b/src/components/pages/master-data/supplier/SupplierTable.tsx @@ -1,27 +1,42 @@ '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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; +import Modal, { 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 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 { SupplierApi } from '@/services/api/master-data'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { usePathname } from 'next/navigation'; 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 = ({ popoverPosition = 'bottom', @@ -114,21 +129,126 @@ const SuppliersTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: searchValue, + search: '', + categoryFilter: '', + flagFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', + categoryFilter: 'category_id', + flagFilter: 'flag', }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + 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(() => { updateFilter('search', searchValue); }, [searchValue, updateFilter]); useEffect(() => { setTableState('suppliers-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const [sorting, setSorting] = useState([]); @@ -261,7 +381,7 @@ const SuppliersTable = () => { - {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -340,6 +467,74 @@ const SuppliersTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/warehouse/WarehousesTable.tsx b/src/components/pages/master-data/warehouse/WarehousesTable.tsx index 7cf6f918..ed777ee9 100644 --- a/src/components/pages/master-data/warehouse/WarehousesTable.tsx +++ b/src/components/pages/master-data/warehouse/WarehousesTable.tsx @@ -1,27 +1,44 @@ '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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import { useModal } from '@/components/Modal'; +import Modal, { 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 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 { WarehouseApi } from '@/services/api/master-data'; +import { WarehouseApi, AreaApi } from '@/services/api/master-data'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { usePathname } from 'next/navigation'; 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 = ({ popoverPosition = 'bottom', @@ -114,21 +131,131 @@ const WarehousesTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: searchValue, + search: '', + areaFilter: '', + activeProjectFlockFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', + areaFilter: 'area_id', + activeProjectFlockFilter: 'active_project_flock', }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + 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( + 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(() => { updateFilter('search', searchValue); }, [searchValue, updateFilter]); useEffect(() => { setTableState('warehouses-table', pathname); - }, [pathname]); + }, [pathname, setTableState]); const [sorting, setSorting] = useState([]); @@ -264,7 +391,7 @@ const WarehousesTable = () => { - {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -344,6 +478,77 @@ const WarehousesTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
); };