From fccb22d27029fe42b1da3d9f45374442a918991d Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Sun, 11 Jan 2026 17:23:01 +0430 Subject: [PATCH] Fix report title update when reimbursable expense changes offline --- .../ReportActionItem/MoneyRequestView.tsx | 9 ++++-- src/libs/ReportUtils.ts | 6 ++++ src/libs/actions/IOU/index.ts | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c3b3e386fead..0afeecdda25c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -51,13 +51,13 @@ import { isTaxTrackingEnabled, } from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {getReportName} from '@libs/ReportNameUtils'; import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import { canEditFieldOfMoneyRequest, canEditMoneyRequest, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, // eslint-disable-next-line @typescript-eslint/no-deprecated - getReportName, getTransactionDetails, getTripIDFromTransactionParentReportID, isExpenseReport, @@ -104,6 +104,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import reportsSelector from '@src/selectors/Attributes'; import type * as OnyxTypes from '@src/types/onyx'; import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -175,6 +176,8 @@ function MoneyRequestView({ const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID; const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true}); + const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportsSelector, canBeMissing: true}); + // When this component is used when merging from the search page, we might not have the parent report stored in the main collection let [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, {canBeMissing: true}); parentReport = parentReport ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; @@ -735,7 +738,9 @@ function MoneyRequestView({ }); // eslint-disable-next-line @typescript-eslint/no-deprecated - const reportNameToDisplay = isFromMergeTransaction ? (updatedTransaction?.reportName ?? translate('common.none')) : getReportName(parentReport) || parentReport?.reportName; + const reportNameToDisplay = isFromMergeTransaction + ? (updatedTransaction?.reportName ?? translate('common.none')) + : getReportName(parentReport, reportAttributes) || parentReport?.reportName; const shouldShowReport = !!parentReportID || (isFromMergeTransaction && !!reportNameToDisplay); const reportCopyValue = !canEditReport && reportNameToDisplay !== translate('common.none') ? reportNameToDisplay : undefined; const shouldShowCategoryAnalyzing = isCategoryBeingAnalyzed(updatedTransaction ?? transaction); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4bc3e042d780..fed648ab83f4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6641,6 +6641,11 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe const createdDate = report.lastVisibleActionCreated ? new Date(report.lastVisibleActionCreated) : undefined; + const totalAmount = report.total !== undefined && !Number.isNaN(report.total) ? Math.abs(report.total) : 0; + const nonReimbursableTotal = + 'nonReimbursableTotal' in report && report.nonReimbursableTotal !== undefined && !Number.isNaN(report.nonReimbursableTotal) ? Math.abs(report.nonReimbursableTotal) : 0; + const reimbursableAmount = totalAmount - nonReimbursableTotal; + const result = formula // We don't translate because the server response is always in English .replaceAll(/\{report:type\}/gi, 'Expense Report') @@ -6648,6 +6653,7 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe .replaceAll(/\{report:enddate\}/gi, createdDate ? format(createdDate, CONST.DATE.FNS_FORMAT_STRING) : '') .replaceAll(/\{report:id\}/gi, getBase62ReportID(Number(report.reportID))) .replaceAll(/\{report:total\}/gi, report.total !== undefined && !Number.isNaN(report.total) ? convertToDisplayString(Math.abs(report.total), report.currency).toString() : '') + .replaceAll(/\{report:reimbursable\}/gi, report.total !== undefined && !Number.isNaN(report.total) ? convertToDisplayString(reimbursableAmount, report.currency).toString() : '') .replaceAll(/\{report:currency\}/gi, report.currency ?? '') .replaceAll(/\{report:policyname\}/gi, policy?.name ?? '') .replaceAll(/\{report:workspacename\}/gi, policy?.name ?? '') diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index ef80a4dc9895..6e29e48db3b5 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -194,6 +194,7 @@ import { isSettled, isTestTransactionReport, isTrackExpenseReport, + populateOptimisticReportFormula, prepareOnboardingOnyxData, shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, shouldEnableNegative, @@ -2931,6 +2932,22 @@ function getDeleteTrackExpenseInformation( return {parameters, optimisticData, successData, failureData, shouldDeleteTransactionThread, chatReport}; } +/** + * Recalculates the report name using the policy's custom title formula. + * This is needed when report totals change (e.g., adding expenses or changing reimbursable status) + * to ensure the report title reflects the updated values like {report:reimbursable}. + */ +function recalculateOptimisticReportName(iouReport: OnyxTypes.Report, policy: OnyxEntry): string | undefined { + if (!policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]) { + return undefined; + } + const titleFormula = policy.fieldList[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]?.defaultValue ?? ''; + if (!titleFormula) { + return undefined; + } + return populateOptimisticReportFormula(titleFormula, iouReport as Parameters[1], policy); +} + /** * Gathers all the data needed to submit an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead @@ -3053,6 +3070,12 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma iouReport.nonReimbursableTotal = (iouReport.nonReimbursableTotal ?? 0) - amount; } } + + // Recalculate reportName to reflect updated totals + const updatedReportName = recalculateOptimisticReportName(iouReport, policy); + if (updatedReportName) { + iouReport.reportName = updatedReportName; + } } if (typeof iouReport.unheldTotal === 'number') { // Use newReportTotal in scenarios where the total is based on more than just the current transaction amount, and we need to override it manually @@ -4141,6 +4164,12 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U updatedMoneyRequestReport.unheldNonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount; } } + + // Recalculate reportName after all totals are updated + const updatedReportName = recalculateOptimisticReportName(updatedMoneyRequestReport, policy); + if (updatedReportName) { + updatedMoneyRequestReport.reportName = updatedReportName; + } } else { updatedMoneyRequestReport = updateIOUOwnerAndTotal( iouReport,