diff --git a/src/CONST.ts b/src/CONST.ts index 9013873bf383..4edd778f72c1 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -812,6 +812,7 @@ const CONST = { RECEIPT_LINE_ITEMS: 'receiptLineItems', WALLET: 'newdotWallet', GLOBAL_REIMBURSEMENTS_ON_ND: 'globalReimbursementsOnND', + RETRACT_NEWDOT: 'retractNewDot', PRIVATE_DOMAIN_ONBOARDING: 'privateDomainOnboarding', IS_TRAVEL_VERIFIED: 'isTravelVerified', }, @@ -1202,6 +1203,7 @@ const CONST = { VIEW_DETAILS: 'viewDetails', DELETE: 'delete', ADD_EXPENSE: 'addExpense', + REOPEN: 'reopen', }, PRIMARY_ACTIONS: { SUBMIT: 'submit', @@ -1290,6 +1292,7 @@ const CONST = { REMOVED_FROM_APPROVAL_CHAIN: 'REMOVEDFROMAPPROVALCHAIN', DEMOTED_FROM_WORKSPACE: 'DEMOTEDFROMWORKSPACE', RENAMED: 'RENAMED', + REOPENED: 'REOPENED', REPORT_PREVIEW: 'REPORTPREVIEW', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action SHARE: 'SHARE', // OldDot Action diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index db2a10342dad..bf0f91dcf480 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePaymentAnimations from '@hooks/usePaymentAnimations'; +import usePermissions from '@hooks/usePermissions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; @@ -28,6 +29,7 @@ import { getArchiveReason, getBankAccountRoute, getIntegrationIcon, + getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils, getMoneyRequestSpendBreakdown, getNonHeldAndFullAmount, getTransactionsWithReceipts, @@ -65,6 +67,7 @@ import { getNextApproverAccountID, payInvoice, payMoneyRequest, + reopenReport, startMoneyRequest, submitReport, unapproveExpenseReport, @@ -152,11 +155,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const isExported = isExportedUtils(reportActions); + const integrationNameFromExportMessage = isExported ? getIntegrationNameFromExportMessageUtils(reportActions) : null; const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const [isCancelPaymentModalVisible, setIsCancelPaymentModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false); + const [isReopenWarningModalVisible, setIsReopenWarningModalVisible] = useState(false); const [exportModalStatus, setExportModalStatus] = useState(null); @@ -506,12 +511,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea ), }; + const {canUseRetractNewDot} = usePermissions(); + const secondaryActions = useMemo(() => { if (!moneyRequestReport) { return []; } - return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions); - }, [moneyRequestReport, policy, transactions, violations, reportNameValuePairs, reportActions]); + return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot); + }, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, canUseRetractNewDot]); const secondaryActionsImplemenation: Record, DropdownOption>> = { [CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: { @@ -645,6 +652,18 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsDeleteModalVisible(true); }, }, + [CONST.REPORT.SECONDARY_ACTIONS.REOPEN]: { + text: translate('iou.undoClose'), + icon: Expensicons.CircularArrowBackwards, + value: CONST.REPORT.SECONDARY_ACTIONS.REOPEN, + onSelected: () => { + if (isExported) { + setIsReopenWarningModalVisible(true); + return; + } + reopenReport(moneyRequestReport); + }, + }, [CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE]: { text: translate('iou.addExpense'), icon: Expensicons.Plus, @@ -699,6 +718,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea ); } + const reopenExportedReportWarningText = ( + + {translate('iou.headsUp')} + {translate('iou.reopenExportedReportConfirmation', {connectionName: integrationNameFromExportMessage ?? ''})} + + ); + return ( setIsUnapproveModalVisible(false)} prompt={unapproveWarningText} /> + { + setIsReopenWarningModalVisible(false); + reopenReport(moneyRequestReport); + }} + cancelText={translate('common.cancel')} + onCancel={() => setIsReopenWarningModalVisible(false)} + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + prompt={reopenExportedReportWarningText} + /> + `This report has already been exported to ${connectionName}. Changing it may lead to data discrepancies. Are you sure you want to reopen this report?`, reason: 'Reason', holdReasonRequired: 'A reason is required when holding.', expenseWasPutOnHold: 'Expense was put on hold', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3e4da544c286..fdd46c0049f1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1114,6 +1114,11 @@ const translations = { heldExpense: 'retuvo este gasto', unheldExpense: 'desbloqueó este gasto', explainHold: 'Explica la razón para retener esta solicitud.', + undoClose: 'Deshacer cierre', + reopened: 'reabrir', + reopenReport: 'Reabrir informe', + reopenExportedReportConfirmation: ({connectionName}: {connectionName: string}) => + `Este informe ya ha sido exportado a ${connectionName}. Cambiarlo puede provocar discrepancias en los datos. ¿Estás seguro de que deseas reabrir este informe?`, reason: 'Razón', holdReasonRequired: 'Se requiere una razón para retener.', expenseWasPutOnHold: 'Este gasto está retenido', diff --git a/src/libs/API/parameters/ReopenReportParams.ts b/src/libs/API/parameters/ReopenReportParams.ts new file mode 100644 index 000000000000..9b4845ed1084 --- /dev/null +++ b/src/libs/API/parameters/ReopenReportParams.ts @@ -0,0 +1,6 @@ +type ReopenReportParams = { + reportID: string; + reportActionID: string; +}; + +export default ReopenReportParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 0c0e57557350..8994a8afc38b 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -394,3 +394,4 @@ export type {default as GetEmphemeralTokenParams} from './GetEmphemeralTokenPara export type {default as CreateAppleDigitalWalletParams} from './CreateAppleDigitalWalletParams'; export type {default as CompleteConciergeCallParams} from './CompleteConciergeCallParams'; export type {default as FinishCorpayBankAccountOnboardingParams} from './FinishCorpayBankAccountOnboardingParams'; +export type {default as ReopenReportParams} from './ReopenReportParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4616e906bfe0..2910c542b038 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -475,6 +475,7 @@ const WRITE_COMMANDS = { PAY_AND_DOWNGRADE: 'PayAndDowngrade', COMPLETE_CONCIERGE_CALL: 'CompleteConciergeCall', FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING: 'FinishCorpayBankAccountOnboarding', + REOPEN_REPORT: 'ReopenReport', GET_GUIDE_CALL_AVAILABILITY_SCHEDULE: 'GetGuideCallAvailabilitySchedule', } as const; @@ -820,6 +821,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_PROHIBITED_EXPENSES]: Parameters.SetPolicyProhibitedExpensesParams; [WRITE_COMMANDS.COMPLETE_CONCIERGE_CALL]: Parameters.CompleteConciergeCallParams; [WRITE_COMMANDS.FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING]: Parameters.FinishCorpayBankAccountOnboardingParams; + [WRITE_COMMANDS.REOPEN_REPORT]: Parameters.ReopenReportParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 251a0db581b9..006f7bca99c6 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -55,6 +55,7 @@ import { getMessageOfOldDotReportAction, getOneTransactionThreadReportID, getOriginalMessage, + getReopenedMessage, getReportActionHtml, getReportActionMessageText, getSortedReportActions, @@ -818,6 +819,8 @@ function getLastMessageTextForReport( lastMessageTextFromReport = translateLocal('violations.resolvedDuplicates'); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION)) { lastMessageTextFromReport = getUpdateRoomDescriptionMessage(lastReportAction); + } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.REOPENED)) { + lastMessageTextFromReport = getReopenedMessage(); } // we do not want to show report closed in LHN for non archived report so use getReportLastMessage as fallback instead of lastMessageText from report diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index d71a92b58868..9125a311db68 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -70,6 +70,10 @@ function canUsePrivateDomainOnboarding(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.PRIVATE_DOMAIN_ONBOARDING) || canUseAllBetas(betas); } +function canUseRetractNewDot(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.RETRACT_NEWDOT) || canUseAllBetas(betas); +} + function canUseCallScheduling() { return false; } @@ -90,5 +94,6 @@ export default { canUseInAppProvisioning, canUseGlobalReimbursementsOnND, canUsePrivateDomainOnboarding, + canUseRetractNewDot, canUseCallScheduling, }; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 476ddc9343c4..9911efba4063 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1537,6 +1537,10 @@ function getLeaveRoomMessage() { return translateLocal('report.actions.type.leftTheChat'); } +function getReopenedMessage(): string { + return translateLocal('iou.reopened'); +} + function getUpdateRoomDescriptionFragment(reportAction: ReportAction): Message { const html = getUpdateRoomDescriptionMessage(reportAction); return { @@ -1568,6 +1572,11 @@ function getReportActionMessageFragments(action: ReportAction): Message[] { return [{text: message, html: `${message}`, type: 'COMMENT'}]; } + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REOPENED)) { + const message = getReopenedMessage(); + return [{text: message, html: `${message}`, type: 'COMMENT'}]; + } + const actionMessage = action.previousMessage ?? action.message; if (Array.isArray(actionMessage)) { return actionMessage.filter((item): item is Message => !!item); @@ -2519,6 +2528,7 @@ export { getWorkspaceReportFieldUpdateMessage, getWorkspaceReportFieldDeleteMessage, getReportActions, + getReopenedMessage, getLeaveRoomMessage, }; diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 2322efdabeb8..6ca869d4c6b6 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -347,6 +347,25 @@ function isDeleteAction(report: Report, reportTransactions: Transaction[]): bool return isReportOpen || isProcessingReport; } +function isReopenAction(report: Report, policy?: Policy): boolean { + const isExpenseReport = isExpenseReportUtils(report); + if (!isExpenseReport) { + return false; + } + + const isClosedReport = isClosedReportUtils(report); + if (!isClosedReport) { + return false; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + if (!isAdmin) { + return false; + } + + return true; +} + function getSecondaryReportActions( report: Report, reportTransactions: Transaction[], @@ -354,6 +373,7 @@ function getSecondaryReportActions( policy?: Policy, reportNameValuePairs?: ReportNameValuePairs, reportActions?: ReportAction[], + canUseRetractNewDot?: boolean, ): Array> { const options: Array> = []; @@ -385,6 +405,10 @@ function getSecondaryReportActions( options.push(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED); } + if (canUseRetractNewDot && isReopenAction(report, policy)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.REOPEN); + } + if (isHoldAction(report, reportTransactions)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 56b242bd3210..9d2a71a1b022 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12,6 +12,7 @@ import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type { OriginalMessageChangePolicy, + OriginalMessageExportIntegration, OriginalMessageIOU, OriginalMessageModifiedExpense, OriginalMessageMovedTransaction, @@ -151,6 +152,7 @@ import { getPolicyChangeLogDefaultTitleEnforcedMessage, getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, getRenamedAction, + getReopenedMessage, getReportAction, getReportActionHtml, getReportActionMessage as getReportActionMessageReportUtils, @@ -456,6 +458,11 @@ type OptimisticHoldReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachmentOnly' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' >; +type OptimisticReopenedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachmentOnly' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; + type OptimisticCancelPaymentReportAction = Pick< ReportAction, 'actionName' | 'actorAccountID' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' @@ -4701,6 +4708,9 @@ function getReportNameInternal({ if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) { return getRejectedReportMessage(); } + if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REOPENED) { + return getReopenedMessage(); + } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE) { return getUpgradeWorkspaceMessage(); } @@ -6838,6 +6848,33 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt }; } +function buildOptimisticReopenedReportAction(created = DateUtils.getDBTime()): OptimisticReopenedReportAction { + return { + reportActionID: rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.REOPENED, + actorAccountID: currentUserAccountID, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: 'reopened', + html: 'reopened', + }, + ], + person: [ + { + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + }, + ], + automatic: false, + avatar: getCurrentUserAvatar(), + created, + shouldShow: true, + }; +} + function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): OptimisticEditedTaskReportAction { // We do not modify title & description in one request, so we need to create a different optimistic action for each field modification let field = ''; @@ -10335,6 +10372,23 @@ function canBeExported(report: OnyxEntry) { return isExpenseReport(report) && isCorrectState; } +function getIntegrationNameFromExportMessage(reportActions: OnyxEntry | ReportAction[]) { + if (!reportActions) { + return ''; + } + + if (Array.isArray(reportActions)) { + const exportIntegrationAction = reportActions.find((action) => isExportIntegrationAction(action)); + if (!exportIntegrationAction || !isExportIntegrationAction(exportIntegrationAction)) { + return null; + } + + const originalMessage = (getOriginalMessage(exportIntegrationAction) ?? {}) as OriginalMessageExportIntegration; + const {label} = originalMessage; + return label ?? null; + } +} + function isExported(reportActions: OnyxEntry | ReportAction[]) { if (!reportActions) { return false; @@ -10691,6 +10745,7 @@ export { buildOptimisticGroupChatReport, buildOptimisticHoldReportAction, buildOptimisticHoldReportActionComment, + buildOptimisticReopenedReportAction, buildOptimisticIOUReport, buildOptimisticIOUReportAction, buildOptimisticModifiedExpenseReportAction, @@ -10998,6 +11053,7 @@ export { getReportLastVisibleActionCreated, getMostRecentlyVisitedReport, getSourceIDFromReportAction, + getIntegrationNameFromExportMessage, // This will get removed as part of https://github.com/Expensify/App/issues/59961 // eslint-disable-next-line deprecation/deprecation diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 7cd66ff50561..99ee6332e651 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -39,6 +39,7 @@ import { getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, getRemovedConnectionMessage, getRenamedAction, + getReopenedMessage, getReportAction, getReportActionMessageText, getSortedReportActions, @@ -662,6 +663,8 @@ function getOptionData({ result.alternateText = getReportActionMessageText(lastAction) ?? ''; } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION) { result.alternateText = getRemovedConnectionMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REOPENED) { + result.alternateText = getReopenedMessage(); } else { result.alternateText = lastMessageTextFromReport.length > 0 diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 592e66282bcc..fa18d1a98201 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -19,6 +19,7 @@ import type { MergeDuplicatesParams, PayInvoiceParams, PayMoneyRequestParams, + ReopenReportParams, ReplaceReceiptParams, RequestMoneyParams, ResolveDuplicatesParams, @@ -107,6 +108,7 @@ import { buildOptimisticModifiedExpenseReportAction, buildOptimisticMoneyRequestEntities, buildOptimisticMovedTransactionAction, + buildOptimisticReopenedReportAction, buildOptimisticReportPreview, buildOptimisticResolvedDuplicatesReportAction, buildOptimisticSubmittedReportAction, @@ -9167,6 +9169,129 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); } +function reopenReport(expenseReport: OnyxEntry) { + if (!expenseReport) { + return; + } + + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; + const optimisticReopenedReportAction = buildOptimisticReopenedReportAction(); + const predictedNextState = CONST.REPORT.STATE_NUM.OPEN; + const predictedNextStatus = CONST.REPORT.STATUS_NUM.OPEN; + + const optimisticNextStep = buildNextStep(expenseReport, predictedNextStatus); + const optimisticReportActionsData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticReopenedReportAction.reportActionID]: { + ...(optimisticReopenedReportAction as OnyxTypes.ReportAction), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }; + const optimisticIOUReportData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + ...expenseReport, + lastMessageText: getReportActionText(optimisticReopenedReportAction), + lastMessageHtml: getReportActionHtml(optimisticReopenedReportAction), + stateNum: predictedNextState, + statusNum: predictedNextStatus, + pendingFields: { + partial: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }; + + const optimisticNextStepData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }; + + const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData, optimisticNextStepData]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticReopenedReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + pendingFields: { + partial: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticReopenedReportAction.reportActionID]: { + pendingAction: null, + errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: currentNextStep, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + stateNum: expenseReport.stateNum, + statusNum: expenseReport.statusNum, + }, + }, + ]; + + if (expenseReport.parentReportID && expenseReport.parentReportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.parentReportID}`, + value: { + [expenseReport.parentReportActionID]: { + childStateNum: predictedNextState, + childStatusNum: predictedNextStatus, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.parentReportID}`, + value: { + [expenseReport.parentReportActionID]: { + childStateNum: expenseReport.stateNum, + childStatusNum: expenseReport.statusNum, + }, + }, + }); + } + + const parameters: ReopenReportParams = { + reportID: expenseReport.reportID, + reportActionID: optimisticReopenedReportAction.reportActionID, + }; + + API.write(WRITE_COMMANDS.REOPEN_REPORT, parameters, {optimisticData, successData, failureData}); +} + function unapproveExpenseReport(expenseReport: OnyxEntry) { if (isEmptyObject(expenseReport)) { return; @@ -10743,6 +10868,7 @@ export { adjustRemainingSplitShares, getNextApproverAccountID, approveMoneyRequest, + reopenReport, canApproveIOU, canUnapproveIOU, cancelPayment, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 36aa086be5e7..825b3e3db924 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -38,6 +38,7 @@ import { getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, getRemovedConnectionMessage, getRenamedAction, + getReopenedMessage, getReportActionMessageText, getUpdateRoomDescriptionMessage, getWorkspaceCategoryUpdateMessage, @@ -615,6 +616,8 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getPolicyChangeLogDeleteMemberMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { setClipboardMessage(getDeletedTransactionMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REOPENED) { + setClipboardMessage(getReopenedMessage()); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { const {label, errorMessage} = getOriginalMessage(reportAction) ?? {label: '', errorMessage: ''}; setClipboardMessage(translateLocal('report.actions.type.integrationSyncFailed', {label, errorMessage})); diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index e4b2ee3f9967..a14eef997c4a 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -72,6 +72,7 @@ import { getRemovedConnectionMessage, getRemovedFromApprovalChainMessage, getRenamedAction, + getReopenedMessage, getReportActionMessage, getReportActionText, getWhisperedTo, @@ -1016,6 +1017,8 @@ function PureReportActionItem({ children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REOPENED) { + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index e607d60e334f..20d075e9ff29 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -826,6 +826,7 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED]: OriginalMessageIntegrationSyncFailed; [CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION]: OriginalMessageDeletedTransaction; [CONST.REPORT.ACTIONS.TYPE.CONCIERGE_CATEGORY_OPTIONS]: OriginalMessageConciergeCategoryOptions; + [CONST.REPORT.ACTIONS.TYPE.REOPENED]: never; } & OldDotOriginalMessageMap & { [T in ValueOf]: OriginalMessagePolicyChangeLog; } & {