From c040c0e9bb2f940d74e441d408ce395861392ebf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 2 Apr 2026 09:51:22 +0700 Subject: [PATCH 1/3] feat: create PurchaseFilterModal component --- .../pages/purchase/PurchaseFilterModal.tsx | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/components/pages/purchase/PurchaseFilterModal.tsx diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx new file mode 100644 index 00000000..a9cd00cd --- /dev/null +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { RefObject, useState, useEffect } from 'react'; +import { useFormik } from 'formik'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; + +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { PurchaseFilter } from '@/types/api/purchase/purchase'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { ProductCategoryApi } from '@/services/api/master-data'; +import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line'; + +interface PurchaseFilterModalProps { + ref: RefObject; + onSubmit?: (values: PurchaseFilter) => void; + onReset?: () => void; +} + +const PurchaseFilterModal = ({ + ref, + onSubmit, + onReset, +}: PurchaseFilterModalProps) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + + // ===== CLEANUP TOAST ON UNMOUNT ===== + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + // ===== CLEANUP TOAST WHEN MODAL CLOSES ===== + useEffect(() => { + const dialogElement = ref.current; + const handleModalClose = () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + + dialogElement?.addEventListener('close', handleModalClose); + + return () => { + dialogElement?.removeEventListener('close', handleModalClose); + }; + }, [ref, dateErrorShown]); + + const { + setInputValue: setProductCategoryInputValue, + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategoryOptions, + loadMore: loadMoreProductCategory, + } = useSelect( + ProductCategoryApi.basePath, + 'id', + 'name', + 'search' + ); + + const formik = useFormik<{ + poDate: string; + category: { label: string; value: number }[]; + status: { label: string; value: string }[]; + }>({ + initialValues: { + poDate: '', + category: [], + status: [], + }, + onSubmit: async (values) => { + const formattedValues = { + ...values, + category: values.category.map((item) => String(item.value)), + status: values.status.map((item) => String(item.value)), + }; + + onSubmit?.(formattedValues); + closeModalHandler(); + }, + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const productCategoryChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + formik.setFieldValue('category', val); + }; + + const statusChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('status', val); + }; + + return ( + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+
+ + + + + ({ + label: item.step_name, + value: item.step_name, + }))} + /> +
+
+ + {/* Modal Footer */} +
+ + + +
+
+
+ ); +}; + +export default PurchaseFilterModal; From cae19d905b04cd3ccfcfc79e3ed8c68bad35ec1a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 2 Apr 2026 09:57:40 +0700 Subject: [PATCH 2/3] feat: add filter modal --- .../pages/purchase/PurchaseTable.tsx | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 43ddab1d..d074a583 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -14,6 +14,7 @@ import useSWRInfinite from 'swr/infinite'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import Link from 'next/link'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; @@ -25,18 +26,19 @@ import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal'; import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { BaseApiResponse } from '@/types/api/api-general'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { Purchase } from '@/types/api/purchase/purchase'; +import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase'; import { PurchaseApi } from '@/services/api/purchase'; import { ExpenseApi } from '@/services/api/expense'; import { Expense } from '@/types/api/expense'; import { Color } from '@/types/theme'; -import Link from 'next/link'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -165,14 +167,21 @@ const PurchaseTable = () => { } = useTableFilter({ initial: { search: '', + po_date: '', + approval_status: '', + product_category_id: '', }, paramMap: { page: 'page', pageSize: 'limit', + po_date: 'po_date', + approval_status: 'approval_status', + product_category_id: 'product_category_id', }, }); // ===== MODAL HOOKS ===== + const filterModal = useModal(); const deleteModal = useModal(); // ===== API DATA FETCHING ===== @@ -410,13 +419,17 @@ const PurchaseTable = () => { [updateFilter, setSearchValue] ); - // const pageSizeChangeHandler = useCallback( - // (val: OptionType | OptionType[] | null) => { - // const newVal = val as OptionType; - // setPageSize(newVal.value as number); - // }, - // [setPageSize] - // ); + const filterSubmitHandler = (values: PurchaseFilter) => { + updateFilter('po_date', values.poDate); + updateFilter('product_category_id', values.category.join(',')); + updateFilter('approval_status', values.status.join(',')); + }; + + const filterResetHandler = () => { + updateFilter('po_date', ''); + updateFilter('product_category_id', ''); + updateFilter('approval_status', ''); + }; return ( <> @@ -455,6 +468,20 @@ const PurchaseTable = () => { 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + + @@ -513,6 +540,13 @@ const PurchaseTable = () => { {/* ===== MODAL COMPONENTS ===== */} + + + Date: Thu, 2 Apr 2026 09:57:48 +0700 Subject: [PATCH 3/3] feat: create PurchaseFilter type --- src/types/api/purchase/purchase.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index d39719a3..b0abe694 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -144,3 +144,9 @@ export type DeletePurchaseRequestItemPayload = { }; export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; + +export type PurchaseFilter = { + poDate: string; + category: string[]; + status: string[]; +};