From adb8d0f69e9f455dba5172277b1e56dcca369c2e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 10:04:51 +0700 Subject: [PATCH 1/9] 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 8d7adbbd27b4948ad86c842c5fd7f74d873ef723 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 14 Jan 2026 10:35:51 +0700 Subject: [PATCH 2/9] feat: implement lazy loading in SelectInput --- src/components/input/SelectInput.tsx | 136 ++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 21 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index d35e7589..74e23bc4 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -9,15 +9,20 @@ import Select, { SingleValue, components as ReactSelectComponents, ControlProps, + MenuListProps, } from 'react-select'; import CreatableSelect from 'react-select/creatable'; import makeAnimated from 'react-select/animated'; import { useDebounce } from 'use-debounce'; import { cn, getByPath } from '@/lib/helper'; -import useSWR from 'swr'; +import useSWRInfinite from 'swr/infinite'; import { httpClientFetcher } from '@/services/http/client'; -import { BaseApiResponse } from '@/types/api/api-general'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { + BaseApiResponse, + ErrorApiResponse, + SuccessApiResponse, +} from '@/types/api/api-general'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; export interface OptionType { value: string | number; @@ -56,6 +61,7 @@ interface SelectInputBaseProps { onInputChange?: (search: string) => void; startAdornment?: ReactNode; menuPortalTarget?: HTMLElement | null; + onMenuScrollToBottom?: ((event: WheelEvent | TouchEvent) => void) | undefined; } interface SelectInputProps extends SelectInputBaseProps { @@ -93,6 +99,29 @@ const CustomControl = < ); }; +const CustomMenuList = < + Option, + IsMulti extends boolean, + Group extends GroupBase
- { 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 54e05b71501252ae36ded7e8540cd6b0f1f47c2b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 14 Jan 2026 11:36:57 +0700 Subject: [PATCH 7/9] feat: implement lazy loading in SelectInput component in master data form --- .../customer/form/CustomerForm.tsx | 33 +++---- .../master-data/kandang/form/KandangForm.tsx | 58 +++++-------- .../location/form/LocationForm.tsx | 32 +++---- .../nonstock/form/NonstockForm.tsx | 61 +++++-------- .../master-data/product/form/ProductForm.tsx | 26 +++--- .../warehouse/form/WarehouseForm.tsx | 85 +++++++------------ 6 files changed, 115 insertions(+), 180 deletions(-) diff --git a/src/components/pages/master-data/customer/form/CustomerForm.tsx b/src/components/pages/master-data/customer/form/CustomerForm.tsx index 0a629b36..fed5b14e 100644 --- a/src/components/pages/master-data/customer/form/CustomerForm.tsx +++ b/src/components/pages/master-data/customer/form/CustomerForm.tsx @@ -23,13 +23,17 @@ import TextInput from '@/components/input/TextInput'; import { cn } from '@/lib/helper'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import TextArea from '@/components/input/TextArea'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import useSWR from 'swr'; import { UserApi } from '@/services/api/user'; import { TYPE_OPTIONS } from '@/config/constant'; import RequirePermission from '@/components/helper/RequirePermission'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { User } from '@/types/api/api-general'; interface CustomerFormProps { formType?: 'add' | 'edit' | 'detail'; @@ -47,25 +51,15 @@ const CustomerForm = ({ // Setup State const [customerFormErrorMessage, setCustomerFormErrorMessage] = useState(''); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [picSelectInputValue, setPicSelectInputValue] = useState(''); - // Fetch Data - const picUrl = `${UserApi.basePath}?${new URLSearchParams({ - search: picSelectInputValue ?? '', - })}`; - - const { data: pic, isLoading: isLoadingPic } = useSWR( - picUrl, - UserApi.getAllFetcher - ); + const { + setInputValue: setPicSelectInputValue, + options: picOptions, + isLoadingOptions: isLoadingPicOptions, + loadMore: loadMorePic, + } = useSelect(UserApi.basePath, 'id', 'name'); // -- Options data mapping - const picOptions = isResponseSuccess(pic) - ? pic?.data.map((area) => ({ - value: area.id, - label: area.name, - })) - : []; const typeOptions = TYPE_OPTIONS; // Handler Event @@ -240,11 +234,12 @@ const CustomerForm = ({ required placeholder='Pilih PIC' label='PIC' - value={formik.values.pic ?? undefined} + value={formik.values.pic?.value ? formik.values.pic : undefined} onChange={picChangeHandler} options={picOptions} onInputChange={setPicSelectInputValue} - isLoading={isLoadingPic} + onMenuScrollToBottom={loadMorePic} + isLoading={isLoadingPicOptions} isError={formik.touched.picId && Boolean(formik.errors.picId)} errorMessage={formik.errors.picId as string} isDisabled={formType === 'detail'} diff --git a/src/components/pages/master-data/kandang/form/KandangForm.tsx b/src/components/pages/master-data/kandang/form/KandangForm.tsx index ffea5718..acced3c5 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.tsx +++ b/src/components/pages/master-data/kandang/form/KandangForm.tsx @@ -9,7 +9,10 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import TextInput from '@/components/input/TextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -31,6 +34,7 @@ import { UserApi } from '@/services/api/user'; import NumberInput from '@/components/input/NumberInput'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { User } from '@/types/api/api-general'; interface KandangFormProps { type?: 'add' | 'edit' | 'detail'; @@ -128,23 +132,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { const { setValues: formikSetValues } = formik; // location - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); - - const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ - search: locationSelectInputValue ?? '', - }).toString()}`; - - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationsUrl, - LocationApi.getAllFetcher - ); - - const locationOptions = isResponseSuccess(locations) - ? locations?.data.map((location) => ({ - value: location.id, - label: location.name, - })) - : []; + const { + setInputValue: setLocationSelectInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); const locationChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('location', true); @@ -155,23 +148,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { }; // PIC - const [picSelectInputValue, setPicSelectInputValue] = useState(''); - - const picsUrl = `${UserApi.basePath}?${new URLSearchParams({ - search: picSelectInputValue ?? '', - }).toString()}`; - - const { data: pics, isLoading: isLoadingPics } = useSWR( - picsUrl, - LocationApi.getAllFetcher - ); - - const picOptions = isResponseSuccess(pics) - ? pics?.data.map((pic) => ({ - value: pic.id, - label: pic.name, - })) - : []; + const { + setInputValue: setPicSelectInputValue, + options: picOptions, + isLoadingOptions: isLoadingPicOptions, + loadMore: loadMorePics, + } = useSelect(UserApi.basePath, 'id', 'name'); const picChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('pic', true); @@ -249,7 +231,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { onChange={locationChangeHandler} options={locationOptions} onInputChange={setLocationSelectInputValue} - isLoading={isLoadingLocations} + onMenuScrollToBottom={loadMoreLocations} + isLoading={isLoadingLocationOptions} isError={ formik.touched.locationId && Boolean(formik.errors.locationId) } @@ -280,7 +263,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { onChange={picChangeHandler} options={picOptions} onInputChange={setPicSelectInputValue} - isLoading={isLoadingPics} + onMenuScrollToBottom={loadMorePics} + isLoading={isLoadingPicOptions} isError={formik.touched.picId && Boolean(formik.errors.picId)} errorMessage={formik.errors.picId as string} isDisabled={type === 'detail'} diff --git a/src/components/pages/master-data/location/form/LocationForm.tsx b/src/components/pages/master-data/location/form/LocationForm.tsx index 9f77cf86..224c0f35 100644 --- a/src/components/pages/master-data/location/form/LocationForm.tsx +++ b/src/components/pages/master-data/location/form/LocationForm.tsx @@ -9,7 +9,10 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import TextInput from '@/components/input/TextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -29,6 +32,7 @@ import { AreaApi, LocationApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { Area } from '@/types/api/master-data/area'; interface LocationFormProps { type?: 'add' | 'edit' | 'detail'; @@ -117,23 +121,12 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => { const { setValues: formikSetValues } = formik; - const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); - - const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({ - search: areaSelectInputValue ?? '', - }).toString()}`; - - const { data: areas, isLoading: isLoadingAreas } = useSWR( - areasUrl, - AreaApi.getAllFetcher - ); - - const areaOptions = isResponseSuccess(areas) - ? areas?.data.map((area) => ({ - value: area.id, - label: area.name, - })) - : []; + const { + setInputValue: setAreaSelectInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name'); const areaChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('area', true); @@ -224,7 +217,8 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => { onChange={areaChangeHandler} options={areaOptions} onInputChange={setAreaSelectInputValue} - isLoading={isLoadingAreas} + onMenuScrollToBottom={loadMoreAreas} + isLoading={isLoadingAreaOptions} isError={formik.touched.areaId && Boolean(formik.errors.areaId)} errorMessage={formik.errors.areaId as string} isDisabled={type === 'detail'} diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 7d8b8784..cd2c361b 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -9,7 +9,10 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import TextInput from '@/components/input/TextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -31,6 +34,8 @@ import { flags } from '@/types/api/api-general'; import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Uom } from '@/types/api/master-data/uom'; interface NonstockFormProps { type?: 'add' | 'edit' | 'detail'; @@ -129,23 +134,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const { setValues: formikSetValues } = formik; // UOM - const [uomSelectInputValue, setUomSelectInputValue] = useState(''); - - const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ - search: uomSelectInputValue ?? '', - }).toString()}`; - - const { data: uoms, isLoading: isLoadingUoms } = useSWR( - uomsUrl, - UomApi.getAllFetcher - ); - - const uomOptions = isResponseSuccess(uoms) - ? uoms?.data.map((uom) => ({ - value: uom.id, - label: uom.name, - })) - : []; + const { + setInputValue: setUomSelectInputValue, + options: uomOptions, + isLoadingOptions: isLoadingUomOptions, + loadMore: loadMoreUoms, + } = useSelect(UomApi.basePath, 'id', 'name'); const uomChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('uom', true); @@ -156,25 +150,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { }; // supplier - const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); - - const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ - search: supplierSelectInputValue ?? '', - }).toString()}`; - - const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( - suppliersUrl, - SupplierApi.getAllFetcher - ); - - const supplierOptions = isResponseSuccess(suppliers) - ? suppliers?.data - .filter((sup) => sup.category === 'BOP') - .map((supplier) => ({ - value: supplier.id, - label: supplier.name, - })) - : []; + const { + setInputValue: setSupplierSelectInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name'); const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('suppliers', true); @@ -264,7 +245,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { onChange={uomChangeHandler} options={uomOptions} onInputChange={setUomSelectInputValue} - isLoading={isLoadingUoms} + isLoading={isLoadingUomOptions} + onMenuScrollToBottom={loadMoreUoms} isError={formik.touched.uomId && Boolean(formik.errors.uomId)} errorMessage={formik.errors.uomId as string} isDisabled={type === 'detail'} @@ -278,7 +260,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { onChange={supplierChangeHandler} options={supplierOptions ?? []} onInputChange={setSupplierSelectInputValue} - isLoading={isLoadingSuppliers} + onMenuScrollToBottom={loadMoreSuppliers} + isLoading={isLoadingSupplierOptions} isError={ formik.touched.suppliers && Boolean(formik.errors.suppliers) } diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 204422dd..2fc3b267 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -40,6 +40,7 @@ import { import { cn } from '@/lib/helper'; import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import { Supplier } from '@/types/api/master-data/supplier'; interface ProductFormProps { type?: 'add' | 'edit' | 'detail'; @@ -145,6 +146,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { setInputValue: setUomSelectInputValue, options: uomOptions, isLoadingOptions: isLoadingUoms, + loadMore: loadMoreUoms, } = useSelect(UomApi.basePath, 'id', 'name'); const uomChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('uom', true); @@ -158,6 +160,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { setInputValue: setCategorySelectInputValue, options: categoryOptions, isLoadingOptions: isLoadingCategories, + loadMore: loadMoreCategories, } = useSelect(ProductCategoryApi.basePath, 'id', 'name'); const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('product_category', true); @@ -167,17 +170,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { }; // Supplier (multi select) - using SWR to filter by category - const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); - const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`; - const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( - suppliersUrl, - SupplierApi.getAllFetcher - ); - const supplierOptions = isResponseSuccess(suppliers) - ? suppliers?.data - .filter((sup) => sup.category === 'SAPRONAK') - .map((sup) => ({ value: sup.id, label: sup.name })) - : []; + const { + setInputValue: setSupplierSelectInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSuppliers, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { + category: 'SAPRONAK', + }); + const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; formik.setFieldTouched('supplier_ids', true); @@ -291,6 +292,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { onChange={uomChangeHandler} options={uomOptions} onInputChange={setUomSelectInputValue} + onMenuScrollToBottom={loadMoreUoms} isLoading={isLoadingUoms} isError={ (formik.touched.uom || formik.touched.uom_id) && @@ -308,6 +310,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { onChange={categoryChangeHandler} options={categoryOptions} onInputChange={setCategorySelectInputValue} + onMenuScrollToBottom={loadMoreCategories} isLoading={isLoadingCategories} isError={ (formik.touched.product_category || @@ -412,6 +415,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { onChange={supplierChangeHandler} options={supplierOptions} onInputChange={setSupplierSelectInputValue} + onMenuScrollToBottom={loadMoreSuppliers} isLoading={isLoadingSuppliers} isError={ formik.touched.supplier_ids && diff --git a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx index 0fb55a2a..cab9f750 100644 --- a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx +++ b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx @@ -9,7 +9,10 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import TextInput from '@/components/input/TextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -35,6 +38,8 @@ import { cn } from '@/lib/helper'; import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { Area } from '@/types/api/master-data/area'; +import { Kandang } from '@/types/api/master-data/kandang'; interface WarehouseFormProps { type?: 'add' | 'edit' | 'detail'; @@ -221,61 +226,28 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { const { setValues: formikSetValues } = formik; // Area - const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); - - const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({ - search: areaSelectInputValue ?? '', - }).toString()}`; - - const { data: areas, isLoading: isLoadingAreas } = useSWR( - areasUrl, - AreaApi.getAllFetcher - ); - - const areaOptions = isResponseSuccess(areas) - ? areas?.data.map((area) => ({ - value: area.id, - label: area.name, - })) - : []; + const { + setInputValue: setAreaSelectInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name'); // Location - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); - - const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ - search: locationSelectInputValue ?? '', - }).toString()}`; - - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationsUrl, - LocationApi.getAllFetcher - ); - - const locationOptions = isResponseSuccess(locations) - ? locations?.data.map((location) => ({ - value: location.id, - label: location.name, - })) - : []; + const { + setInputValue: setLocationSelectInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); // Kandang - const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); - - const kandangsUrl = `${KandangApi.basePath}?${new URLSearchParams({ - search: kandangSelectInputValue ?? '', - }).toString()}`; - - const { data: kandangs, isLoading: isLoadingKandangs } = useSWR( - kandangsUrl, - KandangApi.getAllFetcher - ); - - const kandangOptions = isResponseSuccess(kandangs) - ? kandangs?.data.map((kandang) => ({ - value: kandang.id, - label: kandang.name, - })) - : []; + const { + setInputValue: setKandangSelectInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name'); const typeChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('type', true); @@ -393,7 +365,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { onChange={areaChangeHandler} options={areaOptions} onInputChange={setAreaSelectInputValue} - isLoading={isLoadingAreas} + onMenuScrollToBottom={loadMoreAreas} + isLoading={isLoadingAreaOptions} isError={formik.touched.areaId && Boolean(formik.errors.areaId)} errorMessage={formik.errors.areaId as string} isDisabled={type === 'detail'} @@ -409,7 +382,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { onChange={locationChangeHandler} options={locationOptions} onInputChange={setLocationSelectInputValue} - isLoading={isLoadingLocations} + onMenuScrollToBottom={loadMoreLocations} + isLoading={isLoadingLocationOptions} isError={ formik.touched.locationId && Boolean(formik.errors.locationId) } @@ -427,7 +401,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { onChange={kandangChangeHandler} options={kandangOptions} onInputChange={setKandangSelectInputValue} - isLoading={isLoadingKandangs} + onMenuScrollToBottom={loadMoreKandangs} + isLoading={isLoadingKandangOptions} isError={ formik.touched.kandangId && Boolean(formik.errors.kandangId) } From 3f285a74bc2ecb34e2fe0339a0bc6100c3d09d68 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 11:56:09 +0700 Subject: [PATCH 8/9] 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' }} />