Merge branch 'fix/finance-select-input' into 'development'

[FIX/FE] Implement Lazy Loading Select Input

See merge request mbugroup/lti-web-client!191
This commit is contained in:
Rivaldi A N S
2026-01-16 12:00:09 +00:00
6 changed files with 80 additions and 87 deletions
+10 -16
View File
@@ -1,21 +1,17 @@
import { ChangeEventHandler, useMemo, useState } from 'react'; import { ChangeEventHandler, useMemo, useState } from 'react';
import { CellContext, Row } from '@tanstack/react-table'; import { CellContext } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Dropdown from '@/components/dropdown/Dropdown';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Tooltip from '@/components/Tooltip';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Finance } from '@/types/api/finance/finance'; import { Finance } from '@/types/api/finance/finance';
@@ -23,11 +19,10 @@ import {
FINANCE_INITIAL_BALANCE_STATUS, FINANCE_INITIAL_BALANCE_STATUS,
FINANCE_INJECTION_STATUS, FINANCE_INJECTION_STATUS,
FINANCE_TRANSACTION_STATUS, FINANCE_TRANSACTION_STATUS,
ROWS_OPTIONS,
} from '@/config/constant'; } from '@/config/constant';
import { FinanceApi } from '@/services/api/finance'; import { FinanceApi } from '@/services/api/finance';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { BankApi } from '@/services/api/master-data';
import { Bank } from '@/types/api/master-data/bank'; import { Bank } from '@/types/api/master-data/bank';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
@@ -219,15 +214,12 @@ const FinanceTable = () => {
{ label: 'Tanggal Dibuat', value: 'created_at' }, { label: 'Tanggal Dibuat', value: 'created_at' },
]; ];
}, []); }, []);
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>( const {
BankApi.basePath, options: bankOptions,
'id', rawData: bankRawData,
'alias', setInputValue: bankInputValue,
'', loadMore: bankLoadMore,
{ } = useSelect<Bank>(BankApi.basePath, 'id', 'alias');
limit: 'limit',
}
);
// ===== Handler ===== // ===== Handler =====
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
@@ -501,6 +493,8 @@ const FinanceTable = () => {
label='Bank' label='Bank'
value={selectedBank} value={selectedBank}
onChange={bankChangeHandler} onChange={bankChangeHandler}
onInputChange={bankInputValue}
onMenuScrollToBottom={bankLoadMore}
isClearable isClearable
/> />
<SelectInput <SelectInput
@@ -113,20 +113,22 @@ const FormFinanceAdd = ({
options: partyOptions, options: partyOptions,
isLoadingOptions: isLoadingPartyOptions, isLoadingOptions: isLoadingPartyOptions,
rawData: partyRawData, rawData: partyRawData,
setInputValue: setPartyInputValue,
loadMore: loadMorePartyOptions,
} = useSelect<PartyCommonProps>( } = useSelect<PartyCommonProps>(
formik.values.party_type_option?.value === 'CUSTOMER' formik.values.party_type_option?.value === 'CUSTOMER'
? CustomerApi.basePath ? CustomerApi.basePath
: SupplierApi.basePath, : SupplierApi.basePath,
'id', 'id',
'name', 'name'
'',
{ limit: 'limit' }
); );
const { const {
options: bankOptions, options: bankOptions,
rawData: bankRawData, rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions, isLoadingOptions: isLoadingBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); setInputValue: setBankInputValue,
loadMore: loadMoreBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
// ===== Helper Functions ===== // ===== Helper Functions =====
const transformFormValuesToPayload = ( const transformFormValuesToPayload = (
@@ -219,6 +221,8 @@ const FormFinanceAdd = ({
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`} placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
options={partyOptions} options={partyOptions}
value={formik.values.party_id_option} value={formik.values.party_id_option}
onInputChange={setPartyInputValue}
onMenuScrollToBottom={loadMorePartyOptions}
onChange={(value) => { onChange={(value) => {
formik.setFieldValue('party_id_option', value); formik.setFieldValue('party_id_option', value);
if (isResponseSuccess(partyRawData) && value) { if (isResponseSuccess(partyRawData) && value) {
@@ -304,6 +308,8 @@ const FormFinanceAdd = ({
: [] : []
} }
value={formik.values.bank_id_option} value={formik.values.bank_id_option}
onInputChange={setBankInputValue}
onMenuScrollToBottom={loadMoreBankOptions}
onChange={(value) => { onChange={(value) => {
formik.setFieldValue('bank_id_option', value); formik.setFieldValue('bank_id_option', value);
}} }}
@@ -104,21 +104,25 @@ const FormFinanceAddInitialBalance = ({
}); });
// ===== Options ===== // ===== Options =====
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } = const {
useSelect( options: partyOptions,
formik.values.party_type_option?.value === 'CUSTOMER' isLoadingOptions: isLoadingPartyOptions,
? CustomerApi.basePath setInputValue: setPartyInputValue,
: SupplierApi.basePath, loadMore: loadMorePartyOptions,
'id', } = useSelect(
'name', formik.values.party_type_option?.value === 'CUSTOMER'
'', ? CustomerApi.basePath
{ limit: 'limit' } : SupplierApi.basePath,
); 'id',
'name'
);
const { const {
options: bankOptions, options: bankOptions,
rawData: bankRawData, rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions, isLoadingOptions: isLoadingBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); setInputValue: setBankInputValue,
loadMore: loadMoreBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
// ===== Helper Functions ===== // ===== Helper Functions =====
const transformFormValuesToPayload = ( const transformFormValuesToPayload = (
@@ -189,6 +193,8 @@ const FormFinanceAddInitialBalance = ({
placeholder='Pilih jenis pihak' placeholder='Pilih jenis pihak'
options={FINANCE_PARTY_TYPE_OPTIONS} options={FINANCE_PARTY_TYPE_OPTIONS}
value={formik.values.party_type_option} value={formik.values.party_type_option}
onInputChange={setPartyInputValue}
onMenuScrollToBottom={loadMorePartyOptions}
onChange={(value) => { onChange={(value) => {
formik.setFieldValue('party_type_option', value); formik.setFieldValue('party_type_option', value);
formik.setFieldValue('party_id_option', null); formik.setFieldValue('party_id_option', null);
@@ -218,6 +224,8 @@ const FormFinanceAddInitialBalance = ({
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`} placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
options={partyOptions} options={partyOptions}
value={formik.values.party_id_option} value={formik.values.party_id_option}
onInputChange={setPartyInputValue}
onMenuScrollToBottom={loadMorePartyOptions}
onChange={(value) => { onChange={(value) => {
formik.setFieldValue('party_id_option', value); formik.setFieldValue('party_id_option', value);
}} }}
@@ -80,7 +80,9 @@ const FormFinanceInjection = ({
options: bankOptions, options: bankOptions,
rawData: bankRawData, rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions, isLoadingOptions: isLoadingBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); setInputValue: setBankInputValue,
loadMore: loadMoreBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
// ===== Helper Functions ===== // ===== Helper Functions =====
const transformFormValuesToPayload = ( const transformFormValuesToPayload = (
@@ -162,6 +164,8 @@ const FormFinanceInjection = ({
: [] : []
} }
value={formik.values.bank_id_option} value={formik.values.bank_id_option}
onInputChange={setBankInputValue}
onMenuScrollToBottom={loadMoreBankOptions}
onChange={(value) => { onChange={(value) => {
formik.setFieldValue('bank_id_option', value); formik.setFieldValue('bank_id_option', value);
}} }}
@@ -184,12 +184,16 @@ const MarketingTable = () => {
const { const {
options: productsOptions, options: productsOptions,
isLoadingOptions: isLoadingProductsOptions, isLoadingOptions: isLoadingProductsOptions,
setInputValue: setProductsInputValue,
loadMore: loadMoreProducts,
} = useSelect(ProductApi.basePath, 'id', 'name', '', { } = useSelect(ProductApi.basePath, 'id', 'name', '', {
limit: 'limit', limit: 'limit',
}); });
const { const {
options: customersOptions, options: customersOptions,
isLoadingOptions: isLoadingCustomersOptions, isLoadingOptions: isLoadingCustomersOptions,
setInputValue: setCustomersInputValue,
loadMore: loadMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', '', { } = useSelect(CustomerApi.basePath, 'id', 'name', '', {
limit: 'limit', limit: 'limit',
}); });
@@ -400,6 +404,8 @@ const MarketingTable = () => {
.join(',') || '' .join(',') || ''
) )
} }
onInputChange={setProductsInputValue}
onMenuScrollToBottom={loadMoreProducts}
isMulti isMulti
/> />
{/* select status */} {/* select status */}
@@ -444,6 +450,8 @@ const MarketingTable = () => {
(value as OptionType)?.value.toString() || '' (value as OptionType)?.value.toString() || ''
) )
} }
onInputChange={setCustomersInputValue}
onMenuScrollToBottom={loadMoreCustomers}
/> />
</TableRowSizeSelector> </TableRowSizeSelector>
</div> </div>
@@ -5,7 +5,10 @@ import Button from '@/components/Button';
import FloatingActionsButton from '@/components/FloatingActionsButton'; import FloatingActionsButton from '@/components/FloatingActionsButton';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
@@ -59,9 +62,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const selectedRowIds = Object.keys(rowSelection) const selectedRowIds = Object.keys(rowSelection)
.filter((id) => rowSelection[id]) .filter((id) => rowSelection[id])
.map((id) => parseInt(id)); .map((id) => parseInt(id));
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null); const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>( const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null null
@@ -90,55 +90,25 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
{ revalidateOnMount: true } { revalidateOnMount: true }
); );
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({ // ===== Fetch Data Select =====
search: areaSelectInputValue, const {
limit: '100', options: optionsArea,
}).toString()}`; isLoadingOptions: isLoadingArea,
const { data: areas, isLoading: isLoadingAreas } = useSWR( setInputValue: setAreaSelectInputValue,
areaUrl, loadMore: loadMoreArea,
AreaApi.getAllFetcher } = useSelect(AreaApi.basePath, 'id', 'name');
); const {
options: optionsLocation,
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({ isLoadingOptions: isLoadingLocation,
search: locationSelectInputValue, setInputValue: setLocationSelectInputValue,
area_id: selectedArea != null ? selectedArea.value.toString() : '', loadMore: loadMoreLocation,
limit: '100', } = useSelect(LocationApi.basePath, 'id', 'name');
}).toString()}`; const {
const { data: locations, isLoading: isLoadingLocations } = useSWR( options: optionsKandang,
locationUrl, isLoadingOptions: isLoadingKandang,
LocationApi.getAllFetcher setInputValue: setKandangSelectInputValue,
); loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name');
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: kandangSelectInputValue,
location_id:
selectedLocation != null ? selectedLocation.value.toString() : '',
limit: '100',
}).toString()}`;
const { data: kandangs, isLoading: isLoadingKandang } = useSWR(
kandangUrl,
KandangApi.getAllFetcher
);
// ===== Data to Options Mapping ======
const optionsArea = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const optionsKandang = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const optionsLocation = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
// ====== HANDLER ====== // ====== HANDLER ======
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
@@ -385,7 +355,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<SelectInput <SelectInput
label='Area' label='Area'
options={optionsArea} options={optionsArea}
isLoading={isLoadingAreas} isLoading={isLoadingArea}
value={selectedArea} value={selectedArea}
onChange={(val) => { onChange={(val) => {
setSelectedArea(val as OptionType); setSelectedArea(val as OptionType);
@@ -395,12 +365,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
); );
}} }}
onInputChange={setAreaSelectInputValue} onInputChange={setAreaSelectInputValue}
onMenuScrollToBottom={loadMoreArea}
isClearable isClearable
/> />
<SelectInput <SelectInput
label='Lokasi' label='Lokasi'
options={optionsLocation} options={optionsLocation}
isLoading={isLoadingLocations} isLoading={isLoadingLocation}
value={selectedLocation} value={selectedLocation}
onChange={(val) => { onChange={(val) => {
setSelectedLocation(val as OptionType); setSelectedLocation(val as OptionType);
@@ -410,6 +381,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
); );
}} }}
onInputChange={setLocationSelectInputValue} onInputChange={setLocationSelectInputValue}
onMenuScrollToBottom={loadMoreLocation}
isClearable isClearable
/> />
<SelectInput <SelectInput
@@ -425,6 +397,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
); );
}} }}
onInputChange={setKandangSelectInputValue} onInputChange={setKandangSelectInputValue}
onMenuScrollToBottom={loadMoreKandang}
isClearable isClearable
/> />
<DebouncedTextInput <DebouncedTextInput