Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9473e4d
fix: codex review issues
ikevin127 Feb 12, 2026
01fb4ee
feat: release 2.5 - export travel invoice statement
ikevin127 Feb 12, 2026
d30f78f
feat: add offline pattern
ikevin127 Feb 12, 2026
ecb6dc9
feat: translations
ikevin127 Feb 12, 2026
061227d
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 16, 2026
3e35c55
fix: updated copy
ikevin127 Feb 16, 2026
0a27e88
fix: update FE to align with updated BE
ikevin127 Feb 17, 2026
00f2123
fix: eslint
ikevin127 Feb 17, 2026
f2b72c4
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 18, 2026
e472725
fix: added ngrok link fallback for dev testing
ikevin127 Feb 18, 2026
9174265
fix: tests
ikevin127 Feb 18, 2026
56bd3e7
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 19, 2026
26e6215
chore: submodule sync
ikevin127 Feb 19, 2026
660b9d1
fix: CSV & PDF download
ikevin127 Feb 20, 2026
0935486
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 21, 2026
ecf5c61
chore: review adjustments
ikevin127 Feb 21, 2026
6199ea7
chore: submodule sync
ikevin127 Feb 21, 2026
7743e1f
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 23, 2026
517927c
fix: csv write command and type
ikevin127 Feb 23, 2026
b67eb4e
chore: prettier
ikevin127 Feb 23, 2026
b67a652
chore: remove canBeMissing
ikevin127 Feb 23, 2026
b14081a
chore: codex review adjustments
ikevin127 Feb 23, 2026
f271586
chore: prettier
ikevin127 Feb 23, 2026
8d4b1ec
fix: native button width
ikevin127 Feb 24, 2026
236366a
fix: mobile export fixes
ikevin127 Feb 24, 2026
fe8e38f
chore: prettier
ikevin127 Feb 24, 2026
30b06db
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 24, 2026
b7248ed
fix: DRY DateFilterBase and remove isDevelopment check
ikevin127 Feb 24, 2026
f5cb441
chore: prettier
ikevin127 Feb 24, 2026
c18cb34
chore: eslint
ikevin127 Feb 24, 2026
a05471b
fix: typecheck and eslint
ikevin127 Feb 25, 2026
8cf2992
chore: prettier
ikevin127 Feb 25, 2026
0d120e7
fix: try typecheck
ikevin127 Feb 25, 2026
9919569
fix: fileName extension
ikevin127 Feb 25, 2026
fec7a69
fix: try typecheck 1
ikevin127 Feb 25, 2026
32fcf13
fix: typecheck
ikevin127 Feb 25, 2026
6889ce2
fix: type, conditional alert, remove Wrapper & unused methods, fix no…
ikevin127 Feb 26, 2026
fabebdc
fix: prettier & eslint
ikevin127 Feb 26, 2026
495cf59
chore: ignore failing GH typecheck
ikevin127 Feb 26, 2026
2e9f2c3
chore: merge main
ikevin127 Feb 26, 2026
92322c2
fix: DateFilterBase buttons, error adjustments
ikevin127 Feb 26, 2026
5dd843a
fix: remove shouldShowSuccessAlert check from android
ikevin127 Feb 26, 2026
8bcaf5d
fix: before / after date validation
ikevin127 Feb 26, 2026
b7896b3
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 26, 2026
dda04bc
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 26, 2026
95ad1ae
Merge branch 'main' of https://github.com/Expensify/App into ikevin12…
ikevin127 Feb 27, 2026
57c92aa
fix: date range validation
ikevin127 Feb 27, 2026
d49070c
fix: adjusted PDF download logic as per DIFF
ikevin127 Mar 5, 2026
47a69d9
chore: merge main
ikevin127 Mar 5, 2026
3658216
fix: baseURL config, removed caching
ikevin127 Mar 5, 2026
cf122b7
fix: go back offline header
ikevin127 Mar 6, 2026
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 @@ -3321,6 +3321,7 @@ const CONST = {
IMPORT_SPREADSHEET: 'importSpreadsheet',
DOWNLOAD_CSV: 'downloadCSV',
SETTINGS: 'settings',
EXPORT: 'export',
},
MEMBERS_BULK_ACTION_TYPES: {
REMOVE: 'remove',
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
195 changes: 115 additions & 80 deletions src/components/Search/FilterComponents/DateFilterBase.tsx
Original file line number Diff line number Diff line change
@@ -1,147 +1,182 @@
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<string, string | null>) => 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<DateFilterBaseHandle>;
};

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<SearchDatePresetFilterBaseHandle>(null);
const [searchAdvancedFiltersForm, searchAdvancedFiltersFormMetadata] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const isSearchAdvancedFiltersFormLoading = isLoadingOnyxValue(searchAdvancedFiltersFormMetadata);
const [selectedDateModifier, setSelectedDateModifier] = useState<SearchDateModifier | null>(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;
}

if (selectedDateModifier) {
searchDatePresetFilterBaseRef.current.clearDateValueOfSelectedDateModifier();
setSelectedDateModifier(null);
onDateModifierChange?.(false);
return;
}

searchDatePresetFilterBaseRef.current.clearDateValues();
}, [selectedDateModifier]);
};

