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