From adb8d0f69e9f455dba5172277b1e56dcca369c2e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 10:04:51 +0700 Subject: [PATCH 1/7] feat(FE): Add checkbox multi-select and components prop --- src/components/input/SelectInput.tsx | 23 ++++-- src/components/input/SelectInputCheckbox.tsx | 82 +++++++++++++++++++ .../report/finance/tab/CustomerPaymentTab.tsx | 4 +- 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/components/input/SelectInputCheckbox.tsx diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index d35e7589..e3dbc011 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -35,6 +35,7 @@ interface SelectInputBaseProps { bottomLabel?: ReactNode; options: T[]; optionComponent?: OptionComponent; + components?: Partial; isDisabled?: boolean; isLoading?: boolean; isClearable?: boolean; @@ -56,9 +57,12 @@ interface SelectInputBaseProps { onInputChange?: (search: string) => void; startAdornment?: ReactNode; menuPortalTarget?: HTMLElement | null; + closeMenuOnSelect?: boolean; + hideSelectedOptions?: boolean; } -interface SelectInputProps extends SelectInputBaseProps { +export interface SelectInputProps + extends SelectInputBaseProps { createables?: boolean; value?: T | T[] | null; onChange?: (val: T | T[] | null) => void; @@ -101,6 +105,7 @@ const SelectInput = (props: SelectInputProps) => { onChange, options, optionComponent, + components: customComponents, isDisabled, isLoading, isClearable, @@ -119,6 +124,8 @@ const SelectInput = (props: SelectInputProps) => { onInputChange, startAdornment, menuPortalTarget, + closeMenuOnSelect, + hideSelectedOptions, } = props; const [internalInputValue, setInternalInputValue] = useState(''); @@ -128,14 +135,18 @@ const SelectInput = (props: SelectInputProps) => { const components = useMemo(() => { const base = isAnimated ? animatedComponents : {}; - const customComponents = { ...base, IndicatorSeparator: () => null }; + const mergedComponents = { ...base, IndicatorSeparator: () => null }; if (startAdornment) { - customComponents.Control = CustomControl; + mergedComponents.Control = CustomControl; } - return customComponents; - }, [isAnimated, startAdornment]); + if (customComponents) { + Object.assign(mergedComponents, customComponents); + } + + return mergedComponents; + }, [isAnimated, startAdornment, customComponents]); const internalInputChangeHandler = (val: string, meta: InputActionMeta) => { if (meta.action === 'input-change') setInternalInputValue(val); @@ -205,6 +216,8 @@ const SelectInput = (props: SelectInputProps) => { isRtl={isRtl} isSearchable={isSearchable} placeholder={placeholder} + closeMenuOnSelect={closeMenuOnSelect} + hideSelectedOptions={hideSelectedOptions} className={cn('w-full', className?.select)} classNames={{ ...(!startAdornment && { diff --git a/src/components/input/SelectInputCheckbox.tsx b/src/components/input/SelectInputCheckbox.tsx new file mode 100644 index 00000000..0827a70a --- /dev/null +++ b/src/components/input/SelectInputCheckbox.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { useMemo } from 'react'; +import { + OptionProps, + GroupBase, + components as ReactSelectComponents, +} from 'react-select'; +import SelectInput, { OptionType, SelectInputProps } from './SelectInput'; +import { cn } from '@/lib/helper'; + +interface SelectInputCheckboxProps + extends Omit< + SelectInputProps, + 'closeMenuOnSelect' | 'hideSelectedOptions' | 'optionComponent' + > { + closeMenuOnSelect?: boolean; + hideSelectedOptions?: boolean; +} + +const CheckboxOption = < + T extends OptionType, + IsMulti extends boolean, + Group extends GroupBase, +>( + props: OptionProps +) => { + const { isSelected, label, innerRef, innerProps, className } = props; + + return ( +
+ null} + className='checkbox checkbox-sm checkbox-primary pointer-events-none' + /> + +
+ ); +}; + +const SelectInputCheckbox = ( + props: SelectInputCheckboxProps +) => { + const { + closeMenuOnSelect = false, + hideSelectedOptions = false, + isMulti = true, + className, + ...restProps + } = props; + + const customComponents = useMemo(() => { + return { + Option: CheckboxOption as typeof ReactSelectComponents.Option, + }; + }, []); + + return ( + + {...restProps} + isMulti={isMulti} + closeMenuOnSelect={closeMenuOnSelect} + hideSelectedOptions={hideSelectedOptions} + className={{ + ...className, + select: cn(className?.select, 'select-checkbox'), + }} + components={customComponents} + /> + ); +}; + +export default SelectInputCheckbox; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 2b2c09a2..dfadd1ec 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -7,6 +7,7 @@ import SelectInput, { useSelect, OptionType, } from '@/components/input/SelectInput'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; @@ -608,10 +609,9 @@ const CustomerPaymentTab = () => {
- { From 3937c27c777028d0b6c8f378b072780810fe42d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 10:49:21 +0700 Subject: [PATCH 2/7] feat(FE): Show active filter count on Filter button --- src/components/input/SelectInputCheckbox.tsx | 2 +- .../report/finance/tab/CustomerPaymentTab.tsx | 64 ++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/components/input/SelectInputCheckbox.tsx b/src/components/input/SelectInputCheckbox.tsx index 0827a70a..b0ef61a3 100644 --- a/src/components/input/SelectInputCheckbox.tsx +++ b/src/components/input/SelectInputCheckbox.tsx @@ -47,7 +47,7 @@ const CheckboxOption = < ); }; -const SelectInputCheckbox = ( +const SelectInputCheckbox = ( props: SelectInputCheckboxProps ) => { const { diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index dfadd1ec..822d5938 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -116,6 +116,41 @@ const CustomerPaymentTab = () => { filterModal.closeModal(); }, [filterModal]); + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + // Date filter (start_date + end_date = 1 filter) + if (filterStartDate || filterEndDate) { + count += 1; + } + + // Customer filter + if (filterCustomer.length > 0) { + count += 1; + } + + // Sales filter + if (filterSales.length > 0) { + count += 1; + } + + // Filter by (always count if submitted) + if (isSubmitted) { + count += 1; + } + + return count; + }, [ + filterStartDate, + filterEndDate, + filterCustomer, + filterSales, + isSubmitted, + ]); + + const hasFilters = activeFiltersCount > 0; + // ===== DATA FETCHING ===== const { data: customerPayment, isLoading } = useSWR( isSubmitted @@ -533,14 +568,37 @@ const CustomerPaymentTab = () => { className={{ wrapper: 'w-full', body: 'p-1!' }} >
- +
); }; From 53277b5893e5a875c29ab49db9f706a587f71746 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 11:16:33 +0700 Subject: [PATCH 4/7] refactor(FE): Use customerOptions type for filterCustomer state --- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 8a3a1e91..9cc79a7d 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -43,7 +43,9 @@ const CustomerPaymentTab = () => { const [isSubmitted, setIsSubmitted] = useState(false); // ===== FILTER STATE ===== - const [filterCustomer, setFilterCustomer] = useState([]); + const [filterCustomer, setFilterCustomer] = useState( + [] + ); const [filterSales, setFilterSales] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); From d28fa77405b9e9f3dba10e629e43ed9d77b96087 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 11:35:10 +0700 Subject: [PATCH 5/7] feat(FE): Fetch sales from UserApi and use sales_id param --- .../report/finance/tab/CustomerPaymentTab.tsx | 28 +++++++++---------- src/services/api/report/finance-report.ts | 4 +-- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 9cc79a7d..7900056e 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -11,6 +11,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; +import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; @@ -46,7 +47,7 @@ const CustomerPaymentTab = () => { const [filterCustomer, setFilterCustomer] = useState( [] ); - const [filterSales, setFilterSales] = useState([]); + const [filterSales, setFilterSales] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); @@ -55,14 +56,11 @@ const CustomerPaymentTab = () => { const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); - const salesOptions = useMemo( - () => [ - { value: 'Sales A', label: 'Sales A' }, - { value: 'Sales B', label: 'Sales B' }, - { value: 'Sales C', label: 'Sales C' }, - // TODO: Fetch sales options from API - ], - [] + const { options: salesOptions, isLoadingOptions: isLoadingSales } = useSelect( + UserApi.basePath, + 'id', + 'name', + 'search' ); const dataTypeOptions = useMemo( @@ -161,7 +159,7 @@ const CustomerPaymentTab = () => { filterCustomer.length > 0 ? filterCustomer.map((v) => String(v.value)).join(',') : undefined, - sales: + sales_id: filterSales.length > 0 ? filterSales.map((v) => String(v.value)).join(',') : undefined, @@ -178,7 +176,7 @@ const CustomerPaymentTab = () => { ([, params]) => FinanceApi.getCustomerPaymentReport( params.customer_id, - params.sales, + params.sales_id, params.filter_by, params.start_date, params.end_date, @@ -204,7 +202,7 @@ const CustomerPaymentTab = () => { filterCustomer.length > 0 ? filterCustomer.map((v) => String(v.value)).join(',') : undefined, - sales: + sales_id: filterSales.length > 0 ? filterSales.map((v) => String(v.value)).join(',') : undefined, @@ -217,7 +215,7 @@ const CustomerPaymentTab = () => { const response = await FinanceApi.getCustomerPaymentReport( params.customer_id, - params.sales, + params.sales_id, params.filter_by, params.start_date, params.end_date, @@ -659,15 +657,15 @@ const CustomerPaymentTab = () => {
- { setFilterSales(Array.isArray(val) ? val : val ? [val] : []); }} + isLoading={isLoadingSales} isClearable className={{ wrapper: 'w-full' }} /> diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index e8ec52c8..9fa4f37c 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -14,7 +14,7 @@ export class FinanceApiService extends BaseApiService< async getCustomerPaymentReport( customer_id?: string, - sales?: string, + sales_id?: string, filter_by?: 'do_date', start_date?: string, end_date?: string, @@ -27,7 +27,7 @@ export class FinanceApiService extends BaseApiService< method: 'GET', params: { customer_id: customer_id, - sales: sales, + sales_id: sales_id, filter_by: filter_by, start_date: start_date, end_date: end_date, From 3f285a74bc2ecb34e2fe0339a0bc6100c3d09d68 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 11:56:09 +0700 Subject: [PATCH 6/7] refactor(FE): Append title classes to Card and style customer card --- src/components/Card.tsx | 8 ++++++-- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index ff4c35f2..e04fa4c7 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -148,7 +148,11 @@ const Card = ({ const hasContent = children || actions || footer; const titleContent = ( -
+
{title &&

{title}

} {subtitle &&

{subtitle}

} @@ -156,7 +160,7 @@ const Card = ({ {collapsible && (
@@ -667,6 +672,7 @@ const CustomerPaymentTab = () => { }} isLoading={isLoadingSales} isClearable + onMenuScrollToBottom={loadMoreSales} className={{ wrapper: 'w-full' }} />