diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 8d6d2aff5ef3..94a33d39969b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3321,6 +3321,7 @@ const CONST = { IMPORT_SPREADSHEET: 'importSpreadsheet', DOWNLOAD_CSV: 'downloadCSV', SETTINGS: 'settings', + EXPORT: 'export', }, MEMBERS_BULK_ACTION_TYPES: { REMOVE: 'remove', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6971f8ec98f6..7d44c11eb4a5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -344,6 +344,9 @@ const ONYXKEYS = { /** Stores information about the user's saved statements */ WALLET_STATEMENT: 'walletStatement', + /** Stores information about the user's travel invoice statements */ + TRAVEL_INVOICE_STATEMENT: 'travelInvoiceStatement', + /** Stores information about the user's purchases */ PURCHASE_LIST: 'purchaseList', @@ -1322,6 +1325,7 @@ type OnyxValuesMapping = { [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; [ONYXKEYS.CARD_LIST]: OnyxTypes.CardList; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; + [ONYXKEYS.TRAVEL_INVOICE_STATEMENT]: OnyxTypes.TravelInvoiceStatement; [ONYXKEYS.PURCHASE_LIST]: OnyxTypes.PurchaseList; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.SHARE_BANK_ACCOUNT]: OnyxTypes.ShareBankAccount; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1927333078e2..61326be09d8a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2637,6 +2637,15 @@ const ROUTES = { route: 'workspaces/:policyID/travel/settings/account', getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/account` as const, }, + WORKSPACE_TRAVEL_EXPORT: { + route: 'workspaces/:policyID/travel/export', + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_TRAVEL_EXPORT route'); + } + return `workspaces/${policyID}/travel/export` as const; + }, + }, WORKSPACE_TRAVEL_SETTINGS_FREQUENCY: { route: 'workspaces/:policyID/travel/settings/frequency', getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/frequency` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b88fa5ad5490..06de07836b25 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -758,6 +758,7 @@ const SCREENS = { TRAVEL: 'Travel', TRAVEL_SETTINGS_ACCOUNT: 'Workspace_Travel_Settings_Account', TRAVEL_SETTINGS_FREQUENCY: 'Workspace_Travel_Settings_Frequency', + TRAVEL_EXPORT: 'Workspace_Travel_Invoicing_Export', TRAVEL_MISSING_PERSONAL_DETAILS: 'Travel_Missing_Personal_Details', CREATE_DISTANCE_RATE: 'Create_Distance_Rate', CREATE_DISTANCE_RATE_UPGRADE: 'Create_Distance_Rate_Upgrade', diff --git a/src/components/Search/FilterComponents/DateFilterBase.tsx b/src/components/Search/FilterComponents/DateFilterBase.tsx index e4ba0d19baec..4993720b22f0 100644 --- a/src/components/Search/FilterComponents/DateFilterBase.tsx +++ b/src/components/Search/FilterComponents/DateFilterBase.tsx @@ -1,75 +1,110 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useImperativeHandle, useRef, useState} from 'react'; import Button from '@components/Button'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScrollView from '@components/ScrollView'; -import type {ReportFieldDateKey, SearchDateFilterKeys} from '@components/Search/types'; +import type {SearchDatePreset} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getDatePresets} from '@libs/SearchUIUtils'; import type {SearchDateModifier, SearchDateModifierLower} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import type {SearchDatePresetFilterBaseHandle} from './DatePresetFilterBase'; +import type {SearchDatePresetFilterBaseHandle, SearchDateValues} from './DatePresetFilterBase'; import DatePresetFilterBase from './DatePresetFilterBase'; +type DateFilterBaseHandle = { + /** Gets the current date values from the filter */ + getDateValues: () => SearchDateValues; + /** Handles back navigation — closes date modifier if open, otherwise calls onBackButtonPress */ + goBack: () => void; +}; + type DateFilterBaseProps = { - title: string; - dateKey: SearchDateFilterKeys; - back: () => void; - onSubmit: (values: Record) => void; + /** The title displayed in the header. Required when shouldShowHeader is true. */ + title?: string; + /** Default date values to initialize the filter with */ + defaultDateValues: SearchDateValues; + /** The date presets to display (e.g. "This month", "Last month") */ + presets: SearchDatePreset[]; + /** Whether the search advanced filters form Onyx data is loading or not */ + isSearchAdvancedFiltersFormLoading?: boolean; + /** Callback when the back button is pressed. Required when shouldShowHeader is true. */ + onBackButtonPress?: () => void; + /** Callback when the filter is submitted with the selected date values */ + onSubmit: (values: SearchDateValues) => void; + /** Callback when a date value changes (e.g. preset click or calendar save) */ + onDateValuesChange?: (values: SearchDateValues) => void; + /** Callback when the date modifier screen is opened or closed (on/after/before) */ + onDateModifierChange?: (isOpen: boolean) => void; + /** If true, the Reset/Save buttons are only shown when a date modifier (On/After/Before) is selected. Defaults to false (always show buttons). */ + shouldShowButtonsOnlyWithDateModifier?: boolean; + /** Whether to render the built-in HeaderWithBackButton. Defaults to true. Set to false when the parent provides its own header. */ + shouldShowHeader?: boolean; + /** The ref handle */ + ref?: React.Ref; }; -function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { +// Component uses ref as a prop, which is supported in modern React +function DateFilterBase({ + title, + defaultDateValues, + presets, + isSearchAdvancedFiltersFormLoading, + onBackButtonPress, + onSubmit, + onDateValuesChange, + onDateModifierChange, + shouldShowButtonsOnlyWithDateModifier = false, + shouldShowHeader = true, + ref, +}: DateFilterBaseProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const searchDatePresetFilterBaseRef = useRef(null); - const [searchAdvancedFiltersForm, searchAdvancedFiltersFormMetadata] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); - const isSearchAdvancedFiltersFormLoading = isLoadingOnyxValue(searchAdvancedFiltersFormMetadata); const [selectedDateModifier, setSelectedDateModifier] = useState(null); - 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) - : (`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.ON}` as const); - - const dateBeforeKey = dateKey.startsWith(CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX) - ? (dateKey.replace(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX, CONST.SEARCH.REPORT_FIELD.BEFORE_PREFIX) as ReportFieldDateKey) - : (`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.BEFORE}` as const); - - const dateAfterKey = dateKey.startsWith(CONST.SEARCH.REPORT_FIELD.GLOBAL_PREFIX) - ? (dateKey.replace(CONST.SEARCH.REPORT_FIELD.DEFAULT_PREFIX, CONST.SEARCH.REPORT_FIELD.AFTER_PREFIX) as ReportFieldDateKey) - : (`${dateKey}${CONST.SEARCH.DATE_MODIFIERS.AFTER}` as const); - - const dateOnValue = searchAdvancedFiltersForm?.[dateOnKey]; - const dateBeforeValue = searchAdvancedFiltersForm?.[dateBeforeKey]; - const dateAfterValue = searchAdvancedFiltersForm?.[dateAfterKey]; - - const defaultDateValues = useMemo( - () => ({ - [CONST.SEARCH.DATE_MODIFIERS.ON]: dateOnValue, - [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: dateBeforeValue, - [CONST.SEARCH.DATE_MODIFIERS.AFTER]: dateAfterValue, - }), - [dateAfterValue, dateBeforeValue, dateOnValue], - ); + const handleSelectDateModifier = (dateModifier: SearchDateModifier | null) => { + setSelectedDateModifier(dateModifier); + onDateModifierChange?.(!!dateModifier); + if (onDateValuesChange) { + const values = searchDatePresetFilterBaseRef.current?.getDateValues() ?? { + [CONST.SEARCH.DATE_MODIFIERS.ON]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.AFTER]: undefined, + }; + onDateValuesChange(values); + } + }; + + const goBack = () => { + if (selectedDateModifier) { + setSelectedDateModifier(null); + onDateModifierChange?.(false); + return; + } - const presets = useMemo(() => { - const hasFeed = !!searchAdvancedFiltersForm?.feed?.length; - return getDatePresets(dateKey, hasFeed); - }, [dateKey, searchAdvancedFiltersForm?.feed]); + onBackButtonPress?.(); + }; - const computedTitle = useMemo(() => { + useImperativeHandle(ref, () => ({ + getDateValues: () => + searchDatePresetFilterBaseRef.current?.getDateValues() ?? { + [CONST.SEARCH.DATE_MODIFIERS.ON]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: undefined, + [CONST.SEARCH.DATE_MODIFIERS.AFTER]: undefined, + }, + goBack, + })); + + function getComputedTitle() { if (selectedDateModifier) { return translate(`common.${selectedDateModifier.toLowerCase() as SearchDateModifierLower}`); } return title; - }, [selectedDateModifier, title, translate]); + } - const reset = useCallback(() => { + const reset = () => { if (!searchDatePresetFilterBaseRef.current) { return; } @@ -77,13 +112,14 @@ function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { if (selectedDateModifier) { searchDatePresetFilterBaseRef.current.clearDateValueOfSelectedDateModifier(); setSelectedDateModifier(null); + onDateModifierChange?.(false); return; } searchDatePresetFilterBaseRef.current.clearDateValues(); - }, [selectedDateModifier]); + }; - const save = useCallback(() => { + const save = () => { if (!searchDatePresetFilterBaseRef.current) { return; } @@ -91,57 +127,56 @@ function DateFilterBase({title, dateKey, back, onSubmit}: DateFilterBaseProps) { if (selectedDateModifier) { searchDatePresetFilterBaseRef.current.setDateValueOfSelectedDateModifier(); setSelectedDateModifier(null); + onDateModifierChange?.(false); return; } const dateValues = searchDatePresetFilterBaseRef.current.getDateValues(); - - onSubmit({ - [dateOnKey]: dateValues[CONST.SEARCH.DATE_MODIFIERS.ON] ?? null, - [dateBeforeKey]: dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE] ?? null, - [dateAfterKey]: dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER] ?? null, - }); - }, [selectedDateModifier, dateOnKey, dateBeforeKey, dateAfterKey, onSubmit]); - - const goBack = () => { - if (selectedDateModifier) { - setSelectedDateModifier(null); - return; - } - - back(); + onSubmit(dateValues); }; - return ( + const computedTitle = getComputedTitle(); + + const content = ( <> - + {shouldShowHeader && ( + + )} -