From 304d4d1864db80c9302014d1fc390f5498935803 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 24 Dec 2025 02:55:45 +0500 Subject: [PATCH 1/6] 78309: Added top Spenders default report --- src/CONST/index.ts | 1 + src/languages/en.ts | 2 ++ src/libs/SearchUIUtils.ts | 46 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 9780d4cb607a..872f197fe612 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7113,6 +7113,7 @@ const CONST = { UNAPPROVED_CASH: 'unapprovedCash', UNAPPROVED_CARD: 'unapprovedCard', RECONCILIATION: 'reconciliation', + TOP_SPENDERS: 'topSpenders', }, GROUP_PREFIX: 'group_', ANIMATION: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 0ecf242d0401..537a7f613e57 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -629,6 +629,7 @@ const translations = { sharedIn: 'Shared in', unreported: 'Unreported', explore: 'Explore', + insights: 'Insights', todo: 'To-do', invoice: 'Invoice', expense: 'Expense', @@ -6618,6 +6619,7 @@ const translations = { unapprovedCash: 'Unapproved cash', unapprovedCard: 'Unapproved card', reconciliation: 'Reconciliation', + topSpenders: 'Top spenders', saveSearch: 'Save search', deleteSavedSearch: 'Delete saved search', deleteSavedSearchConfirm: 'Are you sure you want to delete this search?', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 91a9cddb7707..cc9769b44c83 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -585,6 +585,22 @@ function getSuggestedSearches( return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID; }, }, + [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: { + key: CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, + translationPath: 'search.topSpenders', + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon: 'MoneyBag', + searchQuery: `type:${CONST.SEARCH.DATA_TYPES.EXPENSE} group-by:${CONST.SEARCH.GROUP_BY.FROM} date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH} sort-by:${CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL} sort-order:${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; + }, + }, }; } @@ -606,6 +622,7 @@ function getSuggestedSearchesVisibility( let shouldShowUnapprovedCashSuggestion = false; let shouldShowUnapprovedCardSuggestion = false; let shouldShowReconciliationSuggestion = false; + let shouldShowTopSpendersSuggestion = false; const hasCardFeed = Object.values(cardFeedsByPolicy ?? {}).some((feeds) => feeds.length > 0); @@ -640,6 +657,8 @@ function getSuggestedSearchesVisibility( const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && isPaymentEnabled; const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && (hasCardFeed || !!defaultExpensifyCard); const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasVBBA && hasReimburser) || isECardEnabled); + const isAuditor = policy.role === CONST.POLICY.ROLE.AUDITOR; + const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover || isSubmittedTo); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -649,6 +668,7 @@ function getSuggestedSearchesVisibility( shouldShowUnapprovedCashSuggestion ||= isEligibleForUnapprovedCashSuggestion; shouldShowUnapprovedCardSuggestion ||= isEligibleForUnapprovedCardSuggestion; shouldShowReconciliationSuggestion ||= isEligibleForReconciliationSuggestion; + shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion; // We don't need to check the rest of the policies if we already determined that all suggestions should be displayed return ( @@ -659,7 +679,8 @@ function getSuggestedSearchesVisibility( shouldShowStatementsSuggestion && shouldShowUnapprovedCashSuggestion && shouldShowUnapprovedCardSuggestion && - shouldShowReconciliationSuggestion + shouldShowReconciliationSuggestion && + shouldShowTopSpendersSuggestion ); }); @@ -675,6 +696,7 @@ function getSuggestedSearchesVisibility( [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: shouldShowUnapprovedCashSuggestion, [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]: shouldShowUnapprovedCardSuggestion, [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: shouldShowReconciliationSuggestion, + [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, }; } @@ -2835,6 +2857,28 @@ function createTypeMenuSections( } } + // Insights section + { + const insightsSection: SearchTypeMenuSection = { + translationPath: 'common.insights', + menuItems: [], + }; + + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]) { + insightsSection.menuItems.push({ + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS], + emptyState: { + title: 'search.searchResults.emptyResults.title', + subtitle: 'search.searchResults.emptyResults.subtitle', + }, + }); + } + + if (insightsSection.menuItems.length > 0) { + typeMenuSections.push(insightsSection); + } + } + // Explore section { const exploreSection: SearchTypeMenuSection = { From f55e8c02d2d32d89c5c9c9a92cfff1a340086b92 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 25 Dec 2025 18:03:57 +0500 Subject: [PATCH 2/6] Added transalation throug Open AI Key --- 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/languages/zh-hans.ts | 2 ++ 9 files changed, 18 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index eb37cbe58bfb..b0648f3dcadc 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -675,6 +675,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag', nonReimbursableTotal: 'Nicht erstattungsfähiger Gesamtbetrag', originalAmount: 'Ursprünglicher Betrag', + insights: 'Einblicke', }, supportalNoAccess: { title: 'Nicht so schnell', @@ -6862,6 +6863,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard selectAllMatchingItems: 'Alle passenden Elemente auswählen', allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt', }, + topSpenders: 'Top-Ausgaben', }, genericErrorPage: { title: 'Oh je, etwas ist schiefgelaufen!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 63c7e9c33001..c4eb9fe30962 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -345,6 +345,7 @@ const translations: TranslationDeepObject = { sharedIn: 'Compartido en', unreported: 'No reportado', explore: 'Explorar', + insights: 'Información', todo: 'Tereas', invoice: 'Factura', expense: 'Gasto', @@ -6318,6 +6319,7 @@ ${amount} para ${merchant} - ${date}`, unapprovedCash: 'Efectivo no aprobado', unapprovedCard: 'Tarjeta no aprobada', reconciliation: 'Conciliación', + topSpenders: 'Mayores gastadores', 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 67b6b83058c4..55b907f0c71c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -677,6 +677,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Total remboursable', nonReimbursableTotal: 'Total non remboursable', originalAmount: 'Montant d’origine', + insights: 'Analyses', }, supportalNoAccess: { title: 'Pas si vite', @@ -6872,6 +6873,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin selectAllMatchingItems: 'Sélectionner tous les éléments correspondants', allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés', }, + topSpenders: 'Plus gros dépensiers', }, genericErrorPage: { title: 'Oh oh, quelque chose s’est mal passé !', diff --git a/src/languages/it.ts b/src/languages/it.ts index 8f84945e384e..862e8315dfe9 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -676,6 +676,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Totale rimborsabile', nonReimbursableTotal: 'Totale non rimborsabile', originalAmount: 'Importo originale', + insights: 'Analisi', }, supportalNoAccess: { title: 'Non così in fretta', @@ -6846,6 +6847,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori selectAllMatchingItems: 'Seleziona tutti gli elementi corrispondenti', allMatchingItemsSelected: 'Tutti gli elementi corrispondenti selezionati', }, + topSpenders: 'Maggiori spenditori', }, genericErrorPage: { title: 'Uh-oh, qualcosa è andato storto!', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 8bb2cee78dd6..bfeba31b8a5b 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -675,6 +675,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: '経費精算対象の合計', nonReimbursableTotal: '非払い戻し合計', originalAmount: '元の金額', + insights: 'インサイト', }, supportalNoAccess: { title: 'ちょっと待ってください', @@ -6790,6 +6791,7 @@ ${reportName} selectAllMatchingItems: '一致する項目をすべて選択', allMatchingItemsSelected: '一致する項目をすべて選択済み', }, + topSpenders: 'トップ支出者', }, genericErrorPage: { title: 'おっと、問題が発生しました!', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c62880f4dab6..7e9ff74f5ff1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -676,6 +676,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Totaal te vergoeden', nonReimbursableTotal: 'Niet-vergoedbaar totaal', originalAmount: 'Oorspronkelijk bedrag', + insights: 'Inzichten', }, supportalNoAccess: { title: 'Niet zo snel', @@ -6833,6 +6834,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten selectAllMatchingItems: 'Selecteer alle overeenkomende items', allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd', }, + topSpenders: 'Grootste uitgaven', }, genericErrorPage: { title: 'O jee, er is iets misgegaan!', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ede8d6ea8749..c850c95218f6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -676,6 +676,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Łączna kwota podlegająca zwrotowi', nonReimbursableTotal: 'Suma niepodlegająca zwrotowi', originalAmount: 'Kwota pierwotna', + insights: 'Analizy', }, supportalNoAccess: { title: 'Nie tak szybko', @@ -6821,6 +6822,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i selectAllMatchingItems: 'Zaznacz wszystkie pasujące elementy', allMatchingItemsSelected: 'Wybrano wszystkie pasujące elementy', }, + topSpenders: 'Najwięksi wydający', }, genericErrorPage: { title: 'Ups, coś poszło nie tak!', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fdbfcb33a469..872a838bb582 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -675,6 +675,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Total reembolsável', nonReimbursableTotal: 'Total não reembolsável', originalAmount: 'Valor original', + insights: 'Insights', }, supportalNoAccess: { title: 'Não tão rápido', @@ -6826,6 +6827,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe selectAllMatchingItems: 'Selecionar todos os itens correspondentes', allMatchingItemsSelected: 'Todos os itens correspondentes selecionados', }, + topSpenders: 'Maiores gastadores', }, genericErrorPage: { title: 'Opa, algo deu errado!', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 163e5d0ab72d..ea25cdcacb23 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -672,6 +672,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: '可报销总额', nonReimbursableTotal: '不可报销总额', originalAmount: '原始金额', + insights: '洞察', }, supportalNoAccess: { title: '先别急', @@ -6688,6 +6689,7 @@ ${reportName} selectAllMatchingItems: '选择所有匹配的项目', allMatchingItemsSelected: '已选择所有匹配的项目', }, + topSpenders: '最高支出者', }, genericErrorPage: { title: '哎呀,出错了!', From 07005da63dac7337fec548cae224ba0396129482 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 31 Dec 2025 18:55:17 +0500 Subject: [PATCH 3/6] Resolved feedback --- src/libs/SearchUIUtils.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e052f911185d..b3140d552e63 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -601,8 +601,13 @@ function getSuggestedSearches( key: CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, translationPath: 'search.topSpenders', type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: 'MoneyBag', - searchQuery: `type:${CONST.SEARCH.DATA_TYPES.EXPENSE} group-by:${CONST.SEARCH.GROUP_BY.FROM} date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH} sort-by:${CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL} sort-order:${CONST.SEARCH.SORT_ORDER.DESC}`, + icon: Expensicons.MoneySearch, + get searchQuery() { + const queryString = `type:${CONST.SEARCH.DATA_TYPES.EXPENSE} group-by:${CONST.SEARCH.GROUP_BY.FROM} date:${CONST.SEARCH.DATE_PRESETS.LAST_MONTH} sort-by:${CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL} sort-order:${CONST.SEARCH.SORT_ORDER.DESC}`; + // Normalize the query using buildSearchQueryJSON and buildSearchQueryString (same pattern as buildCannedSearchQuery) + const normalizedQueryJSON = buildSearchQueryJSON(queryString); + return buildSearchQueryString(normalizedQueryJSON); + }, get searchQueryJSON() { return buildSearchQueryJSON(this.searchQuery); }, @@ -670,7 +675,7 @@ function getSuggestedSearchesVisibility( const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && isAdmin && isApprovalEnabled && (hasCardFeed || !!defaultExpensifyCard); const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasVBBA && hasReimburser) || isECardEnabled); const isAuditor = policy.role === CONST.POLICY.ROLE.AUDITOR; - const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover || isSubmittedTo); + const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; From cad122fe001cba603d95ffad0f9f4ef76f42c037 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 1 Jan 2026 02:04:15 +0500 Subject: [PATCH 4/6] resolved feedbacl --- src/libs/SearchQueryUtils.ts | 11 ++++++++--- src/libs/SearchUIUtils.ts | 17 +++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index ca48d380c4f2..d9242bb1d300 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -542,13 +542,18 @@ function getSanitizedRawFilters(queryJSON: SearchQueryJSON): RawQueryFilter[] | return sanitizedFilters; } +type BuildQueryStringOptions = { + sortBy?: string; + sortOrder?: string; +}; + /** * Formats a given object with search filter values into the string version of the query. * Main usage is to consume data format that comes from AdvancedFilters Onyx Form Data, and generate query string. * * Reverse operation of buildFilterFormValuesFromQuery() */ -function buildQueryStringFromFilterFormValues(filterValues: Partial) { +function buildQueryStringFromFilterFormValues(filterValues: Partial, options?: BuildQueryStringOptions) { const supportedFilterValues = {...filterValues}; // When switching types/setting the type, ensure we aren't polluting our query with filters that are @@ -566,8 +571,8 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial Date: Thu, 1 Jan 2026 02:29:35 +0500 Subject: [PATCH 5/6] updated Icon --- 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 ba97ce38828c..6d51b0a860bb 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -601,7 +601,7 @@ function getSuggestedSearches( key: CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, translationPath: 'search.topSpenders', type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: Expensicons.MoneySearch, + icon: Expensicons.User, searchQuery: buildQueryStringFromFilterFormValues( { type: CONST.SEARCH.DATA_TYPES.EXPENSE, From d84b8f075ca6c3052202ec7e4f535f51a346d65b Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 7 Jan 2026 14:28:06 +0500 Subject: [PATCH 6/6] fixed short-by issue after changing workspace --- .../Search/SearchPageHeader/SearchFiltersBar.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 46a3f3c99a5b..4f5fbc652ced 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -308,14 +308,18 @@ function SearchFiltersBar({ updatedFilterFormValues.columns = []; } - const queryString = buildQueryStringFromFilterFormValues(updatedFilterFormValues); + // Preserve the current sortBy and sortOrder from queryJSON when updating filters + const queryString = buildQueryStringFromFilterFormValues(updatedFilterFormValues, { + sortBy: queryJSON.sortBy, + sortOrder: queryJSON.sortOrder, + }); close(() => { - // We want to explicitly clear stale rawQuery since it’s only used for manually typed-in queries. + // We want to explicitly clear stale rawQuery since it's only used for manually typed-in queries. Navigation.setParams({q: queryString, rawQuery: undefined}); }); }, - [searchAdvancedFiltersForm], + [searchAdvancedFiltersForm, queryJSON.sortBy, queryJSON.sortOrder], ); const openAdvancedFilters = useCallback(() => {