Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7426,6 +7426,7 @@ const CONST = {
TOP_SPENDERS: 'topSpenders',
TOP_CATEGORIES: 'topCategories',
TOP_MERCHANTS: 'topMerchants',
SPEND_OVER_TIME: 'spendOverTime',
},
GROUP_PREFIX: 'group_',
ANIMATION: {
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useSearchTypeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
const expensifyIcons = useMemoizedLazyExpensifyIcons([
'Basket',
'Bookmark',
'CalendarSolid',
'Checkmark',
'Pencil',
'Receipt',
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7266,6 +7266,7 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
selectAllMatchingItems: 'Alle passenden Einträge auswählen',
allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt',
},
spendOverTime: 'Ausgaben im Zeitverlauf',
},
genericErrorPage: {
title: 'Ups, da ist etwas schiefgelaufen!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7082,6 +7082,7 @@ const translations = {
savedSearchesMenuItemTitle: 'Saved',
topCategories: 'Top categories',
topMerchants: 'Top merchants',
spendOverTime: 'Spend over time',
groupedExpenses: 'grouped expenses',
bulkActions: {
approve: 'Approve',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6867,6 +6867,7 @@ ${amount} para ${merchant} - ${date}`,
savedSearchesMenuItemTitle: 'Guardadas',
topCategories: 'Categorías principales',
topMerchants: 'Principales comerciantes',
spendOverTime: 'Evolución de gastos',
searchName: 'Nombre de la búsqueda',
deleteSavedSearch: 'Eliminar búsqueda guardada',
deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7281,6 +7281,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
selectAllMatchingItems: 'Sélectionnez tous les éléments correspondants',
allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés',
},
spendOverTime: 'Dépenses dans le temps',
},
genericErrorPage: {
title: 'Oups, quelque chose s’est mal passé !',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7247,6 +7247,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
selectAllMatchingItems: 'Seleziona tutti gli elementi corrispondenti',
allMatchingItemsSelected: 'Tutti gli elementi corrispondenti sono stati selezionati',
},
spendOverTime: 'Spesa nel tempo',
},
genericErrorPage: {
title: 'Oops, qualcosa è andato storto!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7188,6 +7188,7 @@ ${reportName}
selectAllMatchingItems: '一致する項目をすべて選択',
allMatchingItemsSelected: '一致する項目をすべて選択済み',
},
spendOverTime: '時間経過による支出',
},
genericErrorPage: {
title: 'おっと、問題が発生しました!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7236,6 +7236,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
selectAllMatchingItems: 'Selecteer alle overeenkomende items',
allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd',
},
spendOverTime: 'Uitgaven in de tijd',
},
genericErrorPage: {
title: 'Oeps, er is iets misgegaan!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7218,6 +7218,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
selectAllMatchingItems: 'Zaznacz wszystkie pasujące elementy',
allMatchingItemsSelected: 'Zaznaczono wszystkie pasujące elementy',
},
spendOverTime: 'Wydatki w czasie',
},
genericErrorPage: {
title: 'Ups, coś poszło nie tak!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7218,6 +7218,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
selectAllMatchingItems: 'Selecione todos os itens correspondentes',
allMatchingItemsSelected: 'Todos os itens correspondentes selecionados',
},
spendOverTime: 'Gastos ao longo do tempo',
},
genericErrorPage: {
title: 'Opa, algo deu errado!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7083,6 +7083,7 @@ ${reportName}
selectAllMatchingItems: '选择所有匹配的项目',
allMatchingItemsSelected: '已选中所有匹配项',
},
spendOverTime: '随时间支出',
},
genericErrorPage: {
title: '哎呀,出错了!',
Expand Down
45 changes: 43 additions & 2 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,12 @@ type SearchTypeMenuItem = {
key: SearchKey;
translationPath: TranslationPaths;
type: SearchDataTypes;
icon?: IconAsset | Extract<ExpensifyIconName, 'Receipt' | 'ChatBubbles' | 'MoneyBag' | 'CreditCard' | 'MoneyHourglass' | 'CreditCardHourglass' | 'Bank' | 'User' | 'Folder' | 'Basket'>;
icon?:
| IconAsset
| Extract<
ExpensifyIconName,
'Receipt' | 'ChatBubbles' | 'MoneyBag' | 'CreditCard' | 'MoneyHourglass' | 'CreditCardHourglass' | 'Bank' | 'User' | 'Folder' | 'Basket' | 'CalendarSolid'
>;
searchQuery: string;
searchQueryJSON: SearchQueryJSON | undefined;
hash: number;
Expand Down Expand Up @@ -806,6 +811,33 @@ function getSuggestedSearches(
CONST.SEARCH.GROUP_BY.MERCHANT,
CONST.SEARCH.TOP_SEARCH_LIMIT,
),
[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: {
key: CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME,
translationPath: 'search.spendOverTime',
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
icon: 'CalendarSolid',
searchQuery: buildQueryStringFromFilterFormValues(
{
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
groupBy: CONST.SEARCH.GROUP_BY.MONTH,
dateOn: CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE,
view: CONST.SEARCH.VIEW.LINE,
},
{
sortBy: CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH,
sortOrder: CONST.SEARCH.SORT_ORDER.ASC,
},
),
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;
},
},
};
}

Expand All @@ -830,6 +862,7 @@ function getSuggestedSearchesVisibility(
let shouldShowTopSpendersSuggestion = false;
let shouldShowTopCategoriesSuggestion = false;
let shouldShowTopMerchantsSuggestion = false;
let shouldShowSpendOverTimeSuggestion = false;

const hasCardFeed = Object.values(cardFeedsByPolicy ?? {}).some((feeds) => feeds.length > 0);

Expand Down Expand Up @@ -868,6 +901,7 @@ function getSuggestedSearchesVisibility(
const isEligibleForTopSpendersSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover);
const isEligibleForTopCategoriesSuggestion = isPaidPolicy && policy.areCategoriesEnabled === true;
const isEligibleForTopMerchantsSuggestion = isPaidPolicy;
const isEligibleForSpendOverTimeSuggestion = isPaidPolicy && (isAdmin || isAuditor || isApprover);

shouldShowSubmitSuggestion ||= isEligibleForSubmitSuggestion;
shouldShowPaySuggestion ||= isEligibleForPaySuggestion;
Expand All @@ -880,6 +914,7 @@ function getSuggestedSearchesVisibility(
shouldShowTopSpendersSuggestion ||= isEligibleForTopSpendersSuggestion;
shouldShowTopCategoriesSuggestion ||= isEligibleForTopCategoriesSuggestion;
shouldShowTopMerchantsSuggestion ||= isEligibleForTopMerchantsSuggestion;
shouldShowSpendOverTimeSuggestion ||= isEligibleForSpendOverTimeSuggestion;

// We don't need to check the rest of the policies if we already determined that all suggestions should be displayed
return (
Expand Down Expand Up @@ -912,6 +947,7 @@ function getSuggestedSearchesVisibility(
[CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS]: shouldShowTopSpendersSuggestion,
[CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES]: shouldShowTopCategoriesSuggestion,
[CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS]: shouldShowTopMerchantsSuggestion,
[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME]: shouldShowSpendOverTimeSuggestion,
};
}

Expand Down Expand Up @@ -3683,7 +3719,12 @@ function createTypeMenuSections(
menuItems: [],
};

const insightsSearchKeys = [CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS, CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES, CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS];
const insightsSearchKeys = [
CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME,
CONST.SEARCH.SEARCH_KEYS.TOP_SPENDERS,
CONST.SEARCH.SEARCH_KEYS.TOP_CATEGORIES,
CONST.SEARCH.SEARCH_KEYS.TOP_MERCHANTS,
];

for (const key of insightsSearchKeys) {
if (!suggestedSearchesVisibility[key]) {
Expand Down
1 change: 1 addition & 0 deletions src/pages/Search/SearchTypeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
const expensifyIcons = useMemoizedLazyExpensifyIcons([
'Basket',
'Bookmark',
'CalendarSolid',
'Pencil',
'Receipt',
'ChatBubbles',
Expand Down
162 changes: 162 additions & 0 deletions tests/unit/Search/SearchUIUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5237,6 +5237,168 @@ describe('SearchUIUtils', () => {
const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined);
expect(response.topCategories).toBe(false);
});

test('Should show Spend Over Time for Admin role in paid policy', () => {
const policyKey = `policy_${policyID}`;

const policies: OnyxCollection<OnyxTypes.Policy> = {
[policyKey]: {
id: policyID,
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.ADMIN,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined);
expect(response.spendOverTime).toBe(true);
});

test('Should show Spend Over Time for Auditor role in paid policy', () => {
const policyKey = `policy_${policyID}`;

const policies: OnyxCollection<OnyxTypes.Policy> = {
[policyKey]: {
id: policyID,
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.AUDITOR,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility('auditor@policy.com', {}, policies, undefined);
expect(response.spendOverTime).toBe(true);
});

test('Should show Spend Over Time for Approver role in paid policy', () => {
const policyKey = `policy_${policyID}`;

const policies: OnyxCollection<OnyxTypes.Policy> = {
[policyKey]: {
id: policyID,
type: CONST.POLICY.TYPE.TEAM,
approver: approverEmail,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility(approverEmail, {}, policies, undefined);
expect(response.spendOverTime).toBe(true);
});

test('Should hide Spend Over Time for User role in paid policy', () => {
const policyKey = `policy_${policyID}`;

const policies: OnyxCollection<OnyxTypes.Policy> = {
[policyKey]: {
id: policyID,
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.USER,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined);
expect(response.spendOverTime).toBe(false);
});

test('Should hide Spend Over Time for free policies even with Admin role', () => {
const policyKey = `policy_${policyID}`;

const policies: OnyxCollection<OnyxTypes.Policy> = {
[policyKey]: {
id: policyID,
type: CONST.POLICY.TYPE.PERSONAL,
role: CONST.POLICY.ROLE.ADMIN,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined);
expect(response.spendOverTime).toBe(false);
});

test('Should show Spend Over Time if at least one policy has Admin/Auditor/Approver role', () => {
const policies: OnyxCollection<OnyxTypes.Policy> = {
policyOne: {
id: 'policyOne',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.USER,
} as OnyxTypes.Policy,
policyTwo: {
id: 'policyTwo',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.ADMIN,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility(adminEmail, {}, policies, undefined);
expect(response.spendOverTime).toBe(true);
});

test('Should hide Spend Over Time if all policies have User role', () => {
const policies: OnyxCollection<OnyxTypes.Policy> = {
policyOne: {
id: 'policyOne',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.USER,
} as OnyxTypes.Policy,
policyTwo: {
id: 'policyTwo',
type: CONST.POLICY.TYPE.TEAM,
role: CONST.POLICY.ROLE.USER,
} as OnyxTypes.Policy,
};

const response = SearchUIUtils.getSuggestedSearchesVisibility('user@policy.com', {}, policies, undefined);
expect(response.spendOverTime).toBe(false);
});

test('Should return Spend Over Time search with correct properties', () => {
const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined);
const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME];

expect(spendOverTimeSearch).toBeDefined();
expect(spendOverTimeSearch.key).toBe(CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME);
expect(spendOverTimeSearch.translationPath).toBe('search.spendOverTime');
expect(spendOverTimeSearch.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE);
expect(spendOverTimeSearch.icon).toBe('CalendarSolid');
});

test('Should return Spend Over Time search query with correct parameters', () => {
const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined);
const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME];
const searchQueryJSON = spendOverTimeSearch.searchQueryJSON;

expect(searchQueryJSON).toBeDefined();
expect(searchQueryJSON?.type).toBe(CONST.SEARCH.DATA_TYPES.EXPENSE);
expect(searchQueryJSON?.groupBy).toBe(CONST.SEARCH.GROUP_BY.MONTH);

// Check that date filter with year-to-date preset exists in flatFilters
const dateFilter = searchQueryJSON?.flatFilters?.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE);
expect(dateFilter).toBeDefined();
expect(dateFilter?.filters?.some((f) => f.value === CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE)).toBe(true);

expect(searchQueryJSON?.view).toBe(CONST.SEARCH.VIEW.LINE);
expect(searchQueryJSON?.sortBy).toBe(CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH);
expect(searchQueryJSON?.sortOrder).toBe(CONST.SEARCH.SORT_ORDER.ASC);
});

test('Should return Spend Over Time search with valid hash', () => {
const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined);
const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME];

expect(spendOverTimeSearch.hash).toBeGreaterThan(0);
expect(spendOverTimeSearch.similarSearchHash).toBeGreaterThan(0);
});

test('Should return Spend Over Time search query string with correct format', () => {
const suggestedSearches = SearchUIUtils.getSuggestedSearches(adminAccountID, undefined, undefined);
const spendOverTimeSearch = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SPEND_OVER_TIME];
const searchQuery = spendOverTimeSearch.searchQuery;

expect(searchQuery).toContain(`type:${CONST.SEARCH.DATA_TYPES.EXPENSE}`);
expect(searchQuery).toContain(`groupBy:${CONST.SEARCH.GROUP_BY.MONTH}`);
expect(searchQuery).toContain(`date:${CONST.SEARCH.DATE_PRESETS.YEAR_TO_DATE}`);
expect(searchQuery).toContain(`view:${CONST.SEARCH.VIEW.LINE}`);
expect(searchQuery).toContain(`sortBy:${CONST.SEARCH.TABLE_COLUMNS.GROUP_MONTH}`);
expect(searchQuery).toContain(`sortOrder:${CONST.SEARCH.SORT_ORDER.ASC}`);
});
});

describe('Test getColumnsToShow', () => {
Expand Down
Loading