From fc71defa08f9ae0d1e762c6695bd86531e00e543 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 11:43:10 +0700 Subject: [PATCH] feat(FE): adding button filter component --- src/components/helper/ButtonFilter.tsx | 40 ++++ .../finance/filter/DebtSupplierFilter.ts | 36 ++++ .../report/finance/tab/DebtSupplierTab.tsx | 184 +++++++++++------- src/lib/formik-helper.ts | 65 ++++++- src/types/api/report/debt-supplier.d.ts | 8 + 5 files changed, 258 insertions(+), 75 deletions(-) create mode 100644 src/components/helper/ButtonFilter.tsx create mode 100644 src/components/pages/report/finance/filter/DebtSupplierFilter.ts diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx new file mode 100644 index 00000000..81f70b92 --- /dev/null +++ b/src/components/helper/ButtonFilter.tsx @@ -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 ( + + ); +}; + +export default ButtonFilter; diff --git a/src/components/pages/report/finance/filter/DebtSupplierFilter.ts b/src/components/pages/report/finance/filter/DebtSupplierFilter.ts new file mode 100644 index 00000000..1c1c2fac --- /dev/null +++ b/src/components/pages/report/finance/filter/DebtSupplierFilter.ts @@ -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 = + yup.object({ + startDate: yup.string().optional().notRequired(), + endDate: yup.string().optional().notRequired(), + supplierIds: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .notRequired(), + filterBy: yup + .object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + .optional() + .notRequired(), + }); + +export type DebtSupplierFilterValues = yup.InferType< + typeof DebtSupplierFilterSchema +>; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 14bd7e70..2214ecd6 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -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({ + start_date: undefined, + end_date: undefined, + supplier_ids: undefined, + filter_by: undefined, + }); const [isSubmitted, setIsSubmitted] = useState(false); - // ===== FILTER STATE ===== - const [filterSupplier, setFilterSupplier] = useState([]); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); - const [filterDateType, setFilterDateType] = useState(); - const [filterErrors, setFilterErrors] = useState>({}); - 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 = {}; - - setFilterErrors(errors); - - if (Object.keys(errors).length === 0) { - setIsSubmitted(true); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + 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!' }} >
- + { modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm', }} > -
+
{/* Modal Header */}
@@ -534,37 +569,31 @@ const DebtSupplierTab = () => {
{ - 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 && ( -

- {filterErrors.start_date} -

- )}
{ - 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 && ( -

- {filterErrors.end_date} -

- )}
@@ -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} />
@@ -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} />
@@ -606,18 +645,15 @@ const DebtSupplierTab = () => { - - + ); diff --git a/src/lib/formik-helper.ts b/src/lib/formik-helper.ts index 9c457e86..17c6bc7d 100644 --- a/src/lib/formik-helper.ts +++ b/src/lib/formik-helper.ts @@ -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(errors: FormikErrors): string[] { export function getAllFormikErrors(errors: FormikErrors): 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( + values: T +): number { + let count = 0; + + Object.keys(values).forEach((key) => { + const value = values[key]; + if (isValueFilled(value)) { + count++; + } + }); + + return count; +} diff --git a/src/types/api/report/debt-supplier.d.ts b/src/types/api/report/debt-supplier.d.ts index 46849599..c00db7df 100644 --- a/src/types/api/report/debt-supplier.d.ts +++ b/src/types/api/report/debt-supplier.d.ts @@ -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; +}