mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +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 { 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'>
|
||||||
|
|||||||
Reference in New Issue
Block a user