From 1fb9687142a75670f926b405d357893d9b40fc01 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Mar 2026 10:44:22 +0700 Subject: [PATCH] feat(FE): Add filter functionality to inventory tables --- .../adjustment/InventoryAdjustmentTable.tsx | 416 +++++++++++++++--- .../adjustment/filter/AdjustmentFilter.ts | 13 + .../inventory/movement/MovementTable.tsx | 357 +++++++++++---- .../movement/filter/MovementFilter.ts | 11 + 4 files changed, 659 insertions(+), 138 deletions(-) create mode 100644 src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts create mode 100644 src/components/pages/inventory/movement/filter/MovementFilter.ts diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index ec5ebe44..fe7a2f77 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -1,22 +1,48 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + ChangeEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { usePathname } from 'next/navigation'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table'; +import { useFormik } from 'formik'; import Button from '@/components/Button'; import Table from '@/components/Table'; import RequirePermission from '@/components/helper/RequirePermission'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import { OptionType } from '@/components/input/SelectInput'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import Modal, { useModal } from '@/components/Modal'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper'; import { InventoryAdjustmentApi } from '@/services/api/inventory'; +import { WarehouseApi, ProductApi } from '@/services/api/master-data'; import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { useUiStore } from '@/stores/ui/ui.store'; import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant'; +import { Product } from '@/types/api/master-data/product'; import StatusBadge from '@/components/helper/StatusBadge'; import InventoryAdjustmentTableSkeleton from '@/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton'; -import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant'; + +import { + AdjustmentFilterSchema, + AdjustmentFilterType, +} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const InventoryAdjustmentTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -30,6 +56,9 @@ const InventoryAdjustmentTable = () => { productSort: '', warehouseSort: '', stockSort: '', + productFilter: '', + warehouseFilter: '', + transactionTypeFilter: '', }, paramMap: { page: 'page', @@ -38,9 +67,133 @@ const InventoryAdjustmentTable = () => { productSort: 'sort_product', warehouseSort: 'sort_warehouse', stockSort: 'sort_stock', + productFilter: 'product_id', + warehouseFilter: 'warehouse_id', + transactionTypeFilter: 'transaction_type', }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + product_id: null, + warehouse_id: null, + transaction_type: null, + }, + validationSchema: AdjustmentFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('productFilter', values.product_id || ''); + updateFilter('warehouseFilter', values.warehouse_id || ''); + updateFilter('transactionTypeFilter', values.transaction_type || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('productFilter', ''); + updateFilter('warehouseFilter', ''); + updateFilter('transactionTypeFilter', ''); + }, + }); + + // ===== PRODUCT OPTIONS ===== + const { + setInputValue: setProductInputValue, + options: productOptions, + isLoadingOptions: isLoadingProductOptions, + loadMore: loadMoreProducts, + } = useSelect( + filterModal.open ? ProductApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== WAREHOUSE OPTIONS ===== + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, + } = useSelect( + filterModal.open ? WarehouseApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== TRANSACTION TYPE OPTIONS ===== + const transactionTypeOptions = useMemo(() => { + return [ + { value: 'increase', label: 'Increase' }, + { value: 'decrease', label: 'Decrease' }, + ]; + }, []); + + // ===== FILTER HANDLERS ===== + const handleFilterProductChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const product = val as OptionType | null; + const productId = product?.value ? String(product.value) : null; + formik.setFieldValue('product_id', productId); + }, + [formik] + ); + + const handleFilterWarehouseChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const warehouse = val as OptionType | null; + const warehouseId = warehouse?.value ? String(warehouse.value) : null; + formik.setFieldValue('warehouse_id', warehouseId); + }, + [formik] + ); + + const handleFilterTransactionTypeChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const type = val as OptionType | null; + const typeValue = type?.value ? String(type.value) : null; + formik.setFieldValue('transaction_type', typeValue); + }, + [formik] + ); + + // ===== FILTER HELPERS ===== + const productIdValue = useMemo(() => { + if (!formik.values.product_id) return null; + return ( + productOptions.find( + (opt) => String(opt.value) === formik.values.product_id + ) || null + ); + }, [formik.values.product_id, productOptions]); + + const warehouseIdValue = useMemo(() => { + if (!formik.values.warehouse_id) return null; + return ( + warehouseOptions.find( + (opt) => String(opt.value) === formik.values.warehouse_id + ) || null + ); + }, [formik.values.warehouse_id, warehouseOptions]); + + const transactionTypeValue = useMemo(() => { + if (!formik.values.transaction_type) return null; + return ( + transactionTypeOptions.find( + (opt) => String(opt.value) === formik.values.transaction_type + ) || null + ); + }, [formik.values.transaction_type, transactionTypeOptions]); + + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + const { data: inventoryAdjustments, isLoading } = useSWR( `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, InventoryAdjustmentApi.getAllFetcher @@ -48,6 +201,19 @@ const InventoryAdjustmentTable = () => { const [sorting, setSorting] = useState([]); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('inventory-adjustment-table', pathname); + }, [pathname, setTableState]); + + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); + updateFilter('search', e.target.value); + }; + const inventoryAdjustmentsColumns: ColumnDef[] = useMemo( () => [ { @@ -182,76 +348,200 @@ const InventoryAdjustmentTable = () => { }, [sorting, updateSortingFilter]); return ( -
- {/* Header Section */} -
-
- - - -
-
- - {/* Table Section */} -
- {isLoading ? ( -
- + <> +
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + +
- ) : !isResponseSuccess(inventoryAdjustments) || - inventoryAdjustments.data?.length === 0 ? ( -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + +
- ) : ( - - data={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.data - : [] - } - columns={inventoryAdjustmentsColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.meta?.total_results - : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0'), - headerColumnClassName: 'text-nowrap', - }} - /> - )} +
+ + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(inventoryAdjustments) || + inventoryAdjustments.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.data + : [] + } + columns={inventoryAdjustmentsColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
-
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
+ ); }; diff --git a/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts new file mode 100644 index 00000000..4568618f --- /dev/null +++ b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts @@ -0,0 +1,13 @@ +import { string, object } from 'yup'; + +export const AdjustmentFilterSchema = object().shape({ + product_id: string().nullable(), + warehouse_id: string().nullable(), + transaction_type: string().nullable(), +}); + +export type AdjustmentFilterType = { + product_id: string | null; + warehouse_id: string | null; + transaction_type: string | null; +}; diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index f53b0ccb..597537d7 100644 --- a/src/components/pages/inventory/movement/MovementTable.tsx +++ b/src/components/pages/inventory/movement/MovementTable.tsx @@ -1,22 +1,42 @@ 'use client'; -import { ChangeEventHandler, useMemo, useState } from 'react'; +import { + ChangeEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { usePathname } from 'next/navigation'; import useSWR from 'swr'; import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table'; +import { useFormik } from 'formik'; import Table from '@/components/Table'; import { Icon } from '@iconify/react'; import { Movement } from '@/types/api/inventory/movement'; import { MovementApi } from '@/services/api/inventory'; +import { WarehouseApi, ProductApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { useUiStore } from '@/stores/ui/ui.store'; import Button from '@/components/Button'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import { OptionType } from '@/components/input/SelectInput'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import Modal, { useModal } from '@/components/Modal'; import RequirePermission from '@/components/helper/RequirePermission'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import MovementTableSkeleton from '@/components/pages/inventory/movement/skeleton/MovementTableSkeleton'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { Product } from '@/types/api/master-data/product'; +import { + MovementFilterSchema, + MovementFilterType, +} from '@/components/pages/inventory/movement/filter/MovementFilter'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -70,6 +90,9 @@ const RowOptionsMenu = ({ }; const MovementTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -79,13 +102,105 @@ const MovementTable = () => { } = useTableFilter({ initial: { search: '', + productFilter: '', + warehouseFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', + productFilter: 'product_id', + warehouseFilter: 'warehouse_id', }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + product_id: null, + warehouse_id: null, + }, + validationSchema: MovementFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('productFilter', values.product_id || ''); + updateFilter('warehouseFilter', values.warehouse_id || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('productFilter', ''); + updateFilter('warehouseFilter', ''); + }, + }); + + // ===== PRODUCT OPTIONS ===== + const { + setInputValue: setProductInputValue, + options: productOptions, + isLoadingOptions: isLoadingProductOptions, + loadMore: loadMoreProducts, + } = useSelect( + filterModal.open ? ProductApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== WAREHOUSE OPTIONS ===== + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, + } = useSelect( + filterModal.open ? WarehouseApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== FILTER HANDLERS ===== + const handleFilterProductChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const product = val as OptionType | null; + const productId = product?.value ? String(product.value) : null; + formik.setFieldValue('product_id', productId); + }, + [formik] + ); + + const handleFilterWarehouseChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const warehouse = val as OptionType | null; + const warehouseId = warehouse?.value ? String(warehouse.value) : null; + formik.setFieldValue('warehouse_id', warehouseId); + }, + [formik] + ); + + // ===== FILTER HELPERS ===== + const productIdValue = useMemo(() => { + if (!formik.values.product_id) return null; + return ( + productOptions.find((opt) => String(opt.value) === formik.values.product_id) || null + ); + }, [formik.values.product_id, productOptions]); + + const warehouseIdValue = useMemo(() => { + if (!formik.values.warehouse_id) return null; + return ( + warehouseOptions.find((opt) => String(opt.value) === formik.values.warehouse_id) || null + ); + }, [formik.values.warehouse_id, warehouseOptions]); + + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + const [sorting, setSorting] = useState([]); const { data: movements, isLoading } = useSWR( @@ -93,7 +208,16 @@ const MovementTable = () => { MovementApi.getAllFetcher ); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('movement-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -160,85 +284,168 @@ const MovementTable = () => { ); return ( -
- {/* Header Section */} -
- {/* Action Buttons */} -
- - - -
- - {/* Search */} -
- - } - className={{ - wrapper: 'w-full min-w-24 max-w-3xs', - inputWrapper: 'rounded-xl! shadow-button-soft', - input: - 'placeholder:font-semibold placeholder:text-base-content/50', - }} - /> -
-
- - {/* Table Section */} -
- {isLoading ? ( -
- + <> +
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + +
- ) : !isResponseSuccess(movements) || movements.data?.length === 0 ? ( -
- + + {/* Search and Filter */} +
+ } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + +
- ) : ( - - data={isResponseSuccess(movements) ? movements?.data : []} - columns={movementColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(movements) ? movements?.meta?.page : 0} - totalItems={ - isResponseSuccess(movements) ? movements?.meta?.total_results : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0'), - headerColumnClassName: 'text-nowrap', - }} - /> - )} +
+ + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(movements) || movements.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(movements) ? movements?.data : []} + columns={movementColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(movements) ? movements?.meta?.page : 0} + totalItems={ + isResponseSuccess(movements) ? movements?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
-
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + +
+ + {/* Modal Footer */} +
+ + +
+
+
+ ); }; diff --git a/src/components/pages/inventory/movement/filter/MovementFilter.ts b/src/components/pages/inventory/movement/filter/MovementFilter.ts new file mode 100644 index 00000000..fc27b898 --- /dev/null +++ b/src/components/pages/inventory/movement/filter/MovementFilter.ts @@ -0,0 +1,11 @@ +import { string, object } from 'yup'; + +export const MovementFilterSchema = object().shape({ + product_id: string().nullable(), + warehouse_id: string().nullable(), +}); + +export type MovementFilterType = { + product_id: string | null; + warehouse_id: string | null; +};