Skip to content

Commit 2f009c3

Browse files
committed
Merge branch 'main' into add-ai-rule
2 parents 52aa1a8 + ff3909e commit 2f009c3

29 files changed

Lines changed: 538 additions & 237 deletions

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@
193193
"react-native-localize": "^3.5.4",
194194
"react-native-nitro-modules": "0.35.0",
195195
"react-native-nitro-sqlite": "9.6.0",
196-
"react-native-onyx": "3.0.79",
196+
"react-native-onyx": "3.0.80",
197197
"react-native-pager-view": "8.0.0",
198198
"react-native-pdf": "7.0.2",
199199
"react-native-permissions": "^5.4.0",

src/CONST/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7820,7 +7820,6 @@ const CONST = {
78207820
SELECTION_MODE_MULTI: 'multi',
78217821
SEARCH_CONTEXT_GENERAL: 'general',
78227822
SEARCH_CONTEXT_SEARCH: 'search',
7823-
SEARCH_CONTEXT_MEMBER_INVITE: 'memberInvite',
78247823
SEARCH_CONTEXT_SHARE_DESTINATION: 'shareDestination',
78257824
SEARCH_CONTEXT_ATTENDEES: 'attendees',
78267825
},

src/ROUTES.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,10 @@ const DYNAMIC_ROUTES = {
821821
entryScreens: [SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT],
822822
getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`,
823823
},
824+
WORKSPACE_REPORT_FIELDS_INITIAL_LIST_VALUE: {
825+
path: 'initial-list-value',
826+
entryScreens: [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE],
827+
},
824828
} as const satisfies DynamicRoutes;
825829

826830
const ROUTES = {

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,7 @@ const SCREENS = {
795795
REPORT_FIELDS_VALUE_SETTINGS: 'Workspace_ReportFields_ValueSettings',
796796
REPORT_FIELDS_EDIT_VALUE: 'Workspace_ReportFields_EditValue',
797797
REPORT_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_ReportFields_EditInitialValue',
798+
DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE: 'Dynamic_Report_Fields_Initial_List_Value',
798799
TAX_EDIT: 'Workspace_Tax_Edit',
799800
TAX_NAME: 'Workspace_Tax_Name',
800801
TAX_VALUE: 'Workspace_Tax_Value',

src/hooks/useParticipantSubmission.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ function useParticipantSubmission({
185185
policy: movingPolicy,
186186
isPolicyExpenseChat: false,
187187
lastSelectedDistanceRates: distanceRates,
188+
expenseDate: transaction.created,
188189
});
189190
setCustomUnitRateID(transaction.transactionID, rateID, transaction, movingPolicy);
190191
const shouldSetParticipantAutoAssignment = iouType === CONST.IOU.TYPE.CREATE;
@@ -238,14 +239,25 @@ function useParticipantSubmission({
238239
if (!isMovingTransactionFromTrackExpense || !isPolicyExpenseChat) {
239240
// If not moving the transaction from track expense, select the default rate automatically.
240241
// Otherwise, keep the original p2p rate and let the user manually change it to the one they want from the workspace.
241-
const rateID = DistanceRequestUtils.getCustomUnitRateID({reportID: firstParticipantReportID, isPolicyExpenseChat, policy, lastSelectedDistanceRates: distanceRates});
242-
243242
if (drafts.length > 0) {
244243
for (const transaction of drafts) {
244+
const rateID = DistanceRequestUtils.getCustomUnitRateID({
245+
reportID: firstParticipantReportID,
246+
isPolicyExpenseChat,
247+
policy,
248+
lastSelectedDistanceRates: distanceRates,
249+
expenseDate: transaction.created,
250+
});
245251
setCustomUnitRateID(transaction.transactionID, rateID, transaction, policy);
246252
}
247253
} else {
248254
// Fallback to using initialTransactionID directly
255+
const rateID = DistanceRequestUtils.getCustomUnitRateID({
256+
reportID: firstParticipantReportID,
257+
isPolicyExpenseChat,
258+
policy,
259+
lastSelectedDistanceRates: distanceRates,
260+
});
249261
setCustomUnitRateID(initialTransactionID, rateID, undefined, policy);
250262
}
251263
}

src/hooks/useSearchSelector/base.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type * as OnyxTypes from '@src/types/onyx';
1919

2020
type SearchSelectorContext = (typeof CONST.SEARCH_SELECTOR)[keyof Pick<
2121
typeof CONST.SEARCH_SELECTOR,
22-
'SEARCH_CONTEXT_GENERAL' | 'SEARCH_CONTEXT_SEARCH' | 'SEARCH_CONTEXT_MEMBER_INVITE' | 'SEARCH_CONTEXT_SHARE_DESTINATION' | 'SEARCH_CONTEXT_ATTENDEES'
22+
'SEARCH_CONTEXT_GENERAL' | 'SEARCH_CONTEXT_SEARCH' | 'SEARCH_CONTEXT_SHARE_DESTINATION' | 'SEARCH_CONTEXT_ATTENDEES'
2323
>];
2424
type SearchSelectorSelectionMode = (typeof CONST.SEARCH_SELECTOR)[keyof Pick<typeof CONST.SEARCH_SELECTOR, 'SELECTION_MODE_SINGLE' | 'SELECTION_MODE_MULTI'>];
2525

@@ -241,27 +241,6 @@ function useSearchSelectorBase({
241241
sortedActions,
242242
conciergeReportID,
243243
});
244-
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE:
245-
return getValidOptions(optionsWithContacts, allPolicies, draftComments, loginList, currentUserAccountID, currentUserEmail, conciergeReportID, {
246-
betas: betas ?? [],
247-
includeP2P: true,
248-
includeSelectedOptions: false,
249-
excludeLogins,
250-
excludeFromSuggestionsOnly,
251-
includeRecentReports,
252-
maxElements: maxResults,
253-
maxRecentReportElements: maxRecentReportsToShow,
254-
searchString: computedSearchTerm,
255-
searchInputValue: trimmedSearchInput,
256-
includeUserToInvite,
257-
personalDetails,
258-
includeCurrentUser,
259-
includeSelfDM,
260-
countryCode,
261-
reportAttributesDerived: reportAttributesDerived?.reports,
262-
allPolicyTags,
263-
sortedActions,
264-
});
265244
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL:
266245
return getValidOptions(optionsWithContacts, allPolicies, draftComments, loginList, currentUserAccountID, currentUserEmail, conciergeReportID, {
267246
betas: betas ?? [],

src/libs/DistanceRequestUtils.ts

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type MileageRate = {
2020
name?: string;
2121
enabled?: boolean;
2222
index?: number;
23+
startDate?: string | null;
24+
endDate?: string | null;
2325
};
2426

2527
/** @private Only for getRate function */
@@ -63,6 +65,8 @@ function getMileageRates(policy: OnyxInputOrEntry<Policy>, includeDisabledRates
6365
customUnitRateID: rate.customUnitRateID,
6466
enabled: rate.enabled,
6567
index: rate.index,
68+
startDate: rate.startDate,
69+
endDate: rate.endDate,
6670
};
6771
}
6872

@@ -341,22 +345,117 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number {
341345
}
342346

343347
/**
344-
* Returns custom unit rate ID for the distance transaction
348+
* Checks if a mileage rate is eligible for a given expense date.
349+
* A rate is eligible if the date falls within its startDate/endDate bounds (inclusive).
350+
* Missing bounds mean unbounded in that direction.
351+
*/
352+
function isRateEligibleForDate(rate: MileageRate, expenseDate: string): boolean {
353+
if (rate.startDate && expenseDate < rate.startDate) {
354+
return false;
355+
}
356+
if (rate.endDate && expenseDate > rate.endDate) {
357+
return false;
358+
}
359+
return true;
360+
}
361+
362+
/**
363+
* Returns a boundedness score: 2 = fully bounded (both dates), 1 = partially bounded (one date), 0 = unbounded.
364+
*/
365+
function getBoundednessScore(rate: MileageRate): number {
366+
if (rate.startDate && rate.endDate) {
367+
return 2;
368+
}
369+
if (rate.startDate || rate.endDate) {
370+
return 1;
371+
}
372+
return 0;
373+
}
374+
375+
function getFullyBoundedDateRangeMs(rate: MileageRate): number | undefined {
376+
if (!rate.startDate || !rate.endDate) {
377+
return undefined;
378+
}
379+
380+
return new Date(rate.endDate).getTime() - new Date(rate.startDate).getTime();
381+
}
382+
383+
/**
384+
* Finds the best eligible rate for a given expense date from a set of mileage rates.
385+
* Selection order per design doc:
386+
* 1. Most specific date range (fully bounded > partially bounded > unbounded)
387+
* 2. Narrower date range for two fully bounded ranges
388+
* 3. Latest start date
389+
* 4. Lowest index (creation order)
390+
*/
391+
function getBestEligibleRate(mileageRates: Record<string, MileageRate>, expenseDate: string): MileageRate | undefined {
392+
const eligibleRates = Object.values(mileageRates).filter((rate) => rate.enabled !== false && isRateEligibleForDate(rate, expenseDate));
393+
394+
if (eligibleRates.length === 0) {
395+
return undefined;
396+
}
397+
398+
eligibleRates.sort((a, b) => {
399+
const aScore = getBoundednessScore(a);
400+
const bScore = getBoundednessScore(b);
401+
if (aScore !== bScore) {
402+
return bScore - aScore;
403+
}
404+
405+
if (aScore === 2 && bScore === 2) {
406+
const aRange = getFullyBoundedDateRangeMs(a);
407+
const bRange = getFullyBoundedDateRangeMs(b);
408+
if (aRange !== undefined && bRange !== undefined && aRange !== bRange) {
409+
return aRange - bRange;
410+
}
411+
}
412+
413+
const aStart = a.startDate ?? '';
414+
const bStart = b.startDate ?? '';
415+
if (aStart !== bStart) {
416+
return aStart < bStart ? 1 : -1;
417+
}
418+
419+
const aIndex = a.index ?? CONST.DEFAULT_NUMBER_ID;
420+
const bIndex = b.index ?? CONST.DEFAULT_NUMBER_ID;
421+
return aIndex - bIndex;
422+
});
423+
424+
return eligibleRates.at(0);
425+
}
426+
427+
function getBestEligibleRateOrPolicyDefault(mileageRates: Record<string, MileageRate>, expenseDate: string, policy: OnyxEntry<Policy>): MileageRate | undefined {
428+
const bestRate = getBestEligibleRate(mileageRates, expenseDate);
429+
if (bestRate) {
430+
return bestRate;
431+
}
432+
433+
return getDefaultMileageRate(policy);
434+
}
435+
436+
/**
437+
* Returns custom unit rate ID for the distance transaction.
438+
* When an expenseDate is provided, uses date-aware rate selection:
439+
* 1. Last selected rate, if enabled and valid for the expense date
440+
* 2. Best eligible rate for the expense date
441+
* 3. Default rate fallback
345442
*/
346443
function getCustomUnitRateID({
347444
reportID,
348445
isPolicyExpenseChat,
349446
policy,
350447
isTrackDistanceExpense = false,
351448
lastSelectedDistanceRates,
449+
expenseDate,
352450
}: {
353451
reportID: string | undefined;
354452
isPolicyExpenseChat: boolean;
355453
policy: OnyxEntry<Policy> | undefined;
356454
lastSelectedDistanceRates?: OnyxEntry<LastSelectedDistanceRates>;
357455
isTrackDistanceExpense?: boolean;
456+
expenseDate?: string;
358457
}): string {
359-
let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
458+
const customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
360459

361460
if (!reportID) {
362461
return customUnitRateID;
@@ -371,14 +470,32 @@ function getCustomUnitRateID({
371470
const distanceUnit = Object.values(policy.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
372471
const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy.id];
373472
const lastSelectedDistanceRate = lastSelectedDistanceRateID ? distanceUnit?.rates[lastSelectedDistanceRateID] : undefined;
374-
if (lastSelectedDistanceRate?.enabled && lastSelectedDistanceRateID) {
375-
customUnitRateID = lastSelectedDistanceRateID;
376-
} else {
473+
474+
if (!expenseDate) {
475+
if (lastSelectedDistanceRate?.enabled && lastSelectedDistanceRateID) {
476+
return lastSelectedDistanceRateID;
477+
}
478+
377479
const defaultMileageRate = getDefaultMileageRate(policy);
378-
if (!defaultMileageRate?.customUnitRateID) {
379-
return customUnitRateID;
480+
if (defaultMileageRate?.customUnitRateID) {
481+
return defaultMileageRate.customUnitRateID;
380482
}
381-
customUnitRateID = defaultMileageRate.customUnitRateID;
483+
484+
return customUnitRateID;
485+
}
486+
487+
const mileageRates = getMileageRates(policy);
488+
if (lastSelectedDistanceRate?.enabled && lastSelectedDistanceRateID) {
489+
const lastSelectedMileageRate = mileageRates[lastSelectedDistanceRateID];
490+
// mileageRates may be empty when the distance unit has no attributes. Guard against undefined before calling isRateEligibleForDate, and preserve the user's last selected ID when rate metadata is unavailable.
491+
if (!lastSelectedMileageRate || isRateEligibleForDate(lastSelectedMileageRate, expenseDate)) {
492+
return lastSelectedDistanceRateID;
493+
}
494+
}
495+
496+
const bestRate = getBestEligibleRateOrPolicyDefault(mileageRates, expenseDate, policy);
497+
if (bestRate?.customUnitRateID) {
498+
return bestRate.customUnitRateID;
382499
}
383500
}
384501

@@ -544,6 +661,8 @@ export default {
544661
isDistanceAmountWithinLimit,
545662
normalizeOdometerText,
546663
prepareTextForDisplay,
664+
isRateEligibleForDate,
665+
getBestEligibleRate,
547666
};
548667

549668
export type {MileageRate};

src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
995995
),
996996
[SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/settings/Subscription/PaymentCard').default),
997997
[SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require<ReactComponentModule>('../../../../pages/workspace/reports/CreateReportFieldsPage').default,
998+
[SCREENS.WORKSPACE.DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE]: () =>
999+
require<ReactComponentModule>('../../../../pages/workspace/reports/InitialListValueSelector/DynamicReportFieldsInitialListValuePage').default,
9981000
[SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/reports/ReportFieldsSettingsPage').default,
9991001
[SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require<ReactComponentModule>('../../../../pages/workspace/reports/ReportFieldsListValuesPage').default,
10001002
[SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require<ReactComponentModule>('../../../../pages/workspace/reports/ReportFieldsAddListValuePage').default,

src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ const WORKSPACE_TO_RHP: Partial<Record<keyof WorkspaceSplitNavigatorParamList, s
295295
SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS,
296296
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE,
297297
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
298+
SCREENS.WORKSPACE.DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE,
298299
],
299300
[SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE, SCREENS.WORKSPACE.INVOICES_VERIFY_ACCOUNT],
300301
[SCREENS.WORKSPACE.COMPANY_CARDS]: [

0 commit comments

Comments
 (0)