From 22b1102454b00d79fb72c853d82fb3572d742a74 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:40:42 +0700 Subject: [PATCH] refactor(FE): Refactor ExpensesTable to use ExpensesFilterModal --- .../pages/expense/ExpensesTable.tsx | 406 ++++++++---------- .../pages/expense/filter/ExpensesFilter.ts | 28 ++ .../expense/filter/ExpensesFilterModal.tsx | 206 +++++++++ 3 files changed, 423 insertions(+), 217 deletions(-) create mode 100644 src/components/pages/expense/filter/ExpensesFilter.ts create mode 100644 src/components/pages/expense/filter/ExpensesFilterModal.tsx diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index e141ad67..5875bc0c 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -16,10 +16,6 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; @@ -27,17 +23,15 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import DateInput from '@/components/input/DateInput'; import RequirePermission from '@/components/helper/RequirePermission'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { LocationApi, SupplierApi } from '@/services/api/master-data'; -import { Location } from '@/types/api/master-data/location'; -import { Supplier } from '@/types/api/master-data/supplier'; import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ @@ -179,6 +173,9 @@ const ExpensesTable = () => { const approveModal = useModal(); const rejectModal = useModal(); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + const [selectedExpense, setSelectedExpense] = useState( undefined ); @@ -535,51 +532,32 @@ const ExpensesTable = () => { setIsRejectLoading(false); }; - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - } = useSelect(LocationApi.basePath, 'id', 'name'); - - const [selectedLocation, setSelectedLocation] = useState( - null - ); - - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'locationId', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const { - setInputValue: setVendorInputValue, - options: vendorOptions, - isLoadingOptions: isLoadingVendorOptions, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const [selectedVendor, setSelectedVendor] = useState(null); - - const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedVendor(val as OptionType); - updateFilter('vendorId', val ? ((val as OptionType).value as string) : ''); - }; - const searchChangeHandler: ChangeEventHandler = (e) => { updateFilter('search', e.target.value); }; - const transactionDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('transactionDate', e.target.value); + // ===== FILTER MODAL HANDLERS ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); }; - const realizationDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('realizationDate', e.target.value); + const handleFilterSubmit = (values: { + transaction_date?: string | null; + realization_date?: string | null; + location_id?: string | null; + vendor_id?: string | null; + }) => { + updateFilter('transactionDate', values.transaction_date || ''); + updateFilter('realizationDate', values.realization_date || ''); + updateFilter('locationId', values.location_id || ''); + updateFilter('vendorId', values.vendor_id || ''); + }; + + const handleFilterReset = () => { + updateFilter('transactionDate', ''); + updateFilter('realizationDate', ''); + updateFilter('locationId', ''); + updateFilter('vendorId', ''); }; // track sorting @@ -595,188 +573,176 @@ const ExpensesTable = () => { return ( <> -
-
-
-
-
- +
+
+ {/* Action Buttons */} +
+ + + + + {selectedRowIds.length > 0 && ( + <> +
+ + - {selectedRowIds.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(expenses) ? expenses?.data : []} - columns={expensesColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} - totalItems={ - isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={tableEnableRowSelectionHandler} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(expenses) && expenses?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - /> + {/* Table Section */} +
+ + data={isResponseSuccess(expenses) ? expenses?.data : []} + columns={expensesColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} + totalItems={ + isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(expenses) && expenses?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
{ onClick: confirmationModalRejectClickHandler, }} /> + + ); }; diff --git a/src/components/pages/expense/filter/ExpensesFilter.ts b/src/components/pages/expense/filter/ExpensesFilter.ts new file mode 100644 index 00000000..8ee14a90 --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilter.ts @@ -0,0 +1,28 @@ +import * as yup from 'yup'; + +export type ExpensesFilterType = { + transaction_date: string | null; + realization_date: string | null; + location_id: string | null; + vendor_id: string | null; +}; + +export const ExpensesFilterSchema = yup.object({ + transaction_date: yup.string().nullable(), + realization_date: yup + .string() + .nullable() + .test( + 'is-greater-or-equal-transaction', + 'Tanggal realisasi tidak boleh sebelum tanggal transaksi', + function (value) { + const { transaction_date } = this.parent; + if (!transaction_date || !value) return true; + return new Date(value) >= new Date(transaction_date); + } + ), + location_id: yup.string().nullable(), + vendor_id: yup.string().nullable(), +}); + +export type ExpensesFilterValues = yup.InferType; diff --git a/src/components/pages/expense/filter/ExpensesFilterModal.tsx b/src/components/pages/expense/filter/ExpensesFilterModal.tsx new file mode 100644 index 00000000..99f5a75a --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilterModal.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { RefObject } from 'react'; +import { useFormik } from 'formik'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInput from '@/components/input/SelectInput'; + +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { LocationApi, SupplierApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { + ExpensesFilterSchema, + ExpensesFilterValues, +} from '@/components/pages/expense/filter/ExpensesFilter'; + +interface ExpensesFilterModalProps { + ref: RefObject; + initialValues?: ExpensesFilterValues; + onSubmit?: (values: Partial) => void; + onReset?: () => void; +} + +const ExpensesFilterModal = ({ + ref, + initialValues, + onSubmit, + onReset, +}: ExpensesFilterModalProps) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setVendorInputValue, + options: vendorOptions, + isLoadingOptions: isLoadingVendorOptions, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const formik = useFormik({ + initialValues: initialValues || { + transaction_date: null, + realization_date: null, + location_id: null, + vendor_id: null, + }, + validationSchema: ExpensesFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const locationValue = formik.values.location_id + ? locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + : null; + + const vendorValue = formik.values.vendor_id + ? vendorOptions.find( + (opt) => String(opt.value) === formik.values.vendor_id + ) || null + : null; + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const locationId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('location_id', locationId); + }; + + const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { + const vendorId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('vendor_id', vendorId); + }; + + return ( + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+ + + + {formik.touched.realization_date && + formik.errors.realization_date && ( + + {formik.errors.realization_date} + + )} + + + + +
+ + {/* Modal Footer */} +
+ + + +
+
+
+ ); +}; + +export default ExpensesFilterModal;