diff --git a/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts new file mode 100644 index 00000000..60359038 --- /dev/null +++ b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts @@ -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 +>; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4e0e3f25..723a1ebf 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -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({}); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const [filterCustomer, setFilterCustomer] = useState( - [] - ); - // TODO: Uncomment when BE is ready - // const [filterSales, setFilterSales] = useState([]); - 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({ + 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) => { 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) => { 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) => { -
-
- -
- -
+
+
+
+ +
+ +
- + +
+ + { + 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' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('filter_by', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + />
- { - setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setCustomerInputValue} - isLoading={isLoadingCustomers} - isClearable - onMenuScrollToBottom={loadMoreCustomers} - className={{ wrapper: 'w-full' }} - /> - - {/* TODO: Uncomment when BE is ready */} - {/*
- { - setFilterSales(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setSalesInputValue} - isLoading={isLoadingSales} - isClearable - onMenuScrollToBottom={loadMoreSales} - className={{ wrapper: 'w-full' }} - /> -
*/} - - { - if (val && !Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - /> - - {/* Action Buttons */} -
-
- - -
+ {/* Modal Footer */} +
+ + +
+ );