diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1c218e78e89a..c6bad2e8ce70 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7124,6 +7124,7 @@ const CONST = { UNAPPROVED_CASH: 'unapprovedCash', UNAPPROVED_CARD: 'unapprovedCard', RECONCILIATION: 'reconciliation', + TOP_SPENDERS: 'topSpenders', }, GROUP_PREFIX: 'group_', ANIMATION: { 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(() => { diff --git a/src/languages/de.ts b/src/languages/de.ts index a1306bd5c746..a9e88139a01f 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -671,6 +671,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag', nonReimbursableTotal: 'Nicht erstattungsfähiger Gesamtbetrag', originalAmount: 'Ursprünglicher Betrag', + insights: 'Einblicke', }, supportalNoAccess: { title: 'Nicht so schnell', @@ -6861,6 +6862,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/en.ts b/src/languages/en.ts index 7860fd92db81..aa32d606904b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -625,6 +625,7 @@ const translations = { sharedIn: 'Shared in', unreported: 'Unreported', explore: 'Explore', + insights: 'Insights', todo: 'To-do', invoice: 'Invoice', expense: 'Expense', @@ -6620,6 +6621,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/languages/es.ts b/src/languages/es.ts index 56e7107a9f19..d291ef4fbbf9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -348,6 +348,7 @@ const translations: TranslationDeepObject = { sharedIn: 'Compartido en', unreported: 'No reportado', explore: 'Explorar', + insights: 'Información', todo: 'Tereas', invoice: 'Factura', expense: 'Gasto', @@ -6327,6 +6328,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 d0c98d366799..db4ceb84e20a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -673,6 +673,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 fab23e7311f4..29405f9620a1 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -672,6 +672,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 6ca71b775e51..4bf985a2fcf7 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -671,6 +671,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 0c512d5dbdb1..c95c78af1ee5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -672,6 +672,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 1ddecd140acd..6b3fef985f63 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -672,6 +672,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 06f7cabb8e45..992083877f00 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -671,6 +671,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', @@ -6825,6 +6826,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 db79a20eba5c..d2717e2c88d0 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -668,6 +668,7 @@ const translations: TranslationDeepObject = { reimbursableTotal: '可报销总额', nonReimbursableTotal: '不可报销总额', originalAmount: '原始金额', + insights: '洞察', }, supportalNoAccess: { title: '先别急', @@ -6683,6 +6684,7 @@ ${reportName} selectAllMatchingItems: '选择所有匹配的项目', allMatchingItemsSelected: '已选择所有匹配的项目', }, + topSpenders: '最高支出者', }, genericErrorPage: { title: '哎呀,出错了!', 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 feeds.length > 0); @@ -652,6 +679,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); shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -661,6 +690,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 ( @@ -671,7 +701,8 @@ function getSuggestedSearchesVisibility( shouldShowStatementsSuggestion && shouldShowUnapprovedCashSuggestion && shouldShowUnapprovedCardSuggestion && - shouldShowReconciliationSuggestion + shouldShowReconciliationSuggestion && + shouldShowTopSpendersSuggestion ); }); @@ -687,6 +718,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, }; } @@ -2849,6 +2881,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 = {