mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
refactor(FE): Move filters into modal and refactor filter state
This commit is contained in:
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user