App icon indicating copy to clipboard operation
App copied to clipboard

Add Top Spenders default report

Open puneetlath opened this issue 3 weeks ago • 16 comments

Part of the Insights project

Main issue: https://github.com/Expensify/Expensify/issues/570577 Doc section: https://docs.google.com/document/d/1zMWsgWBXY5Lx4V1M8LVTovOaTX-q34LempKIKWH8rwI/edit?tab=t.0#heading=h.mvmf31h2iib Project: <Project Link>

Feature Description

Add a new default report called Top Spenders to the Reports page.

In order to do this, we will:

  1. Add a new section to the Reports page LHN called Insights that lives above explore a. This section will only be shown when there is at least one Insights report to display
  2. Within the Insights section add a Top Spenders report a. The report should only be shown if the user is an admin, auditor, or approver on a workspace b. The query for this report is type:expense group-by:from date:last-month sort-by:groupTotal sort-order:desc
Image

When there is nothing to display, we'll use the same empty state as Reports > Expenses

Image
Issue OwnerCurrent Issue Owner: @

puneetlath avatar Dec 22 '25 21:12 puneetlath

Triggered auto assignment to Contributor-plus team member for initial proposal review - @mananjadhav (External)

melvin-bot[bot] avatar Dec 22 '25 21:12 melvin-bot[bot]

Triggered auto assignment to @jliexpensify (NewFeature), see https://stackoverflowteams.com/c/expensify/questions/14418#:~:text=BugZero%20process%20steps%20for%20feature%20requests for more details. Please add this Feature request to a GH project, as outlined in the SO. Remember to draft any necessary HelpDot updates by following the instructions here.

melvin-bot[bot] avatar Dec 22 '25 21:12 melvin-bot[bot]

:warning: It looks like this issue is labelled as a New Feature but not tied to any GitHub Project. Keep in mind that all new features should be tied to GitHub Projects in order to properly track external CAP software time :warning:

melvin-bot[bot] avatar Dec 22 '25 21:12 melvin-bot[bot]

Auto-assign attempt failed, all eligible assignees are OOO.

melvin-bot[bot] avatar Dec 22 '25 21:12 melvin-bot[bot]

Is this open for external contributors?

samranahm avatar Dec 22 '25 21:12 samranahm

@samranahm yes it is.

puneetlath avatar Dec 22 '25 21:12 puneetlath

@puneetlath I'm open to take this one, since the implementation is already mentioned.

samranahm avatar Dec 22 '25 22:12 samranahm

🚨 Edited by proposal-police: This proposal was edited at 2025-12-22 22:18:51 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

Add Top Spenders default report

What is the root cause of that problem?

New feat

What changes do you think we should make in order to solve the problem?

We should add a new Insights section in SearchUIUtils

// 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.emptyExpenseResults.title',
            subtitle: 'search.searchResults.emptyExpenseResults.subtitleWithOnlyCreateButton',
            buttons: [
                {
                    buttonText: 'iou.createExpense',
                    buttonAction: () =>
                        interceptAnonymousUser(() => {
                            if (shouldRedirectToExpensifyClassic) {
                                setIsOpenConfirmNavigateExpensifyClassicModalOpen(true);
                                return;
                            }
                            startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, draftTransactions);
                        }),
                    success: true,
                },
            ],
        },
    });
}

if (insightsSection.menuItems.length > 0) {
    typeMenuSections.push(insightsSection);
}
}

and add top spender logic in getSuggestedSearches

        [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:expense group-by:from date:last-month sort-by:groupTotal 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;
            },
        },

What alternative solutions did you explore? (Optional)

Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.

samranahm avatar Dec 22 '25 22:12 samranahm

⚠️ @samranahm Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

github-actions[bot] avatar Dec 22 '25 22:12 github-actions[bot]

Proposal

Please re-state the problem that we are trying to solve in this issue.

The Reports page currently lacks quick access to spending insights that help workspace admins, auditors, and approvers identify and monitor top spenders. Users need an easy way to see who is spending the most on their workspace without manually creating complex search queries or exports.

What is the root cause of that problem?

The Reports page LHN currently only has "To-do", "Accounting", "Saved", and "Explore" sections. There is no dedicated section for spending insights or analytics, and no default report that shows top spenders.

Current Structure:

The Reports page menu sections are created in App/src/libs/SearchUIUtils.ts within the createTypeMenuSections() function (lines 2688-2860). This function creates menu sections based on:

  1. User's role and permissions (determined by getSuggestedSearchesVisibility())
  2. Available search types (defined in getSuggestedSearches())
  3. Policy settings and features

Currently, there is no:

  • TOP_SPENDERS key in CONST.SEARCH.SEARCH_KEYS
  • Top Spenders search definition in getSuggestedSearches()
  • Visibility logic for top spenders in getSuggestedSearchesVisibility()
  • Insights section in createTypeMenuSections()
  • Translation keys for "Insights" and "Top spenders"

What changes do you think we should make in order to solve the problem?

1. Add TOP_SPENDERS Constant

File: App/src/CONST/index.ts

Add the TOP_SPENDERS key to the SEARCH_KEYS object (around line 7104):

SEARCH_KEYS: {
    EXPENSES: 'expenses',
    REPORTS: 'reports',
    CHATS: 'chats',
    SUBMIT: 'submit',
    APPROVE: 'approve',
    PAY: 'pay',
    EXPORT: 'export',
    STATEMENTS: 'statements',
    UNAPPROVED_CASH: 'unapprovedCash',
    UNAPPROVED_CARD: 'unapprovedCard',
    RECONCILIATION: 'reconciliation',
    TOP_SPENDERS: 'topSpenders', // Add this line
},

