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,76 +75,35 @@ 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); setIsSubmitted(false);
}, setFilterCustomer([]);
[updateFilter] setFilterSales([]);
); setFilterDateType(null);
setFilterStartDate('');
setFilterEndDate('');
setFilterErrors({});
}, []);
const salesChangeHandler = useCallback( const handleApplyFilters = useCallback(() => {
(val: OptionType | OptionType[] | null) => { const errors: Record<string, string> = {};
const arr = Array.isArray(val) ? val : val ? [val] : [];
updateFilter(
'sales',
arr.map((v) => String((v as OptionType).value))
);
setIsSubmitted(false);
},
[updateFilter]
);
const dataTypeChangeHandler = useCallback( if (!filterStartDate) {
(val: OptionType | OptionType[] | null) => { errors.start_date = 'Tanggal mulai wajib diisi';
const newVal = val as OptionType; }
const dateType = if (!filterEndDate) {
(newVal?.value as 'do_date' | 'payment_date') || 'do_date'; errors.end_date = 'Tanggal akhir wajib diisi';
updateFilter('date_type', dateType); }
setIsSubmitted(false);
},
[updateFilter]
);
const startDateChangeHandler = useCallback< setFilterErrors(errors);
ChangeEventHandler<HTMLInputElement>
>(
(e) => {
const val = e.target.value;
updateFilter('start_date', val || '');
setIsSubmitted(false);
},
[updateFilter]
);
const endDateChangeHandler = useCallback< if (Object.keys(errors).length === 0) {
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);
}, [updateFilter]);
const handleSubmit = useCallback(() => {
setIsSubmitted(true); setIsSubmitted(true);
setCurrentPage(1); setCurrentPage(1);
}, []); filterModal.closeModal();
}
}, [filterModal, filterStartDate, filterEndDate]);
// ===== DATA FETCHING ===== // ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR( const { data: customerPayment, isLoading } = useSWR(
@@ -153,16 +111,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,
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'>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
}}
>
<div className='space-y-6'>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-semibold'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
<DateInput
label='Tanggal'
name='start_date'
value={filterStartDate}
onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div>
<DateInput
label=' '
name='end_date'
value={filterEndDate}
onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div>
<div>
<SelectInput <SelectInput
label='Customer' label='Customer'
placeholder='Pilih Customer' placeholder='Pilih Customer'
isMulti isMulti
options={customerOptions} options={customerOptions}
value={customerOptions.filter((opt) => value={filterCustomer}
(tableFilterState.customer_id || []) onChange={(val) => {
.map(String) setFilterCustomer(
.includes(String(opt.value)) Array.isArray(val) ? val : val ? [val] : []
)} );
onChange={customerChangeHandler} }}
isLoading={isLoadingCustomers} isLoading={isLoadingCustomers}
isClearable isClearable
className={{ wrapper: 'w-full' }}
/> />
</div>
<div>
<SelectInput <SelectInput
label='Sales' label='Sales'
placeholder='Pilih Sales' placeholder='Pilih Sales'
isMulti isMulti
options={salesOptions} options={salesOptions}
value={salesOptions.filter((opt) => value={filterSales}
(tableFilterState.sales || []) onChange={(val) => {
.map(String) setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
.includes(String(opt.value)) }}
)}
onChange={salesChangeHandler}
isLoading={false}
isClearable isClearable
className={{ wrapper: 'w-full' }}
/> />
</div>
<div>
<SelectInput <SelectInput
label='Filter Berdasarkan' label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan' placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions} options={dataTypeOptions}
value={ value={filterDateType}
dataTypeOptions?.find( onChange={(val) => {
(option) => option.value === tableFilterState.date_type setFilterDateType(val as OptionType | null);
) || null }}
}
onChange={dataTypeChangeHandler}
isLoading={false}
isClearable={false} isClearable={false}
/> className={{ wrapper: 'w-full' }}
</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'>
<DateInput
label='Tanggal Awal'
name='start_date'
placeholder='Pilih Tanggal Awal'
value={tableFilterState.start_date}
onChange={startDateChangeHandler}
/>
<DateInput
label='Tanggal Akhir'
name='end_date'
placeholder='Pilih Tanggal Akhir'
value={tableFilterState.end_date}
onChange={endDateChangeHandler}
/> />
</div> </div>
</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>
</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'>