feat(FE): adding button filter component

This commit is contained in:
randy-ar
2026-01-14 11:43:10 +07:00
parent bdf84c3802
commit fc71defa08
5 changed files with 258 additions and 75 deletions
+40
View File
@@ -0,0 +1,40 @@
import Button, { ButtonProps } from '@/components/Button';
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import { Icon } from '@iconify/react';
import { FormikValues } from 'formik';
export type ButtonFilterProps = ButtonProps & {
values: FormikValues;
onClick: () => void;
};
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
return (
<Button
{...props}
onClick={onClick}
className={
getFilledFormikValuesCount(values) > 0
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
: ''
}
>
<Icon
icon='heroicons:funnel'
width={20}
height={20}
className={
getFilledFormikValuesCount(values) > 0 ? 'text-blue-600' : ''
}
/>
Filter
{getFilledFormikValuesCount(values) > 0 && (
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
{getFilledFormikValuesCount(values)}
</span>
)}
</Button>
);
};
export default ButtonFilter;
@@ -0,0 +1,36 @@
import { OptionType } from '@/components/input/SelectInput';
import * as yup from 'yup';
export type DebtSupplierFilterType = {
startDate: string | null | undefined;
endDate: string | null | undefined;
supplierIds: OptionType[] | null | undefined;
filterBy: OptionType | null | undefined;
};
export const DebtSupplierFilterSchema: yup.ObjectSchema<DebtSupplierFilterType> =
yup.object({
startDate: yup.string().optional().notRequired(),
endDate: yup.string().optional().notRequired(),
supplierIds: yup
.array()
.of(
yup.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
)
.optional()
.notRequired(),
filterBy: yup
.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
.optional()
.notRequired(),
});
export type DebtSupplierFilterValues = yup.InferType<
typeof DebtSupplierFilterSchema
>;
@@ -13,7 +13,11 @@ import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { SupplierApi } from '@/services/api/master-data';
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
import {
DebtRow,
DebtSupplier,
DebtSupplierFilter,
} from '@/types/api/report/debt-supplier';
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
import { Icon } from '@iconify/react';
@@ -22,6 +26,13 @@ import { useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
import { useFormik } from 'formik';
import {
DebtSupplierFilterSchema,
DebtSupplierFilterType,
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import ButtonFilter from '@/components/helper/ButtonFilter';
const DebtSupplierTab = () => {
// ===== STATE MANAGEMENT =====
@@ -30,15 +41,14 @@ const DebtSupplierTab = () => {
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
start_date: undefined,
end_date: undefined,
supplier_ids: undefined,
filter_by: undefined,
});
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== FILTER STATE =====
const [filterSupplier, setFilterSupplier] = useState<OptionType[]>([]);
const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState('');
const [filterDateType, setFilterDateType] = useState<OptionType>();
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
const filterModal = useModal();
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
@@ -54,38 +64,51 @@ const DebtSupplierTab = () => {
[]
);
// ===== FILTER HANDLERS =====
const handleResetFilters = useCallback(() => {
setIsSubmitted(false);
setFilterSupplier([]);
setFilterStartDate('');
setFilterEndDate('');
setFilterErrors({});
}, []);
const handleFilterModalOpen = () => {
filterModal.openModal();
};
const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true);
// ===== FORMIK SETUP =====
const formik = useFormik<DebtSupplierFilterType>({
initialValues: {
startDate: null,
endDate: null,
supplierIds: null,
filterBy: null,
},
validationSchema: DebtSupplierFilterSchema,
onSubmit: (values) => {
setFilterParams({
start_date: values.startDate?.toString() || undefined,
end_date: values.endDate?.toString() || undefined,
supplier_ids:
values.supplierIds?.map((v) => String(v.value)).join(',') ||
undefined,
filter_by: values.filterBy?.value?.toString() || undefined,
});
filterModal.closeModal();
}
}, [filterModal, filterStartDate, filterEndDate]);
setIsSubmitted(true);
},
onReset: (values) => {
setFilterParams({
start_date: undefined,
end_date: undefined,
supplier_ids: undefined,
filter_by: undefined,
});
setIsSubmitted(false);
},
});
// ===== DATA FETCHING =====
const { data: debtSupplier, isLoading } = useSWR(
isSubmitted
? () => {
const params = {
supplier_ids:
filterSupplier.length > 0
? filterSupplier.map((v) => String(v.value)).join(',')
: undefined,
filter_by: filterDateType?.value?.toString() || undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
supplier_ids: filterParams.supplier_ids,
filter_by: filterParams.filter_by,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
};
return ['debt-supplier-report', params];
@@ -94,7 +117,7 @@ const DebtSupplierTab = () => {
([, params]) =>
DebtSupplierApi.getDebtSupplierReport(
params.supplier_ids,
params.filter_by?.toString() || undefined,
params.filter_by,
params.start_date,
params.end_date
)
@@ -118,13 +141,15 @@ const DebtSupplierTab = () => {
> => {
const params = {
supplier_ids:
filterSupplier.length > 0
? filterSupplier.map((v) => String(v.value)).join(',')
formik.values.supplierIds && formik.values.supplierIds.length > 0
? formik.values.supplierIds.map((v) => String(v.value)).join(',')
: undefined,
filter_by: filterDateType?.value?.toString() || undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
date_type: filterDateType ? filterDateType.value : undefined,
filter_by: formik.values.filterBy?.value?.toString() || undefined,
start_date: formik.values.startDate || undefined,
end_date: formik.values.endDate || undefined,
date_type: formik.values.filterBy
? formik.values.filterBy.value
: undefined,
limit: 100,
page: 1,
};
@@ -139,7 +164,12 @@ const DebtSupplierTab = () => {
return isResponseSuccess(response)
? (response.data as unknown as DebtSupplier[])
: null;
}, [filterSupplier, filterStartDate, filterEndDate]);
}, [
formik.values.supplierIds,
formik.values.startDate,
formik.values.endDate,
formik.values.filterBy,
]);
// ===== EXPORT HANDLERS =====
const handleExportExcel = useCallback(async () => {
@@ -396,10 +426,11 @@ const DebtSupplierTab = () => {
className={{ wrapper: 'w-full', body: 'p-1!' }}
>
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
<Button variant='outline' onClick={filterModal.openModal}>
<Icon icon='heroicons:funnel' width={18} height={18} />
Filter
</Button>
<ButtonFilter
values={formik.values}
onClick={handleFilterModalOpen}
variant='outline'
/>
<Dropdown
trigger={
@@ -514,7 +545,11 @@ const DebtSupplierTab = () => {
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
}}
>
<div className='space-y-6'>
<form
className='space-y-6'
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
{/* 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'>
@@ -534,37 +569,31 @@ const DebtSupplierTab = () => {
<div>
<DateInput
label='Tanggal'
name='start_date'
value={filterStartDate}
name='startDate'
value={formik.values.startDate || ''}
onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
formik.setFieldValue('startDate', e.target.value || null);
}}
className={{ wrapper: 'w-full' }}
isError={
formik.touched.startDate && !!formik.errors.startDate
}
errorMessage={formik.errors.startDate}
/>
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div className='mt-auto'>
<DateInput
label=' '
name='end_date'
value={filterEndDate}
name='endDate'
value={formik.values.endDate || ''}
onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
formik.setFieldValue('endDate', e.target.value || null);
}}
className={{ wrapper: 'w-full' }}
isError={formik.touched.endDate && !!formik.errors.endDate}
errorMessage={formik.errors.endDate}
/>
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div>
@@ -574,15 +603,20 @@ const DebtSupplierTab = () => {
placeholder='Pilih Supplier'
isMulti
options={supplierOptions}
value={filterSupplier}
value={formik.values.supplierIds || []}
onChange={(val) => {
setFilterSupplier(
Array.isArray(val) ? val : val ? [val] : []
formik.setFieldValue(
'supplierIds',
Array.isArray(val) ? val : val ? [val] : null
);
}}
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
isError={
formik.touched.supplierIds && !!formik.errors.supplierIds
}
errorMessage={formik.errors.supplierIds as string}
/>
</div>
@@ -591,12 +625,17 @@ const DebtSupplierTab = () => {
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={filterDateType}
value={formik.values.filterBy || null}
onChange={(val) => {
setFilterDateType(val ? (val as OptionType) : undefined);
formik.setFieldValue(
'filterBy',
val ? (val as OptionType) : null
);
}}
className={{ wrapper: 'w-full' }}
isClearable
isError={formik.touched.filterBy && !!formik.errors.filterBy}
errorMessage={formik.errors.filterBy as string}
/>
</div>
</div>
@@ -606,18 +645,15 @@ const DebtSupplierTab = () => {
<Button
variant='soft'
className='ms-4 min-w-36 rounded-lg'
onClick={handleResetFilters}
type='reset'
>
Reset Filter
</Button>
<Button
className='me-4 min-w-36 rounded-lg'
onClick={handleApplyFilters}
>
<Button className='me-4 min-w-36 rounded-lg' type='submit'>
Apply Filter
</Button>
</div>
</div>
</form>
</Modal>
</>
);
+64 -1
View File
@@ -1,4 +1,4 @@
import { FormikErrors } from 'formik';
import { FormikErrors, FormikValues } from 'formik';
export type ErrorMessage = {
key: string;
@@ -69,3 +69,66 @@ export function getUniqueFormikErrors<T>(errors: FormikErrors<T>): string[] {
export function getAllFormikErrors<T>(errors: FormikErrors<T>): ErrorMessage[] {
return parseFormikErrors(errors);
}
/**
* Check if a value is considered "filled" (not empty)
* @param value - Value to check
* @returns True if value is filled, false otherwise
*/
function isValueFilled(value: unknown): boolean {
// Check for null or undefined
if (value === null || value === undefined) {
return false;
}
// Check for empty string
if (typeof value === 'string' && value.trim() === '') {
return false;
}
// Check for empty array
if (Array.isArray(value) && value.length === 0) {
return false;
}
// Check for empty object (but not Date or other special objects)
if (
typeof value === 'object' &&
!Array.isArray(value) &&
!(value instanceof Date) &&
Object.keys(value).length === 0
) {
return false;
}
return true;
}
/**
* Count the number of filled (non-empty) values in Formik values object
* @param values - Formik values object
* @returns Number of filled values
* @example
* const values = {
* name: 'John',
* email: '',
* age: null,
* tags: ['tag1', 'tag2'],
* emptyArray: [],
* };
* getFilledFormikValuesCount(values); // Returns 2 (name and tags)
*/
export function getFilledFormikValuesCount<T extends FormikValues>(
values: T
): number {
let count = 0;
Object.keys(values).forEach((key) => {
const value = values[key];
if (isValueFilled(value)) {
count++;
}
});
return count;
}
+8
View File
@@ -33,3 +33,11 @@ export interface DebtRow {
travel_number: string;
balance: number;
}
// Filter Param
export interface DebtSupplierFilter {
start_date?: string;
end_date?: string;
supplier_ids?: string;
filter_by?: string;
}