2. Define Top Spenders Search

File: App/src/libs/SearchUIUtils.ts

Add the Top Spenders entry in the getSuggestedSearches() function (around line 587, after the RECONCILIATION entry):

[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: buildQueryStringFromFilterFormValues({
        type: CONST.SEARCH.DATA_TYPES.EXPENSE,
        groupBy: CONST.SEARCH.GROUP_BY.FROM,
        date: 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;
    },
},

3. Add Visibility Logic

File: App/src/libs/SearchUIUtils.ts

In the getSuggestedSearchesVisibility() function:

a) Add variable declaration (around line 623):

let shouldShowSubmitSuggestion = false;
let shouldShowPaySuggestion = false;
let shouldShowApproveSuggestion = false;
let shouldShowExportSuggestion = false;
let shouldShowStatementsSuggestion = false;
let shouldShowUnapprovedCashSuggestion = false;
let shouldShowUnapprovedCardSuggestion = false;
let shouldShowReconciliationSuggestion = false;
let shouldShowTopSpendersSuggestion = false; // Add this line

b) Add eligibility check in the policies loop (around line 657, after RECONCILIATION check):

const isAuditor = policy.role === CONST.POLICY.ROLE.AUDITOR;
const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover || isSubmittedTo);

c) Update the flag (around line 673):

shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion;
shouldShowPaySuggestion ||= isEligibleForPaySuggestion;
shouldShowApproveSuggestion ||= isEligibleForApproveSuggestion;
shouldShowExportSuggestion ||= isEligibleForExportSuggestion;
shouldShowStatementsSuggestion ||= isEligibleForStatementsSuggestion;
shouldShowUnapprovedCashSuggestion ||= isEligibleForUnapprovedCashSuggestion;
shouldShowUnapprovedCardSuggestion ||= isEligibleForUnapprovedCardSuggestion;
shouldShowReconciliationSuggestion ||= isEligibleForReconciliationSuggestion;
shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion; // Add this line

d) Update early return check (around line 676):

return (
    shouldShowSubmitSuggestion &&
    shouldShowPaySuggestion &&
    shouldShowApproveSuggestion &&
    shouldShowExportSuggestion &&
    shouldShowStatementsSuggestion &&
    shouldShowUnapprovedCashSuggestion &&
    shouldShowUnapprovedCardSuggestion &&
    shouldShowReconciliationSuggestion &&
    shouldShowTopSpendersSuggestion // Add this line
);

e) Add to return object (around line 688):

return {
    [CONST.SEARCH.SEARCH_KEYS.EXPENSES]: true,
    [CONST.SEARCH.SEARCH_KEYS.REPORTS]: true,
    [CONST.SEARCH.SEARCH_KEYS.CHATS]: true,
    [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: shouldShowSubmitSuggestion,
    [CONST.SEARCH.SEARCH_KEYS.PAY]: shouldShowPaySuggestion,
    [CONST.SEARCH.SEARCH_KEYS.APPROVE]: shouldShowApproveSuggestion,
    [CONST.SEARCH.SEARCH_KEYS.EXPORT]: shouldShowExportSuggestion,
    [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.TOP_SPENDERS]: shouldShowTopSpendersSuggestion, // Add this line
};

4. Add Insights Section to Menu

File: App/src/libs/SearchUIUtils.ts

In the createTypeMenuSections() function, add the Insights section before the Explore section (around line 2866, after the Saved section and before the Explore section):

// 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);
    }
}

Why this placement?

  • Comes after "Saved" (user-created searches)
  • Comes before "Explore" (general browsing)
  • Follows the pattern of conditional section display (only shows when there are items to display)

5. Add Translation Keys

File: App/src/languages/en.ts

a) Add "insights" key (around line 631, in the common section):

explore: 'Explore',
insights: 'Insights', // Add this line
todo: 'To-do',

b) Add "topSpenders" key (around line 6591, in the search section):

statements: 'Statements',
unapprovedCash: 'Unapproved cash',
unapprovedCard: 'Unapproved card',
reconciliation: 'Reconciliation',
topSpenders: 'Top spenders', // Add this line
saveSearch: 'Save search',

What alternative solutions did you explore?

None

Demo

Image Image

abbasifaizan70 avatar Dec 22 '25 23:12 abbasifaizan70

@abbasifaizan70 's proposal is more comprehensive and covers everything that is required including the access checks, etc.

🎀 👀 🎀 C+ reviewed.

mananjadhav avatar Dec 23 '25 18:12 mananjadhav

Current assignee @puneetlath is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

melvin-bot[bot] avatar Dec 23 '25 18:12 melvin-bot[bot]

@mananjadhav Thanks for your review but all the required details were already mentioned in my proposal except some obvious implementation that each author definitely cover during PR. I respect your decision but would you mind to take another look since the selected proposal is just duplicate of mine.

samranahm avatar Dec 23 '25 20:12 samranahm

@mananjadhav @puneetlath The PR is ready for review. I used my OpenAI API keys for translation using the translation script.

abbasifaizan70 avatar Dec 25 '25 13:12 abbasifaizan70

Thanks @abbasifaizan70. I am away today I'll take a look tomorrow.

@samranahm Thanks for the comment. I feel the details related to access should be covered.

mananjadhav avatar Dec 25 '25 16:12 mananjadhav

@mananjadhav I got it, thanks for clarifying.

samranahm avatar Dec 25 '25 20:12 samranahm