refactor(FE): Refactor CustomerPaymentTab to use Formik for filter

management
This commit is contained in:
rstubryan
2026-02-12 10:28:36 +07:00
parent 28dabcbeb6
commit fd78ca6ac1
2 changed files with 211 additions and 239 deletions
@@ -0,0 +1,31 @@
import * as yup from 'yup';
export type CustomerPaymentFilterType = {
start_date: string | null;
end_date: string | null;
customer_ids: string | null;
filter_by: string | null;
};
export const CustomerPaymentFilterSchema = yup.object({
start_date: yup.string().optional().nullable(),
end_date: yup
.string()
.optional()
.nullable()
.test(
'is-greater-than-start',
'Tanggal akhir tidak boleh masa lampau',
function (value) {
const { start_date } = this.parent;
if (!start_date || !value) return true;
return new Date(value) >= new Date(start_date);
}
),
customer_ids: yup.string().nullable(),
filter_by: yup.string().nullable(),
});
export type CustomerPaymentFilterValues = yup.InferType<
typeof CustomerPaymentFilterSchema
>;
@@ -9,7 +9,6 @@ import SelectInputRadio from '@/components/input/SelectInputRadio';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import { CustomerApi } from '@/services/api/master-data'; import { CustomerApi } from '@/services/api/master-data';
import { FinanceApi } from '@/services/api/report/finance-report'; import { FinanceApi } from '@/services/api/report/finance-report';
// import { UserApi } from '@/services/api/user';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper';
@@ -22,18 +21,30 @@ import Button from '@/components/Button';
import Dropdown from '@/components/Dropdown'; import Dropdown from '@/components/Dropdown';
import MenuItem from '@/components/menu/MenuItem'; import MenuItem from '@/components/menu/MenuItem';
import Menu from '@/components/menu/Menu'; import Menu from '@/components/menu/Menu';
import Modal from '@/components/Modal'; import Modal, { useModal } from '@/components/Modal';
import { useModal } from '@/components/Modal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import {
CustomerPaymentFilterSchema,
CustomerPaymentFilterType,
} from '@/components/pages/report/finance/filter/CustomerPaymentFilter';
import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX';
import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF';
import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store';
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
import { OptionType } from '@/components/table/TableRowSizeSelector';
interface CustomerPaymentTabProps { interface CustomerPaymentTabProps {
tabId: string; tabId: string;
} }
interface FilterParams {
customer_ids?: string;
start_date?: string;
end_date?: string;
filter_by?: string;
}
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
@@ -46,31 +57,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
const [filterParams, setFilterParams] = useState<FilterParams>({});
// ===== FILTER STATE =====
const [appliedFilterCustomer, setAppliedFilterCustomer] = useState<
typeof customerOptions
>([]);
// TODO: Uncomment when BE is ready
// const [appliedFilterSales, setAppliedFilterSales] = useState<
// typeof salesOptions
// >([]);
const [appliedFilterByType, setAppliedFilterByType] = useState<
(typeof dataTypeOptions)[0] | null
>(null);
const [appliedFilterStartDate, setAppliedFilterStartDate] = useState('');
const [appliedFilterEndDate, setAppliedFilterEndDate] = useState('');
const [dateErrorShown, setDateErrorShown] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false); const [hasDateError, setHasDateError] = useState(false);
const [filterCustomer, setFilterCustomer] = useState<typeof customerOptions>(
[]
);
// TODO: Uncomment when BE is ready
// const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState('');
const filterModal = useModal(); const filterModal = useModal();
const dataTypeOptions = useMemo( const dataTypeOptions = useMemo(
@@ -81,10 +71,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
[] []
); );
const [filterByType, setFilterByType] = useState<
(typeof dataTypeOptions)[0] | null
>(null);
const { const {
options: customerOptions, options: customerOptions,
setInputValue: setCustomerInputValue, setInputValue: setCustomerInputValue,
@@ -92,14 +78,43 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
loadMore: loadMoreCustomers, loadMore: loadMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); } = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
// TODO: Uncomment when BE is ready const handleFilterModalOpen = () => {
// const { filterModal.openModal();
// options: salesOptions, formik.validateForm();
// setInputValue: setSalesInputValue, };
// isLoadingOptions: isLoadingSales,
// loadMore: loadMoreSales, // ===== FORMIK SETUP =====
// hasMore: hasMoreSales, const formik = useFormik<CustomerPaymentFilterType>({
// } = useSelect(UserApi.basePath, 'id', 'name', 'search'); initialValues: {
start_date: null,
end_date: null,
customer_ids: null,
filter_by: null,
},
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();
setIsSubmitted(true);
setCurrentPage(1);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setIsSubmitted(false);
setCurrentPage(1);
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
},
});
const getPaymentStatusColor = (notes: string) => { const getPaymentStatusColor = (notes: string) => {
const normalizedValue = notes.toLowerCase(); const normalizedValue = notes.toLowerCase();
@@ -137,63 +152,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
.join(' '); .join(' ');
}; };
// ===== FILTER HANDLERS ===== // ===== DATE CHANGE HANDLERS =====
const handleFilterModalOpen = useCallback(() => {
setFilterCustomer(appliedFilterCustomer);
// setFilterSales(appliedFilterSales);
setFilterByType(appliedFilterByType);
setFilterStartDate(appliedFilterStartDate);
setFilterEndDate(appliedFilterEndDate);
filterModal.openModal();
}, [
filterModal,
appliedFilterCustomer,
appliedFilterByType,
appliedFilterStartDate,
appliedFilterEndDate,
]);
const handleResetFilters = useCallback(() => {
setIsSubmitted(false);
setFilterCustomer([]);
setFilterByType(null);
setFilterStartDate('');
setFilterEndDate('');
setAppliedFilterCustomer([]);
setAppliedFilterByType(null);
setAppliedFilterStartDate('');
setAppliedFilterEndDate('');
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
}, [dateErrorShown]);
const handleApplyFilters = useCallback(() => {
setAppliedFilterCustomer(filterCustomer);
setAppliedFilterByType(filterByType);
setAppliedFilterStartDate(filterStartDate);
setAppliedFilterEndDate(filterEndDate);
setIsSubmitted(true);
setCurrentPage(1);
filterModal.closeModal();
}, [
filterModal,
filterCustomer,
filterByType,
filterStartDate,
filterEndDate,
]);
const handleStartDateChange = useCallback( const handleStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
setFilterStartDate(value); formik.setFieldValue('start_date', value || null);
if (value && filterEndDate) { if (value && formik.values.end_date) {
const startDate = new Date(value); const startDate = new Date(value);
const endDateObj = new Date(filterEndDate); const endDateObj = new Date(formik.values.end_date);
if (endDateObj < startDate) { if (endDateObj < startDate) {
setHasDateError(true); setHasDateError(true);
@@ -214,16 +181,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
setHasDateError(false); setHasDateError(false);
} }
}, },
[filterEndDate, dateErrorShown] [formik, dateErrorShown]
); );
const handleEndDateChange = useCallback( const handleEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
setFilterEndDate(value); formik.setFieldValue('end_date', value || null);
if (value && filterStartDate) { if (value && formik.values.start_date) {
const startDateObj = new Date(filterStartDate); const startDateObj = new Date(formik.values.start_date);
const endDate = new Date(value); const endDate = new Date(value);
if (endDate < startDateObj) { if (endDate < startDateObj) {
@@ -244,41 +211,46 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
setDateErrorShown(false); setDateErrorShown(false);
} }
}, },
[filterStartDate, dateErrorShown] [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]);
// ===== ACTIVE FILTERS COUNT ===== // ===== ACTIVE FILTERS COUNT =====
const activeFiltersCount = useMemo(() => { const activeFiltersCount = useMemo(() => {
let count = 0; let count = 0;
// Date filter (start_date + end_date = 1 filter) // Date filter (start_date + end_date = 1 filter)
if (appliedFilterStartDate || appliedFilterEndDate) { if (filterParams.start_date || filterParams.end_date) {
count += 1; count += 1;
} }
// Customer filter // Customer filter
if (appliedFilterCustomer.length > 0) { if (filterParams.customer_ids) {
count += 1; count += 1;
} }
// Filter by type filter (hanya dihitung jika ada nilai yang dipilih) // Filter by type filter (hanya dihitung jika ada nilai yang dipilih)
if (appliedFilterByType) { if (filterParams.filter_by) {
count += 1; count += 1;
} }
// TODO: Uncomment when BE is ready
// // Sales filter
// if (appliedFilterSales.length > 0) {
// count += 1;
// }
return count; return count;
}, [ }, [filterParams]);
appliedFilterStartDate,
appliedFilterEndDate,
appliedFilterCustomer,
appliedFilterByType,
]);
const hasFilters = activeFiltersCount > 0; const hasFilters = activeFiltersCount > 0;
@@ -287,21 +259,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
isSubmitted isSubmitted
? () => { ? () => {
const params = { const params = {
customer_ids: customer_ids: filterParams.customer_ids,
appliedFilterCustomer.length > 0 filter_by: filterParams.filter_by as
? appliedFilterCustomer.map((v) => String(v.value)).join(',')
: undefined,
// TODO: Uncomment when BE is ready
// sales_id:
// appliedFilterSales.length > 0
// ? appliedFilterSales.map((v) => String(v.value)).join(',')
// : undefined,
filter_by: appliedFilterByType?.value as
| 'trans_date' | 'trans_date'
| 'realization_date' | 'realization_date'
| undefined, | undefined,
start_date: appliedFilterStartDate || undefined, start_date: filterParams.start_date,
end_date: appliedFilterEndDate || undefined, end_date: filterParams.end_date,
page: currentPage, page: currentPage,
limit: pageSize, limit: pageSize,
}; };
@@ -333,21 +297,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
CustomerPaymentReport[] | null CustomerPaymentReport[] | null
> => { > => {
const params = { const params = {
customer_ids: customer_ids: filterParams.customer_ids,
appliedFilterCustomer.length > 0 filter_by: filterParams.filter_by as
? appliedFilterCustomer.map((v) => String(v.value)).join(',')
: undefined,
// TODO: Uncomment when BE is ready
// sales_id:
// appliedFilterSales.length > 0
// ? appliedFilterSales.map((v) => String(v.value)).join(',')
// : undefined,
filter_by: appliedFilterByType?.value as
| 'trans_date' | 'trans_date'
| 'realization_date' | 'realization_date'
| undefined, | undefined,
start_date: appliedFilterStartDate || undefined, start_date: filterParams.start_date,
end_date: appliedFilterEndDate || undefined, end_date: filterParams.end_date,
limit: 100, limit: 100,
page: 1, page: 1,
}; };
@@ -364,13 +320,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
return isResponseSuccess(response) return isResponseSuccess(response)
? (response.data as unknown as CustomerPaymentReport[]) ? (response.data as unknown as CustomerPaymentReport[])
: null; : null;
}, [ }, [filterParams]);
appliedFilterCustomer,
// appliedFilterSales,
appliedFilterStartDate,
appliedFilterEndDate,
appliedFilterByType,
]);
// ===== EXPORT HANDLERS ===== // ===== EXPORT HANDLERS =====
const handleExportExcel = useCallback(async () => { const handleExportExcel = useCallback(async () => {
@@ -410,21 +360,22 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
return; 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';
await generateCustomerPaymentPDF({ await generateCustomerPaymentPDF({
data: allDataForExport, data: allDataForExport,
params: { params: {
customer_name: customer_name: customerName,
appliedFilterCustomer.length > 0 start_date: filterParams.start_date,
? appliedFilterCustomer.map((c) => c.label).join(', ') end_date: filterParams.end_date,
: undefined, filter_by: filterParams.filter_by as
// TODO: Uncomment when BE is ready
// sales:
// appliedFilterSales.length > 0
// ? appliedFilterSales.map((s) => s.label).join(', ')
// : undefined,
start_date: appliedFilterStartDate || undefined,
end_date: appliedFilterEndDate || undefined,
filter_by: appliedFilterByType?.value as
| 'trans_date' | 'trans_date'
| 'realization_date' | 'realization_date'
| undefined, | undefined,
@@ -436,7 +387,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
} finally { } finally {
setIsPdfExportLoading(false); setIsPdfExportLoading(false);
} }
}, [customerPaymentExport]); }, [customerPaymentExport, filterParams, customerOptions]);
// ===== REGISTER TAB ACTIONS TO STORE ===== // ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useFinanceTabStore((state) => state.setTabActions); const setTabActions = useFinanceTabStore((state) => state.setTabActions);
@@ -517,7 +468,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
setTabActions, setTabActions,
]); ]);
// Cleanup on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
clearTabActions(tabId); clearTabActions(tabId);
@@ -931,95 +881,86 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
<Icon icon='heroicons:x-mark' width={20} height={20} /> <Icon icon='heroicons:x-mark' width={20} height={20} />
</Button> </Button>
</div> </div>
<div className='p-4 flex flex-col gap-1.5'> <form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<div> <div className='p-4 flex flex-col gap-1.5'>
<label className='block text-xs font-semibold text-base-content py-2'> <div>
Tanggal <label className='block text-xs font-semibold text-base-content py-2'>
</label> Tanggal
<div className='flex flex-row gap-1.5 items-center justify-between'> </label>
<DateInput <div className='flex flex-row gap-1.5 items-center justify-between'>
name='start_date' <DateInput
value={filterStartDate} name='start_date'
onChange={handleStartDateChange} value={formik.values.start_date || ''}
className={{ wrapper: 'w-full' }} onChange={handleStartDateChange}
isNestedModal className={{ wrapper: 'w-full' }}
/> isNestedModal
<hr className='w-full max-w-3 h-px border-base-content/10' /> />
<hr className='w-full max-w-3 h-px border-base-content/10' />
<DateInput <DateInput
name='end_date' name='end_date'
value={filterEndDate} value={formik.values.end_date || ''}
onChange={handleEndDateChange} onChange={handleEndDateChange}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isNestedModal isNestedModal
/> isError={hasDateError}
/>
</div>
</div> </div>
<SelectInputCheckbox
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
);
}}
onInputChange={setCustomerInputValue}
isLoading={isLoadingCustomers}
isClearable
onMenuScrollToBottom={loadMoreCustomers}
className={{ wrapper: 'w-full' }}
/>
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={filterByValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('filter_by', val?.value || null);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
</div> </div>
<SelectInputCheckbox {/* Modal Footer */}
label='Customer' <div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
placeholder='Pilih Customer' <Button
options={customerOptions} type='reset'
value={filterCustomer} variant='soft'
onChange={(val) => { className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); >
}} Reset Filter
onInputChange={setCustomerInputValue} </Button>
isLoading={isLoadingCustomers} <Button
isClearable type='submit'
onMenuScrollToBottom={loadMoreCustomers} className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
className={{ wrapper: 'w-full' }} disabled={hasDateError || !formik.isValid || formik.isSubmitting}
/> >
Apply Filter
{/* TODO: Uncomment when BE is ready */} </Button>
{/* <div> </div>
<SelectInputCheckbox </form>
label='Sales'
placeholder='Pilih Sales'
options={salesOptions}
value={filterSales}
onChange={(val) => {
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
}}
onInputChange={setSalesInputValue}
isLoading={isLoadingSales}
isClearable
onMenuScrollToBottom={loadMoreSales}
className={{ wrapper: 'w-full' }}
/>
</div> */}
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={filterByType}
onChange={(val) => {
if (val && !Array.isArray(val)) {
setFilterByType(val);
}
}}
className={{ wrapper: 'w-full' }}
/>
{/* Action Buttons */}
</div>
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
onClick={handleResetFilters}
>
Reset Filter
</Button>
<Button
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
onClick={handleApplyFilters}
disabled={hasDateError}
>
Apply Filter
</Button>
</div>
</Modal> </Modal>
</> </>
); );