From 7047fc6908228d0a2ef7f5829a8e1a586e0c939c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:04:36 +0200 Subject: [PATCH 01/12] Fix adjustTimeRangeToDateFilters --- src/libs/SearchUIUtils.ts | 64 ++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 89de5f1b4deb6..ac8e9fef4ba50 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ // TODO: Remove this disable once SearchUIUtils is refactored (see dedicated refactor issue) -import {endOfMonth, format, startOfMonth, startOfYear, subMonths} from 'date-fns'; +import {addDays, endOfMonth, format, parse, startOfMonth, startOfYear, subDays, subMonths} from 'date-fns'; import type {TextStyle, ViewStyle} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -2576,8 +2576,10 @@ function getYearSections(data: OnyxTypes.SearchResults['data'], queryJSON: Searc continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; - const yearStart = `${yearGroup.year}-01-01`; - const yearEnd = `${yearGroup.year}-12-31`; + const {start: yearStart, end: yearEnd} = adjustTimeRangeToDateFilters( + DateUtils.getYearDateRange(yearGroup.year), + queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), + ); if (queryJSON && yearGroup.year !== undefined) { const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ @@ -3884,52 +3886,41 @@ function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, d } const {start: timeRangeStart, end: timeRangeEnd} = timeRange; - const startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO); - const endLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO); const equalToFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO); let limitsStart: string | undefined; let limitsEnd: string | undefined; - - if (startLimitFilter?.value) { - const value = String(startLimitFilter.value); + // Date presets come with the equals operator, so we need to check if the value is a date preset + if (equalToFilter?.value) { + const value = String(equalToFilter.value); if (isDatePreset(value)) { const presetRange = getDateRangeForPreset(value); limitsStart = presetRange.start || undefined; + limitsEnd = presetRange.end || undefined; } else { limitsStart = value; + limitsEnd = value; } } - if (endLimitFilter?.value) { - const value = String(endLimitFilter.value); - if (isDatePreset(value)) { - const presetRange = getDateRangeForPreset(value); - limitsEnd = presetRange.end || undefined; - } else { - limitsEnd = value; - } + let startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO); + if (startLimitFilter?.value) { + limitsStart = String(startLimitFilter.value); + } + startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN); + if (startLimitFilter?.value) { + const date = parse(String(startLimitFilter.value), 'yyyy-MM-dd', new Date()); + limitsStart = format(addDays(date, 1), 'yyyy-MM-dd'); } - if (equalToFilter?.value) { - const value = String(equalToFilter.value); - if (isDatePreset(value)) { - const presetRange = getDateRangeForPreset(value); - if (presetRange.start && presetRange.end) { - if (!limitsStart) { - limitsStart = presetRange.start; - } - if (!limitsEnd) { - limitsEnd = presetRange.end; - } - if (limitsStart && presetRange.start > limitsStart) { - limitsStart = presetRange.start; - } - if (limitsEnd && presetRange.end < limitsEnd) { - limitsEnd = presetRange.end; - } - } - } + let endLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO); + if (endLimitFilter?.value) { + limitsEnd = String(endLimitFilter.value); + } + endLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN); + if (endLimitFilter?.value) { + const date = parse(String(endLimitFilter.value), 'yyyy-MM-dd', new Date()); + limitsEnd = format(subDays(date, 1), 'yyyy-MM-dd'); } let adjustedStart = timeRangeStart; @@ -3942,6 +3933,9 @@ function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, d adjustedEnd = limitsEnd; } + console.log('cristi - adjustedStart', adjustedStart); + console.log('cristi - adjustedEnd', adjustedEnd); + return { start: adjustedStart, end: adjustedEnd, From 4a20fb283ab9aea3945062af46812ce46cf5fe8f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:05:14 +0200 Subject: [PATCH 02/12] remove logging to console --- src/libs/SearchUIUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index ac8e9fef4ba50..2647f4fc4a76a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3933,9 +3933,6 @@ function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, d adjustedEnd = limitsEnd; } - console.log('cristi - adjustedStart', adjustedStart); - console.log('cristi - adjustedEnd', adjustedEnd); - return { start: adjustedStart, end: adjustedEnd, From d97d75e5c6b6cf81d9f9c5f7bdca7ad16f8fcbb4 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:05:42 +0200 Subject: [PATCH 03/12] Adjust year to date filters --- src/components/Search/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 7f5c6828d09c8..71f59e81057ef 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -954,8 +954,10 @@ function Search({ if (yearGroupItem.year === undefined) { return; } + // Extract the existing date filter to check for year-to-date or other date limits + const existingDateFilter = queryJSON.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + const {start: yearStart, end: yearEnd} = adjustTimeRangeToDateFilters(DateUtils.getYearDateRange(yearGroupItem.year), existingDateFilter); const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); - const {start: yearStart, end: yearEnd} = DateUtils.getYearDateRange(yearGroupItem.year); newFlatFilters.push({ key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ From 441f6dd3ad23542cad5df614d5e3c1521fb4e4b3 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:11:35 +0200 Subject: [PATCH 04/12] Add a test for year range adjustments to date filters --- tests/unit/Search/SearchUIUtilsTest.ts | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index a42a060668fee..4bcec0a21b773 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -33,6 +33,7 @@ import {setOptimisticDataForTransactionThreadPreview} from '@userActions/Search' import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {CardFeedForDisplay} from '@src/libs/CardFeedUtils'; +import DateUtils from '@src/libs/DateUtils'; import {getUserFriendlyValue} from '@src/libs/SearchQueryUtils'; import * as SearchUIUtils from '@src/libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -2952,6 +2953,64 @@ describe('SearchUIUtils', () => { expect(SearchUIUtils.isTransactionYearGroupListItemType(yearItem)).toBe(true); }); + it('should apply date filter when expanding year group', () => { + // Test that adjustTimeRangeToDateFilters correctly applies a date filter + // when expanding a year group (e.g., date >= 2025-12-01) + const yearDateRange = DateUtils.getYearDateRange(2025); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-12-01', + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, dateFilter); + + // The start date should be adjusted to 2025-12-01 (the filter limit) + // instead of 2025-01-01 (the year start) + expect(result.start).toBe('2025-12-01'); + // The end date should remain 2025-12-31 (the year end) + expect(result.end).toBe('2025-12-31'); + }); + + it('should apply date filter with both start and end limits when expanding year group', () => { + // Test that adjustTimeRangeToDateFilters correctly applies both start and end date filters + const yearDateRange = DateUtils.getYearDateRange(2025); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-06-15', + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, + value: '2025-09-30', + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, dateFilter); + + // The start date should be adjusted to 2025-06-15 (the filter limit) + expect(result.start).toBe('2025-06-15'); + // The end date should be adjusted to 2025-09-30 (the filter limit) + expect(result.end).toBe('2025-09-30'); + }); + + it('should return original time range when no date filter is provided', () => { + const yearDateRange = DateUtils.getYearDateRange(2025); + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, undefined); + + // Should return the original year date range unchanged + expect(result.start).toBe('2025-01-01'); + expect(result.end).toBe('2025-12-31'); + }); + it('should return getQuarterSections result when type is EXPENSE and groupBy is quarter', () => { const transactionQuarterGroupListItems: TransactionQuarterGroupListItemType[] = [ { From 0d6a33bb7186d844b43eb8597bd31a0330c4537c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:35:24 +0200 Subject: [PATCH 05/12] Adjust month group expanding to date filters --- src/components/Search/index.tsx | 4 +++- src/libs/SearchUIUtils.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 71f59e81057ef..63b856bfdb477 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -905,8 +905,10 @@ function Search({ } if (isTransactionMonthGroupListItemType(item)) { + // Extract the existing date filter to check for year-to-date or other date limits + const existingDateFilter = queryJSON.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + const {start: monthStart, end: monthEnd} = adjustTimeRangeToDateFilters(DateUtils.getMonthDateRange(item.year, item.month), existingDateFilter); const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); - const {start: monthStart, end: monthEnd} = DateUtils.getMonthDateRange(item.year, item.month); newFlatFilters.push({ key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 2647f4fc4a76a..1a3e981692465 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2480,9 +2480,11 @@ function getMonthSections(data: OnyxTypes.SearchResults['data'], queryJSON: Sear continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; + const {start: monthStart, end: monthEnd} = adjustTimeRangeToDateFilters( + DateUtils.getMonthDateRange(monthGroup.year, monthGroup.month), + queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), + ); if (queryJSON && monthGroup.year && monthGroup.month) { - // Create date range for the month (first day to last day of the month) - const {start: monthStart, end: monthEnd} = DateUtils.getMonthDateRange(monthGroup.year, monthGroup.month); const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, From 007941b1bd037d93de70dde819d6fc343c8c217a Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:35:40 +0200 Subject: [PATCH 06/12] Test month group adjustment to date filters --- tests/unit/Search/SearchUIUtilsTest.ts | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 4bcec0a21b773..0708f113d4617 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -3011,6 +3011,54 @@ describe('SearchUIUtils', () => { expect(result.end).toBe('2025-12-31'); }); + it('should apply date filter when expanding month group', () => { + // Test that adjustTimeRangeToDateFilters correctly applies a date filter + // when expanding a month group (e.g., date >= 2025-12-15) + const monthDateRange = DateUtils.getMonthDateRange(2025, 12); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-12-15', + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(monthDateRange, dateFilter); + + // The start date should be adjusted to 2025-12-15 (the filter limit) + // instead of 2025-12-01 (the month start) + expect(result.start).toBe('2025-12-15'); + // The end date should remain 2025-12-31 (the month end) + expect(result.end).toBe('2025-12-31'); + }); + + it('should apply date filter with both start and end limits when expanding month group', () => { + // Test that adjustTimeRangeToDateFilters correctly applies both start and end date filters + const monthDateRange = DateUtils.getMonthDateRange(2025, 6); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-06-10', + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, + value: '2025-06-20', + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(monthDateRange, dateFilter); + + // The start date should be adjusted to 2025-06-10 (the filter limit) + expect(result.start).toBe('2025-06-10'); + // The end date should be adjusted to 2025-06-20 (the filter limit) + expect(result.end).toBe('2025-06-20'); + }); + it('should return getQuarterSections result when type is EXPENSE and groupBy is quarter', () => { const transactionQuarterGroupListItems: TransactionQuarterGroupListItemType[] = [ { From 475e61a4f37ac2155e1f75fb395c5b03e61a2acf Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:38:36 +0200 Subject: [PATCH 07/12] Adjust quarter date range to date filters --- src/components/Search/index.tsx | 7 ++++++- src/libs/SearchUIUtils.ts | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 63b856bfdb477..a1660180a7dd1 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -982,8 +982,13 @@ function Search({ if (quarterGroupItem.year === undefined || quarterGroupItem.quarter === undefined) { return; } + // Extract the existing date filter to check for year-to-date or other date limits + const existingDateFilter = queryJSON.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); + const {start: quarterStart, end: quarterEnd} = adjustTimeRangeToDateFilters( + DateUtils.getQuarterDateRange(quarterGroupItem.year, quarterGroupItem.quarter), + existingDateFilter, + ); const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); - const {start: quarterStart, end: quarterEnd} = DateUtils.getQuarterDateRange(quarterGroupItem.year, quarterGroupItem.quarter); newFlatFilters.push({ key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, filters: [ diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 1a3e981692465..17fe131f920f4 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2622,7 +2622,10 @@ function getQuarterSections(data: OnyxTypes.SearchResults['data'], queryJSON: Se continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; - const {start: quarterStart, end: quarterEnd} = DateUtils.getQuarterDateRange(quarterGroup.year, quarterGroup.quarter); + const {start: quarterStart, end: quarterEnd} = adjustTimeRangeToDateFilters( + DateUtils.getQuarterDateRange(quarterGroup.year, quarterGroup.quarter), + queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), + ); if (queryJSON && quarterGroup.year !== undefined && quarterGroup.quarter !== undefined) { const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ From 10e3cdfaafe3312f5439351719d7cf57ed2bcf69 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 17:38:55 +0200 Subject: [PATCH 08/12] Test quarter date range adjustments to date filters --- tests/unit/Search/SearchUIUtilsTest.ts | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 0708f113d4617..89e6f73b963b2 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -3059,6 +3059,54 @@ describe('SearchUIUtils', () => { expect(result.end).toBe('2025-06-20'); }); + it('should apply date filter when expanding quarter group', () => { + // Test that adjustTimeRangeToDateFilters correctly applies a date filter + // when expanding a quarter group (e.g., date >= 2025-09-15) + const quarterDateRange = DateUtils.getQuarterDateRange(2025, 3); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-09-15', + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(quarterDateRange, dateFilter); + + // The start date should be adjusted to 2025-09-15 (the filter limit) + // instead of 2025-07-01 (the quarter start) + expect(result.start).toBe('2025-09-15'); + // The end date should remain 2025-09-30 (the quarter end) + expect(result.end).toBe('2025-09-30'); + }); + + it('should apply date filter with both start and end limits when expanding quarter group', () => { + // Test that adjustTimeRangeToDateFilters correctly applies both start and end date filters + const quarterDateRange = DateUtils.getQuarterDateRange(2025, 2); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-05-10', + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, + value: '2025-06-20', + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(quarterDateRange, dateFilter); + + // The start date should be adjusted to 2025-05-10 (the filter limit) + expect(result.start).toBe('2025-05-10'); + // The end date should be adjusted to 2025-06-20 (the filter limit) + expect(result.end).toBe('2025-06-20'); + }); + it('should return getQuarterSections result when type is EXPENSE and groupBy is quarter', () => { const transactionQuarterGroupListItems: TransactionQuarterGroupListItemType[] = [ { From c4fb227b9d9d31c045228bdabc41709a085c28bd Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 19:55:47 +0200 Subject: [PATCH 09/12] Performance improvement --- src/libs/SearchUIUtils.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 17fe131f920f4..dc34ef79e0ffb 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2472,6 +2472,7 @@ function getTagSections(data: OnyxTypes.SearchResults['data'], queryJSON: Search */ function getMonthSections(data: OnyxTypes.SearchResults['data'], queryJSON: SearchQueryJSON | undefined): [TransactionMonthGroupListItemType[], number] { const monthSections: Record = {}; + const dateFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); for (const key in data) { if (isGroupEntry(key)) { const monthGroup = data[key]; @@ -2480,10 +2481,7 @@ function getMonthSections(data: OnyxTypes.SearchResults['data'], queryJSON: Sear continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; - const {start: monthStart, end: monthEnd} = adjustTimeRangeToDateFilters( - DateUtils.getMonthDateRange(monthGroup.year, monthGroup.month), - queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), - ); + const {start: monthStart, end: monthEnd} = adjustTimeRangeToDateFilters(DateUtils.getMonthDateRange(monthGroup.year, monthGroup.month), dateFilters); if (queryJSON && monthGroup.year && monthGroup.month) { const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ @@ -2523,6 +2521,7 @@ function getMonthSections(data: OnyxTypes.SearchResults['data'], queryJSON: Sear */ function getWeekSections(data: OnyxTypes.SearchResults['data'], queryJSON: SearchQueryJSON | undefined): [TransactionWeekGroupListItemType[], number] { const weekSections: Record = {}; + const dateFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); for (const key in data) { if (isGroupEntry(key)) { const weekGroup = data[key]; @@ -2531,10 +2530,7 @@ function getWeekSections(data: OnyxTypes.SearchResults['data'], queryJSON: Searc continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; - const {start: weekStart, end: weekEnd} = adjustTimeRangeToDateFilters( - DateUtils.getWeekDateRange(weekGroup.week), - queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), - ); + const {start: weekStart, end: weekEnd} = adjustTimeRangeToDateFilters(DateUtils.getWeekDateRange(weekGroup.week), dateFilters); if (queryJSON && weekGroup.week) { const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ @@ -2570,6 +2566,7 @@ function getWeekSections(data: OnyxTypes.SearchResults['data'], queryJSON: Searc */ function getYearSections(data: OnyxTypes.SearchResults['data'], queryJSON: SearchQueryJSON | undefined): [TransactionYearGroupListItemType[], number] { const yearSections: Record = {}; + const dateFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); for (const key in data) { if (isGroupEntry(key)) { const yearGroup = data[key]; @@ -2578,10 +2575,7 @@ function getYearSections(data: OnyxTypes.SearchResults['data'], queryJSON: Searc continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; - const {start: yearStart, end: yearEnd} = adjustTimeRangeToDateFilters( - DateUtils.getYearDateRange(yearGroup.year), - queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), - ); + const {start: yearStart, end: yearEnd} = adjustTimeRangeToDateFilters(DateUtils.getYearDateRange(yearGroup.year), dateFilters); if (queryJSON && yearGroup.year !== undefined) { const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ @@ -2614,6 +2608,7 @@ function getYearSections(data: OnyxTypes.SearchResults['data'], queryJSON: Searc function getQuarterSections(data: OnyxTypes.SearchResults['data'], queryJSON: SearchQueryJSON | undefined): [TransactionQuarterGroupListItemType[], number] { const quarterSections: Record = {}; + const dateFilters = queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); for (const key in data) { if (isGroupEntry(key)) { const quarterGroup = data[key]; @@ -2622,10 +2617,7 @@ function getQuarterSections(data: OnyxTypes.SearchResults['data'], queryJSON: Se continue; } let transactionsQueryJSON: SearchQueryJSON | undefined; - const {start: quarterStart, end: quarterEnd} = adjustTimeRangeToDateFilters( - DateUtils.getQuarterDateRange(quarterGroup.year, quarterGroup.quarter), - queryJSON?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE), - ); + const {start: quarterStart, end: quarterEnd} = adjustTimeRangeToDateFilters(DateUtils.getQuarterDateRange(quarterGroup.year, quarterGroup.quarter), dateFilters); if (queryJSON && quarterGroup.year !== undefined && quarterGroup.quarter !== undefined) { const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE); newFlatFilters.push({ From fe9b0a9539d6fb1bb7c3c880b4ce12930fad26d5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 20:02:50 +0200 Subject: [PATCH 10/12] Update adjustTimeRangeToDateFilters to intersect if multiple date filters --- src/libs/SearchUIUtils.ts | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index dc34ef79e0ffb..de64d4c2a4e16 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3902,22 +3902,44 @@ function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, d let startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO); if (startLimitFilter?.value) { - limitsStart = String(startLimitFilter.value); + const constraintStart = String(startLimitFilter.value); + if (limitsStart) { + limitsStart = limitsStart > constraintStart ? limitsStart : constraintStart; + } else { + limitsStart = constraintStart; + } } + startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN); if (startLimitFilter?.value) { const date = parse(String(startLimitFilter.value), 'yyyy-MM-dd', new Date()); - limitsStart = format(addDays(date, 1), 'yyyy-MM-dd'); + const constraintStart = format(addDays(date, 1), 'yyyy-MM-dd'); + if (limitsStart) { + limitsStart = limitsStart > constraintStart ? limitsStart : constraintStart; + } else { + limitsStart = constraintStart; + } } let endLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO); if (endLimitFilter?.value) { - limitsEnd = String(endLimitFilter.value); + const constraintEnd = String(endLimitFilter.value); + if (limitsEnd) { + limitsEnd = limitsEnd < constraintEnd ? limitsEnd : constraintEnd; + } else { + limitsEnd = constraintEnd; + } } + endLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN); if (endLimitFilter?.value) { const date = parse(String(endLimitFilter.value), 'yyyy-MM-dd', new Date()); - limitsEnd = format(subDays(date, 1), 'yyyy-MM-dd'); + const constraintEnd = format(subDays(date, 1), 'yyyy-MM-dd'); + if (limitsEnd) { + limitsEnd = limitsEnd < constraintEnd ? limitsEnd : constraintEnd; + } else { + limitsEnd = constraintEnd; + } } let adjustedStart = timeRangeStart; From 3f276fb6fb6e38b84dc71cd801fb52bb1b653fa8 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 20:03:09 +0200 Subject: [PATCH 11/12] Test that multiple date filters take the time range intersection --- tests/unit/Search/SearchUIUtilsTest.ts | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 89e6f73b963b2..bec8c06bec976 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -3107,6 +3107,60 @@ describe('SearchUIUtils', () => { expect(result.end).toBe('2025-06-20'); }); + it('should intersect date preset with additional constraints instead of overwriting', () => { + // Test that when combining a date preset (EQUAL_TO) with additional constraints, + // we intersect the ranges (take max for start, min for end) rather than overwriting + const yearDateRange = DateUtils.getYearDateRange(2026); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN_OR_EQUAL_TO, + value: '2025-04-01', // Earlier than preset start + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, dateFilter); + + // Should intersect: max(preset start, constraint start) = max(2026-01-01, 2025-04-01) = 2026-01-01 + // The preset start should be preserved, not overwritten by the earlier constraint + expect(result.start).toBe('2026-01-01'); + // End should remain the preset end (2026-01-31) + expect(result.end).toBe('2026-01-31'); + }); + + it('should intersect date preset end limit with additional constraints', () => { + // Test that when combining a date preset with an end constraint, + // we take the minimum (earliest) end date to intersect ranges + const yearDateRange = DateUtils.getYearDateRange(2026); + const dateFilter = { + key: CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, + filters: [ + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, + value: CONST.SEARCH.DATE_PRESETS.LAST_MONTH, // e.g., January 2026: 2026-01-01 to 2026-01-31 + }, + { + operator: CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO, + value: '2026-01-15', // Earlier than preset end + }, + ], + }; + + const result = SearchUIUtils.adjustTimeRangeToDateFilters(yearDateRange, dateFilter); + + // Start should remain the preset start (2026-01-01) + expect(result.start).toBe('2026-01-01'); + // Should intersect: min(preset end, constraint end) = min(2026-01-31, 2026-01-15) = 2026-01-15 + // The constraint end should be used (earlier date) + expect(result.end).toBe('2026-01-15'); + }); + it('should return getQuarterSections result when type is EXPENSE and groupBy is quarter', () => { const transactionQuarterGroupListItems: TransactionQuarterGroupListItemType[] = [ { From 5183a60a28ab2e3a2c11d0a41ec13929a5ce924f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 4 Feb 2026 20:05:06 +0200 Subject: [PATCH 12/12] Add function doc. --- src/libs/SearchUIUtils.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index de64d4c2a4e16..9d1f256d85f1b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3877,6 +3877,15 @@ function isDatePreset(value: string | number | undefined): value is SearchDatePr return Object.values(CONST.SEARCH.DATE_PRESETS).some((datePreset) => datePreset === value); } +/** + * Adjusts a time range based on date filters, intersecting preset ranges with additional constraints. + * When combining date presets (e.g., `date:on=last_month`) with constraints (e.g., `date:>=2025-04-01`), + * takes the intersection to narrow the range rather than overwriting it. + * + * @param timeRange - The base time range to adjust (e.g., a year/month/quarter range) + * @param dateFilter - Optional date filter containing preset and/or constraint filters + * @returns Adjusted time range that respects all date filters (intersected, not overwritten) + */ function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, dateFilter: QueryFilters[0] | undefined): {start: string; end: string} { if (!dateFilter?.filters) { return timeRange; @@ -3909,7 +3918,7 @@ function adjustTimeRangeToDateFilters(timeRange: {start: string; end: string}, d limitsStart = constraintStart; } } - + startLimitFilter = dateFilter.filters.find((filter) => filter.operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN); if (startLimitFilter?.value) { const date = parse(String(startLimitFilter.value), 'yyyy-MM-dd', new Date());