Skip to content

Commit

Permalink
Merge pull request #44229 from rushatgabhane/unapprove-button
Browse files Browse the repository at this point in the history
[Unapprove] Add unapprove feature to approved reports
  • Loading branch information
Beamanator authored Jul 8, 2024
2 parents b0947e0 + c90b703 commit a280f82
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 4 deletions.
9 changes: 9 additions & 0 deletions assets/images/circular-arrow-backwards.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -2335,6 +2335,7 @@ const CONST = {
PRIVATE_NOTES: 'privateNotes',
DELETE: 'delete',
MARK_AS_INCOMPLETE: 'markAsIncomplete',
UNAPPROVE: 'unapprove',
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -201,6 +202,7 @@ export {
Wrench,
BackArrow,
Bank,
CircularArrowBackwards,
Bill,
Bell,
BellSlash,
Expand Down
20 changes: 20 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
20 changes: 20 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/UnapproveExpenseReportParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type UnapproveExpenseReportParams = {
reportID: string;
reportActionID: string;
};

export default UnapproveExpenseReportParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ type OptimisticApprovedReportAction = Pick<
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;

type OptimisticUnapprovedReportAction = Pick<
ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.UNAPPROVED>,
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;

type OptimisticSubmittedReportAction = Pick<
ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.SUBMITTED>,
| 'actionName'
Expand Down Expand Up @@ -778,6 +783,13 @@ function isReportApproved(reportOrID: OnyxInputOrEntry<Report> | 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<Report>): 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.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -7039,6 +7084,7 @@ export {
areAllRequestsBeingSmartScanned,
buildOptimisticAddCommentReportAction,
buildOptimisticApprovedReportAction,
buildOptimisticUnapprovedReportAction,
buildOptimisticCancelPaymentReportAction,
buildOptimisticChangedTaskAssigneeReportAction,
buildOptimisticChatReport,
Expand Down Expand Up @@ -7251,6 +7297,7 @@ export {
isPublicAnnounceRoom,
isPublicRoom,
isReportApproved,
isReportManuallyReimbursed,
isReportDataReady,
isReportFieldDisabled,
isReportFieldOfTypeTitle,
Expand Down
91 changes: 91 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
StartSplitBillParams,
SubmitReportParams,
TrackExpenseParams,
UnapproveExpenseReportParams,
UpdateMoneyRequestParams,
} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
Expand Down Expand Up @@ -6359,6 +6360,95 @@ function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, full?:
API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData});
}

function unapproveExpenseReport(expenseReport: OnyxEntry<OnyxTypes.Report>) {
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));
Expand Down Expand Up @@ -7016,6 +7106,7 @@ function getIOURequestPolicyID(transaction: OnyxEntry<OnyxTypes.Transaction>, re

export {
approveMoneyRequest,
unapproveExpenseReport,
canApproveIOU,
canIOUBePaid,
cancelPayment,
Expand Down
Loading

0 comments on commit a280f82

Please sign in to comment.