From 5d29082fa004fbf1e3bec66e59af344dd724dcdd Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 5 Aug 2025 16:15:20 -0600 Subject: [PATCH 01/15] add reconciliation --- src/CONST/index.ts | 1 + src/components/Search/index.tsx | 7 ++++--- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + src/libs/SearchUIUtils.ts | 29 ++++++++++++++++++++++++++++- 13 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a16fca06f879..bd082333df8d 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6475,6 +6475,7 @@ const CONST = { STATEMENTS: 'statements', UNAPPROVED_CASH: 'unapprovedCash', UNAPPROVED_CARD: 'unapprovedCard', + RECONCILIATION: 'reconciliation', }, ANIMATION: { FADE_DURATION: 200, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 1742cf8b14ca..d9ec78d9e772 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -234,7 +234,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS }); /** - * The total should be calculated for all accounting queries (statements, unapprovedCash and unapprovedCard) + * The total should be calculated for all accounting queries (statements, unapprovedCash, unapprovedCard and reconciliation) * We can't use `searchKey` directly because we want to also match similar queries e.g. the statements suggested search query with a custom feed should be matched too. */ const isStatementsLikeQuery = queryJSON.flatFilters.length === 2 && hasFeedFilter && hasPostedFilter; @@ -248,9 +248,10 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS hasFeedFilter && queryJSON.status[0] === CONST.SEARCH.STATUS.EXPENSE.DRAFTS && queryJSON.status[1] === CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING; + const isReconciliationLikeQuery = queryJSON.groupBy === CONST.SEARCH.GROUP_BY.BANK_WITHDRAWAL; - return isStatementsLikeQuery || isUnapprovedCashLikeQuery || isUnapprovedCardLikeQuery; - }, [offset, queryJSON.flatFilters, queryJSON.type, queryJSON.status]); + return isStatementsLikeQuery || isUnapprovedCashLikeQuery || isUnapprovedCardLikeQuery || isReconciliationLikeQuery; + }, [offset, queryJSON.flatFilters, queryJSON.type, queryJSON.status, queryJSON.groupBy]); const previousReportActions = usePrevious(reportActions); const reportActionsArray = useMemo( diff --git a/src/languages/de.ts b/src/languages/de.ts index 7ae2952b68f4..14fd15181b82 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5935,6 +5935,7 @@ const translations = { statements: 'Erklärungen', unapprovedCash: 'Nicht genehmigtes Bargeld', unapprovedCard: 'Nicht genehmigte Karte', + reconciliation: 'Versöhnung', saveSearch: 'Suche speichern', deleteSavedSearch: 'Gespeicherte Suche löschen', deleteSavedSearchConfirm: 'Möchten Sie diese Suche wirklich löschen?', diff --git a/src/languages/en.ts b/src/languages/en.ts index 5f2840d55892..e54f7ec481de 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5907,6 +5907,7 @@ const translations = { statements: 'Statements', unapprovedCash: 'Unapproved cash', unapprovedCard: 'Unapproved card', + reconciliation: 'Reconciliation', saveSearch: 'Save search', deleteSavedSearch: 'Delete saved search', deleteSavedSearchConfirm: 'Are you sure you want to delete this search?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 997d40ed2088..4e0dedb58598 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5929,6 +5929,7 @@ const translations = { statements: 'Extractos', unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', + reconciliation: 'Reconciliación', saveSearch: 'Guardar búsqueda', savedSearchesMenuItemTitle: 'Guardadas', searchName: 'Nombre de la búsqueda', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 50bbffdfce57..e3c207ed7fc4 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5948,6 +5948,7 @@ const translations = { statements: 'Relevés', unapprovedCash: 'Espèces non approuvées', unapprovedCard: 'Carte non approuvée', + reconciliation: 'Reconciliation', saveSearch: 'Enregistrer la recherche', deleteSavedSearch: 'Supprimer la recherche enregistrée', deleteSavedSearchConfirm: 'Êtes-vous sûr de vouloir supprimer cette recherche ?', diff --git a/src/languages/it.ts b/src/languages/it.ts index d6ee69ae446f..288aa0d34c97 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5950,6 +5950,7 @@ const translations = { statements: 'Dichiarazioni', unapprovedCash: 'Contanti non approvati', unapprovedCard: 'Carta non approvata', + reconciliation: 'Reconciliation', saveSearch: 'Salva ricerca', deleteSavedSearch: 'Elimina ricerca salvata', deleteSavedSearchConfirm: 'Sei sicuro di voler eliminare questa ricerca?', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 01f2117b4c55..3e1f95cb0883 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5908,6 +5908,7 @@ const translations = { statements: 'ステートメント', unapprovedCash: '未承認現金', unapprovedCard: '未承認のカード', + reconciliation: '調整', saveSearch: '検索を保存', deleteSavedSearch: '保存された検索を削除', deleteSavedSearchConfirm: 'この検索を削除してもよろしいですか?', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ff7b9ce910a2..1879c41a58c2 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5942,6 +5942,7 @@ const translations = { statements: 'Verklaringen', unapprovedCash: 'Niet goedgekeurd contant geld', unapprovedCard: 'Niet-goedgekeurde kaart', + reconciliation: 'Verzoening', saveSearch: 'Zoekopdracht opslaan', deleteSavedSearch: 'Verwijder opgeslagen zoekopdracht', deleteSavedSearchConfirm: 'Weet je zeker dat je deze zoekopdracht wilt verwijderen?', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 9f37e60dc9b4..92138b92c06f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5927,6 +5927,7 @@ const translations = { statements: 'Oświadczenia', unapprovedCash: 'Niezatwierdzone środki pieniężne', unapprovedCard: 'Niezatwierdzona karta', + reconciliation: 'Pojednanie', saveSearch: 'Zapisz wyszukiwanie', deleteSavedSearch: 'Usuń zapisaną wyszukiwarkę', deleteSavedSearchConfirm: 'Czy na pewno chcesz usunąć to wyszukiwanie?', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 3a2e701ad050..e0de000ba3c3 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5941,6 +5941,7 @@ const translations = { statements: 'Declarações', unapprovedCash: 'Dinheiro não aprovado', unapprovedCard: 'Cartão não aprovado', + reconciliation: 'Reconciliamento', saveSearch: 'Salvar pesquisa', deleteSavedSearch: 'Excluir pesquisa salva', deleteSavedSearchConfirm: 'Tem certeza de que deseja excluir esta pesquisa?', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 94b992065eee..f9ef145b940a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5832,6 +5832,7 @@ const translations = { statements: '发言', unapprovedCash: '未经批准的现金', unapprovedCard: '未批准的卡', + reconciliation: '对账', saveSearch: '保存搜索', deleteSavedSearch: '删除已保存的搜索', deleteSavedSearchConfirm: '您确定要删除此搜索吗?', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index da51b56bb89a..190ab0693c09 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -364,6 +364,19 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, + [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: { + key: CONST.SEARCH.SEARCH_KEYS.RECONCILIATION, + translationPath: 'search.reconciliation', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: Expensicons.Bank, + searchQuery: buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + groupBy: CONST.SEARCH.GROUP_BY.BANK_WITHDRAWAL, + }), + get hash() { + return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; + }, + }, }; } @@ -393,6 +406,9 @@ function getSuggestedSearchesVisibility( const isApprover = policy.approver === currentUserEmail; const isApprovalEnabled = policy.approvalMode ? policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; const isPaymentEnabled = arePaymentsEnabled(policy); + const hasVBBA = !!policy?.achAccount?.bankAccountID; + const hasReimburser = !!policy?.achAccount?.reimburser; + const isECardEnabled = !!policy?.areExpensifyCardsEnabled; const isSubmittedTo = Object.values(policy.employeeList ?? {}).some((employee) => { return employee.submitsTo === currentUserEmail || employee.forwardsTo === currentUserEmail; }); @@ -404,7 +420,7 @@ function getSuggestedSearchesVisibility( const isEligibleForStatementsSuggestion = isPaidPolicy && !!policy.areCompanyCardsEnabled && cardFeedsByPolicy[policy.id]?.length > 0; const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && isPaymentEnabled; const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && cardFeedsByPolicy[policy.id]?.length > 0; - const isEligibleForReconciliationSuggestion = isPaidPolicy && false; // s77rt TODO + const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasReimburser && hasVBBA) || isECardEnabled) shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -439,6 +455,7 @@ 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, }; } @@ -1785,6 +1802,16 @@ function createTypeMenuSections( }, }); } + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]) { + accountingSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION], + emptyState: { + headerMedia: DotLottieAnimations.Fireworks, + title: 'search.searchResults.emptyUnapprovedResults.title', + subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', + }, + }); + } if (accountingSection.menuItems.length > 0) { typeMenuSections.push(accountingSection); From 1822f4a189e63d860e586524fb5c1e90a6ae1f55 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 5 Aug 2025 16:27:06 -0600 Subject: [PATCH 02/15] add conditional --- src/hooks/useSearchTypeMenuSections.ts | 2 ++ src/libs/SearchUIUtils.ts | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index aaffe3b03504..2de477afc513 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -23,6 +23,8 @@ const policySelector = (policy: OnyxEntry): OnyxEntry => employeeList: policy.employeeList, reimbursementChoice: policy.reimbursementChoice, areCompanyCardsEnabled: policy.areCompanyCardsEnabled, + areExpensifyCardsEnabled: policy.areExpensifyCardsEnabled, + achAccount: policy.achAccount, }; /** diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 190ab0693c09..11d6ab27b7aa 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -406,9 +406,9 @@ function getSuggestedSearchesVisibility( const isApprover = policy.approver === currentUserEmail; const isApprovalEnabled = policy.approvalMode ? policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; const isPaymentEnabled = arePaymentsEnabled(policy); - const hasVBBA = !!policy?.achAccount?.bankAccountID; - const hasReimburser = !!policy?.achAccount?.reimburser; - const isECardEnabled = !!policy?.areExpensifyCardsEnabled; + const hasVBBA = !!policy.achAccount?.bankAccountID; + const hasReimburser = !!policy.achAccount?.reimburser; + const isECardEnabled = !!policy.areExpensifyCardsEnabled; const isSubmittedTo = Object.values(policy.employeeList ?? {}).some((employee) => { return employee.submitsTo === currentUserEmail || employee.forwardsTo === currentUserEmail; }); @@ -420,7 +420,7 @@ function getSuggestedSearchesVisibility( const isEligibleForStatementsSuggestion = isPaidPolicy && !!policy.areCompanyCardsEnabled && cardFeedsByPolicy[policy.id]?.length > 0; const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && isPaymentEnabled; const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && cardFeedsByPolicy[policy.id]?.length > 0; - const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasReimburser && hasVBBA) || isECardEnabled) + const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasReimburser && hasVBBA) || isECardEnabled); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; From e4601f9a4e2c013c04ab45970b6607939264b4f2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 5 Aug 2025 16:31:05 -0600 Subject: [PATCH 03/15] add filters --- src/libs/SearchUIUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 11d6ab27b7aa..2ec02343e52b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -372,6 +372,7 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb searchQuery: buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, groupBy: CONST.SEARCH.GROUP_BY.BANK_WITHDRAWAL, + withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.BANK_WITHDRAWAL, }), get hash() { return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; From 9a83b8431eb8542a6647fcebb15aa9624d4284c4 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 21 Aug 2025 12:48:30 -0600 Subject: [PATCH 04/15] reoder logic --- src/libs/SearchUIUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 936d4cdcaa7e..e8db7fdc944d 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -490,7 +490,7 @@ function getSuggestedSearchesVisibility( const isApprover = policy.approver === currentUserEmail; const isApprovalEnabled = policy.approvalMode ? policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; const isPaymentEnabled = arePaymentsEnabled(policy); - const hasVBBA = !!policy.achAccount?.bankAccountID; + const hasVBBA = !!policy.achAccount?.bankAccountID && policy.achAccount.state !== CONST.BANK_ACCOUNT.STATE.OPEN; const hasReimburser = !!policy.achAccount?.reimburser; const isECardEnabled = !!policy.areExpensifyCardsEnabled; const isSubmittedTo = Object.values(policy.employeeList ?? {}).some((employee) => { @@ -504,7 +504,7 @@ function getSuggestedSearchesVisibility( const isEligibleForStatementsSuggestion = isPaidPolicy && !!policy.areCompanyCardsEnabled && cardFeedsByPolicy[policy.id]?.length > 0; const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && isPaymentEnabled; const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && cardFeedsByPolicy[policy.id]?.length > 0; - const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasReimburser && hasVBBA) || isECardEnabled); + const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasVBBA && hasReimburser) || isECardEnabled); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; From 80c52ec6d85691b9fa0ff44a677aafb5aeaf4fc0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 21 Aug 2025 13:26:28 -0600 Subject: [PATCH 05/15] add tests --- src/components/Search/index.tsx | 7 +- tests/unit/Search/SearchUIUtilsTest.ts | 243 +++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a6f3962cdab8..88de08303b69 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -222,7 +222,12 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return false; } - const eligibleSearchKeys: Partial = [CONST.SEARCH.SEARCH_KEYS.STATEMENTS, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD, CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]; + const eligibleSearchKeys: Partial = [ + CONST.SEARCH.SEARCH_KEYS.STATEMENTS, + CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, + CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD, + CONST.SEARCH.SEARCH_KEYS.RECONCILIATION, + ]; return eligibleSearchKeys.includes(searchKey); }, [offset, searchKey]); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 6103a82d3404..5c8731d273a5 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -13,6 +13,7 @@ import type { import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; +import type {CardFeedForDisplay} from '@src/libs/CardFeedUtils'; import * as SearchUIUtils from '@src/libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -1800,6 +1801,248 @@ describe('SearchUIUtils', () => { ); }); + it('should show todo section with submit, approve, pay, and export items when user has appropriate permissions', () => { + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Test Policy', + owner: adminEmail, + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + approver: adminEmail, + exporter: adminEmail, + achAccount: { + bankAccountID: 1, + reimburser: adminEmail, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + accountNumber: '1234567890', + routingNumber: '1234567890', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + areExpensifyCardsEnabled: true, + areCompanyCardsEnabled: true, + employeeList: { + [adminEmail]: { + email: adminEmail, + role: CONST.POLICY.ROLE.ADMIN, + submitsTo: approverEmail, + }, + [approverEmail]: { + email: approverEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: adminEmail, + }, + }, + }, + }; + + const mockCardFeedsByPolicy: Record = { + policy1: [ + { + id: 'card1', + feed: 'Expensify Card' as const, + fundID: 'fund1', + name: 'Test Card Feed', + }, + ], + }; + + const mockSavedSearches = {}; + + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, mockCardFeedsByPolicy, undefined, mockPolicies, mockSavedSearches, false); + + const todoSection = sections.find((section) => section.translationPath === 'common.todo'); + expect(todoSection).toBeDefined(); + expect(todoSection?.menuItems.length).toBeGreaterThan(0); + + const menuItemKeys = todoSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.SUBMIT); + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.APPROVE); + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.EXPORT); + }); + + it('should show accounting section with statements, unapproved cash, unapproved card, and reconciliation items', () => { + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Test Policy', + owner: adminEmail, + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + areExpensifyCardsEnabled: true, + areCompanyCardsEnabled: true, + achAccount: { + bankAccountID: 1234, + reimburser: adminEmail, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + accountNumber: '1234567890', + routingNumber: '1234567890', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }, + }; + + const mockCardFeedsByPolicy: Record = { + policy1: [ + { + id: 'card1', + feed: 'Expensify Card' as const, + fundID: 'fund1', + name: 'Test Card Feed', + }, + ], + }; + + const mockSavedSearches = {}; + + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, mockCardFeedsByPolicy, undefined, mockPolicies, mockSavedSearches, false); + + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + expect(accountingSection).toBeDefined(); + expect(accountingSection?.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); + }); + + it('should show saved section when there are saved searches', () => { + const mockSavedSearches = { + search1: { + id: 'search1', + name: 'My Saved Search', + query: 'type:expense', + pendingAction: undefined, + }, + search2: { + id: 'search2', + name: 'Another Search', + query: 'type:report', + pendingAction: undefined, + }, + }; + + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, {}, undefined, {}, mockSavedSearches, false); + + const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); + expect(savedSection).toBeDefined(); + }); + + it('should not show saved section when there are no saved searches', () => { + const mockSavedSearches = {}; + + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, {}, undefined, {}, mockSavedSearches, false); + + const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); + expect(savedSection).toBeUndefined(); + }); + + it('should not show saved section when all saved searches are pending deletion and not offline', () => { + const mockSavedSearches = { + search1: { + id: 'search1', + name: 'Deleted Search', + query: 'type:expense', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }; + + const sections = SearchUIUtils.createTypeMenuSections( + adminEmail, + adminAccountID, + {}, + undefined, + {}, + mockSavedSearches, + false, // not offline + ); + + const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); + expect(savedSection).toBeUndefined(); + }); + + it('should show saved section when searches are pending deletion but user is offline', () => { + const mockSavedSearches = { + search1: { + id: 'search1', + name: 'Deleted Search', + query: 'type:expense', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }; + + const sections = SearchUIUtils.createTypeMenuSections( + adminEmail, + adminAccountID, + {}, + undefined, + {}, + mockSavedSearches, + true, // offline + ); + + const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); + expect(savedSection).toBeDefined(); + }); + + it('should not show todo section when user has no appropriate permissions', () => { + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Personal Policy', + owner: adminEmail, + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + role: CONST.POLICY.ROLE.USER, + type: CONST.POLICY.TYPE.PERSONAL, // personal policy, not team + approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, + }, + }; + + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, {}, undefined, mockPolicies, {}, false); + + const todoSection = sections.find((section) => section.translationPath === 'common.todo'); + expect(todoSection).toBeUndefined(); + }); + + it('should not show accounting section when user has no admin permissions or card feeds', () => { + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Team Policy', + owner: adminEmail, + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + role: CONST.POLICY.ROLE.USER, // not admin + type: CONST.POLICY.TYPE.TEAM, + areCompanyCardsEnabled: false, + }, + }; + + const sections = SearchUIUtils.createTypeMenuSections( + adminEmail, + adminAccountID, + {}, // no card feeds + undefined, + mockPolicies, + {}, + false, + ); + + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + expect(accountingSection).toBeUndefined(); + }); + it('should generate correct routes', () => { const menuItems = SearchUIUtils.createTypeMenuSections(undefined, undefined, {}, undefined, {}, {}, false) .map((section) => section.menuItems) From 64509b165fb2719e47d8490b9634784c3c7bae94 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 21 Aug 2025 13:52:51 -0600 Subject: [PATCH 06/15] rm group by --- src/libs/SearchUIUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e8db7fdc944d..c7d455d5cd94 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -448,7 +448,6 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb icon: Expensicons.Bank, searchQuery: buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT, }), get searchQueryJSON() { From 40bbcab82347918c04ae7fa969985ce6aa82b8a9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 21 Aug 2025 15:41:32 -0600 Subject: [PATCH 07/15] add withdrawn filter --- src/libs/SearchUIUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c7d455d5cd94..0ffc24f1622b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -449,6 +449,7 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb searchQuery: buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, withdrawalType: CONST.SEARCH.WITHDRAWAL_TYPE.REIMBURSEMENT, + withdrawnOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, }), get searchQueryJSON() { return buildSearchQueryJSON(this.searchQuery); From 6628a0c6c9949919d4630b2299c70d5ea79e386a Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 21 Aug 2025 18:09:17 -0600 Subject: [PATCH 08/15] address comments --- src/languages/de.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/libs/SearchUIUtils.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 8e01fc2befa3..9a48a2e3e849 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6021,7 +6021,7 @@ const translations = { statements: 'Erklärungen', unapprovedCash: 'Nicht genehmigtes Bargeld', unapprovedCard: 'Nicht genehmigte Karte', - reconciliation: 'Versöhnung', + reconciliation: 'Abgleich', saveSearch: 'Suche speichern', deleteSavedSearch: 'Gespeicherte Suche löschen', deleteSavedSearchConfirm: 'Möchten Sie diese Suche wirklich löschen?', diff --git a/src/languages/es.ts b/src/languages/es.ts index b7f02bf87154..5b395f679eb8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6017,7 +6017,7 @@ const translations = { statements: 'Extractos', unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', - reconciliation: 'Reconciliación', + reconciliation: 'Conciliación', saveSearch: 'Guardar búsqueda', savedSearchesMenuItemTitle: 'Guardadas', searchName: 'Nombre de la búsqueda', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2f461c903101..44d1e1648945 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5221,7 +5221,7 @@ const translations = { autoSync: 'Synchronisation automatique', autoSyncDescription: 'Synchronisez NetSuite et Expensify automatiquement, chaque jour. Exportez le rapport finalisé en temps réel.', reimbursedReports: 'Synchroniser les rapports remboursés', - cardReconciliation: 'Rapprochement de carte', + cardReconciliation: 'Rapprochement', reconciliationAccount: 'Compte de réconciliation', continuousReconciliation: 'Réconciliation Continue', saveHoursOnReconciliation: diff --git a/src/languages/it.ts b/src/languages/it.ts index 06ff8e84aac9..699703a60d8d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6037,7 +6037,7 @@ const translations = { statements: 'Dichiarazioni', unapprovedCash: 'Contanti non approvati', unapprovedCard: 'Carta non approvata', - reconciliation: 'Reconciliation', + reconciliation: 'Riconciliazione', saveSearch: 'Salva ricerca', deleteSavedSearch: 'Elimina ricerca salvata', deleteSavedSearchConfirm: 'Sei sicuro di voler eliminare questa ricerca?', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ca27f393e050..1e247732187c 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5995,7 +5995,7 @@ const translations = { statements: 'ステートメント', unapprovedCash: '未承認現金', unapprovedCard: '未承認のカード', - reconciliation: '調整', + reconciliation: '照合', saveSearch: '検索を保存', deleteSavedSearch: '保存された検索を削除', deleteSavedSearchConfirm: 'この検索を削除してもよろしいですか?', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 359436f2ad88..7ea64ea35b6c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6030,7 +6030,7 @@ const translations = { statements: 'Verklaringen', unapprovedCash: 'Niet goedgekeurd contant geld', unapprovedCard: 'Niet-goedgekeurde kaart', - reconciliation: 'Verzoening', + reconciliation: 'Afstemming', saveSearch: 'Zoekopdracht opslaan', deleteSavedSearch: 'Verwijder opgeslagen zoekopdracht', deleteSavedSearchConfirm: 'Weet je zeker dat je deze zoekopdracht wilt verwijderen?', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 221c102adafe..20c71b74fc4c 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6015,7 +6015,7 @@ const translations = { statements: 'Oświadczenia', unapprovedCash: 'Niezatwierdzone środki pieniężne', unapprovedCard: 'Niezatwierdzona karta', - reconciliation: 'Pojednanie', + reconciliation: 'Uzgodnienie', saveSearch: 'Zapisz wyszukiwanie', deleteSavedSearch: 'Usuń zapisaną wyszukiwarkę', deleteSavedSearchConfirm: 'Czy na pewno chcesz usunąć to wyszukiwanie?', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0fbbb21d7c18..22edcfe35499 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6029,7 +6029,7 @@ const translations = { statements: 'Declarações', unapprovedCash: 'Dinheiro não aprovado', unapprovedCard: 'Cartão não aprovado', - reconciliation: 'Reconciliamento', + reconciliation: 'Conciliação', saveSearch: 'Salvar pesquisa', deleteSavedSearch: 'Excluir pesquisa salva', deleteSavedSearchConfirm: 'Tem certeza de que deseja excluir esta pesquisa?', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 0ffc24f1622b..9e6d97a00141 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -490,7 +490,7 @@ function getSuggestedSearchesVisibility( const isApprover = policy.approver === currentUserEmail; const isApprovalEnabled = policy.approvalMode ? policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; const isPaymentEnabled = arePaymentsEnabled(policy); - const hasVBBA = !!policy.achAccount?.bankAccountID && policy.achAccount.state !== CONST.BANK_ACCOUNT.STATE.OPEN; + const hasVBBA = !!policy.achAccount?.bankAccountID && policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.OPEN; const hasReimburser = !!policy.achAccount?.reimburser; const isECardEnabled = !!policy.areExpensifyCardsEnabled; const isSubmittedTo = Object.values(policy.employeeList ?? {}).some((employee) => { From 81c8b575b26428918b1372fac5a54bb87358d1d0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 21 Aug 2025 18:20:32 -0600 Subject: [PATCH 09/15] add tests --- tests/unit/Search/SearchUIUtilsTest.ts | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 5c8731d273a5..5fe0b2d4548f 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -2043,6 +2043,63 @@ describe('SearchUIUtils', () => { expect(accountingSection).toBeUndefined(); }); + it('should show reconciliation for ACH-only scenario (payments enabled, active VBBA, reimburser set, areExpensifyCardsEnabled = false)', () => { + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'ACH Only Policy', + owner: adminEmail, + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + areExpensifyCardsEnabled: false, + achAccount: { + bankAccountID: 1234, + reimburser: adminEmail, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + accountNumber: '1234567890', + routingNumber: '1234567890', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }, + }; + + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, {}, undefined, mockPolicies, {}, false); + + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + expect(accountingSection).toBeDefined(); + + const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; + 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)', () => { + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Card Only Policy', + owner: adminEmail, + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + areExpensifyCardsEnabled: true, + }, + }; + + const mockCardFeedsByPolicy: Record = {}; + const sections = SearchUIUtils.createTypeMenuSections(adminEmail, adminAccountID, mockCardFeedsByPolicy, undefined, mockPolicies, {}, false); + const accountingSection = sections.find((section) => section.translationPath === 'workspace.common.accounting'); + + expect(accountingSection).toBeDefined(); + const menuItemKeys = accountingSection?.menuItems.map((item) => item.key) ?? []; + expect(menuItemKeys).toContain(CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + }); + it('should generate correct routes', () => { const menuItems = SearchUIUtils.createTypeMenuSections(undefined, undefined, {}, undefined, {}, {}, false) .map((section) => section.menuItems) From 0922e0cb5dfd1f78b075479beb184ad100c4cf2e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 Aug 2025 09:02:19 -0600 Subject: [PATCH 10/15] update copy --- src/libs/SearchUIUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 9e6d97a00141..6556184c06f7 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1914,8 +1914,8 @@ function createTypeMenuSections( ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION], emptyState: { headerMedia: DotLottieAnimations.Fireworks, - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', + title: 'search.searchResults.emptyStatementsResults.title', + subtitle: 'search.searchResults.emptyStatementsResults.subtitle', }, }); } From 81ec9a830d4206c06cdbe1504a913dc6b13dc738 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 Aug 2025 14:06:46 -0600 Subject: [PATCH 11/15] show withdrawal-type --- src/components/Search/SearchPageHeader/SearchFiltersBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index fcb99c9936e0..9951e33f3332 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -400,7 +400,7 @@ function SearchFiltersBar({queryJSON, headerButtonsOptions, isMobileSelectionMod const shouldDisplayGroupCurrencyFilter = shouldDisplayGroupByFilter && hasMultipleOutputCurrency; const shouldDisplayFeedFilter = feedOptions.length > 1 && !!filterFormValues.feed; const shouldDisplayPostedFilter = !!filterFormValues.feed && (!!filterFormValues.postedOn || !!filterFormValues.postedAfter || !!filterFormValues.postedBefore); - const shouldDisplayWithdrawalTypeFilter = groupBy?.value === CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID && !!filterFormValues.withdrawalType; + const shouldDisplayWithdrawalTypeFilter = !!filterFormValues.withdrawalType; const shouldDisplayWithdrawnFilter = !!filterFormValues.withdrawnOn || !!filterFormValues.withdrawnAfter || !!filterFormValues.withdrawnBefore; const filterList = [ From 62add1a83b832d615b5754a94d60b0123cb70aea Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 Aug 2025 15:17:42 -0600 Subject: [PATCH 12/15] fix lhn --- src/libs/SearchQueryUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 75f25f0f0df8..12710c9cfce6 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -300,11 +300,11 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${Array.isArray(query.status) ? query.status.join(',') : query.status}`; orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY}:${query.groupBy}`; - let similarSearchHashInput = orderedQuery; + const filterSet = new Set(orderedQuery); query.flatFilters .map((filter) => { - similarSearchHashInput += filter.key; + filterSet.add(filter.key); const filters = cloneDeep(filter.filters); filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString())); @@ -313,7 +313,7 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea .sort() .forEach((filterString) => (orderedQuery += ` ${filterString}`)); - const similarSearchHash = hashText(similarSearchHashInput, 2 ** 32); + const similarSearchHash = hashText(Array.from(filterSet).join(''), 2 ** 32); const recentSearchHash = hashText(orderedQuery, 2 ** 32); orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`; From 8249b4c5a2b3ae73e6666edf7604f2f873687f07 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 Aug 2025 15:46:08 -0600 Subject: [PATCH 13/15] fix empty views --- src/components/Search/index.tsx | 2 +- src/pages/Search/EmptySearchView.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 88de08303b69..42208c8d98b9 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -744,7 +744,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return ( ; type: SearchDataTypes; hasResults: boolean; @@ -74,7 +74,7 @@ const tripsFeatures: FeatureListItem[] = [ }, ]; -function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps) { +function EmptySearchView({similarSearchHash, type, groupBy, hasResults}: EmptySearchViewProps) { const theme = useTheme(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -183,7 +183,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps // Begin by going through all of our To-do searches, and returning their empty state // if it exists for (const menuItem of typeMenuItems) { - if (menuItem.hash === hash && menuItem.emptyState) { + if (menuItem.similarSearchHash === similarSearchHash && menuItem.emptyState) { return { headerMedia: menuItem.emptyState.headerMedia, title: translate(menuItem.emptyState.title), @@ -353,7 +353,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps groupBy, type, typeMenuItems, - hash, + similarSearchHash, translate, StyleUtils, theme.todoBG, From 707e85cf28659d4aedd0974f54bec5c6b67f4d27 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 Aug 2025 15:48:21 -0600 Subject: [PATCH 14/15] add test --- tests/unit/Search/SearchQueryUtilsTest.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index ac15be24d6bf..4f01dd986161 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -332,6 +332,13 @@ describe('SearchQueryUtils', () => { expect(queryJSONa?.similarSearchHash).toEqual(queryJSONb?.similarSearchHash); }); + it('should return same similarSearchHash for queries with a date range', () => { + const queryJSONa = buildSearchQueryJSON('type:expense withdrawal-type:reimbursement withdrawn:last-month'); + const queryJSONb = buildSearchQueryJSON('type:expense withdrawal-type:reimbursement withdrawn>2025-01-01 withdrawn<2025-01-03'); + + expect(queryJSONa?.similarSearchHash).toEqual(queryJSONb?.similarSearchHash); + }); + it('should return different similarSearchHash for two queries that have different types', () => { const queryJSONa = buildSearchQueryJSON('sortBy:date sortOrder:desc type:expense feed:"oauth.americanexpressfdx.com 1001"'); const queryJSONb = buildSearchQueryJSON('sortBy:date sortOrder:desc type:trip feed:"oauth.americanexpressfdx.com 1001"'); From 832aa92d54beb3d4b54a0c0ece2edbafe03f8935 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 22 Aug 2025 15:50:42 -0600 Subject: [PATCH 15/15] update tests --- tests/ui/components/EmptySearchViewTest.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ui/components/EmptySearchViewTest.tsx b/tests/ui/components/EmptySearchViewTest.tsx index 3ad5a1bc396f..7fd0eb437805 100644 --- a/tests/ui/components/EmptySearchViewTest.tsx +++ b/tests/ui/components/EmptySearchViewTest.tsx @@ -52,7 +52,7 @@ describe('EmptySearchView', () => { render( @@ -76,7 +76,7 @@ describe('EmptySearchView', () => { render( @@ -118,7 +118,7 @@ describe('EmptySearchView', () => { render( { render( { render( @@ -204,7 +204,7 @@ describe('EmptySearchView', () => { render(