diff --git a/src/CONST.ts b/src/CONST.ts index a9160e606bd9..cf390d773aeb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1188,6 +1188,7 @@ const CONST = { VISIBLE_PASSWORD: 'visible-password', ASCII_CAPABLE: 'ascii-capable', NUMBER_PAD: 'number-pad', + DECIMAL_PAD: 'decimal-pad', }, INPUT_MODE: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index de495568daa3..dbb271a280c2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -44,6 +44,7 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_MERCHANT: 'search/filters/merchant', SEARCH_ADVANCED_FILTERS_DESCRIPTION: 'search/filters/description', SEARCH_ADVANCED_FILTERS_REPORT_ID: 'search/filters/reportID', + SEARCH_ADVANCED_FILTERS_AMOUNT: 'search/filters/amount', SEARCH_ADVANCED_FILTERS_CATEGORY: 'search/filters/category', SEARCH_ADVANCED_FILTERS_KEYWORD: 'search/filters/keyword', SEARCH_ADVANCED_FILTERS_CARD: 'search/filters/card', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 30adc5f89d08..cc4360d7695d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -37,6 +37,7 @@ const SCREENS = { ADVANCED_FILTERS_DESCRIPTION_RHP: 'Search_Advanced_Filters_Description_RHP', ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP', ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP', + ADVANCED_FILTERS_AMOUNT_RHP: 'Search_Advanced_Filters_Amount_RHP', ADVANCED_FILTERS_CATEGORY_RHP: 'Search_Advanced_Filters_Category_RHP', ADVANCED_FILTERS_KEYWORD_RHP: 'Search_Advanced_Filters_Keyword_RHP', ADVANCED_FILTERS_CARD_RHP: 'Search_Advanced_Filters_Card_RHP', diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx new file mode 100644 index 000000000000..78b7c84ecb54 --- /dev/null +++ b/src/components/AmountWithoutCurrencyForm.tsx @@ -0,0 +1,66 @@ +import React, {useCallback, useMemo} from 'react'; +import type {ForwardedRef} from 'react'; +import useLocalize from '@hooks/useLocalize'; +import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils'; +import CONST from '@src/CONST'; +import TextInput from './TextInput'; +import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; + +type AmountFormProps = { + /** Amount supplied by the FormProvider */ + value?: string; + + /** Callback to update the amount in the FormProvider */ + onInputChange?: (value: string) => void; +} & Partial; + +function AmountWithoutCurrencyForm( + {value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + ref: ForwardedRef, +) { + const {toLocaleDigit} = useLocalize(); + + const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]); + + /** + * Sets the selection and the amount accordingly to the value passed to the input + * @param newAmount - Changed amount from user input + */ + const setNewAmount = useCallback( + (newAmount: string) => { + // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value + // More info: https://github.com/Expensify/App/issues/16974 + const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); + const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); + const withLeadingZero = addLeadingZero(replacedCommasAmount); + if (!validateAmount(withLeadingZero, 2)) { + return; + } + onInputChange?.(withLeadingZero); + }, + [onInputChange], + ); + + const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit); + + return ( + + ); +} + +AmountWithoutCurrencyForm.displayName = 'AmountWithoutCurrencyForm'; + +export default React.forwardRef(AmountWithoutCurrencyForm); diff --git a/src/languages/en.ts b/src/languages/en.ts index 1684ed3057da..5a18476332fc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3710,6 +3710,11 @@ export default { keyword: 'Keyword', hasKeywords: 'Has keywords', currency: 'Currency', + amount: { + lessThan: (amount?: string) => `Less than ${amount ?? ''}`, + greaterThan: (amount?: string) => `Greater than ${amount ?? ''}`, + between: (greaterThan: string, lessThan: string) => `Between ${greaterThan} and ${lessThan}`, + }, }, expenseType: 'Expense type', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index b4d071ba4a08..01199c71e057 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3764,6 +3764,11 @@ export default { keyword: 'Palabra clave', hasKeywords: 'Tiene palabras clave', currency: 'Divisa', + amount: { + lessThan: (amount?: string) => `Menos de ${amount ?? ''}`, + greaterThan: (amount?: string) => `Más que ${amount ?? ''}`, + between: (greaterThan: string, lessThan: string) => `Entre ${greaterThan} y ${lessThan}`, + }, }, expenseType: 'Tipo de gasto', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index f105de52a5ac..4694a2e73d5c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -519,6 +519,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchFiltersDescriptionPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchFiltersMerchantPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require('../../../../pages/Search/SearchFiltersReportIDPage').default, + [SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: () => require('../../../../pages/Search/SearchFiltersAmountPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require('../../../../pages/Search/SearchFiltersCategoryPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: () => require('../../../../pages/Search/SearchFiltersKeywordPage').default, [SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require('../../../../pages/Search/SearchFiltersCardPage').default, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 2ce651eff9c9..10282df4a24d 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -47,6 +47,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP, + SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 236b56882dde..6b4d7eca95c1 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1029,6 +1029,7 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_MERCHANT, [SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_DESCRIPTION, [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_REPORT_ID, + [SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_AMOUNT, [SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CATEGORY, [SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_KEYWORD, [SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CARD, diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index bb49697a8345..ce2429c5653b 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -441,6 +441,28 @@ function buildDateFilterQuery(filterValues: Partial) return dateFilter; } +/** + * @private + * returns Date filter query string part, which needs special logic + */ +function buildAmountFilterQuery(filterValues: Partial) { + const lessThan = filterValues[FILTER_KEYS.LESS_THAN]; + const greaterThan = filterValues[FILTER_KEYS.GREATER_THAN]; + + let amountFilter = ''; + if (greaterThan) { + amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}>${greaterThan}`; + } + if (lessThan && greaterThan) { + amountFilter += ' '; + } + if (lessThan) { + amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}<${lessThan}`; + } + + return amountFilter; +} + function sanitizeString(str: string) { if (str.includes(' ') || str.includes(',')) { return `"${str}"`; @@ -464,42 +486,43 @@ function getExpenseTypeTranslationKey(expenseType: ValueOf) { - const filtersString = Object.entries(filterValues) - .map(([filterKey, filterValue]) => { - if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID || filterKey === FILTER_KEYS.KEYWORD) && filterValue) { - const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); - if (keyInCorrectForm) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`; - } + const filtersString = Object.entries(filterValues).map(([filterKey, filterValue]) => { + if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID || filterKey === FILTER_KEYS.KEYWORD) && filterValue) { + const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); + if (keyInCorrectForm) { + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`; } + } - if ( - (filterKey === FILTER_KEYS.CATEGORY || - filterKey === FILTER_KEYS.CARD_ID || - filterKey === FILTER_KEYS.TAX_RATE || - filterKey === FILTER_KEYS.EXPENSE_TYPE || - filterKey === FILTER_KEYS.TAG || - filterKey === FILTER_KEYS.CURRENCY || - filterKey === FILTER_KEYS.FROM || - filterKey === FILTER_KEYS.TO) && - Array.isArray(filterValue) && - filterValue.length > 0 - ) { - const filterValueArray = filterValues[filterKey] ?? []; - const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); - if (keyInCorrectForm) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`; - } + if ( + (filterKey === FILTER_KEYS.CATEGORY || + filterKey === FILTER_KEYS.CARD_ID || + filterKey === FILTER_KEYS.TAX_RATE || + filterKey === FILTER_KEYS.EXPENSE_TYPE || + filterKey === FILTER_KEYS.TAG || + filterKey === FILTER_KEYS.CURRENCY || + filterKey === FILTER_KEYS.FROM || + filterKey === FILTER_KEYS.TO) && + Array.isArray(filterValue) && + filterValue.length > 0 + ) { + const filterValueArray = filterValues[filterKey] ?? []; + const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); + if (keyInCorrectForm) { + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`; } + } - return undefined; - }) - .filter(Boolean) - .join(' '); + return undefined; + }); const dateFilter = buildDateFilterQuery(filterValues); + filtersString.push(dateFilter); + + const amountFilter = buildAmountFilterQuery(filterValues); + filtersString.push(amountFilter); - return dateFilter ? `${filtersString} ${dateFilter}` : filtersString; + return filtersString.filter(Boolean).join(' '); } function getFilters(queryJSON: SearchQueryJSON) { diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx index 644ae64466f7..5143a2d70008 100644 --- a/src/pages/Search/AdvancedSearchFilters.tsx +++ b/src/pages/Search/AdvancedSearchFilters.tsx @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; +import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import {getAllTaxRates} from '@libs/PolicyUtils'; @@ -65,6 +66,19 @@ function getFilterDisplayTitle(filters: Partial, fiel return dateValue; } + if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT) { + const {lessThan, greaterThan} = filters; + if (lessThan && greaterThan) { + return translate('search.filters.amount.between', convertToDisplayStringWithoutCurrency(Number(greaterThan)), convertToDisplayStringWithoutCurrency(Number(lessThan))); + } + if (lessThan) { + return translate('search.filters.amount.lessThan', convertToDisplayStringWithoutCurrency(Number(lessThan))); + } + if (greaterThan) { + return translate('search.filters.amount.greaterThan', convertToDisplayStringWithoutCurrency(Number(greaterThan))); + } + } + if ( (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY || fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG) && filters[fieldName] @@ -149,6 +163,11 @@ function AdvancedSearchFilters() { description: 'common.reportID' as const, route: ROUTES.SEARCH_ADVANCED_FILTERS_REPORT_ID, }, + { + title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT, translate), + description: 'common.total' as const, + route: ROUTES.SEARCH_ADVANCED_FILTERS_AMOUNT, + }, { title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY, translate), description: 'common.category' as const, diff --git a/src/pages/Search/SearchFiltersAmountPage.tsx b/src/pages/Search/SearchFiltersAmountPage.tsx new file mode 100644 index 000000000000..d7533f9ac20e --- /dev/null +++ b/src/pages/Search/SearchFiltersAmountPage.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import AmountWithoutCurrencyForm from '@components/AmountWithoutCurrencyForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {updateAdvancedFilters} from '@libs/actions/Search'; +import {convertToBackendAmount, convertToFrontendAmountAsString} from '@libs/CurrencyUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm'; + +function SearchFiltersAmountPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const greaterThan = searchAdvancedFiltersForm?.[INPUT_IDS.GREATER_THAN]; + const greaterThanFormattedAmount = greaterThan ? convertToFrontendAmountAsString(Number(greaterThan)) : undefined; + const lessThan = searchAdvancedFiltersForm?.[INPUT_IDS.LESS_THAN]; + const lessThanFormattedAmount = lessThan ? convertToFrontendAmountAsString(Number(lessThan)) : undefined; + const {inputCallbackRef} = useAutoFocusInput(); + + const updateAmountFilter = (values: FormOnyxValues) => { + const greater = values[INPUT_IDS.GREATER_THAN]; + const greaterThanBackendAmount = greater ? convertToBackendAmount(Number(greater)) : ''; + const less = values[INPUT_IDS.LESS_THAN]; + const lessThanBackendAmount = less ? convertToBackendAmount(Number(less)) : ''; + updateAdvancedFilters({greaterThan: greaterThanBackendAmount?.toString(), lessThan: lessThanBackendAmount?.toString()}); + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }; + + return ( + + { + Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); + }} + /> + + + + + + + + + + ); +} + +SearchFiltersAmountPage.displayName = 'SearchFiltersAmountPage'; + +export default SearchFiltersAmountPage; diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index f2643c0c987d..6541072cae81 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -11,6 +11,8 @@ const FILTER_KEYS = { MERCHANT: 'merchant', DESCRIPTION: 'description', REPORT_ID: 'reportID', + LESS_THAN: 'lessThan', + GREATER_THAN: 'greaterThan', TAX_RATE: 'taxRate', EXPENSE_TYPE: 'expenseType', TAG: 'tag', @@ -33,6 +35,8 @@ type SearchAdvancedFiltersForm = Form< [FILTER_KEYS.MERCHANT]: string; [FILTER_KEYS.DESCRIPTION]: string; [FILTER_KEYS.REPORT_ID]: string; + [FILTER_KEYS.LESS_THAN]: string; + [FILTER_KEYS.GREATER_THAN]: string; [FILTER_KEYS.KEYWORD]: string; [FILTER_KEYS.TAX_RATE]: string[]; [FILTER_KEYS.EXPENSE_TYPE]: string[];