From d60877d391cc1a43a6c843529460190f74ae8f29 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 20 May 2026 16:10:19 +0700 Subject: [PATCH] refactor: optimize DebtSupplierTab with useTableFilter persistence pattern Replace filterParams/currentPage/pageSize state with useTableFilter (persist:true), switch SWR to httpClientFetcher with explicit type, store OptionType[] directly for suppliers/filterBy, add formikResetHandler using resetFilter(), remove TabActions component anti-pattern and handleFilterModalOpenRef, pass filterModal.openModal directly. Co-Authored-By: Claude Sonnet 4.6 --- .../report/finance/tab/DebtSupplierTab.tsx | 776 ++++++++---------- 1 file changed, 338 insertions(+), 438 deletions(-) diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 95bed1f2..78d27f2a 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -9,23 +9,15 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { SupplierApi } from '@/services/api/master-data'; -import { - DebtRow, - DebtSupplier, - DebtSupplierFilter, -} from '@/types/api/report/debt-supplier'; +import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; import { useFormik } from 'formik'; -import { - DebtSupplierFilterSchema, - DebtSupplierFilterType, -} from '@/components/pages/report/finance/filter/DebtSupplierFilter'; import ButtonFilter from '@/components/helper/ButtonFilter'; import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; @@ -34,6 +26,10 @@ import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; const dueStatus: Record = { 'Sudah Jatuh Tempo': 'error', @@ -51,7 +47,6 @@ const getPillBadge = ( statusText: string, type: 'due' | 'payment' = 'payment' ) => { - // Get color based on type const color = type === 'due' ? dueStatus[statusText] || 'neutral' @@ -68,6 +63,11 @@ const getPillBadge = ( ); }; +const dataTypeOptions: OptionType[] = [ + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'po_date', label: 'Tanggal PO' }, +]; + interface DebtSupplierTabProps { tabId: string; } @@ -81,26 +81,45 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading; - // ===== PAGINATION STATE ===== - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - - // ===== SUBMISSION STATE ===== - const [filterParams, setFilterParams] = useState({ - start_date: undefined, - end_date: undefined, - supplier_ids: undefined, - filter_by: undefined, - }); - - // ===== DATE ERROR STATE ===== const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const handleFilterModalOpenRef = useRef(() => {}); - const filterModal = useModal(); + 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; + suppliers: OptionType[]; + filterBy?: OptionType; + }>({ + initial: { + start_date: '', + end_date: '', + suppliers: [], + filterBy: undefined, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + start_date: 'start_date', + end_date: 'end_date', + suppliers: 'supplier_ids', + filterBy: 'filter_by', + }, + persist: true, + storeName: 'debt-supplier-report-table', + }); + const { setInputValue: setSupplierInputValue, options: supplierOptions, @@ -108,154 +127,149 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { loadMore: loadMoreSuppliers, } = useSelect(SupplierApi.basePath, 'id', 'name'); - const dataTypeOptions = useMemo( - () => [ - { value: 'received_date', label: 'Tanggal Terima' }, - { value: 'po_date', label: 'Tanggal PO' }, - ], - [] - ); - // ===== FORMIK SETUP ===== - const formik = useFormik({ + const formik = useFormik({ initialValues: { - startDate: null, - endDate: null, - supplierIds: null, - filterBy: null, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + suppliers: tableFilterState.suppliers, + filterBy: tableFilterState.filterBy, }, - validationSchema: DebtSupplierFilterSchema, onSubmit: (values) => { - setFilterParams({ - start_date: values.startDate?.toString() || undefined, - end_date: values.endDate?.toString() || undefined, - supplier_ids: - values.supplierIds?.map((v) => String(v.value)).join(',') || - undefined, - filter_by: values.filterBy?.value?.toString() || undefined, - }); - filterModal.closeModal(); - setCurrentPage(1); - }, - onReset: () => { - setFilterParams({ - start_date: undefined, - end_date: undefined, - supplier_ids: undefined, - filter_by: undefined, - }); - setCurrentPage(1); + updateFilter('start_date', values.start_date, true); + updateFilter('end_date', values.end_date, true); + updateFilter('suppliers', values.suppliers, true); + updateFilter('filterBy', values.filterBy, true); filterModal.closeModal(); }, }); - handleFilterModalOpenRef.current = () => { - const restoredFilterBy = - dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) || - null; + const formikResetHandler = () => { + resetFilter(); - const supplierIdList = filterParams.supplier_ids - ? filterParams.supplier_ids.split(',') - : []; - const restoredSupplierIds = supplierOptions.filter((opt) => - supplierIdList.includes(String(opt.value)) - ); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } - formik.setValues({ - startDate: filterParams.start_date || null, - endDate: filterParams.end_date || null, - supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null, - filterBy: restoredFilterBy, + formik.resetForm({ + values: { + start_date: '', + end_date: '', + suppliers: [], + filterBy: undefined, + }, }); - filterModal.openModal(); + + filterModal.closeModal(); + }; + + // ===== DATE CHANGE HANDLERS ===== + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); + + 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); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(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; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }; // ===== DATA FETCHING ===== - const { data: debtSupplier, isLoading } = useSWR( - () => { - const params = { - supplier_ids: filterParams.supplier_ids, - filter_by: filterParams.filter_by, - start_date: filterParams.start_date, - end_date: filterParams.end_date, - page: currentPage, - limit: pageSize, - }; - - return ['debt-supplier-report', params]; - }, - ([, params]) => - DebtSupplierApi.getDebtSupplierReport( - params.supplier_ids, - params.filter_by, - params.start_date, - params.end_date, - params.page, - params.limit - ) + const { data: debtSupplierResponse, isLoading } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + `${DebtSupplierApi.basePath}/debt-supplier${getTableFilterQueryString()}`, + httpClientFetcher ); - const data: DebtSupplier[] = useMemo( - () => - isResponseSuccess(debtSupplier) - ? (debtSupplier?.data as unknown as DebtSupplier[]) || [] - : [], - [debtSupplier] - ); + const data: DebtSupplier[] = isResponseSuccess(debtSupplierResponse) + ? ((debtSupplierResponse?.data as unknown as DebtSupplier[]) ?? []) + : []; - const meta = useMemo( - () => - isResponseSuccess(debtSupplier) && debtSupplier.meta - ? debtSupplier.meta - : null, - [debtSupplier] - ); + const meta = + isResponseSuccess(debtSupplierResponse) && debtSupplierResponse.meta + ? debtSupplierResponse.meta + : null; // ===== EXPORT DATA FETCHER ===== const debtSupplierExport = useCallback(async (): Promise< DebtSupplier[] | null > => { - const params = { - supplier_ids: - formik.values.supplierIds && formik.values.supplierIds.length > 0 - ? formik.values.supplierIds.map((v) => String(v.value)).join(',') - : undefined, - filter_by: formik.values.filterBy?.value?.toString() || undefined, - start_date: formik.values.startDate || undefined, - end_date: formik.values.endDate || undefined, - date_type: formik.values.filterBy - ? formik.values.filterBy.value - : undefined, - limit: 100, - page: 1, - }; + const supplier_ids = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => String(o.value)).join(',') + : undefined; const response = await DebtSupplierApi.getDebtSupplierReport( - params.supplier_ids, - params.filter_by, - params.start_date, - params.end_date + supplier_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined, + 1, + 100 ); return isResponseSuccess(response) ? (response.data as unknown as DebtSupplier[]) : null; - }, [ - formik.values.supplierIds, - formik.values.startDate, - formik.values.endDate, - formik.values.filterBy, - ]); + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { + const supplier_ids = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => String(o.value)).join(',') + : undefined; await DebtSupplierApi.exportToExcelSupplierPerSheet( - filterParams.supplier_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date + supplier_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined ); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -263,7 +277,30 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams]); + }, [tableFilterState]); + + const handleExportExcelGeneral = useCallback(async () => { + setIsExcelGeneralExportLoading(true); + try { + const supplier_ids = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => String(o.value)).join(',') + : undefined; + + await DebtSupplierApi.exportToExcelGeneral( + supplier_ids, + tableFilterState.filterBy?.value, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined + ); + + toast.success('Excel General berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel General. Silakan coba lagi.'); + } finally { + setIsExcelGeneralExportLoading(false); + } + }, [tableFilterState]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); @@ -279,15 +316,18 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { return; } + const supplierName = + tableFilterState.suppliers.length > 0 + ? tableFilterState.suppliers.map((o) => o.label).join(', ') + : undefined; + await generateDebtSupplierPDF({ data: allDataForExport, params: { - supplier_name: formik.values.supplierIds - ?.map((v) => v.label) - .join(', '), - filter_by: formik.values.filterBy?.label, - start_date: formik.values.startDate || undefined, - end_date: formik.values.endDate || undefined, + supplier_name: supplierName, + filter_by: tableFilterState.filterBy?.label, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, }, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -296,131 +336,91 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [ - debtSupplierExport, - formik.values.supplierIds, - formik.values.filterBy, - formik.values.startDate, - formik.values.endDate, - ]); + }, [debtSupplierExport, tableFilterState]); - const handleExportExcelGeneral = useCallback(async () => { - setIsExcelGeneralExportLoading(true); - try { - await DebtSupplierApi.exportToExcelGeneral( - filterParams.supplier_ids, - filterParams.filter_by, - filterParams.start_date, - filterParams.end_date - ); - toast.success('Excel General berhasil dibuat dan diunduh.'); - } catch { - toast.error('Gagal membuat Excel General. Silakan coba lagi.'); - } finally { - setIsExcelGeneralExportLoading(false); - } - }, [filterParams]); + // ===== TAB ACTIONS ===== + useEffect(() => { + setTabActions( + tabId, +
+ - // ===== TAB ACTIONS COMPONENT ===== - const TabActions = useMemo(() => { - return function TabActionsComponent() { - const setTabActions = useTabActionsStore((state) => state.setTabActions); - const clearTabActions = useTabActionsStore( - (state) => state.clearTabActions - ); - - 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, - filterParams, + setTabActions, + tableFilterState, + filterModal.openModal, isAnyExportLoading, handleExportExcel, handleExportExcelGeneral, @@ -430,24 +430,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { isPdfExportLoading, ]); - const TabActionsElement = useMemo(() => , [TabActions]); - useEffect(() => { - return () => { - if (dateErrorShown) { - toast.dismiss(); - } - }; - }, [dateErrorShown]); - - useEffect(() => { - return () => { - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }; - }, [filterModal.open, dateErrorShown]); + return () => clearTabActions(tabId); + }, [tabId, clearTabActions]); const getTableColumns = (supplier?: DebtSupplier): ColumnDef[] => [ { @@ -662,9 +647,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, }, ]; + return ( <> - {TabActionsElement}
{isLoading && (
@@ -693,16 +678,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { - 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} /> @@ -802,16 +787,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { - 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} /> @@ -827,23 +812,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm', }} > -
- {/* Modal Header */} -
-
- -

Filter Data

-
- + {/* Modal Header */} +
+
+ +

Filter Data

+ +
+ {/* Modal Body */}
@@ -852,153 +837,68 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
{ - const value = e.target.value; - formik.setFieldValue('startDate', value || null); - - if (value && formik.values.endDate) { - const startDate = new Date(value); - const endDateObj = new Date(formik.values.endDate); - - 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); - } - } - } else { - setHasDateError(false); - } - }} + name='start_date' + value={formik.values.start_date || ''} + onChange={handleStartDateChange} className={{ wrapper: 'w-full' }} - isError={ - formik.touched.startDate && !!formik.errors.startDate - } - errorMessage={formik.errors.startDate} isNestedModal /> -
+
{ - const value = e.target.value; - formik.setFieldValue('endDate', value || null); - - if (value && formik.values.startDate) { - const startDateObj = new Date(formik.values.startDate); - 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; - } - } - - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }} + name='end_date' + value={formik.values.end_date || ''} + onChange={handleEndDateChange} className={{ wrapper: 'w-full' }} - isError={ - (formik.touched.endDate && !!formik.errors.endDate) || - hasDateError - } - errorMessage={formik.errors.endDate} isNestedModal + isError={hasDateError} />
-
- { - formik.setFieldValue( - 'supplierIds', - Array.isArray(val) ? val : val ? [val] : null - ); - }} - onInputChange={setSupplierInputValue} - onMenuScrollToBottom={loadMoreSuppliers} - isLoading={isLoadingSupplierOptions} - isClearable - className={{ wrapper: 'w-full' }} - isError={ - formik.touched.supplierIds && !!formik.errors.supplierIds - } - errorMessage={formik.errors.supplierIds as string} - /> -
+ + formik.setFieldValue('suppliers', Array.isArray(val) ? val : []) + } + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isLoading={isLoadingSupplierOptions} + isClearable + className={{ wrapper: 'w-full' }} + /> -
- { - formik.setFieldValue( - 'filterBy', - val ? (val as OptionType) : null - ); - }} - className={{ wrapper: 'w-full' }} - isClearable - isError={formik.touched.filterBy && !!formik.errors.filterBy} - errorMessage={formik.errors.filterBy as string} - /> -
+ + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + className={{ wrapper: 'w-full' }} + isClearable + />
- {/* Action Buttons */} + {/* Modal Footer */}