diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c9ea5caa74b2..a4281c1ad1fa 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7285,12 +7285,14 @@ const CONST = { UNAPPROVED_CARD: 'unapprovedCard', RECONCILIATION: 'reconciliation', TOP_SPENDERS: 'topSpenders', + TOP_CATEGORIES: 'topCategories', }, GROUP_PREFIX: 'group_', ANIMATION: { FADE_DURATION: 200, }, TODO_BADGE_MAX_COUNT: 50, + TOP_SEARCH_LIMIT: 10, }, SEARCH_SELECTOR: { SELECTION_MODE_SINGLE: 'single', diff --git a/src/languages/de.ts b/src/languages/de.ts index 5eaf7cc1fe37..a2cf9765ff25 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6859,6 +6859,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard deleteSavedSearchConfirm: 'Möchten Sie diese Suche wirklich löschen?', searchName: 'Name suchen', savedSearchesMenuItemTitle: 'Gespeichert', + topCategories: 'Top-Kategorien', groupedExpenses: 'gruppierte Ausgaben', bulkActions: { approve: 'Genehmigen', diff --git a/src/languages/en.ts b/src/languages/en.ts old mode 100755 new mode 100644 index 74cae9529036..1810f7cd752e --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6745,6 +6745,7 @@ const translations = { deleteSavedSearchConfirm: 'Are you sure you want to delete this search?', searchName: 'Search name', savedSearchesMenuItemTitle: 'Saved', + topCategories: 'Top categories', groupedExpenses: 'grouped expenses', bulkActions: { approve: 'Approve', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4f8ff4f14d46..34cd1c79a4d3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6492,6 +6492,7 @@ ${amount} para ${merchant} - ${date}`, topSpenders: 'Mayores gastadores', saveSearch: 'Guardar búsqueda', savedSearchesMenuItemTitle: 'Guardadas', + topCategories: 'Categorías principales', searchName: 'Nombre de la búsqueda', deleteSavedSearch: 'Eliminar búsqueda guardada', deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 10236f7d8f7a..286d867c4724 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6870,6 +6870,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin deleteSavedSearchConfirm: 'Voulez-vous vraiment supprimer cette recherche ?', searchName: 'Rechercher un nom', savedSearchesMenuItemTitle: 'Enregistré', + topCategories: 'Catégories principales', groupedExpenses: 'dépenses groupées', bulkActions: { approve: 'Approuver', diff --git a/src/languages/it.ts b/src/languages/it.ts index 138e9905c880..43fcb10b4f8e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6849,6 +6849,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori deleteSavedSearchConfirm: 'Sei sicuro di voler eliminare questa ricerca?', searchName: 'Cerca nome', savedSearchesMenuItemTitle: 'Salvato', + topCategories: 'Categorie principali', groupedExpenses: 'spese raggruppate', bulkActions: { approve: 'Approva', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c2e118ac4bbc..213216b21bb6 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6790,6 +6790,7 @@ ${reportName} deleteSavedSearchConfirm: 'この検索を削除してもよろしいですか?', searchName: '名前を検索', savedSearchesMenuItemTitle: '保存済み', + topCategories: 'トップカテゴリ', groupedExpenses: 'グループ化された経費', bulkActions: { approve: '承認', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d858997f4ba1..7639d3315c2d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6831,6 +6831,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten deleteSavedSearchConfirm: 'Weet je zeker dat je deze zoekopdracht wilt verwijderen?', searchName: 'Naam zoeken', savedSearchesMenuItemTitle: 'Opgeslagen', + topCategories: 'Topcategorieën', groupedExpenses: 'gegroepeerde uitgaven', bulkActions: { approve: 'Goedkeuren', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index e308a7ce7dc5..8d79671587ed 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6820,6 +6820,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i deleteSavedSearchConfirm: 'Czy na pewno chcesz usunąć to wyszukiwanie?', searchName: 'Wyszukaj nazwę', savedSearchesMenuItemTitle: 'Zapisano', + topCategories: 'Najlepsze kategorie', groupedExpenses: 'pogrupowane wydatki', bulkActions: { approve: 'Zatwierdź', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 045b62dd2ce3..10ba4095e1bd 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6822,6 +6822,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe deleteSavedSearchConfirm: 'Você tem certeza de que deseja excluir esta pesquisa?', searchName: 'Pesquisar nome', savedSearchesMenuItemTitle: 'Salvo', + topCategories: 'Principais categorias', groupedExpenses: 'despesas agrupadas', bulkActions: { approve: 'Aprovar', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c0f5414775a3..c659920a065b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6671,6 +6671,7 @@ ${reportName} deleteSavedSearchConfirm: '您确定要删除此搜索吗?', searchName: '搜索名称', savedSearchesMenuItemTitle: '已保存', + topCategories: '热门类别', groupedExpenses: '已分组报销费用', bulkActions: { approve: '批准', diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c6d9e447daa7..98550f27fee9 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -389,6 +389,47 @@ type GetSectionsParams = { allTransactionViolations?: OnyxCollection; }; +/** + * Creates a top search menu item with common structure for TOP_SPENDERS and TOP_CATEGORIES + */ +function createTopSearchMenuItem( + key: SearchKey, + translationPath: TranslationPaths, + icon: IconAsset | Extract, + groupBy: ValueOf, + limit?: number, +): SearchTypeMenuItem { + const searchQuery = buildQueryStringFromFilterFormValues( + { + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + groupBy, + dateOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, + }, + { + sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL, + sortOrder: CONST.SEARCH.SORT_ORDER.DESC, + ...(limit && {limit}), + }, + ); + + return { + key, + translationPath, + type: CONST.SEARCH.DATA_TYPES.EXPENSE, + icon, + 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; + }, + }; +} + /** * Returns a list of all possible searches in the LHN, along with their query & hash. * *NOTE* When rendering the LHN, you should use the "createTypeMenuSections" method, which @@ -623,32 +664,14 @@ 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: Expensicons.User, - searchQuery: buildQueryStringFromFilterFormValues( - { - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.FROM, - dateOn: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, - }, - { - sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL, - 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.TOP_SPENDERS]: createTopSearchMenuItem(CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, 'search.topSpenders', Expensicons.User, CONST.SEARCH.GROUP_BY.FROM), + [CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: createTopSearchMenuItem( + CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, + 'search.topCategories', + Expensicons.Folder, + CONST.SEARCH.GROUP_BY.CATEGORY, + CONST.SEARCH.TOP_SEARCH_LIMIT, + ), }; } @@ -671,6 +694,7 @@ function getSuggestedSearchesVisibility( let shouldShowUnapprovedCardSuggestion = false; let shouldShowReconciliationSuggestion = false; let shouldShowTopSpendersSuggestion = false; + let shouldShowTopCategoriesSuggestion = false; const hasCardFeed = Object.values(cardFeedsByPolicy ?? {}).some((feeds) => feeds.length > 0); @@ -707,6 +731,7 @@ function getSuggestedSearchesVisibility( const isEligibleForReconciliationSuggestion = isPaidPolicy && isAdmin && ((isPaymentEnabled && hasVBBA && hasReimburser) || isECardEnabled); const isAuditor = policy.role === CONST.POLICY.ROLE.AUDITOR; const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover); + const isEligibleForTopCategoriesSuggestion = isPaidPolicy; shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion; shouldShowPaySuggestion ||= isEligibleForPaySuggestion; @@ -717,6 +742,7 @@ function getSuggestedSearchesVisibility( shouldShowUnapprovedCardSuggestion ||= isEligibleForUnapprovedCardSuggestion; shouldShowReconciliationSuggestion ||= isEligibleForReconciliationSuggestion; shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion; + shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion; // We don't need to check the rest of the policies if we already determined that all suggestions should be displayed return ( @@ -728,7 +754,8 @@ function getSuggestedSearchesVisibility( shouldShowUnapprovedCashSuggestion && shouldShowUnapprovedCardSuggestion && shouldShowReconciliationSuggestion && - shouldShowTopSpendersSuggestion + shouldShowTopSpendersSuggestion && + shouldShowTopCategoriesSuggestion ); }); @@ -745,6 +772,7 @@ function getSuggestedSearchesVisibility( [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]: shouldShowUnapprovedCardSuggestion, [CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]: shouldShowReconciliationSuggestion, [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, + [CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion, }; } @@ -3019,9 +3047,15 @@ function createTypeMenuSections( menuItems: [], }; - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]) { + const insightsSearchKeys = [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]; + + for (const key of insightsSearchKeys) { + if (!suggestedSearchesVisibility[key]) { + continue; + } + insightsSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS], + ...suggestedSearches[key], emptyState: { title: 'search.searchResults.emptyResults.title', subtitle: 'search.searchResults.emptyResults.subtitle',