diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 912e8dfd..d3586e98 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -1,6 +1,8 @@ 'use client'; import { ChangeEventHandler, useEffect, useState, useMemo } from 'react'; +import { usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; @@ -91,6 +93,9 @@ const RowOptionsMenu = ({ }; const ClosingsTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + // ===== ROUTER ===== const router = useRouter(); @@ -289,8 +294,17 @@ const ClosingsTable = () => { ); }, [formik.values.project_status, projectStatusOptions]); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('closing-table', pathname); + }, [pathname, setTableState]); + // ===== SEARCH CHANGE HANDLER ===== const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 849c1f83..d9118fdf 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -1,6 +1,8 @@ 'use client'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; +import { usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import { CellContext, @@ -151,6 +153,9 @@ const RowOptionsMenu = ({ }; const ExpensesTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -507,7 +512,16 @@ const ExpensesTable = () => { setIsRejectLoading(false); }; + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('expense-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; 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/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 09626d22..b401f11c 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -776,6 +776,16 @@ const InventoryAdjustmentForm = ({ > + {InventoryAdjustmentFormErrorMessage && ( +
+ + {InventoryAdjustmentFormErrorMessage} +
+ )}
{type !== 'detail' && ( <> @@ -799,16 +809,6 @@ const InventoryAdjustmentForm = ({ )}
- {InventoryAdjustmentFormErrorMessage && ( -
- - {InventoryAdjustmentFormErrorMessage} -
- )} diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index c85577de..f953099d 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,109 @@ 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 +212,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 +288,174 @@ 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 ? ( -
- + } + 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; +}; diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index 53d8fcb3..21ded2bc 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -8,10 +8,12 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { InventoryProductApi } from '@/services/api/inventory'; import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { useUiStore } from '@/stores/ui/ui.store'; import { InventoryProduct } from '@/types/api/inventory/product'; import { Icon } from '@iconify/react'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; -import { ChangeEventHandler, useMemo, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; +import { usePathname } from 'next/navigation'; import useSWR from 'swr'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; @@ -69,6 +71,9 @@ const RowOptionsMenu = ({ }; const InventoryProductTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -92,7 +97,16 @@ const InventoryProductTable = () => { InventoryProductApi.getAllFetcher ); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('inventory-product-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -189,7 +203,7 @@ const InventoryProductTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -109,7 +114,7 @@ const AreasTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: '', + search: searchValue, }, paramMap: { page: 'page', @@ -132,7 +137,16 @@ const AreasTable = () => { const [selectedArea, setSelectedArea] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('areas-table', pathname); + }, [pathname]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -219,7 +233,7 @@ const AreasTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -109,7 +114,7 @@ const BanksTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: '', + search: searchValue, }, paramMap: { page: 'page', @@ -132,7 +137,16 @@ const BanksTable = () => { const [selectedBank, setSelectedBank] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('banks-table', pathname); + }, [pathname]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -232,7 +246,7 @@ const BanksTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -109,7 +114,7 @@ const CustomersTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: '', + search: searchValue, }, paramMap: { page: 'page', @@ -134,7 +139,16 @@ const CustomersTable = () => { >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('customers-table', pathname); + }, [pathname]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -239,7 +253,7 @@ const CustomersTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -109,7 +114,7 @@ const FlockTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: '', + search: searchValue, }, paramMap: { page: 'page', @@ -134,7 +139,16 @@ const FlockTable = () => { ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('flocks-table', pathname); + }, [pathname]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -227,7 +241,7 @@ const FlockTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -111,13 +134,110 @@ const KandangsTable = () => { } = useTableFilter({ initial: { 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 { @@ -135,7 +255,16 @@ const KandangsTable = () => { ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('kandangs-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -233,11 +362,11 @@ const KandangsTable = () => {
- {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -312,6 +448,81 @@ const KandangsTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/kandang/filter/KandangFilter.ts b/src/components/pages/master-data/kandang/filter/KandangFilter.ts new file mode 100644 index 00000000..30132611 --- /dev/null +++ b/src/components/pages/master-data/kandang/filter/KandangFilter.ts @@ -0,0 +1,11 @@ +import { string, object } from 'yup'; + +export const KandangFilterSchema = object().shape({ + location_id: string().nullable(), + pic_id: string().nullable(), +}); + +export type KandangFilterType = { + location_id: string | null; + pic_id: string | null; +}; diff --git a/src/components/pages/master-data/location/LocationsTable.tsx b/src/components/pages/master-data/location/LocationsTable.tsx index 0b619079..89a00539 100644 --- a/src/components/pages/master-data/location/LocationsTable.tsx +++ b/src/components/pages/master-data/location/LocationsTable.tsx @@ -1,25 +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 { 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 { useUiStore } from '@/stores/ui/ui.store'; +import { + LocationFilterSchema, + LocationFilterType, +} from '@/components/pages/master-data/location/filter/LocationFilter'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -101,6 +118,9 @@ const RowOptionsMenu = ({ }; const LocationsTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -110,14 +130,72 @@ const LocationsTable = () => { } = useTableFilter({ initial: { 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, @@ -134,7 +212,18 @@ const LocationsTable = () => { >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('locations-table', pathname); + }, [pathname, setTableState]); + + const [sorting, setSorting] = useState([]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -227,11 +316,11 @@ const LocationsTable = () => {
- {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -306,6 +402,68 @@ const LocationsTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/location/filter/LocationFilter.ts b/src/components/pages/master-data/location/filter/LocationFilter.ts new file mode 100644 index 00000000..1235d782 --- /dev/null +++ b/src/components/pages/master-data/location/filter/LocationFilter.ts @@ -0,0 +1,9 @@ +import { string, object } from 'yup'; + +export const LocationFilterSchema = object().shape({ + area_id: string().nullable(), +}); + +export type LocationFilterType = { + area_id: string | null; +}; diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 8f15e529..0e009313 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useMemo, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -20,6 +20,8 @@ import { Nonstock } from '@/types/api/master-data/nonstock'; import { NonstockApi } 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'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -101,6 +103,9 @@ const RowOptionsMenu = ({ }; const NonstocksTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -109,7 +114,7 @@ const NonstocksTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: '', + search: searchValue, }, paramMap: { page: 'page', @@ -117,6 +122,14 @@ const NonstocksTable = () => { }, }); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('nonstocks-table', pathname); + }, [pathname]); + const [sorting, setSorting] = useState([]); const { @@ -135,6 +148,7 @@ const NonstocksTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -236,7 +250,7 @@ const NonstocksTable = () => {
{ - const { searchValue, setSearchValue, resetSearchValue } = useUiStore(); - const previousPathRef = useRef(null); + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); const { state: tableFilterState, @@ -119,7 +120,7 @@ const ProductCategoryTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: searchValue, + search: '', }, paramMap: { page: 'page', @@ -127,6 +128,10 @@ const ProductCategoryTable = () => { }, }); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + const [sorting, setSorting] = useState([]); const { @@ -216,26 +221,8 @@ const ProductCategoryTable = () => { ); useEffect(() => { - // Store current path on mount - previousPathRef.current = window.location.pathname; - - return () => { - const currentPath = window.location.pathname; - - // if both paths are within /master-data/product-category module - const isCurrentPathProductCategory = currentPath.includes( - '/master-data/product-category' - ); - const isPreviousPathProductCategory = previousPathRef.current?.includes( - '/master-data/product-category' - ); - - // reset if we outside product category module entirely - if (isPreviousPathProductCategory && !isCurrentPathProductCategory) { - resetSearchValue(); - } - }; - }, [resetSearchValue]); + setTableState('product-category-table', pathname); + }, [pathname]); return ( <> @@ -260,7 +247,7 @@ const ProductCategoryTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -111,13 +131,78 @@ 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]); + const [sorting, setSorting] = useState([]); const { @@ -135,7 +220,12 @@ const ProductsTable = () => { ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + useEffect(() => { + setTableState('product-table', pathname); + }, [pathname]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -278,11 +368,11 @@ const ProductsTable = () => {
- {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -357,6 +454,68 @@ const ProductsTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/product/filter/ProductFilter.ts b/src/components/pages/master-data/product/filter/ProductFilter.ts new file mode 100644 index 00000000..365dc5de --- /dev/null +++ b/src/components/pages/master-data/product/filter/ProductFilter.ts @@ -0,0 +1,9 @@ +import { string, object } from 'yup'; + +export const ProductFilterSchema = object().shape({ + product_category_id: string().nullable(), +}); + +export type ProductFilterType = { + product_category_id: string | null; +}; diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx index 0ff8b594..d843a929 100644 --- a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -1,23 +1,34 @@ 'use client'; -import { 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 { 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', @@ -99,6 +110,85 @@ const RowOptionsMenu = ({ }; 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, setTableState]); + const [sorting, setSorting] = useState([]); const { @@ -106,7 +196,7 @@ const ProductionStandardTable = () => { isLoading, mutate: refreshProductionStandards, } = useSWR( - `${ProductionStandardApi.basePath}`, + `${ProductionStandardApi.basePath}${getTableFilterQueryString()}`, ProductionStandardApi.getAllFetcher ); @@ -140,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', @@ -176,7 +269,7 @@ const ProductionStandardTable = () => { }, }, ], - [deleteModal] + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); return ( @@ -197,6 +290,16 @@ const ProductionStandardTable = () => {
+ + {/* Filter */} +
+ +
{/* Table Section */} @@ -224,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} @@ -250,6 +358,65 @@ const ProductionStandardTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/production-standard/filter/ProductionStandardFilter.ts b/src/components/pages/master-data/production-standard/filter/ProductionStandardFilter.ts new file mode 100644 index 00000000..cec77dda --- /dev/null +++ b/src/components/pages/master-data/production-standard/filter/ProductionStandardFilter.ts @@ -0,0 +1,9 @@ +import { string, object } from 'yup'; + +export const ProductionStandardFilterSchema = object().shape({ + project_category: string().nullable(), +}); + +export type ProductionStandardFilterType = { + project_category: string | null; +}; diff --git a/src/components/pages/master-data/supplier/SupplierTable.tsx b/src/components/pages/master-data/supplier/SupplierTable.tsx index 2b6cb227..60892876 100644 --- a/src/components/pages/master-data/supplier/SupplierTable.tsx +++ b/src/components/pages/master-data/supplier/SupplierTable.tsx @@ -1,25 +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 { 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 { 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', @@ -101,6 +118,9 @@ const RowOptionsMenu = ({ }; const SuppliersTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -110,13 +130,126 @@ const SuppliersTable = () => { } = useTableFilter({ initial: { 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, setTableState]); + const [sorting, setSorting] = useState([]); const { @@ -135,6 +268,7 @@ const SuppliersTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -247,11 +381,11 @@ const SuppliersTable = () => {
- {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -326,6 +467,74 @@ const SuppliersTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/supplier/filter/SupplierFilter.ts b/src/components/pages/master-data/supplier/filter/SupplierFilter.ts new file mode 100644 index 00000000..4eaa3a1f --- /dev/null +++ b/src/components/pages/master-data/supplier/filter/SupplierFilter.ts @@ -0,0 +1,11 @@ +import { string, boolean, object } from 'yup'; + +export const SupplierFilterSchema = object().shape({ + category_id: string().nullable(), + flag: boolean().nullable(), +}); + +export type SupplierFilterType = { + category_id: string | null; + flag: boolean | null; +}; diff --git a/src/components/pages/master-data/uom/UomsTable.tsx b/src/components/pages/master-data/uom/UomsTable.tsx index aeaae276..9ef9f9ef 100644 --- a/src/components/pages/master-data/uom/UomsTable.tsx +++ b/src/components/pages/master-data/uom/UomsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useMemo, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -20,6 +20,8 @@ import { Uom } from '@/types/api/master-data/uom'; import { UomApi } 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'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -101,6 +103,9 @@ const RowOptionsMenu = ({ }; const UomsTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -109,7 +114,7 @@ const UomsTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: '', + search: searchValue, }, paramMap: { page: 'page', @@ -117,6 +122,14 @@ const UomsTable = () => { }, }); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('uoms-table', pathname); + }, [pathname]); + const [sorting, setSorting] = useState([]); const { @@ -133,6 +146,7 @@ const UomsTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -219,7 +233,7 @@ const UomsTable = () => {
{ + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -110,13 +132,131 @@ const WarehousesTable = () => { } = useTableFilter({ initial: { 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, setTableState]); + const [sorting, setSorting] = useState([]); const { @@ -135,6 +275,7 @@ const WarehousesTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -250,11 +391,11 @@ const WarehousesTable = () => {
- {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + +
@@ -330,6 +478,77 @@ const WarehousesTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/master-data/warehouse/filter/WarehouseFilter.ts b/src/components/pages/master-data/warehouse/filter/WarehouseFilter.ts new file mode 100644 index 00000000..1a9a5593 --- /dev/null +++ b/src/components/pages/master-data/warehouse/filter/WarehouseFilter.ts @@ -0,0 +1,11 @@ +import { string, boolean, object } from 'yup'; + +export const WarehouseFilterSchema = object().shape({ + area_id: string().nullable(), + active_project_flock: boolean().nullable(), +}); + +export type WarehouseFilterType = { + area_id: string | null; + active_project_flock: boolean | null; +}; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 14378852..1ae56fa2 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -21,8 +21,9 @@ import { Kandang } from '@/types/api/master-data/kandang'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { Icon } from '@iconify/react'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; -import { useRouter } from 'next/navigation'; +import { useRouter, usePathname } from 'next/navigation'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; +import { useUiStore } from '@/stores/ui/ui.store'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { useFormik } from 'formik'; @@ -148,6 +149,9 @@ const RowOptionsMenu = ({ }; const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const isSuccess = useProjectFlockStore((s) => s.isSuccess); const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess); const createdProjectFlock = useProjectFlockStore( @@ -416,7 +420,16 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setIsDeleteLoading(false); setRowSelection({}); }; + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('project-flock-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; const confirmApprovalHandler = async ( diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 8f59d7c3..ca9a12eb 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -44,6 +44,7 @@ import Badge from '@/components/Badge'; import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import { useUiStore } from '@/stores/ui/ui.store'; +import { usePathname } from 'next/navigation'; import { Color } from '@/types/theme'; import ButtonFilter from '@/components/helper/ButtonFilter'; @@ -203,8 +204,8 @@ const RowOptionsMenu = ({ }; const RecordingTable = () => { - const { searchValue, setSearchValue, resetSearchValue } = useUiStore(); - const previousPathRef = useRef(null); + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); const { state: tableFilterState, @@ -214,7 +215,7 @@ const RecordingTable = () => { toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { - search: searchValue, + search: '', areaFilter: '', locationFilter: '', kandangFilter: '', @@ -229,6 +230,10 @@ const RecordingTable = () => { }, }); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + // ===== FILTER MODAL STATE ===== const filterModal = useModal(); @@ -526,23 +531,8 @@ const RecordingTable = () => { }, []); useEffect(() => { - previousPathRef.current = window.location.pathname; - - return () => { - const currentPath = window.location.pathname; - - const isCurrentPathRecording = currentPath.includes( - '/production/recording' - ); - const isPreviousPathRecording = previousPathRef.current?.includes( - '/production/recording' - ); - - if (isPreviousPathRecording && !isCurrentPathRecording) { - resetSearchValue(); - } - }; - }, [resetSearchValue]); + setTableState('recording-table', pathname); + }, [pathname]); const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 0b8a299f..e3177791 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -1,6 +1,8 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import { CellContext, @@ -121,6 +123,9 @@ const RowOptionsMenu = ({ }; const TransferToLayingsTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -413,7 +418,16 @@ const TransferToLayingsTable = () => { setIsRejectLoading(false); }; + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('transfer-to-laying-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 9e543902..8c3f5b88 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -1,7 +1,8 @@ 'use client'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams, usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -184,6 +185,8 @@ const UniformityChartWrapper = ({ const UniformityTable = () => { const router = useRouter(); const searchParams = useSearchParams(); + const pathname = usePathname(); + const { searchValue, setSearchValue, setTableState } = useUiStore(); const isSuccess = useUniformityStore((s) => s.isSuccess); const setIsSuccess = useUniformityStore((s) => s.setIsSuccess); const createdUniformity = useUniformityStore((s) => s.createdUniformity); @@ -218,6 +221,14 @@ const UniformityTable = () => { }, }); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('uniformity-table', pathname); + }, [pathname, setTableState]); + const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const [selectedUniformity, setSelectedUniformity] = useState< diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index e84d56d3..e15676cd 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -1,6 +1,14 @@ 'use client'; -import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; +import { + ChangeEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; @@ -139,6 +147,9 @@ const RowOptionsMenu = ({ }; const PurchaseTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + // ===== STATE MANAGEMENT ===== const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [selectedPurchase, setSelectedPurchase] = useState( @@ -385,11 +396,20 @@ const PurchaseTable = () => { setIsDeleteLoading(false); }, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); + + useEffect(() => { + setTableState('purchase-table', pathname); + }, [pathname, setTableState]); + const searchChangeHandler: ChangeEventHandler = useCallback( (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }, - [updateFilter] + [updateFilter, setSearchValue] ); const pageSizeChangeHandler = useCallback( diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 35882869..464bee47 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -175,26 +175,54 @@ const PurchaseOrderAcceptApprovalForm = ({ validateOnBlur: true, enableReinitialize: false, onSubmit: async (values) => { + type ItemPayload = { + purchase_item_id: number; + received_date: string; + travel_number: string; + received_qty: number; + vehicle_number?: string; + expedition_vendor_id?: number; + transport_per_item?: number; + }; + const payload: CreateAcceptApprovalRequestPayload = { action: 'APPROVED', notes: values.notes || '', items: values.items?.map((formItem) => { - return { + const item: ItemPayload = { purchase_item_id: formItem.purchase_item_id || 0, received_date: formItem.received_date || '', travel_number: formItem.travel_number || '', - vehicle_number: formItem.vehicle_number || null, - expedition_vendor_id: formItem.expedition_vendor_id || null, received_qty: typeof formItem.received_qty === 'string' ? parseFloat(formItem.received_qty) || 0 : formItem.received_qty || 0, - transport_per_item: - typeof formItem.transport_per_item === 'string' - ? parseFloat(formItem.transport_per_item) || 0 - : formItem.transport_per_item || null, }; + + if ( + formItem.vehicle_number && + formItem.vehicle_number.trim() !== '' + ) { + item.vehicle_number = formItem.vehicle_number; + } + + if ( + formItem.expedition_vendor_id && + formItem.expedition_vendor_id !== 0 + ) { + item.expedition_vendor_id = formItem.expedition_vendor_id; + } + + const transportValue = + typeof formItem.transport_per_item === 'string' + ? parseFloat(formItem.transport_per_item) || 0 + : formItem.transport_per_item || 0; + if (transportValue > 0) { + item.transport_per_item = transportValue; + } + + return item; }) || [], travel_documents: values.travel_documents @@ -258,7 +286,7 @@ const PurchaseOrderAcceptApprovalForm = ({ if (purchaseItems.length > 0 && initialValues?.items) { const updatedItems = initialValues.items.map((item) => { const expeditionVendorId = - item.expedition_vendor_id || item.expedition_vendor?.id || 0; + item.expedition_vendor_id || item.expedition_vendor?.id || null; return { purchase_item: null, @@ -327,10 +355,14 @@ const PurchaseOrderAcceptApprovalForm = ({ formik.setFieldTouched(`items.${idx}.expedition_vendor`, true); formik.setFieldValue(`items.${idx}.expedition_vendor`, expeditionVendor); formik.setFieldTouched(`items.${idx}.expedition_vendor_id`, true); - formik.setFieldValue( - `items.${idx}.expedition_vendor_id`, - expeditionVendor?.value || 0 - ); + if (expeditionVendor?.value) { + formik.setFieldValue( + `items.${idx}.expedition_vendor_id`, + expeditionVendor.value + ); + } else { + formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null); + } }; // ===== PURCHASE ITEM OPERATIONS ===== diff --git a/src/stores/ui/slices/table.slice.ts b/src/stores/ui/slices/table.slice.ts index eb6e7cc2..bf9a8686 100644 --- a/src/stores/ui/slices/table.slice.ts +++ b/src/stores/ui/slices/table.slice.ts @@ -1,13 +1,34 @@ import { StateCreator } from 'zustand'; +import { TableUISlice } from '@/types/stores'; export interface TableState { + key: string; + path: string; searchValue: string; } -export interface TableUISlice { - searchValue: string; - setSearchValue: (value: string) => void; - resetSearchValue: () => void; +function getPathGroup(path: string): string { + const segments = path.split('/').filter(Boolean); + const subPaths = ['add', 'detail', 'edit']; + const lastSegment = segments[segments.length - 1]; + + // If last segment is a sub-path or numeric ID, remove it + if ( + lastSegment && + (subPaths.includes(lastSegment) || /^\d+$/.test(lastSegment)) + ) { + segments.pop(); + } + + return segments.length > 0 ? '/' + segments.join('/') : path; +} + +function isSamePathGroup( + currentPath: string, + previousPath: string | null +): boolean { + if (!previousPath) return false; + return getPathGroup(currentPath) === getPathGroup(previousPath); } export const createTableUISlice: StateCreator< @@ -17,7 +38,10 @@ export const createTableUISlice: StateCreator< TableUISlice > = (set) => ({ // Initial state + key: '', + path: '', searchValue: '', + previousPath: null, // Actions setSearchValue: (value) => set({ searchValue: value }), @@ -25,4 +49,27 @@ export const createTableUISlice: StateCreator< resetSearchValue: () => { return set({ searchValue: '' }); }, + + setTableState: (key, path, searchValue = '') => { + set((state) => { + const isSameGroup = isSamePathGroup(path, state.path); + + return { + key, + path, + previousPath: state.path, + // Reset search if path group changed, otherwise keep existing search + searchValue: isSameGroup ? state.searchValue : searchValue, + }; + }); + }, + + resetTableState: () => { + set({ + key: '', + path: '', + searchValue: '', + previousPath: null, + }); + }, }); diff --git a/src/stores/ui/ui.store.ts b/src/stores/ui/ui.store.ts index 46c70283..4b8379db 100644 --- a/src/stores/ui/ui.store.ts +++ b/src/stores/ui/ui.store.ts @@ -19,9 +19,12 @@ export const useUiStore = create()( ...createNavbarActionsSlice(...args), }), { - name: 'ui-cache', + name: 'search-store', partialize: (state) => ({ + key: state.key, + path: state.path, searchValue: state.searchValue, + previousPath: state.previousPath, }), } ), diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index c358e2a1..331bbb82 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -28,9 +28,17 @@ type DrawerUISlice = { }; type TableUISlice = { + // State + key: string; + path: string; searchValue: string; + previousPath: string | null; + + // Actions setSearchValue: (value: string) => void; resetSearchValue: () => void; + setTableState: (key: string, path: string, searchValue?: string) => void; + resetTableState: () => void; }; // Navbar Actions Slice