const save = useCallback(() => {
const save = () => {
if (!searchDatePresetFilterBaseRef.current) {
return;
}

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 = (
<>
<HeaderWithBackButton
title={computedTitle}
onBackButtonPress={goBack}
/>
{shouldShowHeader && (
<HeaderWithBackButton
title={computedTitle}
onBackButtonPress={goBack}
/>
)}
<ScrollView contentContainerStyle={[styles.flexGrow1]}>
<DatePresetFilterBase
ref={searchDatePresetFilterBaseRef}
defaultDateValues={defaultDateValues}
selectedDateModifier={selectedDateModifier}
onSelectDateModifier={setSelectedDateModifier}
onSelectDateModifier={handleSelectDateModifier}
presets={presets}
isSearchAdvancedFiltersFormLoading={isSearchAdvancedFiltersFormLoading}
onDateValueChange={onDateValuesChange}
/>
</ScrollView>
<Button
text={translate('common.reset')}
onPress={reset}
style={[styles.mh4, styles.mt4]}
large
/>
<FormAlertWithSubmitButton
buttonText={translate('common.save')}
containerStyles={[styles.m4, styles.mt3, styles.mb5]}
onSubmit={save}
enabledWhenOffline
/>
{(!shouldShowButtonsOnlyWithDateModifier || !!selectedDateModifier) && (
<>
<Button
text={translate('common.reset')}
onPress={reset}
style={[styles.mh4, styles.mt4]}
large
/>
<FormAlertWithSubmitButton
buttonText={translate('common.save')}
containerStyles={[styles.m4, styles.mt3, styles.mb5]}
onSubmit={save}
enabledWhenOffline
/>
</>
)}
</>
);

return content;
}

export type {DateFilterBaseHandle};
export default DateFilterBase;
63 changes: 41 additions & 22 deletions src/components/Search/FilterComponents/DatePresetFilterBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type DatePresetFilterBaseProps = {
/** Whether the search advanced filters form Onyx data is loading or not */
isSearchAdvancedFiltersFormLoading?: boolean;

/** Callback when a date value changes (e.g. preset click or calendar save) */
onDateValueChange?: (values: SearchDateValues) => void;

/** The ref handle */
ref: Ref<DatePresetFilterBaseHandle>;
};
Expand All @@ -59,7 +62,15 @@ type DatePresetFilterBaseProps = {
* - On save: if a date modifier is selected (i.e. user clicked save at the calendar picker) you should `setDateValueOfSelectedDateModifier` otherwise `getDateValues`
* - On reset: if a date modifier is selected (i.e. user clicked reset at the calendar picker) you should `clearDateValueOfSelectedDateModifier` otherwise `clearDateValues`
*/
function DatePresetFilterBase({defaultDateValues, selectedDateModifier, onSelectDateModifier, presets, isSearchAdvancedFiltersFormLoading, ref}: DatePresetFilterBaseProps) {
function DatePresetFilterBase({
defaultDateValues,
selectedDateModifier,
onSelectDateModifier,
presets,
isSearchAdvancedFiltersFormLoading,
onDateValueChange,
ref,
}: DatePresetFilterBaseProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -77,27 +88,35 @@ function DatePresetFilterBase({defaultDateValues, selectedDateModifier, onSelect
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSearchAdvancedFiltersFormLoading]);

const setDateValue = useCallback((dateModifier: SearchDateModifier, value: string | undefined) => {
setDateValues((prevDateValues) => {
if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(value)) {
return {
[CONST.SEARCH.DATE_MODIFIERS.ON]: value,
[CONST.SEARCH.DATE_MODIFIERS.BEFORE]: undefined,
[CONST.SEARCH.DATE_MODIFIERS.AFTER]: undefined,
};
}

if (dateModifier !== CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(prevDateValues[CONST.SEARCH.DATE_MODIFIERS.ON])) {
return {
...prevDateValues,
[dateModifier]: value,
[CONST.SEARCH.DATE_MODIFIERS.ON]: undefined,
};
}

return {...prevDateValues, [dateModifier]: value};
});
}, []);
const setDateValue = useCallback(
(dateModifier: SearchDateModifier, value: string | undefined) => {
setDateValues((prevDateValues) => {
let newValues: SearchDateValues;

if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(value)) {
newValues = {
[CONST.SEARCH.DATE_MODIFIERS.ON]: value,
[CONST.SEARCH.DATE_MODIFIERS.BEFORE]: undefined,
[CONST.SEARCH.DATE_MODIFIERS.AFTER]: undefined,
};
} else if (dateModifier !== CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(prevDateValues[CONST.SEARCH.DATE_MODIFIERS.ON])) {
newValues = {
...prevDateValues,
[dateModifier]: value,
[CONST.SEARCH.DATE_MODIFIERS.ON]: undefined,
};
} else {
newValues = {...prevDateValues, [dateModifier]: value};
}

// Call the callback immediately with the new values so parents don't need to depend on async state updates or refs
onDateValueChange?.(newValues);

return newValues;
});
},
[onDateValueChange],
);

const dateDisplayValues = useMemo<SearchDateValues>(() => {
const dateOn = dateValues[CONST.SEARCH.DATE_MODIFIERS.ON];
Expand Down
Loading
Loading