Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
76ec351
initial logic for expense bulk pay
hungvu193 Sep 8, 2025
1dd7e64
prettier
hungvu193 Sep 11, 2025
70623ff
add hook useBulkOption
hungvu193 Sep 11, 2025
e8d1e8c
add arrow if there's option
hungvu193 Sep 11, 2025
66849ff
remove arrow if there's no option
hungvu193 Sep 11, 2025
c42a6ae
first attempt with KYCWall
hungvu193 Sep 11, 2025
e2827dc
remove unexpected files
hungvu193 Sep 12, 2025
8bfea25
merge main
hungvu193 Sep 12, 2025
fd99778
lint and spelling
hungvu193 Sep 12, 2025
29d04d1
bulk for expense large layout
hungvu193 Sep 12, 2025
3063167
linting
hungvu193 Sep 12, 2025
f52b2b6
change for narrow layout
hungvu193 Sep 13, 2025
10613bc
Merge remote-tracking branch 'origin/main' into feat-expense-bulk
hungvu193 Sep 13, 2025
cb6bb25
remove unnecessary dependency
hungvu193 Sep 13, 2025
e8072d7
fix double API call on bulk selected
hungvu193 Sep 14, 2025
fbc2beb
reuse logic
hungvu193 Sep 15, 2025
817e607
linting
hungvu193 Sep 15, 2025
f3da3fe
more linting
hungvu193 Sep 15, 2025
c151540
lint please
hungvu193 Sep 15, 2025
f50cdbc
merge main
hungvu193 Sep 16, 2025
28ee046
enable bulk for different report type
hungvu193 Sep 16, 2025
acae633
linting
hungvu193 Sep 16, 2025
bc400a9
js docs
hungvu193 Sep 16, 2025
129b323
merge main
hungvu193 Sep 16, 2025
b21742e
merge main
hungvu193 Sep 16, 2025
26b4b96
config gpg
hungvu193 Sep 17, 2025
30a1af2
Merge remote-tracking branch 'origin/main' into feat-expense-bulk
hungvu193 Sep 18, 2025
31682a4
fix report bug
hungvu193 Sep 20, 2025
aff59b5
fix transaction expense
hungvu193 Sep 20, 2025
12581f1
correct params
hungvu193 Sep 20, 2025
c37f32e
merge main
hungvu193 Sep 23, 2025
4e8ffe4
address comment
hungvu193 Sep 23, 2025
8fdcb0e
DRY
hungvu193 Sep 23, 2025
bfc5eea
merge main
hungvu193 Sep 23, 2025
cb682fd
merge latest
hungvu193 Sep 23, 2025
cbda1c5
sync with latest submodule
hungvu193 Sep 23, 2025
7010d14
prettier
hungvu193 Sep 23, 2025
f6ad9f4
merge main again
hungvu193 Sep 24, 2025
f80d2e3
update submodule
hungvu193 Sep 24, 2025
aac8c88
Merge remote-tracking branch 'origin/main' into feat-expense-bulk
hungvu193 Sep 24, 2025
335b87b
remove redundant dependency
hungvu193 Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView, isTextTooLong && shouldUseShortForm && {...styles.pl2, ...styles.pr1}]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
iconRight={Expensicons.DownArrow}
shouldShowRightIcon={!isSplitButton && !isLoading}
shouldShowRightIcon={!isSplitButton && !isLoading && options?.length > 0}
Comment thread
hungvu193 marked this conversation as resolved.
Resolved
isSplitButton={isSplitButton}
testID={testID}
textStyles={[isTextTooLong && shouldUseShortForm ? {...styles.textExtraSmall, ...styles.textBold} : {}]}
Expand Down
110 changes: 78 additions & 32 deletions src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {emailSelector} from '@selectors/Session';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
import type {ReactNode} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
Expand All @@ -8,6 +8,9 @@ import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import * as Expensicons from '@components/Icon/Expensicons';
import KYCWall from '@components/KYCWall';
import type {PaymentMethodType} from '@components/KYCWall/types';
import {LockedAccountContext} from '@components/LockedAccountModalProvider';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import ScrollView from '@components/ScrollView';
import DateSelectPopup from '@components/Search/FilterDropdowns/DateSelectPopup';
Expand All @@ -19,21 +22,24 @@ import SingleSelectPopup from '@components/Search/FilterDropdowns/SingleSelectPo
import UserSelectPopup from '@components/Search/FilterDropdowns/UserSelectPopup';
import {useSearchContext} from '@components/Search/SearchContext';
import type {SearchDateValues} from '@components/Search/SearchDatePresetFilterBase';
import type {SearchDateFilterKeys, SearchQueryJSON, SingularSearchStatus} from '@components/Search/types';
import type {BankAccountMenuItem, SearchDateFilterKeys, SearchQueryJSON, SingularSearchStatus} from '@components/Search/types';
import SearchFiltersSkeleton from '@components/Skeletons/SearchFiltersSkeleton';
import useAdvancedSearchFilters from '@hooks/useAdvancedSearchFilters';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {close} from '@libs/actions/Modal';
import {updateAdvancedFilters} from '@libs/actions/Search';
import {handleBulkPayItemSelected, updateAdvancedFilters} from '@libs/actions/Search';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {getActiveAdminWorkspaces, getAllTaxRates, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {isExpenseReport} from '@libs/ReportUtils';
import {
buildFilterFormValuesFromQuery,
buildQueryStringFromFilterFormValues,
Expand Down Expand Up @@ -65,14 +71,27 @@ type SearchFiltersBarProps = {
queryJSON: SearchQueryJSON;
headerButtonsOptions: Array<DropdownOption<SearchHeaderOptionValue>>;
isMobileSelectionModeEnabled: boolean;
currentSelectedPolicyID?: string | undefined;
currentSelectedReportID?: string | undefined;
confirmPayment?: (paymentType: PaymentMethodType | undefined) => void;
latestBankItems?: BankAccountMenuItem[] | undefined;
};

function SearchFiltersBar({queryJSON, headerButtonsOptions, isMobileSelectionModeEnabled}: SearchFiltersBarProps) {
function SearchFiltersBar({
queryJSON,
headerButtonsOptions,
isMobileSelectionModeEnabled,
currentSelectedPolicyID,
currentSelectedReportID,
confirmPayment,
latestBankItems,
}: SearchFiltersBarProps) {
const scrollRef = useRef<RNScrollView>(null);

const currentPolicy = usePolicy(currentSelectedPolicyID);
const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.validated, canBeMissing: true});
// type, groupBy and status values are not guaranteed to respect the ts type as they come from user input
const {hash, type: unsafeType, groupBy: unsafeGroupBy, status: unsafeStatus, flatFilters} = queryJSON;

const isCurrentSelectedExpenseReport = isExpenseReport(currentSelectedReportID);
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand All @@ -92,7 +111,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions, isMobileSelectionMod
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const [searchResultsErrors] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {canBeMissing: true, selector: (data) => data?.errors});

const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext);
const taxRates = getAllTaxRates();
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions ?? {}), [selectedTransactions]);
Expand Down Expand Up @@ -217,6 +236,8 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions, isMobileSelectionMod
const value = options.find((option) => option.value === filterFormValues.withdrawalType) ?? null;
return [options, value];
}, [translate, filterFormValues.withdrawalType]);
const {accountID} = useCurrentUserPersonalDetails();
const activeAdminPolicies = getActiveAdminWorkspaces(allPolicies, accountID.toString()).sort((a, b) => (a.name || '').localeCompare(b.name || ''));

