From 34ed87d612965f112498e5e8be71da3028894245 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:03:10 +0530 Subject: [PATCH 01/13] Change accounting section into multiple sections --- src/languages/en.ts | 3 + src/languages/es.ts | 3 + src/libs/SearchUIUtils.ts | 289 +++++++++++++++++++++---- src/pages/Search/SearchTypeMenu.tsx | 9 +- tests/unit/Search/SearchUIUtilsTest.ts | 51 +++-- 5 files changed, 288 insertions(+), 67 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 4750b1a0ee63e..5b5e49b73aa41 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7071,9 +7071,12 @@ const translations = { groupColumns: 'Group columns', expenseColumns: 'Expense Columns', statements: 'Statements', + cardStatements: 'Card statements', unapprovedCash: 'Unapproved cash', unapprovedCard: 'Unapproved card', + unapprovedAccruals: 'Unapproved accruals', reconciliation: 'Reconciliation', + bankReconciliation: 'Bank reconciliation', topSpenders: 'Top spenders', saveSearch: 'Save search', deleteSavedSearch: 'Delete saved search', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1d01c26234e93..37319e768126b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6858,9 +6858,12 @@ ${amount} para ${merchant} - ${date}`, groupColumns: 'Columnas de grupo', expenseColumns: 'Columnas de gastos', statements: 'Extractos', + cardStatements: 'Extractos de tarjeta', unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', + unapprovedAccruals: 'Acumulaciones no aprobadas', reconciliation: 'Conciliación', + bankReconciliation: 'Conciliación bancaria', topSpenders: 'Mayores gastadores', view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'}, saveSearch: 'Guardar búsqueda', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 46b3267a2be44..fcd253b7a3e7c 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -429,8 +429,9 @@ type SearchTypeMenuSection = { }; type SearchTypeMenuItem = { - key: SearchKey; - translationPath: TranslationPaths; + key: string; + translationPath?: TranslationPaths; + title?: string; type: SearchDataTypes; icon?: IconAsset | Extract; searchQuery: string; @@ -808,6 +809,136 @@ function getSuggestedSearches( }; } +function createCardStatementsMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + feed: [cardFeed.id], + groupBy: CONST.SEARCH.GROUP_BY.CARD, + postedOn: CONST.SEARCH.DATE_PRESETS.LAST_STATEMENT, + }); + + return { + key: `${CONST.SEARCH.SEARCH_KEYS.STATEMENTS}_${cardFeed.id}`, + title: cardFeed.name, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'CreditCard', + searchQuery, + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }; +} + +function createUnapprovedCashMenuItem(): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], + groupBy: CONST.SEARCH.GROUP_BY.FROM, + reimbursable: CONST.SEARCH.BOOLEAN.YES, + }); + + return { + key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, + translationPath: 'workspace.qbo.accountingMethods.values.CASH', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'MoneyHourglass', + searchQuery, + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }; +} + +function createUnapprovedCardMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + feed: [cardFeed.id], + groupBy: CONST.SEARCH.GROUP_BY.CARD, + status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], + }); + + return { + key: `${CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD}_${cardFeed.id}`, + title: cardFeed.name, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'CreditCardHourglass', + searchQuery, + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }; +} + +function createReimbursementReconciliationMenuItem(): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT, + withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + }); + + return { + key: `${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`, + translationPath: 'workspace.common.reimburse', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'Bank', + searchQuery, + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }; +} + +function createExpensifyCardReconciliationMenuItem(): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD, + withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + }); + + return { + key: `${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`, + translationPath: 'workspace.common.expensifyCard', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'Bank', + searchQuery, + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }; +} + function getDefaultActionableSearchMenuItem(menuItems: SearchTypeMenuItem[]) { return menuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.APPROVE) ?? menuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.SUBMIT); } @@ -3514,9 +3645,9 @@ function isCorrectSearchUserName(displayName?: string) { } function isTodoSearch(hash: number, suggestedSearches: Record) { - const TODO_KEYS: SearchKey[] = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; + const TODO_KEYS = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; const matchedSearchKey = Object.values(suggestedSearches).find((search) => search.hash === hash)?.key; - return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey); + return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey as SearchKey); } // eslint-disable-next-line @typescript-eslint/max-params @@ -3535,9 +3666,46 @@ function createTypeMenuSections( reportCounts: {[CONST.SEARCH.SEARCH_KEYS.SUBMIT]: number; [CONST.SEARCH.SEARCH_KEYS.APPROVE]: number; [CONST.SEARCH.SEARCH_KEYS.PAY]: number; [CONST.SEARCH.SEARCH_KEYS.EXPORT]: number}, ): SearchTypeMenuSection[] { const typeMenuSections: SearchTypeMenuSection[] = []; + const cardFeedsForAccounting = [ + ...Object.values( + Object.values(cardFeedsByPolicy ?? {}) + .flat() + .reduce>((acc, feed) => { + acc[feed.id] = feed; + return acc; + }, {}), + ), + ...(defaultExpensifyCard ? [defaultExpensifyCard] : []), + ]; const suggestedSearches = getSuggestedSearches(currentUserAccountID, defaultCardFeed?.id, icons); const suggestedSearchesVisibility = getSuggestedSearchesVisibility(currentUserEmail, cardFeedsByPolicy, policies, defaultExpensifyCard); + let shouldShowReimbursementsReconciliation = false; + let shouldShowExpensifyCardReconciliation = false; + + for (const policy of Object.values(policies ?? {})) { + if (!policy) { + continue; + } + + const isPaidPolicy = isPaidGroupPolicy(policy); + const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; + if (!isPaidPolicy || !isAdmin) { + continue; + } + + const isPaymentEnabled = arePaymentsEnabled(policy); + const hasVBBA = !!policy.achAccount?.bankAccountID && policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.OPEN; + const hasReimburser = !!policy.achAccount?.reimburser; + const isECardEnabled = !!policy.areExpensifyCardsEnabled; + + shouldShowReimbursementsReconciliation ||= isPaymentEnabled && hasVBBA && hasReimburser; + shouldShowExpensifyCardReconciliation ||= isECardEnabled; + + if (shouldShowReimbursementsReconciliation && shouldShowExpensifyCardReconciliation) { + break; + } + } // Todo section { @@ -3612,53 +3780,86 @@ function createTypeMenuSections( } } - // Accounting section + // Accounting sections { - const accountingSection: SearchTypeMenuSection = { - translationPath: 'workspace.common.accounting', - menuItems: [], - }; - - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]) { - accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.STATEMENTS], - emptyState: { - title: 'search.searchResults.emptyStatementsResults.title', - subtitle: 'search.searchResults.emptyStatementsResults.subtitle', - }, - }); - } - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { - accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH], - emptyState: { - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', - }, + const accountingMenuSections: SearchTypeMenuSection[] = []; + + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS] && cardFeedsForAccounting.length > 0) { + accountingMenuSections.push({ + translationPath: 'search.cardStatements', + menuItems: cardFeedsForAccounting.map((feed) => ({ + ...createCardStatementsMenuItem(feed), + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, + })), }); } - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]) { - accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD], - emptyState: { - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', - }, - }); + + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH] || suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]) { + const unapprovedAccrualsMenuItems: SearchTypeMenuItem[] = []; + + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { + unapprovedAccrualsMenuItems.push({ + ...createUnapprovedCashMenuItem(), + emptyState: { + title: 'search.searchResults.emptyUnapprovedResults.title', + subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', + }, + }); + } + + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD] && cardFeedsForAccounting.length > 0) { + unapprovedAccrualsMenuItems.push( + ...cardFeedsForAccounting.map((feed) => ({ + ...createUnapprovedCardMenuItem(feed), + emptyState: { + title: 'search.searchResults.emptyUnapprovedResults.title', + subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', + }, + })), + ); + } + + if (unapprovedAccrualsMenuItems.length > 0) { + accountingMenuSections.push({ + translationPath: 'search.unapprovedAccruals', + menuItems: unapprovedAccrualsMenuItems, + }); + } } - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]) { - accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION], - emptyState: { - title: 'search.searchResults.emptyStatementsResults.title', - subtitle: 'search.searchResults.emptyStatementsResults.subtitle', - }, + + if (shouldShowReimbursementsReconciliation || shouldShowExpensifyCardReconciliation) { + const reconciliationMenuItems: SearchTypeMenuItem[] = []; + + if (shouldShowReimbursementsReconciliation) { + reconciliationMenuItems.push({ + ...createReimbursementReconciliationMenuItem(), + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, + }); + } + + if (shouldShowExpensifyCardReconciliation) { + reconciliationMenuItems.push({ + ...createExpensifyCardReconciliationMenuItem(), + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, + }); + } + + accountingMenuSections.push({ + translationPath: 'search.bankReconciliation', + menuItems: reconciliationMenuItems, }); } - if (accountingSection.menuItems.length > 0) { - typeMenuSections.push(accountingSection); - } + typeMenuSections.push(...accountingMenuSections); } // Saved section diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index cc673f3636363..e263ebe0c5d56 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -230,8 +230,13 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { return -1; } + const exactMatchIndex = flattenedMenuItems.findIndex((item) => item.hash === hash); + if (exactMatchIndex !== -1) { + return exactMatchIndex; + } + return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash); - }, [similarSearchHash, isSavedSearchActive, flattenedMenuItems]); + }, [hash, similarSearchHash, isSavedSearchActive, flattenedMenuItems]); return ( <> @@ -272,7 +277,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { key={item.key} disabled={false} interactive - title={translate(item.translationPath)} + title={item.title ?? (item.translationPath ? translate(item.translationPath) : '')} badgeStyle={styles.todoBadge} icon={icon} iconWidth={variables.iconSizeNormal} diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 97e8843ec7b34..c761d0308fefb 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -4495,7 +4495,7 @@ describe('SearchUIUtils', () => { expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.EXPORT); }); - it('should show accounting section with statements, unapproved cash, unapproved card, and reconciliation items', () => { + it('should show split accounting sections with per-feed items and reconciliation sub-options', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -4549,15 +4549,24 @@ describe('SearchUIUtils', () => { reportCounts, ); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(accountingSection).toBeDefined(); - expect(accountingSection?.menuItems.length).toBeGreaterThan(0); + const statementsSection = sections.find((section) => section.translationPath === 'search.cardStatements'); + const unapprovedSection = sections.find((section) => section.translationPath === 'search.unapprovedAccruals'); + const reconciliationSection = sections.find((section) => section.translationPath === 'search.bankReconciliation'); - const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.STATEMENTS); - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH); - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD); - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + expect(statementsSection).toBeDefined(); + expect(unapprovedSection).toBeDefined(); + expect(reconciliationSection).toBeDefined(); + + const statementItemKeys = statementsSection?.menuItems.map((item) => item.key) ?? []; + expect(statementItemKeys.some((key) => key.startsWith(`${CONST.SEARCH.SEARCH_KEYS.STATEMENTS}_`))).toBeTruthy(); + + const unapprovedItemKeys = unapprovedSection?.menuItems.map((item) => item.key) ?? []; + expect(unapprovedItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH); + expect(unapprovedItemKeys.some((key) => key.startsWith(`${CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD}_`))).toBeTruthy(); + + const reconciliationItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; + expect(reconciliationItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`); + expect(reconciliationItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`); }); it('should show saved section when there are saved searches', () => { @@ -4674,7 +4683,7 @@ describe('SearchUIUtils', () => { expect(todoSection).toBeUndefined(); }); - it('should not show accounting section when user has no admin permissions or card feeds', () => { + it('should not show accounting sections when user has no admin permissions or card feeds', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -4704,8 +4713,8 @@ describe('SearchUIUtils', () => { reportCounts, ); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(accountingSection).toBeUndefined(); + const accountingSections = sections.filter((section) => ['search.cardStatements', 'search.unapprovedAccruals', 'search.bankReconciliation'].includes(section.translationPath)); + expect(accountingSections).toHaveLength(0); }); it('should show reconciliation for ACH-only scenario (payments enabled, active VBBA, reimburser set, areExpensifyCardsEnabled = false)', () => { @@ -4735,14 +4744,14 @@ describe('SearchUIUtils', () => { const {result: icons} = renderHook(() => useMemoizedLazyExpensifyIcons(['Document', 'Pencil', 'ThumbsUp'])); const sections = SearchUIUtils.createTypeMenuSections(icons.current, adminEmail, adminAccountID, {}, undefined, mockPolicies, {}, false, undefined, false, {}, reportCounts); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(accountingSection).toBeDefined(); + const reconciliationSection = sections.find((section) => section.translationPath === 'search.bankReconciliation'); + expect(reconciliationSection).toBeDefined(); - const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + const menuItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`); }); - it('should not show reconciliation for card-only scenario without card feeds (areExpensifyCardsEnabled = true but no card feeds)', () => { + it('should show Expensify Card reconciliation for card-only scenario without card feeds', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -4773,11 +4782,11 @@ describe('SearchUIUtils', () => { {}, reportCounts, ); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + const reconciliationSection = sections.find((section) => section.translationPath === 'search.bankReconciliation'); - expect(accountingSection).toBeDefined(); - const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + expect(reconciliationSection).toBeDefined(); + const menuItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`); }); it('should generate correct routes', () => { From 8cbcc8b0cb377b152ef08f185fe47fc482d973f6 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:54:59 +0530 Subject: [PATCH 02/13] Prefer hash --- src/hooks/useSearchTypeMenu.tsx | 4 ++-- src/libs/SearchUIUtils.ts | 8 ++++++-- src/pages/Search/SearchTypeMenu.tsx | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index f456e19ffcb1f..ba840b9918098 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -176,8 +176,8 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { ]); const [activeItemIndex, isExploreSectionActive] = useMemo( - () => getActiveSearchItemIndex(flattenedMenuItems, similarSearchHash, isSavedSearchActive, queryJSON?.type), - [similarSearchHash, isSavedSearchActive, flattenedMenuItems, queryJSON?.type], + () => getActiveSearchItemIndex(flattenedMenuItems, hash, similarSearchHash, isSavedSearchActive, queryJSON?.type), + [hash, similarSearchHash, isSavedSearchActive, flattenedMenuItems, queryJSON?.type], ); const allSearchAdvancedFilters = useStickySearchFilters(isExploreSectionActive && !shouldShowSuggestedSearchSkeleton); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 759a4245d1155..9211c806c3667 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -4726,12 +4726,13 @@ function navigateToSearchRHP(route: {route: string; getRoute: (backTo?: string) } /** - * Returns the index of the active item in flattenedMenuItems by comparing similarSearchHash. + * Returns the index of the active item in flattenedMenuItems by comparing hash first, then similarSearchHash. * * Also returns a value indicating whether the item in the Explore section is active */ function getActiveSearchItemIndex( flattenedMenuItems: SearchTypeMenuItem[], + hash: number | undefined, similarSearchHash: number | undefined, isSavedSearchActive: boolean, queryType: string | undefined, @@ -4741,7 +4742,10 @@ function getActiveSearchItemIndex( return [-1, false]; } - let activeItemIndex = flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash); + let activeItemIndex = flattenedMenuItems.findIndex((item) => item.hash === hash); + if (activeItemIndex === -1) { + activeItemIndex = flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash); + } if (activeItemIndex === -1) { activeItemIndex = flattenedMenuItems.findIndex((item) => { if (queryType === CONST.SEARCH.DATA_TYPES.EXPENSE) { diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 867f33aa44ba4..e581db1b69417 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -227,8 +227,8 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { ); const [activeItemIndex, isExploreSectionActive] = useMemo( - () => getActiveSearchItemIndex(flattenedMenuItems, similarSearchHash, isSavedSearchActive, queryJSON?.type), - [similarSearchHash, isSavedSearchActive, flattenedMenuItems, queryJSON?.type], + () => getActiveSearchItemIndex(flattenedMenuItems, hash, similarSearchHash, isSavedSearchActive, queryJSON?.type), + [hash, similarSearchHash, isSavedSearchActive, flattenedMenuItems, queryJSON?.type], ); const allSearchAdvancedFilters = useStickySearchFilters(isExploreSectionActive && !shouldShowSuggestedSearchSkeleton); From 8c73f1bf5f0c3980ae436fe8a5030a39d4c385e8 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:16:25 +0530 Subject: [PATCH 03/13] Address bot comments --- src/hooks/useSearchTypeMenu.tsx | 2 +- src/libs/SearchUIUtils.ts | 119 +++++++++++--------------------- 2 files changed, 40 insertions(+), 81 deletions(-) diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index ba840b9918098..0bd71dc5143a8 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -207,7 +207,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { sectionItems.push({ badgeText: item.badgeText, - text: translate(item.translationPath), + text: item.title ?? (item.translationPath ? translate(item.translationPath) : ''), isSelected, icon, success: isSelected, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 9211c806c3667..91ad052689a30 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -810,19 +810,9 @@ function getSuggestedSearches( }; } -function createCardStatementsMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { - const searchQuery = buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - feed: [cardFeed.id], - groupBy: CONST.SEARCH.GROUP_BY.CARD, - postedOn: CONST.SEARCH.DATE_PRESETS.LAST_STATEMENT, - }); - +function createBaseSearchTypeMenuItem(searchQuery: string, overrides: Omit): SearchTypeMenuItem { return { - key: `${CONST.SEARCH.SEARCH_KEYS.STATEMENTS}_${cardFeed.id}`, - title: cardFeed.name, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'CreditCard', + ...overrides, searchQuery, get searchQueryJSON() { return buildSearchQueryJSON(this.searchQuery); @@ -836,6 +826,22 @@ function createCardStatementsMenuItem(cardFeed: CardFeedForDisplay): SearchTypeM }; } +function createCardStatementsMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + feed: [cardFeed.id], + groupBy: CONST.SEARCH.GROUP_BY.CARD, + postedOn: CONST.SEARCH.DATE_PRESETS.LAST_STATEMENT, + }); + + return createBaseSearchTypeMenuItem(searchQuery, { + key: `${CONST.SEARCH.SEARCH_KEYS.STATEMENTS}_${cardFeed.id}`, + title: cardFeed.name, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'CreditCard', + }); +} + function createUnapprovedCashMenuItem(): SearchTypeMenuItem { const searchQuery = buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -844,22 +850,12 @@ function createUnapprovedCashMenuItem(): SearchTypeMenuItem { reimbursable: CONST.SEARCH.BOOLEAN.YES, }); - return { + return createBaseSearchTypeMenuItem(searchQuery, { key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, translationPath: 'workspace.qbo.accountingMethods.values.CASH', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'MoneyHourglass', - searchQuery, - get searchQueryJSON() { - return buildSearchQueryJSON(this.searchQuery); - }, - get hash() { - return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - get similarSearchHash() { - return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; - }, - }; + }); } function createUnapprovedCardMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { @@ -870,22 +866,12 @@ function createUnapprovedCardMenuItem(cardFeed: CardFeedForDisplay): SearchTypeM status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], }); - return { + return createBaseSearchTypeMenuItem(searchQuery, { key: `${CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD}_${cardFeed.id}`, title: cardFeed.name, type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'CreditCardHourglass', - searchQuery, - get searchQueryJSON() { - return buildSearchQueryJSON(this.searchQuery); - }, - get hash() { - return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - get similarSearchHash() { - return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; - }, - }; + }); } function createReimbursementReconciliationMenuItem(): SearchTypeMenuItem { @@ -896,22 +882,12 @@ function createReimbursementReconciliationMenuItem(): SearchTypeMenuItem { groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, }); - return { + return createBaseSearchTypeMenuItem(searchQuery, { key: `${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`, translationPath: 'workspace.common.reimburse', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'Bank', - searchQuery, - get searchQueryJSON() { - return buildSearchQueryJSON(this.searchQuery); - }, - get hash() { - return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - get similarSearchHash() { - return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; - }, - }; + }); } function createExpensifyCardReconciliationMenuItem(): SearchTypeMenuItem { @@ -922,22 +898,12 @@ function createExpensifyCardReconciliationMenuItem(): SearchTypeMenuItem { groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, }); - return { + return createBaseSearchTypeMenuItem(searchQuery, { key: `${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`, translationPath: 'workspace.common.expensifyCard', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'Bank', - searchQuery, - get searchQueryJSON() { - return buildSearchQueryJSON(this.searchQuery); - }, - get hash() { - return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - get similarSearchHash() { - return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; - }, - }; + }); } function getDefaultActionableSearchMenuItem(menuItems: SearchTypeMenuItem[]) { @@ -3784,16 +3750,21 @@ function createTypeMenuSections( // Accounting sections { const accountingMenuSections: SearchTypeMenuSection[] = []; + const statementsEmptyState = { + title: 'search.searchResults.emptyStatementsResults.title' as const, + subtitle: 'search.searchResults.emptyStatementsResults.subtitle' as const, + }; + const unapprovedEmptyState = { + title: 'search.searchResults.emptyUnapprovedResults.title' as const, + subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle' as const, + }; if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS] && cardFeedsForAccounting.length > 0) { accountingMenuSections.push({ translationPath: 'search.cardStatements', menuItems: cardFeedsForAccounting.map((feed) => ({ ...createCardStatementsMenuItem(feed), - emptyState: { - title: 'search.searchResults.emptyStatementsResults.title', - subtitle: 'search.searchResults.emptyStatementsResults.subtitle', - }, + emptyState: statementsEmptyState, })), }); } @@ -3804,10 +3775,7 @@ function createTypeMenuSections( if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { unapprovedAccrualsMenuItems.push({ ...createUnapprovedCashMenuItem(), - emptyState: { - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', - }, + emptyState: unapprovedEmptyState, }); } @@ -3815,10 +3783,7 @@ function createTypeMenuSections( unapprovedAccrualsMenuItems.push( ...cardFeedsForAccounting.map((feed) => ({ ...createUnapprovedCardMenuItem(feed), - emptyState: { - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', - }, + emptyState: unapprovedEmptyState, })), ); } @@ -3837,20 +3802,14 @@ function createTypeMenuSections( if (shouldShowReimbursementsReconciliation) { reconciliationMenuItems.push({ ...createReimbursementReconciliationMenuItem(), - emptyState: { - title: 'search.searchResults.emptyStatementsResults.title', - subtitle: 'search.searchResults.emptyStatementsResults.subtitle', - }, + emptyState: statementsEmptyState, }); } if (shouldShowExpensifyCardReconciliation) { reconciliationMenuItems.push({ ...createExpensifyCardReconciliationMenuItem(), - emptyState: { - title: 'search.searchResults.emptyStatementsResults.title', - subtitle: 'search.searchResults.emptyStatementsResults.subtitle', - }, + emptyState: statementsEmptyState, }); } From dd0a09a10ca4a67f76e0cd8480c19211f263b23e Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:14:43 +0530 Subject: [PATCH 04/13] TS fixes --- src/components/Search/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 13701ea344ee7..8abdab9be162b 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -76,6 +76,7 @@ import { shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, } from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; @@ -286,7 +287,14 @@ function Search({ const {defaultCardFeed} = useCardFeedsForDisplay(); const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); - const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); + const searchKey = useMemo(() => { + for (const suggestedSearchKey of Object.values(CONST.SEARCH.SEARCH_KEYS)) { + if (suggestedSearches[suggestedSearchKey].similarSearchHash === similarSearchHash) { + return suggestedSearchKey; + } + } + return undefined; + }, [suggestedSearches, similarSearchHash]); const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0); From 159d3a8d38de38c414cca15ff4f7cdc2b224747e Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:52:29 +0530 Subject: [PATCH 05/13] revert extra changes --- src/components/Search/index.tsx | 9 +-------- src/libs/SearchUIUtils.ts | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 8abdab9be162b..035f7ec0998d5 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -287,14 +287,7 @@ function Search({ const {defaultCardFeed} = useCardFeedsForDisplay(); const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); - const searchKey = useMemo(() => { - for (const suggestedSearchKey of Object.values(CONST.SEARCH.SEARCH_KEYS)) { - if (suggestedSearches[suggestedSearchKey].similarSearchHash === similarSearchHash) { - return suggestedSearchKey; - } - } - return undefined; - }, [suggestedSearches, similarSearchHash]); + const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 91ad052689a30..08da24b9ec15e 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3612,9 +3612,9 @@ function isCorrectSearchUserName(displayName?: string) { } function isTodoSearch(hash: number, suggestedSearches: Record) { - const TODO_KEYS = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; + const TODO_KEYS: SearchKey[] = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; const matchedSearchKey = Object.values(suggestedSearches).find((search) => search.hash === hash)?.key; - return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey as SearchKey); + return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey); } // eslint-disable-next-line @typescript-eslint/max-params From b0bf1dae413dba1f69e73afec0d2b6c28717fa9c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:13:06 +0530 Subject: [PATCH 06/13] remove extra import --- src/components/Search/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 035f7ec0998d5..13701ea344ee7 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -76,7 +76,6 @@ import { shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, } from '@libs/SearchUIUtils'; -import type {SearchKey} from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; From cd28a11e7855a95e14ea9e3c53117b1f646b39ae Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:50:17 +0530 Subject: [PATCH 07/13] TS fix --- src/libs/SearchUIUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 08da24b9ec15e..ca844b8c982d3 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3613,7 +3613,7 @@ function isCorrectSearchUserName(displayName?: string) { function isTodoSearch(hash: number, suggestedSearches: Record) { const TODO_KEYS: SearchKey[] = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; - const matchedSearchKey = Object.values(suggestedSearches).find((search) => search.hash === hash)?.key; + const matchedSearchKey = (Object.entries(suggestedSearches).find(([, search]) => search.hash === hash)?.[0] ?? '') as SearchKey; return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey); } From 9d3cf188b1c889179f54be7261add760926e7ea4 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:33:26 +0530 Subject: [PATCH 08/13] Match by hash first --- src/pages/Search/SearchTypeMenu.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index e7502eef9f1a9..007065cc88770 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -231,8 +231,13 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { return -1; } + const exactMatchIndex = flattenedMenuItems.findIndex((item) => item.hash === hash); + if (exactMatchIndex !== -1) { + return exactMatchIndex; + } + return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash); - }, [similarSearchHash, isSavedSearchActive, flattenedMenuItems]); + }, [hash, similarSearchHash, isSavedSearchActive, flattenedMenuItems]); return ( <> From 79754ebfe0a0c8a2311bc98145a0c9ea3454d395 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:49:40 +0530 Subject: [PATCH 09/13] Update the searchKey logic --- src/components/Search/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 13701ea344ee7..6e20df1e04486 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -76,6 +76,7 @@ import { shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, } from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; @@ -286,7 +287,15 @@ function Search({ const {defaultCardFeed} = useCardFeedsForDisplay(); const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); - const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); + const searchKey = useMemo(() => { + const exactMatch = Object.entries(suggestedSearches).find(([, search]) => search.hash === hash)?.[0]; + if (exactMatch) { + return exactMatch as SearchKey; + } + + const similarMatch = Object.entries(suggestedSearches).find(([, search]) => search.similarSearchHash === similarSearchHash)?.[0]; + return similarMatch as SearchKey | undefined; + }, [suggestedSearches, hash, similarSearchHash]); const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0); From 8bde126aaaa8ddb7ea043685d404e352c9a4d7b3 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:18:11 +0530 Subject: [PATCH 10/13] revert --- Mobile-Expensify | 2 +- src/components/Search/index.tsx | 11 +- src/hooks/useSearchTypeMenu.tsx | 2 +- src/languages/en.ts | 3 - src/languages/es.ts | 3 - src/libs/SearchUIUtils.ts | 242 +++++-------------------- src/pages/Search/SearchTypeMenu.tsx | 9 +- tests/unit/Search/SearchUIUtilsTest.ts | 51 +++--- 8 files changed, 67 insertions(+), 256 deletions(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 15b7a731d3ae7..20986bd4a5ad4 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 15b7a731d3ae79f41c379e576f91730533e11f3e +Subproject commit 20986bd4a5ad429b593105a7ba3f43e99068801b diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 821e12b99a0d5..6118504e0a23b 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -76,7 +76,6 @@ import { shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, } from '@libs/SearchUIUtils'; -import type {SearchKey} from '@libs/SearchUIUtils'; import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; @@ -287,15 +286,7 @@ function Search({ const {defaultCardFeed} = useCardFeedsForDisplay(); const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]); - const searchKey = useMemo(() => { - const exactMatch = Object.entries(suggestedSearches).find(([, search]) => search.hash === hash)?.[0]; - if (exactMatch) { - return exactMatch as SearchKey; - } - - const similarMatch = Object.entries(suggestedSearches).find(([, search]) => search.similarSearchHash === similarSearchHash)?.[0]; - return similarMatch as SearchKey | undefined; - }, [suggestedSearches, hash, similarSearchHash]); + const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); const searchDataType = useMemo(() => (shouldUseLiveData ? CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT : searchResults?.search?.type), [shouldUseLiveData, searchResults?.search?.type]); const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, hash, offset === 0); diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index 10fb03e317321..09829148650f9 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -209,7 +209,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { sectionItems.push({ badgeText: item.badgeText, - text: item.title ?? (item.translationPath ? translate(item.translationPath) : ''), + text: translate(item.translationPath), isSelected, icon, success: isSelected, diff --git a/src/languages/en.ts b/src/languages/en.ts index 10cb8d624bf08..f03d988bf59a5 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7033,12 +7033,9 @@ const translations = { groupColumns: 'Group columns', expenseColumns: 'Expense Columns', statements: 'Statements', - cardStatements: 'Card statements', unapprovedCash: 'Unapproved cash', unapprovedCard: 'Unapproved card', - unapprovedAccruals: 'Unapproved accruals', reconciliation: 'Reconciliation', - bankReconciliation: 'Bank reconciliation', topSpenders: 'Top spenders', saveSearch: 'Save search', deleteSavedSearch: 'Delete saved search', diff --git a/src/languages/es.ts b/src/languages/es.ts index d99721be08560..cbeb125f57ff8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6843,12 +6843,9 @@ ${amount} para ${merchant} - ${date}`, groupColumns: 'Columnas de grupo', expenseColumns: 'Columnas de gastos', statements: 'Extractos', - cardStatements: 'Extractos de tarjeta', unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', - unapprovedAccruals: 'Acumulaciones no aprobadas', reconciliation: 'Conciliación', - bankReconciliation: 'Conciliación bancaria', topSpenders: 'Mayores gastadores', view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'}, saveSearch: 'Guardar búsqueda', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 90b768314ab58..48507243fb6a0 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -429,9 +429,8 @@ type SearchTypeMenuSection = { }; type SearchTypeMenuItem = { - key: string; - translationPath?: TranslationPaths; - title?: string; + key: SearchKey; + translationPath: TranslationPaths; type: SearchDataTypes; icon?: | IconAsset @@ -840,102 +839,6 @@ function getSuggestedSearches( }; } -function createBaseSearchTypeMenuItem(searchQuery: string, overrides: Omit): SearchTypeMenuItem { - return { - ...overrides, - searchQuery, - get searchQueryJSON() { - return buildSearchQueryJSON(this.searchQuery); - }, - get hash() { - return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - get similarSearchHash() { - return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; - }, - }; -} - -function createCardStatementsMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { - const searchQuery = buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - feed: [cardFeed.id], - groupBy: CONST.SEARCH.GROUP_BY.CARD, - postedOn: CONST.SEARCH.DATE_PRESETS.LAST_STATEMENT, - }); - - return createBaseSearchTypeMenuItem(searchQuery, { - key: `${CONST.SEARCH.SEARCH_KEYS.STATEMENTS}_${cardFeed.id}`, - title: cardFeed.name, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'CreditCard', - }); -} - -function createUnapprovedCashMenuItem(): SearchTypeMenuItem { - const searchQuery = buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], - groupBy: CONST.SEARCH.GROUP_BY.FROM, - reimbursable: CONST.SEARCH.BOOLEAN.YES, - }); - - return createBaseSearchTypeMenuItem(searchQuery, { - key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, - translationPath: 'workspace.qbo.accountingMethods.values.CASH', - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'MoneyHourglass', - }); -} - -function createUnapprovedCardMenuItem(cardFeed: CardFeedForDisplay): SearchTypeMenuItem { - const searchQuery = buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - feed: [cardFeed.id], - groupBy: CONST.SEARCH.GROUP_BY.CARD, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], - }); - - return createBaseSearchTypeMenuItem(searchQuery, { - key: `${CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD}_${cardFeed.id}`, - title: cardFeed.name, - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'CreditCardHourglass', - }); -} - -function createReimbursementReconciliationMenuItem(): SearchTypeMenuItem { - const searchQuery = buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT, - withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, - groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, - }); - - return createBaseSearchTypeMenuItem(searchQuery, { - key: `${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`, - translationPath: 'workspace.common.reimburse', - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'Bank', - }); -} - -function createExpensifyCardReconciliationMenuItem(): SearchTypeMenuItem { - const searchQuery = buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD, - withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, - groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, - }); - - return createBaseSearchTypeMenuItem(searchQuery, { - key: `${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`, - translationPath: 'workspace.common.expensifyCard', - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'Bank', - }); -} - function getDefaultActionableSearchMenuItem(menuItems: SearchTypeMenuItem[]) { return menuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.APPROVE) ?? menuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.SUBMIT); } @@ -3646,7 +3549,7 @@ function isCorrectSearchUserName(displayName?: string) { function isTodoSearch(hash: number, suggestedSearches: Record) { const TODO_KEYS: SearchKey[] = [CONST.SEARCH.SEARCH_KEYS.SUBMIT, CONST.SEARCH.SEARCH_KEYS.APPROVE, CONST.SEARCH.SEARCH_KEYS.PAY, CONST.SEARCH.SEARCH_KEYS.EXPORT]; - const matchedSearchKey = (Object.entries(suggestedSearches).find(([, search]) => search.hash === hash)?.[0] ?? '') as SearchKey; + const matchedSearchKey = Object.values(suggestedSearches).find((search) => search.hash === hash)?.key; return !!matchedSearchKey && TODO_KEYS.includes(matchedSearchKey); } @@ -3666,46 +3569,9 @@ function createTypeMenuSections( reportCounts: {[CONST.SEARCH.SEARCH_KEYS.SUBMIT]: number; [CONST.SEARCH.SEARCH_KEYS.APPROVE]: number; [CONST.SEARCH.SEARCH_KEYS.PAY]: number; [CONST.SEARCH.SEARCH_KEYS.EXPORT]: number}, ): SearchTypeMenuSection[] { const typeMenuSections: SearchTypeMenuSection[] = []; - const cardFeedsForAccounting = [ - ...Object.values( - Object.values(cardFeedsByPolicy ?? {}) - .flat() - .reduce>((acc, feed) => { - acc[feed.id] = feed; - return acc; - }, {}), - ), - ...(defaultExpensifyCard ? [defaultExpensifyCard] : []), - ]; const suggestedSearches = getSuggestedSearches(currentUserAccountID, defaultCardFeed?.id, icons); const suggestedSearchesVisibility = getSuggestedSearchesVisibility(currentUserEmail, cardFeedsByPolicy, policies, defaultExpensifyCard); - let shouldShowReimbursementsReconciliation = false; - let shouldShowExpensifyCardReconciliation = false; - - for (const policy of Object.values(policies ?? {})) { - if (!policy) { - continue; - } - - const isPaidPolicy = isPaidGroupPolicy(policy); - const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; - if (!isPaidPolicy || !isAdmin) { - continue; - } - - const isPaymentEnabled = arePaymentsEnabled(policy); - const hasVBBA = !!policy.achAccount?.bankAccountID && policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.OPEN; - const hasReimburser = !!policy.achAccount?.reimburser; - const isECardEnabled = !!policy.areExpensifyCardsEnabled; - - shouldShowReimbursementsReconciliation ||= isPaymentEnabled && hasVBBA && hasReimburser; - shouldShowExpensifyCardReconciliation ||= isECardEnabled; - - if (shouldShowReimbursementsReconciliation && shouldShowExpensifyCardReconciliation) { - break; - } - } // Todo section { @@ -3780,79 +3646,53 @@ function createTypeMenuSections( } } - // Accounting sections + // Accounting section { - const accountingMenuSections: SearchTypeMenuSection[] = []; - const statementsEmptyState = { - title: 'search.searchResults.emptyStatementsResults.title' as const, - subtitle: 'search.searchResults.emptyStatementsResults.subtitle' as const, - }; - const unapprovedEmptyState = { - title: 'search.searchResults.emptyUnapprovedResults.title' as const, - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle' as const, + const accountingSection: SearchTypeMenuSection = { + translationPath: 'workspace.common.accounting', + menuItems: [], }; - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS] && cardFeedsForAccounting.length > 0) { - accountingMenuSections.push({ - translationPath: 'search.cardStatements', - menuItems: cardFeedsForAccounting.map((feed) => ({ - ...createCardStatementsMenuItem(feed), - emptyState: statementsEmptyState, - })), + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]) { + accountingSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.STATEMENTS], + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, }); } - - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH] || suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]) { - const unapprovedAccrualsMenuItems: SearchTypeMenuItem[] = []; - - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { - unapprovedAccrualsMenuItems.push({ - ...createUnapprovedCashMenuItem(), - emptyState: unapprovedEmptyState, - }); - } - - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD] && cardFeedsForAccounting.length > 0) { - unapprovedAccrualsMenuItems.push( - ...cardFeedsForAccounting.map((feed) => ({ - ...createUnapprovedCardMenuItem(feed), - emptyState: unapprovedEmptyState, - })), - ); - } - - if (unapprovedAccrualsMenuItems.length > 0) { - accountingMenuSections.push({ - translationPath: 'search.unapprovedAccruals', - menuItems: unapprovedAccrualsMenuItems, - }); - } + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { + accountingSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH], + emptyState: { + title: 'search.searchResults.emptyUnapprovedResults.title', + subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', + }, + }); } - - if (shouldShowReimbursementsReconciliation || shouldShowExpensifyCardReconciliation) { - const reconciliationMenuItems: SearchTypeMenuItem[] = []; - - if (shouldShowReimbursementsReconciliation) { - reconciliationMenuItems.push({ - ...createReimbursementReconciliationMenuItem(), - emptyState: statementsEmptyState, - }); - } - - if (shouldShowExpensifyCardReconciliation) { - reconciliationMenuItems.push({ - ...createExpensifyCardReconciliationMenuItem(), - emptyState: statementsEmptyState, - }); - } - - accountingMenuSections.push({ - translationPath: 'search.bankReconciliation', - menuItems: reconciliationMenuItems, + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]) { + accountingSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD], + emptyState: { + title: 'search.searchResults.emptyUnapprovedResults.title', + subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', + }, + }); + } + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]) { + accountingSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION], + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, }); } - typeMenuSections.push(...accountingMenuSections); + if (accountingSection.menuItems.length > 0) { + typeMenuSections.push(accountingSection); + } } // Saved section diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 007065cc88770..ab2a5b7bf413d 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -231,13 +231,8 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { return -1; } - const exactMatchIndex = flattenedMenuItems.findIndex((item) => item.hash === hash); - if (exactMatchIndex !== -1) { - return exactMatchIndex; - } - return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash); - }, [hash, similarSearchHash, isSavedSearchActive, flattenedMenuItems]); + }, [similarSearchHash, isSavedSearchActive, flattenedMenuItems]); return ( <> @@ -278,7 +273,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { key={item.key} disabled={false} interactive - title={item.title ?? (item.translationPath ? translate(item.translationPath) : '')} + title={translate(item.translationPath)} badgeStyle={styles.todoBadge} icon={icon} iconWidth={variables.iconSizeNormal} diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index e2e56ef7b4ec9..635ce137bc21b 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -4790,7 +4790,7 @@ describe('SearchUIUtils', () => { expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.EXPORT); }); - it('should show split accounting sections with per-feed items and reconciliation sub-options', () => { + it('should show accounting section with statements, unapproved cash, unapproved card, and reconciliation items', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -4844,24 +4844,15 @@ describe('SearchUIUtils', () => { reportCounts, ); - const statementsSection = sections.find((section) => section.translationPath === 'search.cardStatements'); - const unapprovedSection = sections.find((section) => section.translationPath === 'search.unapprovedAccruals'); - const reconciliationSection = sections.find((section) => section.translationPath === 'search.bankReconciliation'); + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + expect(accountingSection).toBeDefined(); + expect(accountingSection?.menuItems.length).toBeGreaterThan(0); - expect(statementsSection).toBeDefined(); - expect(unapprovedSection).toBeDefined(); - expect(reconciliationSection).toBeDefined(); - - const statementItemKeys = statementsSection?.menuItems.map((item) => item.key) ?? []; - expect(statementItemKeys.some((key) => key.startsWith(`${CONST.SEARCH.SEARCH_KEYS.STATEMENTS}_`))).toBeTruthy(); - - const unapprovedItemKeys = unapprovedSection?.menuItems.map((item) => item.key) ?? []; - expect(unapprovedItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH); - expect(unapprovedItemKeys.some((key) => key.startsWith(`${CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD}_`))).toBeTruthy(); - - const reconciliationItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; - expect(reconciliationItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`); - expect(reconciliationItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`); + const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.STATEMENTS); + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH); + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD); + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); }); it('should show saved section when there are saved searches', () => { @@ -4978,7 +4969,7 @@ describe('SearchUIUtils', () => { expect(todoSection).toBeUndefined(); }); - it('should not show accounting sections when user has no admin permissions or card feeds', () => { + it('should not show accounting section when user has no admin permissions or card feeds', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -5008,8 +4999,8 @@ describe('SearchUIUtils', () => { reportCounts, ); - const accountingSections = sections.filter((section) => ['search.cardStatements', 'search.unapprovedAccruals', 'search.bankReconciliation'].includes(section.translationPath)); - expect(accountingSections).toHaveLength(0); + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + expect(accountingSection).toBeUndefined(); }); it('should show reconciliation for ACH-only scenario (payments enabled, active VBBA, reimburser set, areExpensifyCardsEnabled = false)', () => { @@ -5039,14 +5030,14 @@ describe('SearchUIUtils', () => { const {result: icons} = renderHook(() => useMemoizedLazyExpensifyIcons(['Document', 'Send', 'ThumbsUp'])); const sections = SearchUIUtils.createTypeMenuSections(icons.current, adminEmail, adminAccountID, {}, undefined, mockPolicies, {}, false, undefined, false, {}, reportCounts); - const reconciliationSection = sections.find((section) => section.translationPath === 'search.bankReconciliation'); - expect(reconciliationSection).toBeDefined(); + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + expect(accountingSection).toBeDefined(); - const menuItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT}`); + const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); }); - it('should show Expensify Card reconciliation for card-only scenario without card feeds', () => { + it('should not show reconciliation for card-only scenario without card feeds (areExpensifyCardsEnabled = true but no card feeds)', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -5077,11 +5068,11 @@ describe('SearchUIUtils', () => { {}, reportCounts, ); - const reconciliationSection = sections.find((section) => section.translationPath === 'search.bankReconciliation'); + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(reconciliationSection).toBeDefined(); - const menuItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(`${CONST.SEARCH.SEARCH_KEYS.RECONCILIATION}_${CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD}`); + expect(accountingSection).toBeDefined(); + const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); }); it('should generate correct routes', () => { From 3b461e7d67891538d1758246fd4e8f2df72ec2e8 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:12:35 +0530 Subject: [PATCH 11/13] Update --- Mobile-Expensify | 1 + 1 file changed, 1 insertion(+) create mode 160000 Mobile-Expensify diff --git a/Mobile-Expensify b/Mobile-Expensify new file mode 160000 index 0000000000000..9f18fcad20c99 --- /dev/null +++ b/Mobile-Expensify @@ -0,0 +1 @@ +Subproject commit 9f18fcad20c99de19e1511bfd6b3eed765391f10 From da3401b492789780f83a2ebf4fe0d2d3cf684c7d Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:23:27 +0530 Subject: [PATCH 12/13] Follow the new sections system --- src/CONST/index.ts | 1 + src/hooks/useSearchShouldCalculateTotals.ts | 1 + src/languages/de.ts | 4 + src/languages/en.ts | 4 + src/languages/es.ts | 4 + src/languages/fr.ts | 4 + src/languages/it.ts | 4 + src/languages/ja.ts | 4 + src/languages/nl.ts | 4 + src/languages/pl.ts | 4 + src/languages/pt-BR.ts | 4 + src/languages/zh-hans.ts | 4 + src/libs/SearchQueryUtils.ts | 2 +- src/libs/SearchUIUtils.ts | 122 +++++++++++++++----- tests/unit/Search/SearchUIUtilsTest.ts | 52 +++++---- 15 files changed, 167 insertions(+), 51 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index b6f6500808174..65ba3ab685ba7 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7471,6 +7471,7 @@ const CONST = { STATEMENTS: 'statements', UNAPPROVED_CASH: 'unapprovedCash', UNAPPROVED_CARD: 'unapprovedCard', + EXPENSIFY_CARD: 'expensifyCard', RECONCILIATION: 'reconciliation', TOP_SPENDERS: 'topSpenders', TOP_CATEGORIES: 'topCategories', diff --git a/src/hooks/useSearchShouldCalculateTotals.ts b/src/hooks/useSearchShouldCalculateTotals.ts index 9909daacd16eb..f5400064df62e 100644 --- a/src/hooks/useSearchShouldCalculateTotals.ts +++ b/src/hooks/useSearchShouldCalculateTotals.ts @@ -24,6 +24,7 @@ function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, search CONST.SEARCH.SEARCH_KEYS.STATEMENTS, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD, + CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD, CONST.SEARCH.SEARCH_KEYS.RECONCILIATION, ]; diff --git a/src/languages/de.ts b/src/languages/de.ts index e6e196879b7f7..47174f1bc4013 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7094,8 +7094,12 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und groupColumns: 'Spalten gruppieren', expenseColumns: 'Spalten für Ausgaben', statements: 'Abrechnungen', + cardStatements: 'Kartenabrechnungen', + monthlyAccrual: 'Monatliche Abgrenzung', unapprovedCash: 'Nicht genehmigtes Bargeld', unapprovedCard: 'Nicht genehmigte Karte', + expensifyCard: 'Expensify Card', + reimbursements: 'Rückerstattungen', reconciliation: 'Abstimmung', topSpenders: 'Top-Ausgaben', saveSearch: 'Suche speichern', diff --git a/src/languages/en.ts b/src/languages/en.ts index 63b67e3d50a91..e764b9ad0b1ca 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7037,8 +7037,12 @@ const translations = { groupColumns: 'Group columns', expenseColumns: 'Expense Columns', statements: 'Statements', + cardStatements: 'Card statements', + monthlyAccrual: 'Monthly accrual', unapprovedCash: 'Unapproved cash', unapprovedCard: 'Unapproved card', + expensifyCard: 'Expensify Card', + reimbursements: 'Reimbursements', reconciliation: 'Reconciliation', topSpenders: 'Top spenders', saveSearch: 'Save search', diff --git a/src/languages/es.ts b/src/languages/es.ts index c43fc1e37533b..530f63347849c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6847,8 +6847,12 @@ ${amount} para ${merchant} - ${date}`, groupColumns: 'Columnas de grupo', expenseColumns: 'Columnas de gastos', statements: 'Extractos', + cardStatements: 'Extractos de tarjeta', + monthlyAccrual: 'Devengo mensual', unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', + expensifyCard: 'Tarjeta Expensify', + reimbursements: 'Reembolsos', reconciliation: 'Conciliación', topSpenders: 'Mayores gastadores', view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'}, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index de8ee551b7150..dafe7b735f594 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7118,8 +7118,12 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip groupColumns: 'Regrouper les colonnes', expenseColumns: 'Colonnes de dépenses', statements: 'Relevés', + cardStatements: 'Relevés de carte', + monthlyAccrual: 'Régularisation mensuelle', unapprovedCash: 'Espèces non approuvées', unapprovedCard: 'Carte non approuvée', + expensifyCard: 'Carte Expensify', + reimbursements: 'Remboursements', reconciliation: 'Rapprochement', topSpenders: 'Plus gros dépensiers', saveSearch: 'Enregistrer la recherche', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7500619ebca86..efde30b5080fd 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7080,8 +7080,12 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo groupColumns: 'Raggruppa colonne', expenseColumns: 'Colonne spese', statements: 'Estratti conto', + cardStatements: 'Estratti carta', + monthlyAccrual: 'Rateo mensile', unapprovedCash: 'Contanti non approvati', unapprovedCard: 'Carta non approvata', + expensifyCard: 'Carta Expensify', + reimbursements: 'Rimborsi', reconciliation: 'Riconciliazione', topSpenders: 'Maggiori spendaccioni', saveSearch: 'Salva ricerca', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index cf18bfec0d42e..7dd0b01393884 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7010,8 +7010,12 @@ ${reportName} groupColumns: '列をグループ化', expenseColumns: '経費列', statements: 'ステートメント', + cardStatements: 'カード明細', + monthlyAccrual: '月次発生', unapprovedCash: '未承認の現金', unapprovedCard: '未承認のカード', + expensifyCard: 'Expensifyカード', + reimbursements: '払い戻し', reconciliation: '照合', topSpenders: '上位支出者', saveSearch: '検索を保存', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index bd446b207cef1..863d2cd605cfe 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7065,8 +7065,12 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar groupColumns: 'Kolommen groeperen', expenseColumns: 'Onkostencolommen', statements: 'Afschriften', + cardStatements: 'Kaartafschriften', + monthlyAccrual: 'Maandelijkse opbouw', unapprovedCash: 'Niet-goedgekeurde contante uitgaven', unapprovedCard: 'Niet-goedgekeurde kaart', + expensifyCard: 'Expensify Card', + reimbursements: 'Terugbetalingen', reconciliation: 'Afstemming', topSpenders: 'Grootste uitgaven doeners', saveSearch: 'Zoekopdracht opslaan', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 4cc16d3085082..37f9e7a722ff6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7048,8 +7048,12 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i groupColumns: 'Grupuj kolumny', expenseColumns: 'Kolumny wydatków', statements: 'Wyciągi', + cardStatements: 'Wyciągi kartowe', + monthlyAccrual: 'Miesięczne rozliczenie', unapprovedCash: 'Niezaakceptowana gotówka', unapprovedCard: 'Niezaakceptowana karta', + expensifyCard: 'Karta Expensify', + reimbursements: 'Zwroty', reconciliation: 'Uzgodnienie', topSpenders: 'Najwięksi wydający', saveSearch: 'Zapisz wyszukiwanie', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 03c08e51ee5ef..e7c7c89193ed7 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7050,8 +7050,12 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e groupColumns: 'Agrupar colunas', expenseColumns: 'Colunas de despesas', statements: 'Extratos', + cardStatements: 'Extratos de cartão', + monthlyAccrual: 'Acréscimo mensal', unapprovedCash: 'Dinheiro não aprovado', unapprovedCard: 'Cartão não aprovado', + expensifyCard: 'Cartão Expensify', + reimbursements: 'Reembolsos', reconciliation: 'Conciliação', topSpenders: 'Maiores gastadores', saveSearch: 'Salvar pesquisa', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d465f940236f2..143150f745bd6 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6910,8 +6910,12 @@ ${reportName} groupColumns: '分组列', expenseColumns: '报销列', statements: '对账单', + cardStatements: '卡对账单', + monthlyAccrual: '月度计提', unapprovedCash: '未批准现金', unapprovedCard: '未批准的卡片', + expensifyCard: 'Expensify 卡', + reimbursements: '报销', reconciliation: '对账', topSpenders: '最高支出者', saveSearch: '保存搜索', diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index eda96e8d1cead..bcff226e33f88 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -354,7 +354,7 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea // Certain filters' values are significant in deciding which search we are on, so we want to include // their value when computing the similarSearchHash - const similarSearchValueBasedFilters = new Set([CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION]); + const similarSearchValueBasedFilters = new Set([CONST.SEARCH.SYNTAX_FILTER_KEYS.ACTION, CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_TYPE]); const flatFilters = query.flatFilters .map((filter) => { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index d946056ccac2f..a89461dbc1dd1 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -680,7 +680,7 @@ function getSuggestedSearches( }, [CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: { key: CONST.SEARCH.SEARCH_KEYS.STATEMENTS, - translationPath: 'search.statements', + translationPath: 'search.cardStatements', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'CreditCard', searchQuery: buildQueryStringFromFilterFormValues({ @@ -741,17 +741,52 @@ function getSuggestedSearches( return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; }, }, + [CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD]: { + key: CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD, + translationPath: 'search.expensifyCard', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'CreditCard', + searchQuery: buildQueryStringFromFilterFormValues( + { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.EXPENSIFY_CARD, + withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + view: CONST.SEARCH.VIEW.TABLE, + }, + { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WITHDRAWN, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + }, + ), + get searchQueryJSON() { + return buildSearchQueryJSON(this.searchQuery); + }, + get hash() { + return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + get similarSearchHash() { + return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; + }, + }, [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: { key: CONST.SEARCH.SEARCH_KEYS.RECONCILIATION, - translationPath: 'search.reconciliation', + translationPath: 'search.reimbursements', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'Bank', - searchQuery: buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT, - withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, - groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, - }), + searchQuery: buildQueryStringFromFilterFormValues( + { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT, + withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + view: CONST.SEARCH.VIEW.TABLE, + }, + { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_WITHDRAWN, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + }, + ), get searchQueryJSON() { return buildSearchQueryJSON(this.searchQuery); }, @@ -857,7 +892,8 @@ function getSuggestedSearchesVisibility( let shouldShowStatementsSuggestion = false; let shouldShowUnapprovedCashSuggestion = false; let shouldShowUnapprovedCardSuggestion = false; - let shouldShowReconciliationSuggestion = false; + let shouldShowExpensifyCardSuggestion = false; + let shouldShowReimbursementsSuggestion = false; let shouldShowTopSpendersSuggestion = false; let shouldShowTopCategoriesSuggestion = false; let shouldShowTopMerchantsSuggestion = false; @@ -895,7 +931,8 @@ function getSuggestedSearchesVisibility( const isEligibleForStatementsSuggestion = isPaidPolicy && !!policy.areCompanyCardsEnabled && hasCardFeed; const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && isPaymentEnabled; const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && (hasCardFeed || !!defaultExpensifyCard); - const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasVBBA && hasReimburser) || isECardEnabled); + const isEligibleForExpensifyCardSuggestion = isPaidPolicy && isAdmin && isECardEnabled; + const isEligibleForReimbursementsSuggestion = isPaidPolicy && isAdmin && isPaymentEnabled && hasVBBA && hasReimburser; const isAuditor = policy.role === CONST.POLICY.ROLE.AUDITOR; const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); const isEligibleForTopCategoriesSuggestion = isPaidPolicy && policy.areCategoriesEnabled === true; @@ -909,7 +946,8 @@ function getSuggestedSearchesVisibility( shouldShowStatementsSuggestion ||= isEligibleForStatementsSuggestion; shouldShowUnapprovedCashSuggestion ||= isEligibleForUnapprovedCashSuggestion; shouldShowUnapprovedCardSuggestion ||= isEligibleForUnapprovedCardSuggestion; - shouldShowReconciliationSuggestion ||= isEligibleForReconciliationSuggestion; + shouldShowExpensifyCardSuggestion ||= isEligibleForExpensifyCardSuggestion; + shouldShowReimbursementsSuggestion ||= isEligibleForReimbursementsSuggestion; shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion; shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion; shouldShowTopMerchantsSuggestion ||= isEligibleForTopMerchantsSuggestion; @@ -924,7 +962,8 @@ function getSuggestedSearchesVisibility( shouldShowStatementsSuggestion && shouldShowUnapprovedCashSuggestion && shouldShowUnapprovedCardSuggestion && - shouldShowReconciliationSuggestion && + shouldShowExpensifyCardSuggestion && + shouldShowReimbursementsSuggestion && shouldShowTopSpendersSuggestion && shouldShowTopCategoriesSuggestion && shouldShowTopMerchantsSuggestion @@ -942,7 +981,8 @@ function getSuggestedSearchesVisibility( [CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: shouldShowStatementsSuggestion, [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: shouldShowUnapprovedCashSuggestion, [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]: shouldShowUnapprovedCardSuggestion, - [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: shouldShowReconciliationSuggestion, + [CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD]: shouldShowExpensifyCardSuggestion, + [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: shouldShowReimbursementsSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion, @@ -3647,24 +3687,15 @@ function createTypeMenuSections( } } - // Accounting section + // Monthly accrual section { - const accountingSection: SearchTypeMenuSection = { - translationPath: 'workspace.common.accounting', + const monthlyAccrualSection: SearchTypeMenuSection = { + translationPath: 'search.monthlyAccrual', menuItems: [], }; - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]) { - accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.STATEMENTS], - emptyState: { - title: 'search.searchResults.emptyStatementsResults.title', - subtitle: 'search.searchResults.emptyStatementsResults.subtitle', - }, - }); - } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { - accountingSection.menuItems.push({ + monthlyAccrualSection.menuItems.push({ ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH], emptyState: { title: 'search.searchResults.emptyUnapprovedResults.title', @@ -3673,7 +3704,7 @@ function createTypeMenuSections( }); } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]) { - accountingSection.menuItems.push({ + monthlyAccrualSection.menuItems.push({ ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD], emptyState: { title: 'search.searchResults.emptyUnapprovedResults.title', @@ -3681,8 +3712,39 @@ function createTypeMenuSections( }, }); } + + if (monthlyAccrualSection.menuItems.length > 0) { + typeMenuSections.push(monthlyAccrualSection); + } + } + + // Reconciliation section + { + const reconciliationSection: SearchTypeMenuSection = { + translationPath: 'search.reconciliation', + menuItems: [], + }; + + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]) { + reconciliationSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.STATEMENTS], + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, + }); + } + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD]) { + reconciliationSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD], + emptyState: { + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', + }, + }); + } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]) { - accountingSection.menuItems.push({ + reconciliationSection.menuItems.push({ ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION], emptyState: { title: 'search.searchResults.emptyStatementsResults.title', @@ -3691,8 +3753,8 @@ function createTypeMenuSections( }); } - if (accountingSection.menuItems.length > 0) { - typeMenuSections.push(accountingSection); + if (reconciliationSection.menuItems.length > 0) { + typeMenuSections.push(reconciliationSection); } } diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 635ce137bc21b..21276a29cb4e1 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -4790,7 +4790,7 @@ describe('SearchUIUtils', () => { expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.EXPORT); }); - it('should show accounting section with statements, unapproved cash, unapproved card, and reconciliation items', () => { + it('should show monthly accrual and reconciliation sections with expected items', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -4844,15 +4844,22 @@ describe('SearchUIUtils', () => { reportCounts, ); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(accountingSection).toBeDefined(); - expect(accountingSection?.menuItems.length).toBeGreaterThan(0); + const monthlyAccrualSection = sections.find((section) => section.translationPath === 'search.monthlyAccrual'); + expect(monthlyAccrualSection).toBeDefined(); + expect(monthlyAccrualSection?.menuItems.length).toBeGreaterThan(0); - const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.STATEMENTS); - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH); - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD); - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + const monthlyAccrualKeys = monthlyAccrualSection?.menuItems.map((item) => item.key) ?? []; + expect(monthlyAccrualKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH); + expect(monthlyAccrualKeys).toContain(CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD); + + const reconciliationSection = sections.find((section) => section.translationPath === 'search.reconciliation'); + expect(reconciliationSection).toBeDefined(); + expect(reconciliationSection?.menuItems.length).toBeGreaterThan(0); + + const reconciliationKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; + expect(reconciliationKeys).toContain(CONST.SEARCH.SEARCH_KEYS.STATEMENTS); + expect(reconciliationKeys).toContain(CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD); + expect(reconciliationKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); }); it('should show saved section when there are saved searches', () => { @@ -4969,7 +4976,7 @@ describe('SearchUIUtils', () => { expect(todoSection).toBeUndefined(); }); - it('should not show accounting section when user has no admin permissions or card feeds', () => { + it('should not show monthly accrual or reconciliation sections when user has no admin permissions or card feeds', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -4999,8 +5006,10 @@ describe('SearchUIUtils', () => { reportCounts, ); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(accountingSection).toBeUndefined(); + const monthlyAccrualSection = sections.find((section) => section.translationPath === 'search.monthlyAccrual'); + const reconciliationSection = sections.find((section) => section.translationPath === 'search.reconciliation'); + expect(monthlyAccrualSection).toBeUndefined(); + expect(reconciliationSection).toBeUndefined(); }); it('should show reconciliation for ACH-only scenario (payments enabled, active VBBA, reimburser set, areExpensifyCardsEnabled = false)', () => { @@ -5030,14 +5039,15 @@ describe('SearchUIUtils', () => { const {result: icons} = renderHook(() => useMemoizedLazyExpensifyIcons(['Document', 'Send', 'ThumbsUp'])); const sections = SearchUIUtils.createTypeMenuSections(icons.current, adminEmail, adminAccountID, {}, undefined, mockPolicies, {}, false, undefined, false, {}, reportCounts); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); - expect(accountingSection).toBeDefined(); + const reconciliationSection = sections.find((section) => section.translationPath === 'search.reconciliation'); + expect(reconciliationSection).toBeDefined(); - const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; + const menuItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).not.toContain(CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD); expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); }); - it('should not show reconciliation for card-only scenario without card feeds (areExpensifyCardsEnabled = true but no card feeds)', () => { + it('should show only expensify card in reconciliation for card-only scenario without card feeds', () => { const mockPolicies = { policy1: { id: 'policy1', @@ -5068,11 +5078,13 @@ describe('SearchUIUtils', () => { {}, reportCounts, ); - const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + const reconciliationSection = sections.find((section) => section.translationPath === 'search.reconciliation'); - expect(accountingSection).toBeDefined(); - const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; - expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + expect(reconciliationSection).toBeDefined(); + const menuItemKeys = reconciliationSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD); + expect(menuItemKeys).not.toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + expect(menuItemKeys).not.toContain(CONST.SEARCH.SEARCH_KEYS.STATEMENTS); }); it('should generate correct routes', () => { From 5b4e94f2094029c51a5af806e589da166e680f50 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 21 Feb 2026 23:11:30 +0530 Subject: [PATCH 13/13] Translation updates --- src/languages/de.ts | 2 -- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/languages/fr.ts | 2 -- src/languages/it.ts | 2 -- src/languages/ja.ts | 2 -- src/languages/nl.ts | 2 -- src/languages/pl.ts | 2 -- src/languages/pt-BR.ts | 2 -- src/languages/zh-hans.ts | 2 -- src/libs/SearchUIUtils.ts | 4 ++-- 11 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index bf7f56d01aefb..d778f889e9df7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7103,8 +7103,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und monthlyAccrual: 'Monatliche Abgrenzung', unapprovedCash: 'Nicht genehmigtes Bargeld', unapprovedCard: 'Nicht genehmigte Karte', - expensifyCard: 'Expensify Card', - reimbursements: 'Rückerstattungen', reconciliation: 'Abstimmung', topSpenders: 'Top-Ausgaben', saveSearch: 'Suche speichern', diff --git a/src/languages/en.ts b/src/languages/en.ts index 26130e7fa78f4..7899168700346 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7054,8 +7054,6 @@ const translations = { monthlyAccrual: 'Monthly accrual', unapprovedCash: 'Unapproved cash', unapprovedCard: 'Unapproved card', - expensifyCard: 'Expensify Card', - reimbursements: 'Reimbursements', reconciliation: 'Reconciliation', topSpenders: 'Top spenders', saveSearch: 'Save search', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1a48cd25f9f5f..a023a2e6eaf0b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6911,8 +6911,6 @@ ${amount} para ${merchant} - ${date}`, monthlyAccrual: 'Devengo mensual', unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', - expensifyCard: 'Tarjeta Expensify', - reimbursements: 'Reembolsos', reconciliation: 'Conciliación', topSpenders: 'Mayores gastadores', view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'}, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 6088e2b637873..08bbe2421c10d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7124,8 +7124,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip monthlyAccrual: 'Régularisation mensuelle', unapprovedCash: 'Espèces non approuvées', unapprovedCard: 'Carte non approuvée', - expensifyCard: 'Carte Expensify', - reimbursements: 'Remboursements', reconciliation: 'Rapprochement', topSpenders: 'Plus gros dépensiers', saveSearch: 'Enregistrer la recherche', diff --git a/src/languages/it.ts b/src/languages/it.ts index 4e6ad5874df16..31a87174644ae 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7089,8 +7089,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo monthlyAccrual: 'Rateo mensile', unapprovedCash: 'Contanti non approvati', unapprovedCard: 'Carta non approvata', - expensifyCard: 'Carta Expensify', - reimbursements: 'Rimborsi', reconciliation: 'Riconciliazione', topSpenders: 'Maggiori spendaccioni', saveSearch: 'Salva ricerca', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 5a52ede7856bf..079d43fe03139 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7016,8 +7016,6 @@ ${reportName} monthlyAccrual: '月次発生', unapprovedCash: '未承認の現金', unapprovedCard: '未承認のカード', - expensifyCard: 'Expensifyカード', - reimbursements: '払い戻し', reconciliation: '照合', topSpenders: '上位支出者', saveSearch: '検索を保存', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 13b1034080424..ae33b4db2b4ef 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7068,8 +7068,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar monthlyAccrual: 'Maandelijkse opbouw', unapprovedCash: 'Niet-goedgekeurde contante uitgaven', unapprovedCard: 'Niet-goedgekeurde kaart', - expensifyCard: 'Expensify Card', - reimbursements: 'Terugbetalingen', reconciliation: 'Afstemming', topSpenders: 'Grootste uitgaven doeners', saveSearch: 'Zoekopdracht opslaan', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d3f9bbb47eabc..29169b672e0f7 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7056,8 +7056,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i monthlyAccrual: 'Miesięczne rozliczenie', unapprovedCash: 'Niezaakceptowana gotówka', unapprovedCard: 'Niezaakceptowana karta', - expensifyCard: 'Karta Expensify', - reimbursements: 'Zwroty', reconciliation: 'Uzgodnienie', topSpenders: 'Najwięksi wydający', saveSearch: 'Zapisz wyszukiwanie', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 583f4ea12e2d8..264eb5ea4f4e1 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7060,8 +7060,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e monthlyAccrual: 'Acréscimo mensal', unapprovedCash: 'Dinheiro não aprovado', unapprovedCard: 'Cartão não aprovado', - expensifyCard: 'Cartão Expensify', - reimbursements: 'Reembolsos', reconciliation: 'Conciliação', topSpenders: 'Maiores gastadores', saveSearch: 'Salvar pesquisa', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 84d8a45686b14..1ae244ead76aa 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6904,8 +6904,6 @@ ${reportName} monthlyAccrual: '月度计提', unapprovedCash: '未批准现金', unapprovedCard: '未批准的卡片', - expensifyCard: 'Expensify 卡', - reimbursements: '报销', reconciliation: '对账', topSpenders: '最高支出者', saveSearch: '保存搜索', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 330df0bf18b0e..6ee5c753a2eda 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -745,7 +745,7 @@ function getSuggestedSearches( }, [CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD]: { key: CONST.SEARCH.SEARCH_KEYS.EXPENSIFY_CARD, - translationPath: 'search.expensifyCard', + translationPath: 'workspace.common.expensifyCard', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'CreditCard', searchQuery: buildQueryStringFromFilterFormValues( @@ -773,7 +773,7 @@ function getSuggestedSearches( }, [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: { key: CONST.SEARCH.SEARCH_KEYS.RECONCILIATION, - translationPath: 'search.reimbursements', + translationPath: 'workspace.common.reimburse', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: 'Bank', searchQuery: buildQueryStringFromFilterFormValues(