From b3b60018bb9e3acd4a2483dca708371bf2e00d6b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:10:10 +0700 Subject: [PATCH] refactor: optimize CustomerPaymentTab with useTableFilter persistence pattern Replace filterParams/currentPage/pageSize state with useTableFilter (persist:true), switch SWR to httpClientFetcher with explicit type, store OptionType[] directly for customers/filterBy, add formikResetHandler using resetFilter(), remove enableReinitialize and handleFilterModalOpenRef, pass filterModal.openModal directly. Co-Authored-By: Claude Sonnet 4.6 --- .../report/finance/tab/CustomerPaymentTab.tsx | 644 ++++++++---------- 1 file changed, 280 insertions(+), 364 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index e9c20053..e786c9a5 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -1,14 +1,17 @@ -import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; +import { AxiosError } from 'axios'; import Card from '@/components/Card'; import StatusBadge from '@/components/helper/StatusBadge'; -import { useSelect } from '@/components/input/SelectInput'; +import { useSelect, OptionType } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { @@ -27,28 +30,22 @@ import Dropdown from '@/components/Dropdown'; import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; -import { - CustomerPaymentFilterSchema, - CustomerPaymentFilterType, -} from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; -import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; import ButtonFilter from '@/components/helper/ButtonFilter'; import Pagination from '@/components/Pagination'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; interface CustomerPaymentTabProps { tabId: string; } -interface FilterParams { - customer_ids?: string; - start_date?: string; - end_date?: string; - filter_by?: string; -} +const dataTypeOptions: OptionType[] = [ + { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, + { value: 'realization_date', label: 'Tanggal Realisasi' }, +]; const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== @@ -59,26 +56,44 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; - // ===== PAGINATION STATE ===== - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - - // ===== SUBMISSION STATE ===== - const [filterParams, setFilterParams] = useState({}); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const handleFilterModalOpenRef = useRef(() => {}); - const filterModal = useModal(); - const dataTypeOptions = useMemo( - () => [ - { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, - { value: 'realization_date', label: 'Tanggal Realisasi' }, - ], - [] - ); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); + + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter<{ + start_date: string; + end_date: string; + customers: OptionType[]; + filterBy?: OptionType; + }>({ + initial: { + start_date: '', + end_date: '', + customers: [], + filterBy: undefined, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + start_date: 'start_date', + end_date: 'end_date', + customers: 'customer_ids', + filterBy: 'filter_by', + }, + persist: true, + storeName: 'customer-payment-report-table', + }); const { options: customerOptions, @@ -88,222 +103,159 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); // ===== FORMIK SETUP ===== - const formik = useFormik({ + const formik = useFormik({ initialValues: { - start_date: null, - end_date: null, - customer_ids: null, - filter_by: null, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + customers: tableFilterState.customers, + filterBy: tableFilterState.filterBy, }, - validationSchema: CustomerPaymentFilterSchema, - onSubmit: (values, { setSubmitting }) => { - setFilterParams({ - start_date: values.start_date || undefined, - end_date: values.end_date || undefined, - customer_ids: values.customer_ids || undefined, - filter_by: values.filter_by || undefined, - }); - filterModal.closeModal(); - setCurrentPage(1); - setSubmitting(false); - }, - onReset: () => { - setFilterParams({}); - setCurrentPage(1); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } + onSubmit: (values) => { + updateFilter('start_date', values.start_date, true); + updateFilter('end_date', values.end_date, true); + updateFilter('customers', values.customers, true); + updateFilter('filterBy', values.filterBy, true); filterModal.closeModal(); }, }); - handleFilterModalOpenRef.current = () => { - formik.setValues({ - start_date: filterParams.start_date || null, - end_date: filterParams.end_date || null, - customer_ids: filterParams.customer_ids || null, - filter_by: filterParams.filter_by || null, + const formikResetHandler = () => { + resetFilter(); + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + + formik.resetForm({ + values: { + start_date: '', + end_date: '', + customers: [], + filterBy: undefined, + }, }); - filterModal.openModal(); + + filterModal.closeModal(); }; const getPaymentStatusBadgeColor = (notes: string): Color => { const normalizedValue = notes.toLowerCase(); - - if (normalizedValue === 'lunas') { - return 'primary'; - } - - if (normalizedValue.includes('belum')) { - return 'warning'; - } - + if (normalizedValue === 'lunas') return 'primary'; + if (normalizedValue.includes('belum')) return 'warning'; return 'neutral'; }; // ===== DATE CHANGE HANDLERS ===== - const handleStartDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - formik.setFieldValue('start_date', value || null); + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); - if (value && formik.values.end_date) { - const startDate = new Date(value); - const endDateObj = new Date(formik.values.end_date); - - if (endDateObj < startDate) { - setHasDateError(true); - if (!dateErrorShown) { - toast.error('Tanggal akhir tidak boleh masa lampau', { - duration: Infinity, - }); - setDateErrorShown(true); - } - } else { - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } + if (value && formik.values.end_date) { + if (new Date(formik.values.end_date) < new Date(value)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); } } else { setHasDateError(false); - } - }, - [formik, dateErrorShown] - ); - - const handleEndDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - formik.setFieldValue('end_date', value || null); - - if (value && formik.values.start_date) { - const startDateObj = new Date(formik.values.start_date); - const endDate = new Date(value); - - if (endDate < startDateObj) { - setHasDateError(true); - if (!dateErrorShown) { - toast.error('Tanggal akhir tidak boleh masa lampau', { - duration: Infinity, - }); - setDateErrorShown(true); - } - return; + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); } } - + } else { setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); + } + }; + + const handleEndDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value); + + if (value && formik.values.start_date) { + if (new Date(value) < new Date(formik.values.start_date)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; } - }, - [formik, dateErrorShown] - ); + } - // ===== FILTER HELPERS ===== - const customerIdsValue = useMemo(() => { - if (!formik.values.customer_ids) return []; - return customerOptions.filter((opt) => - formik.values.customer_ids?.split(',').includes(String(opt.value)) - ); - }, [formik.values.customer_ids, customerOptions]); - - const filterByValue = useMemo(() => { - if (!formik.values.filter_by) return null; - return ( - dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || - null - ); - }, [formik.values.filter_by, dataTypeOptions]); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; // ===== DATA FETCHING ===== - const { data: customerPayment, isLoading } = useSWR( - () => { - const params = { - customer_ids: filterParams.customer_ids, - filter_by: filterParams.filter_by as - | 'trans_date' - | 'realization_date' - | undefined, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - page: currentPage, - limit: pageSize, - }; - - return ['customer-payment-report', params]; - }, - ([, params]) => - FinanceApi.getCustomerPaymentReport( - params.customer_ids, - params.filter_by, - params.start_date, - params.end_date, - params.page, - params.limit - ) + const { data: customerPayment, isLoading } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + `${FinanceApi.basePath}/customer-payment${getTableFilterQueryString()}`, + httpClientFetcher ); - const data: CustomerPaymentReport[] = useMemo( - () => - isResponseSuccess(customerPayment) - ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] - : [], - [customerPayment] - ); + const data: CustomerPaymentReport[] = isResponseSuccess(customerPayment) + ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] + : []; - const meta = useMemo( - () => - isResponseSuccess(customerPayment) && customerPayment.meta - ? customerPayment.meta - : null, - [customerPayment] - ); + const meta = + isResponseSuccess(customerPayment) && customerPayment.meta + ? customerPayment.meta + : null; // ===== EXPORT DATA FETCHER ===== const customerPaymentExport = useCallback(async (): Promise< CustomerPaymentReport[] | null > => { - const params = { - customer_ids: filterParams.customer_ids, - filter_by: filterParams.filter_by as - | 'trans_date' - | 'realization_date' - | undefined, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - limit: 100, - page: 1, - }; + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; + const filter_by = tableFilterState.filterBy?.value as + | 'trans_date' + | 'realization_date' + | undefined; const response = await FinanceApi.getCustomerPaymentReport( - params.customer_ids, - params.filter_by, - params.start_date, - params.end_date, - params.page, - params.limit + customer_ids, + filter_by, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined, + 1, + 100 ); return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [filterParams]); + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcelGeneral = useCallback(async () => { setIsExcelGeneralExportLoading(true); try { + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; await FinanceApi.exportCustomerPaymentToExcelGeneral( - filterParams.customer_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date + customer_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined ); toast.success('Excel General berhasil dibuat dan diunduh.'); } catch { @@ -311,16 +263,20 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsExcelGeneralExportLoading(false); } - }, [filterParams]); + }, [tableFilterState]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { + const customer_ids = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => String(o.value)).join(',') + : undefined; await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet( - filterParams.customer_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date + customer_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined ); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -328,7 +284,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams]); + }, [tableFilterState]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); @@ -344,22 +300,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return; } - const customerName = filterParams.customer_ids - ? customerOptions - .filter((opt) => - filterParams.customer_ids?.split(',').includes(String(opt.value)) - ) - .map((opt) => opt.label) - .join(', ') || 'Semua Customer' - : 'Semua Customer'; + const customerName = + tableFilterState.customers.length > 0 + ? tableFilterState.customers.map((o) => o.label).join(', ') + : 'Semua Customer'; await generateCustomerPaymentPDF({ data: allDataForExport, params: { customer_name: customerName, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - filter_by: filterParams.filter_by as + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + filter_by: tableFilterState.filterBy?.value as | 'trans_date' | 'realization_date' | undefined, @@ -371,119 +323,103 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [customerPaymentExport, filterParams, customerOptions]); + }, [customerPaymentExport, tableFilterState]); - // ===== TAB ACTIONS COMPONENT ===== - const TabActions = useMemo(() => { - return function TabActionsComponent() { - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore( - (state) => state.clearTabActions - ); + // ===== TAB ACTIONS ===== + useEffect(() => { + setTabActions( + tabId, +
+ - useEffect(() => { - setTabActions( - tabId, -
- handleFilterModalOpenRef.current()} + - - -
- - - Export - -
- - -
- - } + color='none' + isLoading={isAnyExportLoading} + className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft' > - - - - - -
- ); - }, [setTabActions]); - - useEffect(() => { - return () => { - clearTabActions(tabId); - }; - }, [clearTabActions]); - - return null; - }; +
+ + Export +
+ +
+ + } + > + + + + +
+ ); }, [ tabId, + setTabActions, + tableFilterState, + filterModal.openModal, isAnyExportLoading, - handleExportExcelGeneral, handleExportExcel, + handleExportExcelGeneral, handleExportPdf, - isExcelGeneralExportLoading, isExcelExportLoading, + isExcelGeneralExportLoading, isPdfExportLoading, - filterParams, ]); - const TabActionsElement = useMemo(() => , [TabActions]); + useEffect(() => { + return () => clearTabActions(tabId); + }, [tabId, clearTabActions]); const getTableColumns = ( summary: CustomerPaymentSummary @@ -690,11 +626,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { enableSorting: false, cell: (props) => { const value = props.row.original.status; - - if (!value) { - return '-'; - } - + if (!value) return '-'; return ( { return ( <> - {TabActionsElement}
{isLoading && (
@@ -762,16 +693,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr)) - } + currentPage={tableFilterState.page} + onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => - setCurrentPage((curr) => - meta.total_pages && curr < meta.total_pages ? curr + 1 : curr + setPage( + meta.total_pages && tableFilterState.page < meta.total_pages + ? tableFilterState.page + 1 + : tableFilterState.page ) } - onPageChange={(pageNumber) => setCurrentPage(pageNumber)} + onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} /> @@ -878,16 +809,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr)) - } + currentPage={tableFilterState.page} + onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))} onNextPage={() => - setCurrentPage((curr) => - meta.total_pages && curr < meta.total_pages ? curr + 1 : curr + setPage( + meta.total_pages && tableFilterState.page < meta.total_pages + ? tableFilterState.page + 1 + : tableFilterState.page ) } - onPageChange={(pageNumber) => setCurrentPage(pageNumber)} + onPageChange={setPage} rowOptions={[10, 20, 50, 100]} onRowChange={setPageSize} /> @@ -917,7 +848,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
-
+
@@ -958,15 +878,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { label='Customer' placeholder='Pilih Customer' options={customerOptions} - value={customerIdsValue} - onChange={(val) => { - formik.setFieldValue( - 'customer_ids', - Array.isArray(val) && val.length > 0 - ? val.map((v: OptionType) => String(v.value)).join(',') - : null - ); - }} + value={formik.values.customers} + onChange={(val) => + formik.setFieldValue('customers', Array.isArray(val) ? val : []) + } onInputChange={setCustomerInputValue} isLoading={isLoadingCustomers} isClearable @@ -978,14 +893,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { label='Filter Berdasarkan' placeholder='Pilih Filter Berdasarkan' options={dataTypeOptions} - value={filterByValue} - onChange={(val) => { - if (!Array.isArray(val)) { - formik.setFieldValue('filter_by', val?.value || null); - } - }} + value={formik.values.filterBy ?? null} + onChange={(val) => + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } className={{ wrapper: 'w-full' }} - isClearable={true} + isClearable />
@@ -1001,7 +917,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {