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 { ChangeEventHandler } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Card from '@/components/Card';
import SelectInput, {
useSelect,
@@ -23,6 +24,8 @@ 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 toast from 'react-hot-toast';
import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX';
import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF';
@@ -40,20 +43,16 @@ const CustomerPaymentTab = () => {
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== TABLE FILTER STATE =====
const { state: tableFilterState, updateFilter } = useTableFilter({
initial: {
customer_id: [] as string[],
sales: [] as string[],
date_type: 'do_date' as 'do_date' | 'payment_date',
start_date: '',
end_date: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
});
// ===== FILTER STATE =====
const [filterCustomer, setFilterCustomer] = useState<OptionType[]>([]);
const [filterSales, setFilterSales] = useState<OptionType[]>([]);
const [filterDateType, setFilterDateType] = useState<OptionType | null>(null);
const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState('');
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
// Filter Modal
const filterModal = useModal();
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
useSelect(CustomerApi.basePath, 'id', 'name', 'search');
@@ -76,93 +75,55 @@ const CustomerPaymentTab = () => {
[]
);
const customerChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
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', '');
// ===== FILTER HANDLERS =====
const handleResetFilters = useCallback(() => {
setIsSubmitted(false);
}, [updateFilter]);
const handleSubmit = useCallback(() => {
setIsSubmitted(true);
setCurrentPage(1);
setFilterCustomer([]);
setFilterSales([]);
setFilterDateType(null);
setFilterStartDate('');
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 =====
const { data: customerPayment, isLoading } = useSWR(
isSubmitted
? () => {
const params = {
customer_id:
tableFilterState.customer_id.length > 0
? tableFilterState.customer_id.join(',')
filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',')
: undefined,
sales:
tableFilterState.sales.length > 0
? tableFilterState.sales.join(',')
filterSales.length > 0
? filterSales.map((v) => String(v.value)).join(',')
: undefined,
date_type: tableFilterState.date_type || undefined,
start_date: tableFilterState.start_date || undefined,
end_date: tableFilterState.end_date || undefined,
date_type: filterDateType?.value as
| 'do_date'
| 'payment_date'
| undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
page: currentPage,
limit: pageSize,
};
@@ -201,16 +162,19 @@ const CustomerPaymentTab = () => {
> => {
const params = {
customer_id:
tableFilterState.customer_id.length > 0
? tableFilterState.customer_id.join(',')
filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',')
: undefined,
sales:
tableFilterState.sales.length > 0
? tableFilterState.sales.join(',')
filterSales.length > 0
? filterSales.map((v) => String(v.value)).join(',')
: undefined,
date_type: tableFilterState.date_type || undefined,
start_date: tableFilterState.start_date || undefined,
end_date: tableFilterState.end_date || undefined,
date_type: filterDateType?.value as
| 'do_date'
| 'payment_date'
| undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
limit: 100,
page: 1,
};
@@ -228,7 +192,13 @@ const CustomerPaymentTab = () => {
return isResponseSuccess(response)
? (response.data as unknown as CustomerPaymentReport[])
: null;
}, [tableFilterState]);
}, [
filterCustomer,
filterSales,
filterDateType,
filterStartDate,
filterEndDate,
]);
// ===== EXPORT HANDLERS =====
const handleExportExcel = useCallback(async () => {
@@ -527,91 +497,164 @@ const CustomerPaymentTab = () => {
className={{ wrapper: 'w-full', body: 'p-1!' }}
>
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
<Button color='primary' onClick={handleSubmit}>
Cari
</Button>
<Button color='warning' onClick={resetFilters}>
Reset
<Button variant='outline' onClick={filterModal.openModal}>
<Icon icon='heroicons:funnel' width={18} height={18} />
Filter
</Button>
<Dropdown
trigger={
<Button color='success' isLoading={isAnyExportLoading}>
<Button variant='outline' isLoading={isAnyExportLoading}>
<Icon
icon='heroicons:cloud-arrow-down'
width={18}
height={18}
/>
Export
</Button>
}
align='end'
>
<Menu className='w-32'>
<Menu>
<MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPdf} />
</Menu>
</Dropdown>
</div>
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
<SelectInput
label='Customer'
placeholder='Pilih Customer'
isMulti
options={customerOptions}
value={customerOptions.filter((opt) =>
(tableFilterState.customer_id || [])
.map(String)
.includes(String(opt.value))
)}
onChange={customerChangeHandler}
isLoading={isLoadingCustomers}
isClearable
/>
<SelectInput
label='Sales'
placeholder='Pilih Sales'
isMulti
options={salesOptions}
value={salesOptions.filter((opt) =>
(tableFilterState.sales || [])
.map(String)
.includes(String(opt.value))
)}
onChange={salesChangeHandler}
isLoading={false}
isClearable
/>
<SelectInput
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={
dataTypeOptions?.find(
(option) => option.value === tableFilterState.date_type
) || null
}
onChange={dataTypeChangeHandler}
isLoading={false}
isClearable={false}
/>
</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}
/>
{/* 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
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>
</Modal>
{!isSubmitted ? (
<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>
) : isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>