Skip to content

[Retract] Add ability to REOPEN report #61370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down Expand Up @@ -1202,6 +1203,7 @@ const CONST = {
VIEW_DETAILS: 'viewDetails',
DELETE: 'delete',
ADD_EXPENSE: 'addExpense',
REOPEN: 'reopen',
},
PRIMARY_ACTIONS: {
SUBMIT: 'submit',
Expand Down Expand Up @@ -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
Expand Down
44 changes: 42 additions & 2 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,7 @@ import {
getArchiveReason,
getBankAccountRoute,
getIntegrationIcon,
getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils,
getMoneyRequestSpendBreakdown,
getNonHeldAndFullAmount,
getTransactionsWithReceipts,
Expand Down Expand Up @@ -65,6 +67,7 @@ import {
getNextApproverAccountID,
payInvoice,
payMoneyRequest,
reopenReport,
startMoneyRequest,
submitReport,
unapproveExpenseReport,
Expand Down Expand Up @@ -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<ExportType | null>(null);

Expand Down Expand Up @@ -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<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>, DropdownOption<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>>> = {
[CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -699,6 +718,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
);
}

const reopenExportedReportWarningText = (
<Text>
<Text style={[styles.textStrong, styles.noWrap]}>{translate('iou.headsUp')} </Text>
<Text>{translate('iou.reopenExportedReportConfirmation', {connectionName: integrationNameFromExportMessage ?? ''})}</Text>
</Text>
);

return (
<View style={[styles.pt0, styles.borderBottom]}>
<HeaderWithBackButton
Expand Down Expand Up @@ -890,6 +916,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
onCancel={() => setIsUnapproveModalVisible(false)}
prompt={unapproveWarningText}
/>
<ConfirmModal
title={translate('iou.reopenReport')}
isVisible={isReopenWarningModalVisible}
danger
confirmText={translate('iou.reopenReport')}
onConfirm={() => {
setIsReopenWarningModalVisible(false);
reopenReport(moneyRequestReport);
}}
cancelText={translate('common.cancel')}
onCancel={() => setIsReopenWarningModalVisible(false)}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
prompt={reopenExportedReportWarningText}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,11 @@ const translations = {
heldExpense: 'held this expense',
unheldExpense: 'unheld this expense',
explainHold: "Explain why you're holding this expense.",
undoClose: 'Undo close',
reopened: 'reopened',
reopenReport: 'Reopen report',
reopenExportedReportConfirmation: ({connectionName}: {connectionName: string}) =>
`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',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/ReopenReportParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type ReopenReportParams = {
reportID: string;
reportActionID: string;
};

export default ReopenReportParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
getMessageOfOldDotReportAction,
getOneTransactionThreadReportID,
getOriginalMessage,
getReopenedMessage,
getReportActionHtml,
getReportActionMessageText,
getSortedReportActions,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ function canUsePrivateDomainOnboarding(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.PRIVATE_DOMAIN_ONBOARDING) || canUseAllBetas(betas);
}

function canUseRetractNewDot(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.RETRACT_NEWDOT) || canUseAllBetas(betas);
}

function canUseCallScheduling() {
return false;
}
Expand All @@ -90,5 +94,6 @@ export default {
canUseInAppProvisioning,
canUseGlobalReimbursementsOnND,
canUsePrivateDomainOnboarding,
canUseRetractNewDot,
canUseCallScheduling,
};
10 changes: 10 additions & 0 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1568,6 +1572,11 @@ function getReportActionMessageFragments(action: ReportAction): Message[] {
return [{text: message, html: `<muted-text>${message}</muted-text>`, type: 'COMMENT'}];
}

if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REOPENED)) {
const message = getReopenedMessage();
return [{text: message, html: `<muted-text>${message}</muted-text>`, type: 'COMMENT'}];
}

const actionMessage = action.previousMessage ?? action.message;
if (Array.isArray(actionMessage)) {
return actionMessage.filter((item): item is Message => !!item);
Expand Down Expand Up @@ -2519,6 +2528,7 @@ export {
getWorkspaceReportFieldUpdateMessage,
getWorkspaceReportFieldDeleteMessage,
getReportActions,
getReopenedMessage,
getLeaveRoomMessage,
};

Expand Down
24 changes: 24 additions & 0 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,13 +347,33 @@ 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[],
violations: OnyxCollection<TransactionViolation[]>,
policy?: Policy,
reportNameValuePairs?: ReportNameValuePairs,
reportActions?: ReportAction[],
canUseRetractNewDot?: boolean,
): Array<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> {
const options: Array<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> = [];

Expand Down Expand Up @@ -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);
}
Expand Down
Loading