diff --git a/assets/images/circular-arrow-backwards.svg b/assets/images/circular-arrow-backwards.svg
new file mode 100644
index 000000000000..209c0aea5fa7
--- /dev/null
+++ b/assets/images/circular-arrow-backwards.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/CONST.ts b/src/CONST.ts
index 8ecdadefc4e9..2e2608d773ae 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -719,7 +719,7 @@ const CONST = {
TASK_EDITED: 'TASKEDITED',
TASK_REOPENED: 'TASKREOPENED',
TRIPPREVIEW: 'TRIPPREVIEW',
- UNAPPROVED: 'UNAPPROVED', // OldDot Action
+ UNAPPROVED: 'UNAPPROVED',
UNHOLD: 'UNHOLD',
UNSHARE: 'UNSHARE', // OldDot Action
UPDATE_GROUP_CHAT_MEMBER_ROLE: 'UPDATEGROUPCHATMEMBERROLE',
@@ -2335,6 +2335,7 @@ const CONST = {
PRIVATE_NOTES: 'privateNotes',
DELETE: 'delete',
MARK_AS_INCOMPLETE: 'markAsIncomplete',
+ UNAPPROVE: 'unapprove',
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index a0d7a5cb8883..487df5594212 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -42,6 +42,7 @@ import ChatBubbles from '@assets/images/chatbubbles.svg';
import CheckCircle from '@assets/images/check-circle.svg';
import CheckmarkCircle from '@assets/images/checkmark-circle.svg';
import Checkmark from '@assets/images/checkmark.svg';
+import CircularArrowBackwards from '@assets/images/circular-arrow-backwards.svg';
import Close from '@assets/images/close.svg';
import ClosedSign from '@assets/images/closed-sign.svg';
import Coins from '@assets/images/coins.svg';
@@ -201,6 +202,7 @@ export {
Wrench,
BackArrow,
Bank,
+ CircularArrowBackwards,
Bill,
Bell,
BellSlash,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index e3e080f26201..a72e07701c8a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -814,6 +814,11 @@ export default {
removed: 'removed',
transactionPending: 'Transaction pending.',
chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`,
+ unapprove: 'Unapprove',
+ unapproveReport: 'Unapprove report',
+ headsUp: 'Heads up!',
+ unapproveWithIntegrationWarning: (accountingIntegration: string) =>
+ `This report has already been exported to ${accountingIntegration}. Changes to this report in Expensify may lead to data discrepancies and Expensify Card reconciliation issues. Are you sure you want to unapprove this report?`,
},
notificationPreferencesPage: {
header: 'Notification preferences',
@@ -2759,6 +2764,21 @@ export default {
xero: 'Xero',
netsuite: 'NetSuite',
intacct: 'Sage Intacct',
+ connectionName: (integration: ConnectionName) => {
+ switch (integration) {
+ case CONST.POLICY.CONNECTIONS.NAME.QBO:
+ return 'Quickbooks Online';
+ case CONST.POLICY.CONNECTIONS.NAME.XERO:
+ return 'Xero';
+ case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
+ return 'NetSuite';
+ case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
+ return 'Sage Intacct';
+ default: {
+ return '';
+ }
+ }
+ },
setup: 'Connect',
lastSync: (relativeDate: string) => `Last synced ${relativeDate}`,
import: 'Import',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 9e104ca9b1bb..c88b273ae05d 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -817,6 +817,11 @@ export default {
removed: 'eliminó',
transactionPending: 'Transacción pendiente.',
chooseARate: ({unit}: ReimbursementRateParams) => `Selecciona una tasa de reembolso por ${unit} del espacio de trabajo`,
+ unapprove: 'Desaprobar',
+ unapproveReport: 'Anular la aprobación del informe',
+ headsUp: 'Atención!',
+ unapproveWithIntegrationWarning: (accountingIntegration: string) =>
+ `Este informe ya se ha exportado a ${accountingIntegration}. Los cambios realizados en este informe en Expensify pueden provocar discrepancias en los datos y problemas de conciliación de la tarjeta Expensify. ¿Está seguro de que desea anular la aprobación de este informe?`,
},
notificationPreferencesPage: {
header: 'Preferencias de avisos',
@@ -2741,6 +2746,21 @@ export default {
xero: 'Xero',
netsuite: 'NetSuite',
intacct: 'Sage Intacct',
+ connectionName: (integration: ConnectionName) => {
+ switch (integration) {
+ case CONST.POLICY.CONNECTIONS.NAME.QBO:
+ return 'Quickbooks Online';
+ case CONST.POLICY.CONNECTIONS.NAME.XERO:
+ return 'Xero';
+ case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
+ return 'NetSuite';
+ case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
+ return 'Sage Intacct';
+ default: {
+ return '';
+ }
+ }
+ },
setup: 'Configurar',
lastSync: (relativeDate: string) => `Recién sincronizado ${relativeDate}`,
import: 'Importar',
diff --git a/src/libs/API/parameters/UnapproveExpenseReportParams.ts b/src/libs/API/parameters/UnapproveExpenseReportParams.ts
new file mode 100644
index 000000000000..ba25424aeda6
--- /dev/null
+++ b/src/libs/API/parameters/UnapproveExpenseReportParams.ts
@@ -0,0 +1,6 @@
+type UnapproveExpenseReportParams = {
+ reportID: string;
+ reportActionID: string;
+};
+
+export default UnapproveExpenseReportParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index a49cb68fd04f..096e59f399f6 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -153,6 +153,7 @@ export type {default as CreateDistanceRequestParams} from './CreateDistanceReque
export type {default as StartSplitBillParams} from './StartSplitBillParams';
export type {default as SendMoneyParams} from './SendMoneyParams';
export type {default as ApproveMoneyRequestParams} from './ApproveMoneyRequestParams';
+export type {default as UnapproveExpenseReportParams} from './UnapproveExpenseReportParams';
export type {default as EditMoneyRequestParams} from './EditMoneyRequestParams';
export type {default as ReplaceReceiptParams} from './ReplaceReceiptParams';
export type {default as SubmitReportParams} from './SubmitReportParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index dae65e7792bc..a1a5b91d7d99 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -172,6 +172,7 @@ const WRITE_COMMANDS = {
SEND_MONEY_ELSEWHERE: 'SendMoneyElsewhere',
SEND_MONEY_WITH_WALLET: 'SendMoneyWithWallet',
APPROVE_MONEY_REQUEST: 'ApproveMoneyRequest',
+ UNAPPROVE_EXPENSE_REPORT: 'UnapproveExpenseReport',
EDIT_MONEY_REQUEST: 'EditMoneyRequest',
REPLACE_RECEIPT: 'ReplaceReceipt',
SUBMIT_REPORT: 'SubmitReport',
@@ -432,6 +433,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SEND_MONEY_ELSEWHERE]: Parameters.SendMoneyParams;
[WRITE_COMMANDS.SEND_MONEY_WITH_WALLET]: Parameters.SendMoneyParams;
[WRITE_COMMANDS.APPROVE_MONEY_REQUEST]: Parameters.ApproveMoneyRequestParams;
+ [WRITE_COMMANDS.UNAPPROVE_EXPENSE_REPORT]: Parameters.UnapproveExpenseReportParams;
[WRITE_COMMANDS.EDIT_MONEY_REQUEST]: Parameters.EditMoneyRequestParams;
[WRITE_COMMANDS.REPLACE_RECEIPT]: Parameters.ReplaceReceiptParams;
[WRITE_COMMANDS.SUBMIT_REPORT]: Parameters.SubmitReportParams;
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index e8e7b89e34f2..e8a6e8e58408 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -1145,7 +1145,6 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) {
CONST.REPORT.ACTIONS.TYPE.SHARE,
CONST.REPORT.ACTIONS.TYPE.STRIPE_PAID,
CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL,
- CONST.REPORT.ACTIONS.TYPE.UNAPPROVED,
CONST.REPORT.ACTIONS.TYPE.UNSHARE,
CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT,
CONST.REPORT.ACTIONS.TYPE.DONATION,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 342e2439ed66..0493babdb806 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -187,6 +187,11 @@ type OptimisticApprovedReportAction = Pick<
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;
+type OptimisticUnapprovedReportAction = Pick<
+ ReportAction,
+ 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
+>;
+
type OptimisticSubmittedReportAction = Pick<
ReportAction,
| 'actionName'
@@ -778,6 +783,13 @@ function isReportApproved(reportOrID: OnyxInputOrEntry | string, parentR
return report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED;
}
+/**
+ * Checks if the supplied report has been manually reimbursed
+ */
+function isReportManuallyReimbursed(report: OnyxEntry): boolean {
+ return report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
+}
+
/**
* Checks if the supplied report is an expense report in Open state and status.
*/
@@ -4039,6 +4051,9 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num
case CONST.REPORT.ACTIONS.TYPE.APPROVED:
iouMessage = `approved ${amount}`;
break;
+ case CONST.REPORT.ACTIONS.TYPE.UNAPPROVED:
+ iouMessage = `unapproved ${amount}`;
+ break;
case CONST.IOU.REPORT_ACTION_TYPE.CREATE:
iouMessage = `submitted ${amount}${comment && ` for ${comment}`}`;
break;
@@ -4201,6 +4216,36 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
};
}
+/**
+ * Builds an optimistic APPROVED report action with a randomly generated reportActionID.
+ */
+function buildOptimisticUnapprovedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticUnapprovedReportAction {
+ return {
+ actionName: CONST.REPORT.ACTIONS.TYPE.UNAPPROVED,
+ actorAccountID: currentUserAccountID,
+ automatic: false,
+ avatar: getCurrentUserAvatar(),
+ isAttachment: false,
+ originalMessage: {
+ amount,
+ currency,
+ expenseReportID,
+ },
+ message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, Math.abs(amount), '', currency),
+ person: [
+ {
+ style: 'strong',
+ text: getCurrentUserDisplayNameOrEmail(),
+ type: 'TEXT',
+ },
+ ],
+ reportActionID: NumberUtils.rand64(),
+ shouldShow: true,
+ created: DateUtils.getDBTime(),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ };
+}
+
/**
* Builds an optimistic MOVED report action with a randomly generated reportActionID.
* This action is used when we move reports across workspaces.
@@ -7039,6 +7084,7 @@ export {
areAllRequestsBeingSmartScanned,
buildOptimisticAddCommentReportAction,
buildOptimisticApprovedReportAction,
+ buildOptimisticUnapprovedReportAction,
buildOptimisticCancelPaymentReportAction,
buildOptimisticChangedTaskAssigneeReportAction,
buildOptimisticChatReport,
@@ -7251,6 +7297,7 @@ export {
isPublicAnnounceRoom,
isPublicRoom,
isReportApproved,
+ isReportManuallyReimbursed,
isReportDataReady,
isReportFieldDisabled,
isReportFieldOfTypeTitle,
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 48c70021cacc..9fc8f40ae770 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -24,6 +24,7 @@ import type {
StartSplitBillParams,
SubmitReportParams,
TrackExpenseParams,
+ UnapproveExpenseReportParams,
UpdateMoneyRequestParams,
} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
@@ -6359,6 +6360,95 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?:
API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData});
}
+function unapproveExpenseReport(expenseReport: OnyxEntry) {
+ if (isEmptyObject(expenseReport)) {
+ return;
+ }
+
+ const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null;
+
+ const optimisticUnapprovedReportAction = ReportUtils.buildOptimisticUnapprovedReportAction(expenseReport.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID);
+ const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED);
+
+ const optimisticReportActionData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticUnapprovedReportAction.reportActionID]: {
+ ...(optimisticUnapprovedReportAction 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: ReportActionsUtils.getReportActionText(optimisticUnapprovedReportAction),
+ lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticUnapprovedReportAction),
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ 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, optimisticReportActionData, optimisticNextStepData];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticUnapprovedReportAction.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: {
+ [optimisticUnapprovedReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
+ value: currentNextStep,
+ },
+ ];
+
+ const parameters: UnapproveExpenseReportParams = {
+ reportID: expenseReport.reportID,
+ reportActionID: optimisticUnapprovedReportAction.reportActionID,
+ };
+
+ API.write(WRITE_COMMANDS.UNAPPROVE_EXPENSE_REPORT, parameters, {optimisticData, successData, failureData});
+}
+
function submitReport(expenseReport: OnyxTypes.Report) {
if (expenseReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(expenseReport.policyID)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID));
@@ -7016,6 +7106,7 @@ function getIOURequestPolicyID(transaction: OnyxEntry, re
export {
approveMoneyRequest,
+ unapproveExpenseReport,
canApproveIOU,
canIOUBePaid,
cancelPayment,
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 58a0fe1a80b8..c6e25bcaa70a 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -21,6 +21,7 @@ import PromotedActionsBar, {PromotedActions} from '@components/PromotedActionsBa
import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -107,6 +108,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
+ const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`], [policies, report?.policyID]);
const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]);
const isPolicyEmployee = useMemo(() => PolicyUtils.isPolicyEmployee(report?.policyID ?? '-1', policies), [report?.policyID, policies]);
@@ -179,7 +181,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
const isDeletedParentAction = ReportActionsUtils.isDeletedAction(requestParentReportAction);
- const moneyRequestReport = useMemo(() => {
+ const moneyRequestReport: OnyxEntry = useMemo(() => {
if (caseID === CASES.MONEY_REQUEST) {
return parentReport;
}
@@ -197,6 +199,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction;
const shouldShowDeleteButton = shouldShowTaskDeleteButton || canDeleteRequest;
+ const canUnapproveRequest =
+ ReportUtils.isMoneyRequestReport(moneyRequestReport) &&
+ (ReportUtils.isReportManager(moneyRequestReport) || isPolicyAdmin) &&
+ (ReportUtils.isReportApproved(moneyRequestReport) || ReportUtils.isReportManuallyReimbursed(moneyRequestReport));
+
useEffect(() => {
if (canDeleteRequest) {
return;
@@ -224,6 +231,15 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
Report.leaveGroupChat(report.reportID);
}, [isChatRoom, isPolicyEmployee, isPolicyExpenseChat, report.reportID, report.visibility]);
+ const unapproveExpenseReportOrShowModal = useCallback(() => {
+ if (PolicyUtils.hasAccountingConnections(policy)) {
+ setIsUnapproveModalVisible(true);
+ return;
+ }
+ Navigation.dismissModal();
+ IOU.unapproveExpenseReport(moneyRequestReport);
+ }, [moneyRequestReport, policy]);
+
const shouldShowLeaveButton =
!isThread && (isGroupChat || (isChatRoom && ReportUtils.canLeaveChat(report, policy)) || (isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !isPolicyAdmin));
@@ -356,6 +372,16 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
},
});
}
+
+ if (canUnapproveRequest) {
+ items.push({
+ key: CONST.REPORT_DETAILS_MENU_ITEM.UNAPPROVE,
+ icon: Expensicons.CircularArrowBackwards,
+ translationKey: 'iou.unapprove',
+ isAnonymousAction: false,
+ action: () => unapproveExpenseReportOrShowModal(),
+ });
+ }
return items;
}, [
isSelfDM,
@@ -380,6 +406,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
isPolicyAdmin,
session,
leaveChat,
+ canUnapproveRequest,
+ unapproveExpenseReportOrShowModal,
]);
const displayNamesWithTooltips = useMemo(() => {
@@ -399,6 +427,14 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
/>
) : null;
+ const connectedIntegration = Object.values(CONST.POLICY.CONNECTIONS.NAME).find((integration) => !!policy?.connections?.[integration]);
+ const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', connectedIntegration) : '';
+ const unapproveWarningText = (
+
+ {translate('iou.headsUp')} {translate('iou.unapproveWithIntegrationWarning', connectedIntegrationName)}
+
+ );
+
const renderedAvatar = useMemo(() => {
if (isMoneyRequestReport || isInvoiceReport) {
return (
@@ -686,6 +722,20 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
danger
shouldEnableNewFocusManagement
/>
+ {
+ setIsUnapproveModalVisible(false);
+ Navigation.dismissModal();
+ IOU.unapproveExpenseReport(moneyRequestReport);
+ }}
+ cancelText={translate('common.cancel')}
+ onCancel={() => setIsUnapproveModalVisible(false)}
+ prompt={unapproveWarningText}
+ />
);
diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx
index 27227251b0b6..088ee9eb2b6e 100644
--- a/src/pages/home/report/ReportActionItemFragment.tsx
+++ b/src/pages/home/report/ReportActionItemFragment.tsx
@@ -69,6 +69,7 @@ const MUTED_ACTIONS = [
...Object.values(CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG),
CONST.REPORT.ACTIONS.TYPE.IOU,
CONST.REPORT.ACTIONS.TYPE.APPROVED,
+ CONST.REPORT.ACTIONS.TYPE.UNAPPROVED,
CONST.REPORT.ACTIONS.TYPE.MOVED,
CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST,
] as ReportActionName[];
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index c7d9397f3202..3d864523e418 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -402,6 +402,18 @@ type OriginalMessageApproved = {
expenseReportID: string;
};
+/** Model of `unapproved` report action */
+type OriginalMessageUnapproved = {
+ /** Unapproved expense amount */
+ amount: number;
+
+ /** Currency of the unapproved expense amount */
+ currency: string;
+
+ /** Report ID of the expense */
+ expenseReportID: string;
+};
+
/** The map type of original message */
type OriginalMessageMap = {
/** */
@@ -497,7 +509,7 @@ type OriginalMessageMap = {
/** */
[CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL]: never;
/** */
- [CONST.REPORT.ACTIONS.TYPE.UNAPPROVED]: never;
+ [CONST.REPORT.ACTIONS.TYPE.UNAPPROVED]: OriginalMessageUnapproved;
/** */
[CONST.REPORT.ACTIONS.TYPE.UNHOLD]: never;
/** */