const updateFilterForm = useCallback(
(values: Partial<SearchAdvancedFiltersForm>) => {
Expand Down Expand Up @@ -437,7 +458,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions, isMobileSelectionMod
* filter bar
*/
const filters = useMemo<FilterItem[]>(() => {
const fromValue = filterFormValues.from?.map((accountID) => personalDetails?.[accountID]?.displayName ?? accountID) ?? [];
const fromValue = filterFormValues.from?.map((currentAccountID) => personalDetails?.[currentAccountID]?.displayName ?? currentAccountID) ?? [];

const shouldDisplayGroupByFilter = !!groupBy?.value && groupBy?.value !== CONST.SEARCH.GROUP_BY.REPORTS;
const shouldDisplayGroupCurrencyFilter = shouldDisplayGroupByFilter && hasMultipleOutputCurrency;
Expand Down Expand Up @@ -627,30 +648,55 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions, isMobileSelectionMod
return (
<View style={[shouldShowSelectedDropdown && styles.ph5, styles.mb2, styles.searchFiltersBarContainer]}>
{shouldShowSelectedDropdown ? (
<View style={[styles.flexRow, styles.gap3]}>
<ButtonWithDropdownMenu
onPress={() => null}
shouldAlwaysShowDropdownMenu
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.SMALL}
customText={selectionButtonText}
options={headerButtonsOptions}
isSplitButton={false}
anchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
}}
/>
{!areAllMatchingItemsSelected && showSelectAllMatchingItems && (
<Button
link
small
shouldUseDefaultHover={false}
innerStyles={styles.p0}
onPress={() => selectAllMatchingItems(true)}
text={translate('search.exportAll.selectAllMatchingItems')}
/>
<KYCWall
chatReportID={currentSelectedReportID}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={
isCurrentSelectedExpenseReport ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(currentSelectedPolicyID, undefined, Navigation.getActiveRoute()) : undefined
}
onSuccessfulKYC={(paymentType) => confirmPayment?.(paymentType)}
>
{(triggerKYCFlow, buttonRef) => (
<View style={[styles.flexRow, styles.gap3]}>
<ButtonWithDropdownMenu
onPress={() => null}
shouldAlwaysShowDropdownMenu
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.SMALL}
customText={selectionButtonText}
options={headerButtonsOptions}
onSubItemSelected={(subItem) =>
handleBulkPayItemSelected(
subItem,
triggerKYCFlow,
isAccountLocked,
showLockedAccountModal,
currentPolicy,
latestBankItems,
activeAdminPolicies,
isUserValidated,
confirmPayment,
)
}
isSplitButton={false}
buttonRef={buttonRef}
anchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
}}
/>
{!areAllMatchingItemsSelected && showSelectAllMatchingItems && (
<Button
link
small
shouldUseDefaultHover={false}
innerStyles={styles.p0}
onPress={() => selectAllMatchingItems(true)}
text={translate('search.exportAll.selectAllMatchingItems')}
/>
)}
</View>
)}
</View>
</KYCWall>
) : (
<ScrollView
horizontal
Expand Down
15 changes: 14 additions & 1 deletion src/components/Search/SearchPageHeader/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import type {PaymentMethodType} from '@components/KYCWall/types';
import {useSearchContext} from '@components/Search/SearchContext';
import type {SearchQueryJSON} from '@components/Search/types';
import type {BankAccountMenuItem, SearchQueryJSON} from '@components/Search/types';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
import type CONST from '@src/CONST';
Expand All @@ -16,6 +17,10 @@ type SearchPageHeaderProps = {
headerButtonsOptions: Array<DropdownOption<SearchHeaderOptionValue>>;
handleSearch: (value: string) => void;
isMobileSelectionModeEnabled: boolean;
currentSelectedPolicyID?: string | undefined;
currentSelectedReportID?: string | undefined;
confirmPayment?: (paymentType: PaymentMethodType | undefined) => void;
latestBankItems?: BankAccountMenuItem[] | undefined;
};

type SearchHeaderOptionValue = DeepValueOf<typeof CONST.SEARCH.BULK_ACTION_TYPES> | undefined;
Expand All @@ -28,6 +33,10 @@ function SearchPageHeader({
headerButtonsOptions,
handleSearch,
isMobileSelectionModeEnabled,
currentSelectedPolicyID,
currentSelectedReportID,
confirmPayment,
latestBankItems,
}: SearchPageHeaderProps) {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {selectedTransactions} = useSearchContext();
Expand All @@ -39,6 +48,10 @@ function SearchPageHeader({
<SearchSelectedNarrow
options={headerButtonsOptions}
itemsLength={selectedTransactionsKeys.length}
currentSelectedPolicyID={currentSelectedPolicyID}
currentSelectedReportID={currentSelectedReportID}
confirmPayment={confirmPayment}
latestBankItems={latestBankItems}
/>
);
}
Expand Down
12 changes: 12 additions & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {ValueOf} from 'type-fest';
import type {PaymentMethod} from '@components/KYCWall/types';
import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types';
import type {SearchKey} from '@libs/SearchUIUtils';
import type CONST from '@src/CONST';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type IconAsset from '@src/types/utils/IconAsset';

/** Model of the selected transaction */
type SelectedTransactionInfo = {
Expand Down Expand Up @@ -206,6 +208,14 @@ type SearchParams = {
shouldCalculateTotals: boolean;
};

type BankAccountMenuItem = {
text: string;
description: string;
icon: IconAsset;
methodID: number | undefined;
value: PaymentMethod;
};

export type {
SelectedTransactionInfo,
SelectedTransactions,
Expand Down Expand Up @@ -240,5 +250,7 @@ export type {
SearchAction,
SearchCurrencyFilterKeys,
UserFriendlyValue,
SelectedReports,
SearchTextFilterKeys,
BankAccountMenuItem,
};
49 changes: 15 additions & 34 deletions src/components/SettlementButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {isCurrencySupportedForDirectReimbursement} from '@libs/actions/Policy/Policy';
import {getLastPolicyBankAccountID, getLastPolicyPaymentMethod} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import {formatPaymentMethods} from '@libs/PaymentUtils';
import {formatPaymentMethods, getActivePaymentType} from '@libs/PaymentUtils';
import {getActiveAdminWorkspaces, getPolicyEmployeeAccountIDs} from '@libs/PolicyUtils';
import {hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils';
import {
Expand Down Expand Up @@ -352,15 +352,9 @@ function SettlementButton({
onlyShowPayElsewhere,
latestBankItem,
activeAdminPolicies,
latestBankItem,
]);

const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType) => {
if (policy && shouldRestrictUserBillableActions(policy.id)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id));
return;
}

if (iouPaymentType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
if (confirmApproval) {
confirmApproval();
Expand All @@ -373,29 +367,8 @@ function SettlementButton({
onPress(iouPaymentType, false);
};

const selectPaymentMethod = (event: KYCFlowEvent, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => {
if (!isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute()));
return;
}

if (policy && shouldRestrictUserBillableActions(policy.id)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id));
return;
}

let paymentType;
switch (paymentMethod) {
case CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT:
paymentType = CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
break;
case CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT:
paymentType = CONST.IOU.PAYMENT_TYPE.VBBA;
break;
default:
paymentType = CONST.IOU.PAYMENT_TYPE.ELSEWHERE;
}
triggerKYCFlow(event, paymentType, paymentMethod, selectedPolicy ?? (event ? lastPaymentPolicy : undefined));
const selectPaymentMethod = (event: KYCFlowEvent, paymentType: string, triggerKYCFlow: TriggerKYCFlow, paymentMethod?: PaymentMethod, selectedPolicy?: Policy) => {
triggerKYCFlow(event, paymentType as PaymentMethodType, paymentMethod, selectedPolicy ?? (event ? lastPaymentPolicy : undefined));
if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) {
setPersonalBankAccountContinueKYCOnSuccess(ROUTES.ENABLE_PAYMENTS);
}
Expand Down Expand Up @@ -466,12 +439,20 @@ function SettlementButton({
return;
}

const isPaymentMethod = Object.values(CONST.PAYMENT_METHODS).includes(selectedOption as PaymentMethod);
const shouldSelectPaymentMethod = (isPaymentMethod ?? lastPaymentPolicy ?? !isEmpty(latestBankItem)) && !shouldShowApproveButton && !shouldHidePaymentOptions;
const selectedPolicy = activeAdminPolicies.find((activePolicy) => activePolicy.id === selectedOption);
if (!isUserValidated) {
Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute()));
return;
}

if (policy && shouldRestrictUserBillableActions(policy.id)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id));
return;
}

const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(selectedOption, activeAdminPolicies, latestBankItem);

if (!!selectedPolicy || shouldSelectPaymentMethod) {
selectPaymentMethod(event, triggerKYCFlow, selectedOption as PaymentMethod, selectedPolicy);
selectPaymentMethod(event, paymentType, triggerKYCFlow, selectedOption as PaymentMethod, selectedPolicy);
return;
}

Expand Down
Loading