diff --git a/src/CONST.ts b/src/CONST.ts index 6da7a21684dc..fdede280c913 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1205,6 +1205,8 @@ const CONST = { MERGED_WITH_CASH_TRANSACTION: 'MERGEDWITHCASHTRANSACTION', MODIFIED_EXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', + MOVED_TRANSACTION: 'MOVEDTRANSACTION', + UNREPORTED_TRANSACTION: 'UNREPORTEDTRANSACTION', OUTDATED_BANK_ACCOUNT: 'OUTDATEDBANKACCOUNT', // OldDot Action REIMBURSED: 'REIMBURSED', REIMBURSEMENT_ACH_BOUNCE: 'REIMBURSEMENTACHBOUNCE', // OldDot Action diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 203abf7cb0a8..82e3fab59063 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -670,6 +670,11 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = 0, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/subrate/${transactionID}/${reportID}/edit/${pageIndex}`, backTo), }, + MONEY_REQUEST_STEP_REPORT: { + route: ':action/:iouType/report/:transactionID/:reportID', + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${transactionID}/${reportID}`, backTo), + }, SETTINGS_TAGS_ROOT: { route: 'settings/:policyID/tags', getRoute: (policyID: string | undefined, backTo = '') => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 562b047efe97..cd12822eb936 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -257,6 +257,7 @@ const SCREENS = { STEP_DESTINATION_EDIT: 'Money_Request_Destination_Edit', STEP_TIME_EDIT: 'Money_Request_Time_Edit', STEP_SUBRATE_EDIT: 'Money_Request_SubRate_Edit', + STEP_REPORT: 'Money_Request_Report', }, TRANSACTION_DUPLICATE: { diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index c48ff13a7b08..09c7f8991336 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -17,7 +17,7 @@ import {getDestinationForDisplay, getSubratesFields, getSubratesForDisplay, getT import {canSendInvoice, getPerDiemCustomUnit, isMultiLevelTags as isMultiLevelTagsPolicyUtils, isPaidGroupPolicy} from '@libs/PolicyUtils'; import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; -import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; +import {buildOptimisticExpenseReport, getDefaultWorkspaceAvatar, getOutstandingReports, isReportOutsanding, populateOptimisticReportFormula} from '@libs/ReportUtils'; import {hasEnabledTags} from '@libs/TagsOptionsListUtils'; import { getTagForDisplay, @@ -251,6 +251,8 @@ function MoneyRequestConfirmationListFooter({ const {translate, toLocaleDigit} = useLocalize(); const {isOffline} = useNetwork(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); // A flag and a toggler for showing the rest of the form fields @@ -276,6 +278,25 @@ function MoneyRequestConfirmationListFooter({ return canSendInvoice(allPolicies, currentUserLogin) && !!transaction?.isFromGlobalCreate && !isInvoiceRoomParticipant; }, [allPolicies, currentUserLogin, selectedParticipants, transaction?.isFromGlobalCreate]); + /** + * We need to check if the transaction report exists first in order to prevent the outstanding reports from being used. + * Also we need to check if transaction report exists in outstanding reports in order to show a correct report name. + */ + const transactionReport = !!transaction?.reportID && Object.values(allReports ?? {}).find((report) => report?.reportID === transaction.reportID); + const shouldUseTransactionReport = !!transactionReport && isReportOutsanding(transactionReport, selectedParticipants?.at(0)?.policyID); + let reportName: string | undefined; + if (shouldUseTransactionReport) { + reportName = transactionReport.reportName; + } else { + const firstOutstangingReport = getOutstandingReports(selectedParticipants?.at(0)?.policyID, allReports ?? {}).at(0); + reportName = firstOutstangingReport?.reportName; + } + + if (!reportName) { + const optimisticReport = buildOptimisticExpenseReport(reportID, policy?.id, policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID, Number(formattedAmount), currency); + reportName = populateOptimisticReportFormula(policy?.fieldList?.text_title?.defaultValue ?? '', optimisticReport, policy); + } + const isTypeSend = iouType === CONST.IOU.TYPE.PAY; const taxRates = policy?.taxRates ?? null; // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item @@ -629,6 +650,28 @@ function MoneyRequestConfirmationListFooter({ shouldShow: shouldShowBillable, isSupplementary: true, }, + { + item: ( + { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); + }} + interactive + shouldRenderAsHTML + /> + ), + shouldShow: isPolicyExpenseChat, + isSupplementary: true, + }, ]; const subRates = getSubratesFields(perDiemCustomUnit, transaction); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 6dc8d9c758b9..158398477bcf 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -801,6 +801,27 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals /> )} + {!!parentReportID && ( + + { + if (!report?.reportID || !transaction?.transactionID) { + return; + } + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report.reportID, getReportRHPActiveRoute()), + ); + }} + interactive + shouldRenderAsHTML + /> + + )} `deleted an expense on this report, ${merchant} - ${amount}`, + movedTransaction: ({reportUrl, reportName}: MovedTransactionParams) => `moved this expense to ${reportName}`, + unreportedTransaction: ({reportUrl, reportName}: UnreportedTransactionParams) => `removed this expense from ${reportName}`, pendingMatchWithCreditCard: 'Receipt pending match with card transaction', pendingMatch: 'Pending match', pendingMatchWithCreditCardDescription: 'Receipt pending match with card transaction. Mark as cash to cancel.', diff --git a/src/languages/es.ts b/src/languages/es.ts index ce368de65c6f..990fb763733f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -110,6 +110,7 @@ import type { MarkReimbursedFromIntegrationParams, MissingPropertyParams, MovedFromPersonalSpaceParams, + MovedTransactionParams, NeedCategoryForExportToIntegrationParams, NewWorkspaceNameParams, NoLongerHaveAccessParams, @@ -182,6 +183,7 @@ import type { TrialStartedTitleParams, UnapprovedParams, UnapproveWithIntegrationWarningParams, + UnreportedTransactionParams, UnshareParams, UntilTimeParams, UpdatedPolicyCategoryNameParams, @@ -439,6 +441,7 @@ const translations = { showMore: 'Mostrar más', merchant: 'Comerciante', category: 'Categoría', + report: 'Informe', billable: 'Facturable', nonBillable: 'No facturable', tag: 'Etiqueta', @@ -925,6 +928,8 @@ const translations = { markAsCash: 'Marcar como efectivo', routePending: 'Ruta pendiente...', deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `eliminó un gasto de este informe, ${merchant} - ${amount}`, + movedTransaction: ({reportUrl, reportName}: MovedTransactionParams) => `movió este gasto a ${reportName}`, + unreportedTransaction: ({reportUrl, reportName}: UnreportedTransactionParams) => `eliminó este gasto de ${reportName}`, receiptIssuesFound: () => ({ one: 'Problema encontrado', other: 'Problemas encontrados', diff --git a/src/languages/params.ts b/src/languages/params.ts index a1ffa1053bef..7256695a28d7 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -124,6 +124,16 @@ type DeleteTransactionParams = { merchant: string; }; +type MovedTransactionParams = { + reportUrl: string; + reportName?: string; +}; + +type UnreportedTransactionParams = { + reportUrl: string; + reportName?: string; +}; + type SettleExpensifyCardParams = { formattedAmount: string; }; @@ -798,6 +808,8 @@ export type { RequestAmountParams, RequestCountParams, DeleteTransactionParams, + MovedTransactionParams, + UnreportedTransactionParams, RequestedAmountMessageParams, ResolutionConstraintsParams, RoomNameReservedErrorParams, diff --git a/src/libs/API/parameters/ChangeTransactionsReportParams.ts b/src/libs/API/parameters/ChangeTransactionsReportParams.ts new file mode 100644 index 000000000000..e9e3a0fb15aa --- /dev/null +++ b/src/libs/API/parameters/ChangeTransactionsReportParams.ts @@ -0,0 +1,19 @@ +/** + * A map linking the optimistic MOVEDTRANSACTION or UNREPORTEDTRANSACTION reportActionID to the transactionID. + * If we're creating the transactionThread as part of moving the transaction, we should also send the optimistic + * transactionThreadReportID and transactionThreadCreatedReportActionID + */ +type TransactionThreadInfo = { + movedReportActionID: string; + moneyRequestPreviewReportActionID: string; + transactionThreadReportID?: string; + transactionThreadCreatedReportActionID?: string; +}; + +type ChangeTransactionsReportParams = { + transactionList: string; + reportID: string; + transactionIDToReportActionAndThreadData: string; // A map of transactionID to TransactionThreadInfo +}; + +export type {ChangeTransactionsReportParams, TransactionThreadInfo}; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a70093c83b11..01e77f4dacbd 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -379,6 +379,7 @@ export type {default as SaveCorpayOnboardingBeneficialOwnerParams} from './SaveC export type {default as MoveIOUReportToPolicyAndInviteSubmitterParams} from './MoveIOUReportToPolicyAndInviteSubmitterParams'; export type {default as MoveIOUReportToExistingPolicyParams} from './MoveIOUReportToExistingPolicyParams'; export type {default as ChangeReportPolicyParams} from './ChangeReportPolicyParams'; +export type {ChangeTransactionsReportParams, TransactionThreadInfo} from './ChangeTransactionsReportParams'; export type {default as ResetBankAccountSetupParams} from './ResetBankAccountSetupParams'; export type {default as SendRecapInAdminsRoomParams} from './SendRecapInAdminsRoomParams'; export type {default as SetPolicyProhibitedExpensesParams} from './SetPolicyProhibitedExpensesParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a96b93931949..2d97714c607d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -462,6 +462,7 @@ const WRITE_COMMANDS = { SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS: 'SaveCorpayOnboardingCompanyDetails', SAVE_CORPAY_ONBOARDING_BENEFICIAL_OWNER: 'SaveCorpayOnboardingBeneficialOwner', CHANGE_REPORT_POLICY: 'ChangeReportPolicy', + CHANGE_TRANSACTIONS_REPORT: 'ChangeTransactionsReport', SEND_RECAP_IN_ADMINS_ROOM: 'SendRecapInAdminsRoom', COMPLETE_CONCIERGE_CALL: 'CompleteConciergeCall', } as const; @@ -942,6 +943,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.MERGE_WITH_VALIDATE_CODE]: Parameters.MergeWithValidateCodeParams; // Change report policy [WRITE_COMMANDS.CHANGE_REPORT_POLICY]: Parameters.ChangeReportPolicyParams; + + // Change transaction report + [WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT]: Parameters.ChangeTransactionsReportParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 416e5160fafe..f6a3dbfbe50a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -98,6 +98,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default, [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: () => require('../../../../pages/iou/request/step/IOURequestStepSplitPayer').default, [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: () => require('../../../../pages/iou/request/step/IOURequestStepSendFrom').default, + [SCREENS.MONEY_REQUEST.STEP_REPORT]: () => require('../../../../pages/iou/request/step/IOURequestStepReport').default, [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: () => require('../../../../pages/iou/request/step/IOURequestStepCompanyInfo').default, [SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../../pages/iou/HoldReasonPage').default, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 36d3897afcfc..c090d7fe5a64 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1260,6 +1260,7 @@ const config: LinkingOptions['config'] = { [SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route, [SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: ROUTES.SETTINGS_TAGS_ROOT.route, [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: ROUTES.MONEY_REQUEST_STEP_SEND_FROM.route, + [SCREENS.MONEY_REQUEST.STEP_REPORT]: ROUTES.MONEY_REQUEST_STEP_REPORT.route, [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.route, [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8a4cdc29ed31..e65fd8b21090 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1130,6 +1130,13 @@ type MoneyRequestNavigatorParamList = { reportID: string; backTo: Routes; }; + [SCREENS.MONEY_REQUEST.STEP_REPORT]: { + action: IOUAction; + iouType: IOUType; + transactionID: string; + reportID: string; + backTo: Routes; + }; [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: { iouType: IOUType; transactionID: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b3a240091c3e..6ff665ad4877 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10,7 +10,13 @@ import lodashMaxBy from 'lodash/maxBy'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; -import type {OriginalMessageChangePolicy, OriginalMessageIOU, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage'; +import type { + OriginalMessageChangePolicy, + OriginalMessageIOU, + OriginalMessageModifiedExpense, + OriginalMessageMovedTransaction, + OriginalMessageUnreportedTransaction, +} from 'src/types/onyx/OriginalMessage'; import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} from '@components/Icon/Expensicons'; @@ -5658,6 +5664,36 @@ function getDeletedTransactionMessage(action: ReportAction) { return message; } +function getReportDetails(reportID: string): {reportName: string; reportUrl: string} { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + return { + reportName: report?.reportName ?? '', + reportUrl: `${environmentURL}/r/${reportID}`, + }; +} + +function getMovedTransactionMessage(action: ReportAction) { + const movedTransactionOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; + const {toReportID} = movedTransactionOriginalMessage as OriginalMessageMovedTransaction; + const {reportName, reportUrl} = getReportDetails(toReportID); + const message = translateLocal('iou.movedTransaction', { + reportUrl, + reportName, + }); + return message; +} + +function getUnreportedTransactionMessage(action: ReportAction) { + const unreportedTransactionOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; + const {fromReportID} = unreportedTransactionOriginalMessage as OriginalMessageUnreportedTransaction; + const {reportName, reportUrl} = getReportDetails(fromReportID); + const message = translateLocal('iou.unreportedTransaction', { + reportUrl, + reportName, + }); + return message; +} + function getPolicyChangeMessage(action: ReportAction) { const PolicyChangeOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; const {fromPolicy: fromPolicyID, toPolicy: toPolicyID} = PolicyChangeOriginalMessage as OriginalMessageChangePolicy; @@ -6014,6 +6050,57 @@ function buildOptimisticChangePolicyReportAction(fromPolicyID: string | undefine }; } +function buildOptimisticTransactionAction(type: 'MOVEDTRANSACTION' | 'UNREPORTEDTRANSACTION', transactionThreadReportID: string | undefined, targetReportID: string): ReportAction { + const reportName = allReports?.[targetReportID]?.reportName ?? ''; + const url = `${environmentURL}/r/${targetReportID}`; + const [actionText, messageHtml] = + type === CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION + ? [`moved this expense to ${reportName}`, `moved this expense to ${reportName}`] + : [`removed this expense from ${reportName}`, `removed this expense from ${reportName}`]; + + return { + actionName: type, + reportID: transactionThreadReportID, + actorAccountID: currentUserAccountID, + avatar: getCurrentUserAvatar(), + created: DateUtils.getDBTime(), + originalMessage: type === CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION ? {toReportID: targetReportID} : {fromReportID: targetReportID}, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + html: messageHtml, + text: actionText, + }, + ], + person: [ + { + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + type: 'TEXT', + }, + ], + reportActionID: rand64(), + shouldShow: true, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + +/** + * Builds an optimistic MOVED_TRANSACTION report action with a randomly generated reportActionID. + * This action is used when we change the workspace of a report. + */ +function buildOptimisticMovedTransactionAction(transactionThreadReportID: string | undefined, toReportID: string) { + return buildOptimisticTransactionAction(CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION, transactionThreadReportID, toReportID); +} + +/** + * Builds an optimistic UNREPORTED_TRANSACTION report action with a randomly generated reportActionID. + * This action is used when we unreport a transaction. + */ +function buildOptimisticUnreportedTransactionAction(transactionThreadReportID: string | undefined, fromReportID: string) { + return buildOptimisticTransactionAction(CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION, transactionThreadReportID, fromReportID); +} + /** * Builds an optimistic SUBMITTED report action with a randomly generated reportActionID. * @@ -9338,6 +9425,47 @@ function createDraftTransactionAndNavigateToParticipantSelector( return createDraftWorkspaceAndNavigateToConfirmationScreen(transactionID, actionName); } +/** + * Check if a report has any forwarded actions + */ +function hasForwardedAction(reportID: string): boolean { + const reportActions = getAllReportActions(reportID); + return Object.values(reportActions).some((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED); +} + +function isReportOutsanding(iouReport: OnyxInputOrEntry, policyID: string | undefined): boolean { + if (!iouReport || isEmptyObject(iouReport)) { + return false; + } + if ( + isExpenseReport(iouReport) && + iouReport?.stateNum !== undefined && + iouReport?.statusNum !== undefined && + iouReport?.policyID === policyID && + iouReport?.stateNum <= CONST.REPORT.STATE_NUM.SUBMITTED && + iouReport?.statusNum <= CONST.REPORT.STATUS_NUM.SUBMITTED && + !hasForwardedAction(iouReport.reportID) + ) { + return true; + } + return false; +} + +/** + * Get outstanding expense reports for a given policy ID + * @param policyID - The policy ID to filter reports by + * @param reports - Collection of reports to filter + * @returns Array of outstanding expense reports sorted by name + */ +function getOutstandingReports(policyID: string | undefined, reports: OnyxCollection = allReports): Array> { + if (!reports) { + return []; + } + return Object.values(reports) + .filter((report) => isReportOutsanding(report, policyID)) + .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0); +} + /** * @returns the object to update `report.hasOutstandingChildRequest` */ @@ -10582,15 +10710,21 @@ export { isTestTransactionReport, getReportSubtitlePrefix, getPolicyChangeMessage, + getMovedTransactionMessage, + getUnreportedTransactionMessage, getExpenseReportStateAndStatus, - navigateToLinkedReportAction, + buildOptimisticUnreportedTransactionAction, buildOptimisticResolvedDuplicatesReportAction, - populateOptimisticReportFormula, getTitleReportField, getReportFieldsByPolicyID, getGroupChatDraft, getInvoiceReportName, getChatListItemReportName, + buildOptimisticMovedTransactionAction, + navigateToLinkedReportAction, + populateOptimisticReportFormula, + getOutstandingReports, + isReportOutsanding, }; export type { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index d2f996c3e965..f2f55b6b77c7 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -5,16 +5,24 @@ import isEqual from 'lodash/isEqual'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {DismissViolationParams, GetRouteParams, MarkAsCashParams} from '@libs/API/parameters'; +import type {ChangeTransactionsReportParams, DismissViolationParams, GetRouteParams, MarkAsCashParams, TransactionThreadInfo} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; +import DateUtils from '@libs/DateUtils'; import * as NumberUtils from '@libs/NumberUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import {buildOptimisticDismissedViolationReportAction} from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {rand64} from '@libs/NumberUtils'; +import {getAllReportActions, getIOUActionForReportID, getOriginalMessage, isModifiedExpenseAction} from '@libs/ReportActionsUtils'; +import { + buildOptimisticCreatedReportAction, + buildOptimisticDismissedViolationReportAction, + buildOptimisticMovedTransactionAction, + buildOptimisticUnreportedTransactionAction, + buildTransactionThread, +} from '@libs/ReportUtils'; +import {getAmount, getTransaction, waypointHasValidAddress} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, RecentWaypoint, ReportAction, ReportActions, ReviewDuplicates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; +import type {PersonalDetails, RecentWaypoint, Report, ReportAction, ReportActions, ReviewDuplicates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; import type {OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage'; import type {OnyxData} from '@src/types/onyx/Request'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; @@ -26,6 +34,15 @@ Onyx.connect({ callback: (val) => (recentWaypoints = val ?? []), }); +let currentUserEmail = ''; + +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + currentUserEmail = value?.email ?? ''; + }, +}); + const allTransactions: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, @@ -38,6 +55,18 @@ Onyx.connect({ }, }); +let allReports: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + return; + } + allReports = value; + }, +}); + const allTransactionViolation: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, @@ -150,7 +179,7 @@ function removeWaypoint(transaction: OnyxEntry, currentIndex: strin return Promise.resolve(); } - const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed.at(0) ?? {}); + const isRemovedWaypointEmpty = removed.length > 0 && !waypointHasValidAddress(removed.at(0) ?? {}); // When there are only two waypoints we are adding empty waypoint back if (totalWaypoints === 2 && (index === 0 || index === totalWaypoints - 1)) { @@ -356,7 +385,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmissedPersonalDetails: PersonalDetails) { const currentTransactionViolations = transactionIDs.map((id) => ({transactionID: id, violations: allTransactionViolation?.[id] ?? []})); const currentTransactions = transactionIDs.map((id) => allTransactions?.[id]); - const transactionsReportActions = currentTransactions.map((transaction) => ReportActionsUtils.getIOUActionForReportID(transaction.reportID, transaction.transactionID)); + const transactionsReportActions = currentTransactions.map((transaction) => getIOUActionForReportID(transaction.reportID, transaction.transactionID)); const optimisticDissmidedViolationReportActions = transactionsReportActions.map(() => { return buildOptimisticDismissedViolationReportAction({reason: 'manual', violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}); }); @@ -478,13 +507,13 @@ function clearError(transactionID: string) { } function getLastModifiedExpense(reportID?: string): OriginalMessageModifiedExpense | undefined { - const modifiedExpenseActions = Object.values(ReportActionsUtils.getAllReportActions(reportID)).filter(ReportActionsUtils.isModifiedExpenseAction); + const modifiedExpenseActions = Object.values(getAllReportActions(reportID)).filter(isModifiedExpenseAction); modifiedExpenseActions.sort((a, b) => Number(a.reportActionID) - Number(b.reportActionID)); - return ReportActionsUtils.getOriginalMessage(modifiedExpenseActions.at(-1)); + return getOriginalMessage(modifiedExpenseActions.at(-1)); } function revert(transactionID?: string, originalMessage?: OriginalMessageModifiedExpense | undefined) { - const transaction = TransactionUtils.getTransaction(transactionID); + const transaction = getTransaction(transactionID); if (transaction && originalMessage?.oldAmount && originalMessage.oldCurrency && 'amount' in originalMessage && 'currency' in originalMessage) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { @@ -572,6 +601,259 @@ function getAllTransactions() { return Object.keys(allTransactions ?? {}).length; } +function setTransactionReport(transactionID: string, reportID: string, isDraft: boolean) { + Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {reportID}); +} + +function changeTransactionsReport(transactionIDs: string[], reportID: string) { + const newReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + if (!newReport) { + return; + } + + const transactions = transactionIDs.map((id) => allTransactions?.[id]).filter((t): t is NonNullable => t !== undefined); + const transactionIDToReportActionAndThreadData: Record = {}; + const updatedReportTotals: Record = {}; + + const optimisticData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + + transactions.forEach((transaction) => { + const oldIOUAction = getIOUActionForReportID(transaction.reportID, transaction.transactionID); + if (!oldIOUAction?.reportActionID || !transaction.reportID) { + return; + } + + const oldReportID = transaction.reportID; + const oldReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oldReportID}`]; + + // 1. Optimistically change the reportID on the passed transactions + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + reportID, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + reportID: transaction.reportID, + }, + }); + + // 2. Keep track of the new report totals + const transactionAmount = getAmount(transaction); + if (oldReportID) { + updatedReportTotals[oldReportID] = (updatedReportTotals[oldReportID] ? updatedReportTotals[oldReportID] : oldReport?.total ?? 0) + transactionAmount; + } + if (reportID) { + updatedReportTotals[reportID] = (updatedReportTotals[reportID] ? updatedReportTotals[reportID] : newReport.total ?? 0) - transactionAmount; + } + + // 3. Optimistically update the IOU action reportID + const optimisticMoneyRequestReportActionID = rand64(); + const newIOUAction = {...oldIOUAction, reportActionID: optimisticMoneyRequestReportActionID, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}; + optimisticData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [newIOUAction.reportActionID]: newIOUAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldReportID}`, + value: { + [oldIOUAction.reportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + deleted: DateUtils.getDBTime(), + }, + message: [ + { + deleted: DateUtils.getDBTime(), + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + text: '', + }, + ], + }, + }, + }, + ); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [newIOUAction.reportActionID]: {pendingAction: null}, + }, + }); + + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [newIOUAction.reportActionID]: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldReportID}`, + value: {[oldIOUAction.reportActionID]: oldIOUAction}, + }, + ); + + // 4. Optimistically update the transaction thread and all threads in the transaction thread + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${newIOUAction.childReportID}`, + value: { + parentReportID: reportID, + parentReportActionID: optimisticMoneyRequestReportActionID, + policyID: reportID !== CONST.REPORT.UNREPORTED_REPORTID ? newReport.policyID : CONST.POLICY.ID_FAKE, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${oldIOUAction.childReportID}`, + value: { + parentReportID: oldReportID, + optimisticMoneyRequestReportActionID: oldIOUAction.reportActionID, + policyID: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oldReportID}`]?.policyID, + }, + }); + + // 5. (Optional) Create transactionThread if it doesn't exist + let transactionThreadReportID = newIOUAction.childReportID; + let transactionThreadCreatedReportActionID; + if (!transactionThreadReportID) { + const optimisticTransactionThread = buildTransactionThread(newIOUAction, newReport); + const optimisticCreatedActionForTransactionThread = buildOptimisticCreatedReportAction(currentUserEmail); + transactionThreadReportID = optimisticTransactionThread.reportID; + transactionThreadCreatedReportActionID = optimisticCreatedActionForTransactionThread.reportActionID; + newIOUAction.childReportID = transactionThreadReportID; + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: {...optimisticTransactionThread, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: {[optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[newIOUAction.reportActionID]: {childReportID: optimisticTransactionThread.reportID}}, + }, + ); + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: {[optimisticCreatedActionForTransactionThread.reportActionID]: {pendingAction: null}}, + }, + ); + + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: {[optimisticCreatedActionForTransactionThread.reportActionID]: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[newIOUAction.reportActionID]: {childReportID: null}}, + }, + ); + } + + // 6. Add MOVEDTRANSACTION or UNREPORTEDTRANSACTION report actions + const movedAction = + reportID === CONST.REPORT.UNREPORTED_REPORTID + ? buildOptimisticUnreportedTransactionAction(transactionThreadReportID, transaction.reportID) + : buildOptimisticMovedTransactionAction(transactionThreadReportID, reportID); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: {[movedAction?.reportActionID]: movedAction}, + }); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: {[movedAction?.reportActionID]: {pendingAction: null}}, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: {[movedAction?.reportActionID]: null}, + }); + + transactionIDToReportActionAndThreadData[transaction.transactionID] = { + movedReportActionID: movedAction.reportActionID, + moneyRequestPreviewReportActionID: newIOUAction.reportActionID, + ...(oldIOUAction.childReportID + ? {} + : { + transactionThreadReportID, + transactionThreadCreatedReportActionID, + }), + }; + }); + + // 7. Update the report totals + Object.entries(updatedReportTotals).forEach(([reportIDToUpdate, total]) => { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportIDToUpdate}`, + value: {total}, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportIDToUpdate}`, + value: {total: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDToUpdate}`]?.total}, + }); + }); + + const parameters: ChangeTransactionsReportParams = { + transactionList: transactionIDs.join(','), + reportID, + transactionIDToReportActionAndThreadData: JSON.stringify(transactionIDToReportActionAndThreadData), + }; + + API.write(WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT, parameters, { + optimisticData, + successData, + failureData, + }); +} + export { addStop, createInitialWaypoints, @@ -591,4 +873,6 @@ export { getAllTransactions, getLastModifiedExpense, revert, + changeTransactionsReport, + setTransactionReport, }; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 02a8f3587bdd..959574488a86 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -121,10 +121,12 @@ import { getIOUForwardedMessage, getIOUSubmittedMessage, getIOUUnapprovedMessage, + getMovedTransactionMessage, getPolicyChangeMessage, getRejectedReportMessage, getReportAutomaticallyApprovedMessage, getReportAutomaticallySubmittedMessage, + getUnreportedTransactionMessage, getUpgradeWorkspaceMessage, getWhisperDisplayNames, getWorkspaceNameUpdatedMessage, @@ -988,6 +990,18 @@ function PureReportActionItem({ children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION) { + children = ( + + ${getMovedTransactionMessage(action)}`} /> + + ); + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION) { + children = ( + + ${getUnreportedTransactionMessage(action)}`} /> + + ); } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION) { children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION)) { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 864d6cd74ba2..add8d0123f80 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -30,7 +30,7 @@ import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction' import Navigation from '@libs/Navigation/Navigation'; import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import {generateReportID, getBankAccountRoute, isSelectedManagerMcTest} from '@libs/ReportUtils'; +import {generateReportID, getBankAccountRoute, getReportOrDraftReport, isProcessingReport, isSelectedManagerMcTest} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import {getDefaultTaxCode, getRateID, getRequestType, getValidWaypoints} from '@libs/TransactionUtils'; import ReceiptDropUI from '@pages/iou/ReceiptDropUI'; @@ -91,7 +91,13 @@ function IOURequestStepConfirmation({ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getIOURequestPolicyID(transaction, reportReal)}`); const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION); - const report = reportReal ?? reportDraft; + /* + * We want to use a report from the transaction if it exists + * Also if the report was submitted and delayed submittion is on, then we should use an initial report + */ + const transactionReport = getReportOrDraftReport(transaction?.reportID); + const shouldUseTransactionReport = transactionReport && !(isProcessingReport(transactionReport) && !policyReal?.harvesting?.enabled); + const report = shouldUseTransactionReport ? transactionReport : reportReal ?? reportDraft; const policy = policyReal ?? policyDraft; const isDraftPolicy = policy === policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx new file mode 100644 index 000000000000..5faee5fe78d3 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -0,0 +1,118 @@ +import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; +import Navigation from '@libs/Navigation/Navigation'; +import {getOutstandingReports} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Report} from '@src/types/onyx'; +import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; + +type ReportListItem = ListItem & { + /** reportID of the report */ + value: string; +}; + +type IOURequestStepReportProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps; + +/** + * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component. + * This helps minimize re-rendering and makes the entire component more performant. + */ +const reportSelector = (report: OnyxEntry): OnyxEntry => + report && { + reportID: report.reportID, + policyID: report.policyID, + reportName: report.reportName, + stateNum: report.stateNum, + statusNum: report.statusNum, + type: report.type, + }; + +function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { + const {translate} = useLocalize(); + const {backTo, action} = route.params; + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector)}); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const isEditing = action === CONST.IOU.ACTION.EDIT; + // We need to get the policyID because it's not defined in the transaction object before we select a report manually. + const policyID = Object.values(allReports ?? {}).find( + (report) => report?.reportID === transaction?.reportID || report?.reportID === transaction?.participants?.at(0)?.reportID, + )?.policyID; + const expenseReports = getOutstandingReports(policyID, allReports ?? {}); + + const reportOptions: ReportListItem[] = useMemo(() => { + if (!allReports) { + return []; + } + + const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transaction?.reportID); + return expenseReports + .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0) + .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase())) + .filter((report): report is NonNullable => report !== undefined) + .map((report) => ({ + text: report.reportName, + value: report.reportID, + keyForList: report.reportID, + isSelected: isTransactionReportCorrect ? report.reportID === transaction?.reportID : expenseReports.at(0)?.reportID === report.reportID, + })); + }, [allReports, debouncedSearchValue, expenseReports, transaction?.reportID]); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const selectReport = (item: ReportListItem) => { + if (!transaction) { + return; + } + if (item.value !== transaction.reportID) { + setTransactionReport(transaction.transactionID, item.value, !isEditing); + if (isEditing) { + changeTransactionsReport([transaction.transactionID], item.value); + } + } + Navigation.goBack(backTo); + }; + + const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + + return ( + + = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined} + shouldSingleExecuteRowSelect + headerMessage={headerMessage} + initiallyFocusedOptionKey={transaction?.reportID} + ListItem={UserListItem} + /> + + ); +} + +IOURequestStepReport.displayName = 'IOURequestStepReport'; + +export default withWritableReportOrNotFound(withFullTransactionOrNotFound(IOURequestStepReport)); diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 3f0512e94c46..81ae9371f5a2 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -5,7 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import * as IOUUtils from '@libs/IOUUtils'; +import {shouldUseTransactionDraft} from '@libs/IOUUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; @@ -40,6 +40,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_SCAN | typeof SCREENS.MONEY_REQUEST.STEP_CURRENCY | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM + | typeof SCREENS.MONEY_REQUEST.STEP_REPORT | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO | typeof SCREENS.MONEY_REQUEST.STEP_DESTINATION | typeof SCREENS.MONEY_REQUEST.STEP_TIME @@ -58,7 +59,6 @@ export default function diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index e3ffa11a2e24..81a61ae413c7 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -7,8 +7,8 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as ReportActions from '@userActions/Report'; +import {canUserPerformWriteAction} from '@libs/ReportUtils'; +import {openReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -40,6 +40,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT | typeof SCREENS.MONEY_REQUEST.STEP_SCAN | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM + | typeof SCREENS.MONEY_REQUEST.STEP_REPORT | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO | typeof SCREENS.MONEY_REQUEST.STEP_ATTENDEES | typeof SCREENS.MONEY_REQUEST.STEP_UPGRADE @@ -66,13 +67,11 @@ export default function { if (!!report?.reportID || !route.params.reportID || !!reportDraft || !isEditing) { return; } - ReportActions.openReport(route.params.reportID); + openReport(route.params.reportID); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -80,7 +79,7 @@ export default function ; } - if (iouTypeParamIsInvalid || !canUserPerformWriteAction) { + if (iouTypeParamIsInvalid || !canUserPerformWriteAction(report ?? {reportID: ''})) { return ; } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 594639856cfd..991561471901 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -563,6 +563,18 @@ type OriginalMessageChangePolicy = { toPolicy: string; }; +/** Model of `UNREPORTED_TRANSACTION` report action */ +type OriginalMessageUnreportedTransaction = { + /** ID of the old report */ + fromReportID: string; +}; + +/** Model of `MOVEDTRANSACTION` report action */ +type OriginalMessageMovedTransaction = { + /** ID of the new report */ + toReportID: string; +}; + /** Model of `moved` report action */ type OriginalMessageMoved = { /** ID of the old policy */ @@ -769,6 +781,8 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION]: never; [CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE]: OriginalMessageModifiedExpense; [CONST.REPORT.ACTIONS.TYPE.MOVED]: OriginalMessageMoved; + [CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION]: OriginalMessageMovedTransaction; + [CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION]: OriginalMessageUnreportedTransaction; [CONST.REPORT.ACTIONS.TYPE.OUTDATED_BANK_ACCOUNT]: never; [CONST.REPORT.ACTIONS.TYPE.REIMBURSED]: never; [CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_ACH_BOUNCE]: never; @@ -836,4 +850,6 @@ export type { OriginalMessageExportIntegration, IssueNewCardOriginalMessage, OriginalMessageChangePolicy, + OriginalMessageUnreportedTransaction, + OriginalMessageMovedTransaction, };