refactor(FE): Move filters into modal and refactor filter state

This commit is contained in:
rstubryan
2026-01-09 14:58:41 +07:00
parent 7a6b003cb9
commit 6643fe5a60
@@ -1,6 +1,7 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { ChangeEventHandler } from 'react'; import { ChangeEventHandler } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Card from '@/components/Card'; import Card from '@/components/Card';
import SelectInput, { import SelectInput, {
useSelect, useSelect,
@@ -23,6 +24,8 @@ 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 { useModal } from '@/components/Modal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
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';
@@ -40,20 +43,16 @@ const CustomerPaymentTab = () => {
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
// ===== TABLE FILTER STATE ===== // ===== FILTER STATE =====
const { state: tableFilterState, updateFilter } = useTableFilter({ const [filterCustomer, setFilterCustomer] = useState<OptionType[]>([]);
initial: { const [filterSales, setFilterSales] = useState<OptionType[]>([]);
customer_id: [] as string[], const [filterDateType, setFilterDateType] = useState<OptionType | null>(null);
sales: [] as string[], const [filterStartDate, setFilterStartDate] = useState('');
date_type: 'do_date' as 'do_date' | 'payment_date', const [filterEndDate, setFilterEndDate] = useState('');
start_date: '', const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
end_date: '',
}, // Filter Modal
paramMap: { const filterModal = useModal();
page: 'page',
pageSize: 'limit',
},
});
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
useSelect(CustomerApi.basePath, 'id', 'name', 'search'); useSelect(CustomerApi.basePath, 'id', 'name', 'search');
@@ -76,93 +75,55 @@ const CustomerPaymentTab = () => {
[] []
); );
const customerChangeHandler = useCallback( // ===== FILTER HANDLERS =====
(val: OptionType | OptionType[] | null) => { const handleResetFilters = useCallback(() => {
const arr = Array.isArray(val) ? val : val ? [val] : [];
updateFilter(
'customer_id',
arr.map((v) => String((v as OptionType).value))
);
setIsSubmitted(false);
},
[updateFilter]
);
const salesChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
const arr = Array.isArray(val) ? val : val ? [val] : [];
updateFilter(
'sales',
arr.map((v) => String((v as OptionType).value))
);
setIsSubmitted(false);
},
[updateFilter]
);
const dataTypeChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
const dateType =
(newVal?.value as 'do_date' | 'payment_date') || 'do_date';
updateFilter('date_type', dateType);
setIsSubmitted(false);
},
[updateFilter]
);
const startDateChangeHandler = useCallback<
ChangeEventHandler<HTMLInputElement>
>(
(e) => {
const val = e.target.value;
updateFilter('start_date', val || '');
setIsSubmitted(false);
},
[updateFilter]
);
const endDateChangeHandler = useCallback<
ChangeEventHandler<HTMLInputElement>
>(
(e) => {
const val = e.target.value;
updateFilter('end_date', val || '');
setIsSubmitted(false);
},
[updateFilter]
);
const resetFilters = useCallback(() => {
updateFilter('customer_id', []);
updateFilter('sales', []);
updateFilter('date_type', 'do_date');
updateFilter('start_date', '');
updateFilter('end_date', '');
setIsSubmitted(false); setIsSubmitted(false);
}, [updateFilter]); setFilterCustomer([]);
setFilterSales([]);
const handleSubmit = useCallback(() => { setFilterDateType(null);
setIsSubmitted(true); setFilterStartDate('');
setCurrentPage(1); setFilterEndDate('');
setFilterErrors({});
}, []); }, []);
const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
if (!filterStartDate) {
errors.start_date = 'Tanggal mulai wajib diisi';
}
if (!filterEndDate) {
errors.end_date = 'Tanggal akhir wajib diisi';
}
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true);
setCurrentPage(1);
filterModal.closeModal();
}
}, [filterModal, filterStartDate, filterEndDate]);
// ===== DATA FETCHING ===== // ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR( const { data: customerPayment, isLoading } = useSWR(
isSubmitted isSubmitted
? () => { ? () => {
const params = { const params = {
customer_id: customer_id:
tableFilterState.customer_id.length > 0 filterCustomer.length > 0
? tableFilterState.customer_id.join(',') ? filterCustomer.map((v) => String(v.value)).join(',')
: undefined, : undefined,
sales: sales:
tableFilterState.sales.length > 0 filterSales.length > 0
? tableFilterState.sales.join(',') ? filterSales.map((v) => String(v.value)).join(',')
: undefined, : undefined,
date_type: tableFilterState.date_type || undefined, date_type: filterDateType?.value as
start_date: tableFilterState.start_date || undefined, | 'do_date'
end_date: tableFilterState.end_date || undefined, | 'payment_date'
| undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
page: currentPage, page: currentPage,
limit: pageSize, limit: pageSize,
}; };
@@ -201,16 +162,19 @@ const CustomerPaymentTab = () => {
> => { > => {
const params = { const params = {
customer_id: customer_id:
tableFilterState.customer_id.length > 0 filterCustomer.length > 0
? tableFilterState.customer_id.join(',') ? filterCustomer.map((v) => String(v.value)).join(',')
: undefined, : undefined,
sales: sales:
tableFilterState.sales.length > 0 filterSales.length > 0
? tableFilterState.sales.join(',') ? filterSales.map((v) => String(v.value)).join(',')
: undefined, : undefined,
date_type: tableFilterState.date_type || undefined, date_type: filterDateType?.value as
start_date: tableFilterState.start_date || undefined, | 'do_date'
end_date: tableFilterState.end_date || undefined, | 'payment_date'
| undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
limit: 100, limit: 100,
page: 1, page: 1,
}; };
@@ -228,7 +192,13 @@ const CustomerPaymentTab = () => {
return isResponseSuccess(response) return isResponseSuccess(response)
? (response.data as unknown as CustomerPaymentReport[]) ? (response.data as unknown as CustomerPaymentReport[])
: null; : null;
}, [tableFilterState]); }, [
filterCustomer,
filterSales,
filterDateType,
filterStartDate,
filterEndDate,
]);
// ===== EXPORT HANDLERS ===== // ===== EXPORT HANDLERS =====
const handleExportExcel = useCallback(async () => { const handleExportExcel = useCallback(async () => {
@@ -527,91 +497,164 @@ const CustomerPaymentTab = () => {
className={{ wrapper: 'w-full', body: 'p-1!' }} className={{ wrapper: 'w-full', body: 'p-1!' }}
> >
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'> <div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
<Button color='primary' onClick={handleSubmit}> <Button variant='outline' onClick={filterModal.openModal}>
Cari <Icon icon='heroicons:funnel' width={18} height={18} />
</Button> Filter
<Button color='warning' onClick={resetFilters}>
Reset
</Button> </Button>
<Dropdown <Dropdown
trigger={ trigger={
<Button color='success' isLoading={isAnyExportLoading}> <Button variant='outline' isLoading={isAnyExportLoading}>
<Icon
icon='heroicons:cloud-arrow-down'
width={18}
height={18}
/>
Export Export
</Button> </Button>
} }
align='end' align='end'
> >
<Menu className='w-32'> <Menu>
<MenuItem title='Excel' onClick={handleExportExcel} /> <MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPdf} /> <MenuItem title='PDF' onClick={handleExportPdf} />
</Menu> </Menu>
</Dropdown> </Dropdown>
</div> </div>
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
<SelectInput {/* Filter Modal */}
label='Customer' <Modal
placeholder='Pilih Customer' ref={filterModal.ref}
isMulti className={{
options={customerOptions} modal: 'p-0',
value={customerOptions.filter((opt) => modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
(tableFilterState.customer_id || []) }}
.map(String) >
.includes(String(opt.value)) <div className='space-y-6'>
)} {/* Modal Header */}
onChange={customerChangeHandler} <div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
isLoading={isLoadingCustomers} <div className='flex items-center gap-2 text-primary'>
isClearable <Icon icon='heroicons:funnel' width={20} height={20} />
/> <h3 className='font-semibold'>Filter Data</h3>
<SelectInput </div>
label='Sales' <Button
placeholder='Pilih Sales' variant='link'
isMulti onClick={filterModal.closeModal}
options={salesOptions} className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
value={salesOptions.filter((opt) => >
(tableFilterState.sales || []) <Icon icon='heroicons:x-mark' width={20} height={20} />
.map(String) </Button>
.includes(String(opt.value)) </div>
)} <div className='space-y-4 px-4'>
onChange={salesChangeHandler} <div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
isLoading={false} <div>
isClearable <DateInput
/> label='Tanggal'
<SelectInput name='start_date'
label='Filter Berdasarkan' value={filterStartDate}
placeholder='Pilih Filter Berdasarkan' onChange={(e) => {
options={dataTypeOptions} setFilterStartDate(e.target.value);
value={ setFilterErrors((prev) => ({ ...prev, start_date: '' }));
dataTypeOptions?.find( }}
(option) => option.value === tableFilterState.date_type className={{ wrapper: 'w-full' }}
) || null />
} {filterErrors.start_date && (
onChange={dataTypeChangeHandler} <p className='text-red-500 text-sm mt-1'>
isLoading={false} {filterErrors.start_date}
isClearable={false} </p>
/> )}
</div> </div>
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'> <div>
<DateInput <DateInput
label='Tanggal Awal' label=' '
name='start_date' name='end_date'
placeholder='Pilih Tanggal Awal' value={filterEndDate}
value={tableFilterState.start_date} onChange={(e) => {
onChange={startDateChangeHandler} setFilterEndDate(e.target.value);
/> setFilterErrors((prev) => ({ ...prev, end_date: '' }));
<DateInput }}
label='Tanggal Akhir' className={{ wrapper: 'w-full' }}
name='end_date' />
placeholder='Pilih Tanggal Akhir' {filterErrors.end_date && (
value={tableFilterState.end_date} <p className='text-red-500 text-sm mt-1'>
onChange={endDateChangeHandler} {filterErrors.end_date}
/> </p>
)}
</div>
</div>
<div>
<SelectInput
label='Customer'
placeholder='Pilih Customer'
isMulti
options={customerOptions}
value={filterCustomer}
onChange={(val) => {
setFilterCustomer(
Array.isArray(val) ? val : val ? [val] : []
);
}}
isLoading={isLoadingCustomers}
isClearable
className={{ wrapper: 'w-full' }}
/>
</div>
<div>
<SelectInput
label='Sales'
placeholder='Pilih Sales'
isMulti
options={salesOptions}
value={filterSales}
onChange={(val) => {
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
}}
isClearable
className={{ wrapper: 'w-full' }}
/>
</div>
<div>
<SelectInput
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={filterDateType}
onChange={(val) => {
setFilterDateType(val as OptionType | null);
}}
isClearable={false}
className={{ wrapper: 'w-full' }}
/>
</div>
</div>
{/* Action Buttons */}
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
<Button
variant='soft'
className='ms-4 min-w-36 rounded-lg'
onClick={handleResetFilters}
>
Reset Filter
</Button>
<Button
className='me-4 min-w-36 rounded-lg'
onClick={handleApplyFilters}
>
Apply Filter
</Button>
</div>
</div> </div>
</div> </Modal>
{!isSubmitted ? ( {!isSubmitted ? (
<div className='mt-6 text-center text-gray-500'> <div className='mt-6 text-center text-gray-500'>
Silakan pilih filter dan klik tombol Submit untuk menampilkan data. Silakan klik tombol Filter untuk mengatur filter dan menampilkan
data.
</div> </div>
) : isLoading ? ( ) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'> <div className='w-full flex flex-row justify-center items-center p-4'>