From c4f6effd7527195b22c719805cfde60d2742ae39 Mon Sep 17 00:00:00 2001 From: btkcodedev Date: Mon, 9 Feb 2026 23:49:52 +0530 Subject: [PATCH 01/58] feat: Range UI filter --- src/CONST/index.ts | 1 + .../DatePicker/CalendarPicker/ArrowIcon.tsx | 8 +- .../DatePicker/CalendarPicker/index.tsx | 13 +- .../FilterComponents/DateFilterBase.tsx | 97 +++++++++-- .../FilterComponents/DatePresetFilterBase.tsx | 76 ++++++++- .../FilterComponents/RangeDatePicker.tsx | 97 +++++++++++ .../FilterDropdowns/DateSelectPopup.tsx | 158 ++++++++++++++++-- .../Search/FilterDropdowns/DropdownButton.tsx | 13 +- .../SearchPageHeader/SearchFiltersBar.tsx | 75 ++++++--- src/languages/en.ts | 4 + src/libs/DateUtils.ts | 8 +- src/pages/Search/AdvancedSearchFilters.tsx | 71 +++++--- .../SearchFiltersReportFieldPage/index.tsx | 18 +- 13 files changed, 538 insertions(+), 101 deletions(-) create mode 100644 src/components/Search/FilterComponents/RangeDatePicker.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d7c9c26401434..0cb817bc5bba2 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7352,6 +7352,7 @@ const CONST = { ON: 'On', AFTER: 'After', BEFORE: 'Before', + RANGE: 'Range', }, AMOUNT_MODIFIERS: { LESS_THAN: 'LessThan', diff --git a/src/components/DatePicker/CalendarPicker/ArrowIcon.tsx b/src/components/DatePicker/CalendarPicker/ArrowIcon.tsx index f79e180a69f1d..8dff1201858c1 100644 --- a/src/components/DatePicker/CalendarPicker/ArrowIcon.tsx +++ b/src/components/DatePicker/CalendarPicker/ArrowIcon.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import Icon from '@components/Icon'; @@ -14,15 +15,18 @@ type ArrowIconProps = { /** Specifies direction of icon */ direction?: ValueOf; + + /** Optional style overrides for the container */ + containerStyle?: StyleProp; }; -function ArrowIcon({disabled = false, direction = CONST.DIRECTION.RIGHT}: ArrowIconProps) { +function ArrowIcon({disabled = false, direction = CONST.DIRECTION.RIGHT, containerStyle}: ArrowIconProps) { const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); return ( - + void; + + /** Optional style override for the header container */ + headerContainerStyle?: StyleProp; }; function getInitialCurrentDateView(value: Date | string, minDate: Date, maxDate: Date) { @@ -56,6 +60,7 @@ function CalendarPicker({ onSelected, DayComponent = Day, selectableDates, + headerContainerStyle, }: CalendarPickerProps) { // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); @@ -175,11 +180,12 @@ function CalendarPicker({ const webOnlyMarginStyle = isSmallScreenWidth ? {} : styles.mh1; const calendarContainerStyle = isSmallScreenWidth ? [webOnlyMarginStyle, themeStyles.calendarBodyContainer] : [webOnlyMarginStyle, animatedStyle]; + const headerPaddingStyle = headerContainerStyle ?? themeStyles.ph5; return ( {currentYearView} - + (null); + const [shouldShowRangeError, setShouldShowRangeError] = useState(false); + const [trackedDateValues, setTrackedDateValues] = useState({ + [CONST.SEARCH.DATE_MODIFIERS.ON]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.AFTER]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.RANGE]: undefined, + }); + + const handleDateValuesChange = useCallback((dateValues: SearchDateValues) => { + setTrackedDateValues(dateValues); + }, []); const dateOnKey = dateKey.startsWith(CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX) ? (dateKey.replace(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX, CONST.SEARCH.REPORT_FIELD.ON_PREFIX) as ReportFieldDateKey) @@ -52,6 +67,7 @@ function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { [CONST.SEARCH.DATE_MODIFIERS.ON]: dateOnValue, [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: dateBeforeValue, [CONST.SEARCH.DATE_MODIFIERS.AFTER]: dateAfterValue, + [CONST.SEARCH.DATE_MODIFIERS.RANGE]: undefined, }), [dateAfterValue, dateBeforeValue, dateOnValue], ); @@ -61,6 +77,14 @@ function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { return getDatePresets(dateKey, hasFeed); }, [dateKey, searchAdvancedFiltersForm?.feed]); + // Auto-detect Range mode when both after and before values exist on initial load only + useEffect(() => { + if (!isSearchAdvancedFiltersFormLoading && dateAfterValue && dateBeforeValue && !dateOnValue) { + setSelectedDateModifier(CONST.SEARCH.DATE_MODIFIERS.RANGE); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSearchAdvancedFiltersFormLoading]); + const computedTitle = useMemo(() => { if (selectedDateModifier) { return translate(`common.${selectedDateModifier.toLowerCase() as SearchDateModifierLower}`); @@ -89,12 +113,33 @@ function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { } if (selectedDateModifier) { + // For Range, validate that both dates are selected + if (selectedDateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) { + const dateValues = searchDatePresetFilterBaseRef.current.getDateValues(); + const hasFrom = !!dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER]; + const hasTo = !!dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE]; + + if (!hasFrom || !hasTo) { + setShouldShowRangeError(true); + return; + } + } + searchDatePresetFilterBaseRef.current.setDateValueOfSelectedDateModifier(); setSelectedDateModifier(null); + setShouldShowRangeError(false); return; } const dateValues = searchDatePresetFilterBaseRef.current.getDateValues(); + const hasFrom = !!dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER]; + const hasTo = !!dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE]; + + if (hasFrom !== hasTo) { + setSelectedDateModifier(CONST.SEARCH.DATE_MODIFIERS.RANGE); + setShouldShowRangeError(true); + return; + } onSubmit({ [dateOnKey]: dateValues[CONST.SEARCH.DATE_MODIFIERS.ON] ?? null, @@ -113,12 +158,12 @@ function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { }; return ( - <> + - + + {selectedDateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE && (trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER] || trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE]) && ( + + {`${translate('common.range')}: `} + + {trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER] && trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE] + ? DateUtils.getFormattedDateRangeForSearch( + trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER] as string, + trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE] as string, + true, + ) + : format(parseISO((trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER] ?? trackedDateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE]) as string), 'MMM d, yyyy')} + + + )} + +