From 9054944828891069752adb31d42745713f29dba6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 18:31:12 +0530 Subject: [PATCH 01/30] Initial implementation of button --- assets/images/track-expense.svg | 1 + src/CONST.ts | 1 + src/components/Icon/Expensicons.ts | 2 ++ src/libs/Permissions.ts | 5 ++++ .../FloatingActionButtonAndPopover.js | 23 +++++++++++++------ 5 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 assets/images/track-expense.svg diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg new file mode 100644 index 000000000000..6fb7eb9befec --- /dev/null +++ b/assets/images/track-expense.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index 8abd4c087b16..a26bd61f3b1e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -307,6 +307,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + TRACK_EXPENSE: 'trackExpense', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 2a7ed30abf1a..5bdec7ca7174 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -135,6 +135,7 @@ import Sync from '@assets/images/sync.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; +import TrackExpense from '@assets/images/track-expense.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -302,4 +303,5 @@ export { ChatBubbleAdd, ChatBubbleUnread, Lightbulb, + TrackExpense, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index ce5e0e674c59..52276783576d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } +function canUseTrackExpense(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -39,5 +43,6 @@ export default { canUseCommentLinking, canUseLinkPreviews, canUseViolations, + canUseTrackExpense, canUseReportFields, }; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 573cbe370aa7..85c5ddd55dd8 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -9,6 +9,7 @@ import withNavigation from '@components/withNavigation'; import withNavigationFocus from '@components/withNavigationFocus'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -75,6 +76,7 @@ function FloatingActionButtonAndPopover(props) { const {translate} = useLocalize(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); + const {canUseTrackExpense} = usePermissions(); const prevIsFocused = usePrevious(props.isFocused); @@ -179,13 +181,20 @@ function FloatingActionButtonAndPopover(props) { text: translate('iou.sendMoney'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), }, - ...[ - { - icon: Expensicons.Task, - text: translate('newTaskPage.assignTask'), - onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), - }, - ], + ...(canUseTrackExpense + ? [ + { + icon: Expensicons.TrackExpense, + text: 'Track Expense', + onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + }, + ] + : []), + { + icon: Expensicons.Task, + text: translate('newTaskPage.assignTask'), + onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), + }, { icon: Expensicons.Heart, text: translate('sidebarScreen.saveTheWorld'), From c532babaa7b0fcbc8845cbf7c1578705c4f34453 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 19:17:03 +0530 Subject: [PATCH 02/30] Fixed svg --- assets/images/track-expense.svg | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg index 6fb7eb9befec..c15f28b72dd7 100644 --- a/assets/images/track-expense.svg +++ b/assets/images/track-expense.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + + + + \ No newline at end of file From 2feddbf24d392f85e37c3999b9f5e22f75f518e5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 21:19:04 +0530 Subject: [PATCH 03/30] Trying to start track expense flow --- src/CONST.ts | 2 ++ src/libs/IOUUtils.ts | 2 +- src/libs/ReportUtils.ts | 8 ++++++++ .../FloatingActionButtonAndPopover.js | 20 ++++++++++++++++++- src/pages/iou/request/IOURequestStartPage.js | 3 ++- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index a26bd61f3b1e..9b0afa627672 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -690,6 +690,7 @@ const CONST = { DOMAIN_ALL: 'domainAll', POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', + SELF_DM: 'selfDM', }, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', @@ -1264,6 +1265,7 @@ const CONST = { SEND: 'send', SPLIT: 'split', REQUEST: 'request', + TRACK_EXPENSE: 'track-expense', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 56ac47676a37..07bb22f43b31 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -104,7 +104,7 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { * Checks if the iou type is one of request, send, or split. */ function isValidMoneyRequestType(iouType: string): boolean { - const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND]; + const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE]; return moneyRequestType.includes(iouType); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 747ba27780a3..e634689041d5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4248,6 +4248,10 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return !isPolicyExpenseChat(report) || isOwnPolicyExpenseChat; } +function isSelfDM(report: OnyxEntry): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM; +} + /** * Helper method to define what money request options we want to show for particular method. * There are 3 money request options: Request, Split and Send: @@ -4284,6 +4288,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 1; let options: Array> = []; + if (isSelfDM(report)) { + options = [CONST.IOU.TYPE.TRACK_EXPENSE]; + } + // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option // unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 2 other people in the chat. diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 85c5ddd55dd8..bcf9c77ac2f7 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -51,6 +51,12 @@ const propTypes = { name: PropTypes.string, }), + /** The account details for the logged in user */ + account: PropTypes.shape({ + /** Whether or not the user is a policy admin */ + selfDMReportID: PropTypes.string, + }), + /** Indicated whether the report data is loading */ isLoading: PropTypes.bool, @@ -63,6 +69,7 @@ const defaultProps = { allPolicies: {}, isLoading: false, innerRef: null, + account: {}, }; /** @@ -186,7 +193,15 @@ function FloatingActionButtonAndPopover(props) { { icon: Expensicons.TrackExpense, text: 'Track Expense', - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest_temporaryForRefactor( + CONST.IOU.TYPE.TRACK_EXPENSE, + // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. + // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. + props.account.selfDMReportID || ReportUtils.generateReportID(), + ), + ), }, ] : []), @@ -255,5 +270,8 @@ export default compose( isLoading: { key: ONYXKEYS.IS_LOADING_APP, }, + account: { + key: ONYXKEYS.ACCOUNT, + }, }), )(FloatingActionButtonAndPopoverWithRef); diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 05e3d7c96311..8b7ef2a17973 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -75,6 +75,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), + [CONST.IOU.TYPE.TRACK_EXPENSE]: 'Track Expense', }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); @@ -103,7 +104,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK_EXPENSE; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7da97c34cc2b..7de121af52b4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -122,7 +122,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate) { + if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index b23420b5ef69..d6b90c0de439 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -182,7 +182,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate) { + if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } From f2741741f2ea556ab643f07dbd3e3a8fac2dd853 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 22:09:35 +0530 Subject: [PATCH 04/30] Minor fixes --- ...eyTemporaryForRefactorRequestConfirmationList.js | 7 +++++-- .../iou/request/step/IOURequestStepConfirmation.js | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8eeaeaf87eff..894525ca7a02 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -247,6 +247,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const {unit, rate, currency} = mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); @@ -370,7 +371,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; - if (isTypeSplit && iouAmount === 0) { + if (isTypeTrackExpense) { + text = "Track Expense"; + } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { text = translate('iou.request'); @@ -387,7 +390,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeSplit, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 0744fbd600a7..01b0c74d6f64 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -95,7 +95,16 @@ function IOURequestStepConfirmation({ const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); - const headerTitle = iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.split') : translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + const headerTitle = useMemo(() => { + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.splitBill'); + } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return 'Track Expense'; + } + return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + } + , [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { @@ -407,7 +416,7 @@ function IOURequestStepConfirmation({ Date: Wed, 28 Feb 2024 09:25:09 +0530 Subject: [PATCH 05/30] Temporary disable track expense distance --- src/pages/iou/request/IOURequestStartPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 8b7ef2a17973..b0d2983c55be 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -104,7 +104,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK_EXPENSE; + const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); From d6d6c89a2e013af7374de322808cef05d1d46eb5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 07:41:21 +0530 Subject: [PATCH 06/30] Removed duplicate function --- src/libs/ReportUtils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 11a32aa45b8d..2834240ecd82 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4313,10 +4313,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return !isPolicyExpenseChat(report) || isOwnPolicyExpenseChat; } -function isSelfDM(report: OnyxEntry): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM; -} - /** * Helper method to define what money request options we want to show for particular method. * There are 3 money request options: Request, Split and Send: From 7517fbf5cf3020add121de83952989dc47c57b9e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:26 +0530 Subject: [PATCH 07/30] fixed bad merge commit --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- src/pages/iou/request/step/IOURequestStepConfirmation.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 29ab4d53c55f..629f74205046 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -372,7 +372,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; if (isTypeTrackExpense) { - text = "Track Expense"; + text = 'Track Expense'; } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 73ec541fba4e..de5c6811d277 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -104,8 +104,7 @@ function IOURequestStepConfirmation({ return 'Track Expense'; } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); - } - , [iouType, transaction, translate]); + }, [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { From 6c6690cae4f420e8d5b8e14fc6c417e743fdda36 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:48 +0530 Subject: [PATCH 08/30] fixed bad merge commit --- src/libs/Permissions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 37ef44b80af9..4fef0f15ae49 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -28,6 +28,7 @@ function canUseViolations(betas: OnyxEntry): boolean { function canUseTrackExpense(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas); +} function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas); From 82e1fe56e4710afdb0a30824cdc6c736c3ab9aeb Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 14:41:24 +0530 Subject: [PATCH 09/30] Completing UI changes in Request for Track Expense --- ...oraryForRefactorRequestConfirmationList.js | 2 +- src/components/OptionRow.tsx | 7 +++- src/components/ReportWelcomeText.tsx | 10 +++--- src/languages/en.ts | 3 ++ src/languages/es.ts | 3 ++ src/libs/OptionsListUtils.ts | 32 +++++++++++++++++++ src/libs/ReportUtils.ts | 23 +++++++++++-- src/libs/actions/IOU.ts | 10 +++--- .../AttachmentPickerWithMenuItems.tsx | 11 +++++-- .../FloatingActionButtonAndPopover.js | 4 +-- src/pages/iou/request/IOURequestStartPage.js | 2 +- .../iou/request/step/IOURequestStepAmount.js | 2 +- .../step/IOURequestStepConfirmation.js | 15 +++------ .../request/step/IOURequestStepDistance.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- .../step/IOURequestStepTaxAmountPage.js | 1 + 17 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index a3d61e5ad813..e60c99fce6d7 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -381,7 +381,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; if (isTypeTrackExpense) { - text = 'Track Expense'; + text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 7b45fd963fe7..61319de5c56b 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -229,7 +229,12 @@ function OptionRow({ numberOfLines={isMultilineSupported ? 2 : 1} textStyles={displayNameStyle} shouldUseFullTitle={ - !!option.isChatRoom || !!option.isPolicyExpenseChat || !!option.isMoneyRequestReport || !!option.isThread || !!option.isTaskReport + !!option.isChatRoom || + !!option.isPolicyExpenseChat || + !!option.isMoneyRequestReport || + !!option.isThread || + !!option.isTaskReport || + !!option.isSelfDM } /> {option.alternateText ? ( diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index e9bbd0f27bdc..219199c25bc3 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -33,6 +34,7 @@ type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & { function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {canUseTrackExpense} = usePermissions(); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isChatRoom = ReportUtils.isChatRoom(report); const isSelfDM = ReportUtils.isSelfDM(report); @@ -42,7 +44,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense); const additionalText = moneyRequestOptions.map((item) => translate(`reportActionsView.iouTypes.${item}`)).join(', '); const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); const reportName = ReportUtils.getReportName(report); @@ -158,9 +160,9 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP ))} )} - {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && ( - {translate('reportActionsView.usePlusButton', {additionalText})} - )} + {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || + moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST) || + moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)) && {translate('reportActionsView.usePlusButton', {additionalText})}} ); diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a52cca62ef5..53a7758799d3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -503,6 +503,8 @@ export default { send: 'send money', split: 'split a bill', request: 'request money', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'track-expense': 'track an expense', }, }, reportAction: { @@ -592,6 +594,7 @@ export default { participants: 'Participants', requestMoney: 'Request money', sendMoney: 'Send money', + trackExpense: 'Track expense', pay: 'Pay', cancelPayment: 'Cancel payment', cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 013255c1e11e..aaee08c4a9e9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -496,6 +496,8 @@ export default { send: 'enviar dinero', split: 'dividir una factura', request: 'pedir dinero', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'track-expense': 'rastrear un gasto', }, }, reportAction: { @@ -585,6 +587,7 @@ export default { participants: 'Participantes', requestMoney: 'Pedir dinero', sendMoney: 'Enviar dinero', + trackExpense: 'Seguimiento de gastos', pay: 'Pagar', cancelPayment: 'Cancelar el pago', cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 07f0df962455..dbe60d04b45b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -249,9 +249,11 @@ Onyx.connect({ }); const policyExpenseReports: OnyxCollection = {}; +const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { + allReports[key] = report; if (!ReportUtils.isPolicyExpenseChat(report)) { return; } @@ -738,6 +740,35 @@ function createOption( return result; } +/** + * Get the option for a given report. + */ +function getReportOption(participant: Participant): ReportUtils.OptionData { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`]; + + const option = createOption( + report?.visibleChatMemberAccountIDs ?? [], + allPersonalDetails ?? {}, + report ?? null, + {}, + { + showChatPreviewLine: false, + forcePolicyNamePreview: false, + }, + ); + + // Update text & alternateText because createOption returns workspace name only if report is owned by the user + if (option.isSelfDM) { + option.alternateText = Localize.translateLocal('reportActionsView.yourSpace'); + } else { + option.text = ReportUtils.getPolicyName(report); + option.alternateText = Localize.translateLocal('workspace.common.workspace'); + } + option.selected = participant.selected; + option.isSelected = participant.selected; + return option; +} + /** * Get the option for a policy expense report. */ @@ -2068,6 +2099,7 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + getReportOption, }; export type {MemberForList, CategorySection, GetOptions}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 67991d71a559..5ac7ae562de3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -921,6 +921,15 @@ function isConciergeChatReport(report: OnyxEntry): boolean { return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } +function findSelfDMReportID(): string | undefined { + if (!allReports) { + return; + } + + const selfDMReport = Object.values(allReports).find((report) => isSelfDM(report) && !isThread(report)); + return selfDMReport?.reportID; +} + /** * Checks if the supplied report belongs to workspace based on the provided params. If the report's policyID is _FAKE_ or has no value, it means this report is a DM. * In this case report and workspace members must be compared to determine whether the report belongs to the workspace. @@ -4341,7 +4350,7 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o /** * Helper method to define what money request options we want to show for particular method. - * There are 3 money request options: Request, Split and Send: + * There are 4 money request options: Request, Split, Send and Track expense: * - Request option should show for: * - DMs * - own policy expense chats @@ -4353,13 +4362,16 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o * - chat/ policy rooms with more than 1 participants * - groups chats with 3 and more participants * - corporate workspace chats + * - Track expense option should show for: + * - Self DMs + * - admin rooms * * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. */ -function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[]): Array> { +function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], canUseTrackExpense = true): Array> { // In any thread or task report, we do not allow any new money requests yet - if (isChatThread(report) || isTaskReport(report) || isSelfDM(report)) { + if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report))) { return []; } @@ -4387,6 +4399,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; - const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) - ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: true, selected: true}] - : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + const shouldAddAsReport = iouType === CONST.IOU.TYPE.TRACK_EXPENSE && !isEmptyObject(chatReport) && (ReportUtils.isSelfDM(chatReport) || ReportUtils.isAdminRoom(chatReport)); + const participants: Participant[] = + ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport + ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] + : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 68c7f0883683..e1e9fe25efda 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -13,6 +13,7 @@ import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -115,6 +116,7 @@ function AttachmentPickerWithMenuItems({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowHeight} = useWindowDimensions(); + const {canUseTrackExpense} = usePermissions(); /** * Returns the list of IOU Options @@ -136,12 +138,17 @@ function AttachmentPickerWithMenuItems({ text: translate('iou.sendMoney'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID), }, + [CONST.IOU.TYPE.TRACK_EXPENSE]: { + icon: Expensicons.TrackExpense, + text: translate('iou.trackExpense'), + onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), + }, }; - return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({ + return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ ...options[option], })); - }, [report, policy, reportParticipantIDs, translate]); + }, [translate, report, policy, reportParticipantIDs, canUseTrackExpense]); /** * Determines if we can show the task option diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index bcf9c77ac2f7..c075b8a84b89 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -192,14 +192,14 @@ function FloatingActionButtonAndPopover(props) { ? [ { icon: Expensicons.TrackExpense, - text: 'Track Expense', + text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest_temporaryForRefactor( CONST.IOU.TYPE.TRACK_EXPENSE, // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. - props.account.selfDMReportID || ReportUtils.generateReportID(), + props.account.selfDMReportID || ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), ), }, diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 1cd34db66da5..f0557d48da75 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -77,7 +77,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), - [CONST.IOU.TYPE.TRACK_EXPENSE]: 'Track Expense', + [CONST.IOU.TYPE.TRACK_EXPENSE]: translate('iou.trackExpense'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 9fdd2bea24f4..07882e95a9ae 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -144,7 +144,7 @@ function IOURequestStepAmount({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index de5c6811d277..6a2a5dc6f70f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -101,15 +101,15 @@ function IOURequestStepConfirmation({ return translate('iou.splitBill'); } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - return 'Track Expense'; + return translate('iou.trackExpense'); } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + const participantReportID = lodashGet(participant, 'reportID', ''); + return participantReportID ? OptionsListUtils.getReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), [transaction.participants, personalDetails], ); @@ -130,7 +130,7 @@ function IOURequestStepConfirmation({ if (policyExpenseChat) { Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); } - }, [participants, transaction.billable, policy, transactionID]); + }, [isOffline, participants, transaction.billable, policy, transactionID]); const defaultBillable = lodashGet(policy, 'defaultBillable', false); useEffect(() => { @@ -186,13 +186,6 @@ function IOURequestStepConfirmation({ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); - useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); - } - }, [isOffline, participants, transaction.billable, policy]); - /** * @param {Array} selectedParticipants * @param {String} trimmedComment diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 320359192c8d..7df5df4cb203 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -127,7 +127,7 @@ function IOURequestStepDistance({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7de121af52b4..05961bd6c4c3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -129,7 +129,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index f421417b53f6..2ef49af80441 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -189,7 +189,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index 29263d92078f..7a75e9f48805 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -131,6 +131,7 @@ function IOURequestStepTaxAmountPage({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { + // TODO: Is this really needed at all? IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; From c2ef07b39e38d3da0b742db6ce65444164742fd7 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 20:30:56 +0530 Subject: [PATCH 10/30] Completed BE endpoint connection --- src/libs/API/parameters/TrackExpenseParams.ts | 25 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/IOU.ts | 343 ++++++++++++++++++ .../step/IOURequestStepConfirmation.js | 41 ++- 5 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/TrackExpenseParams.ts diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts new file mode 100644 index 000000000000..0e17a316bb9f --- /dev/null +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -0,0 +1,25 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {Receipt} from '@src/types/onyx/Transaction'; + +type TrackExpenseParams = { + amount: number; + currency: string; + comment: string; + created: string; + merchant: string; + iouReportID?: string; + chatReportID: string; + transactionID: string; + reportActionID: string; + createdChatReportActionID: string; + createdExpenseReportActionID?: string; + reportPreviewReportActionID?: string; + receipt: Receipt; + receiptState?: ValueOf; + tag?: string; + transactionThreadReportID: string; + createdReportActionIDForThread: string; +}; + +export default TrackExpenseParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 211bc06f26a3..d05dde006973 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -156,3 +156,4 @@ export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWor export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; +export type {default as TrackExpenseParams} from './TrackExpenseParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 115355210f75..9b47d1efd41d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -157,6 +157,7 @@ const WRITE_COMMANDS = { CANCEL_PAYMENT: 'CancelPayment', ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', + TRACK_EXPENSE: 'TrackExpense', } as const; type WriteCommand = ValueOf; @@ -312,6 +313,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; + [WRITE_COMMANDS.TRACK_EXPENSE]: Parameters.TrackExpenseParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bd1f92ab6490..26e78ee4cca8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -22,6 +22,7 @@ import type { SplitBillParams, StartSplitBillParams, SubmitReportParams, + TrackExpenseParams, UpdateMoneyRequestParams, } from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -84,6 +85,19 @@ type MoneyRequestInformation = { onyxData: OnyxData; }; +type TrackExpenseInformation = { + iouReport?: OnyxTypes.Report; + chatReport: OnyxTypes.Report; + transaction: OnyxTypes.Transaction; + iouAction: OptimisticIOUReportAction; + createdChatReportActionID: string; + createdExpenseReportActionID: string; + reportPreviewAction?: OnyxTypes.ReportAction; + transactionThreadReportID: string; + createdReportActionIDForThread: string; + onyxData: OnyxData; +}; + type SplitData = { chatReportID: string; transactionID: string; @@ -794,6 +808,159 @@ function buildOnyxDataForMoneyRequest( return [optimisticData, successData, failureData]; } +/** Builds the Onyx data for track expense */ +function buildOnyxDataForTrackExpense( + chatReport: OnyxEntry, + transaction: OnyxTypes.Transaction, + iouAction: OptimisticIOUReportAction, + transactionThreadReport: OptimisticChatReport, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction, +): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { + const isScanRequest = TransactionUtils.isScanRequest(transaction); + const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); + const optimisticData: OnyxUpdate[] = []; + + if (chatReport) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + lastMessageText: iouAction.message?.[0].text, + lastMessageHtml: iouAction.message?.[0].html, + lastReadTime: DateUtils.getDBTime(), + }, + }); + } + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: transaction, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: transactionThreadReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + }, + }, + + // Remove the temporary transaction used during the creation flow + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, + value: null, + }, + ); + + const successData: OnyxUpdate[] = []; + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + ); + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + lastReadTime: chatReport?.lastReadTime, + lastMessageText: chatReport?.lastMessageText, + lastMessageHtml: chatReport?.lastMessageHtml, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + }, + }, + }, + ]; + + return [optimisticData, successData, failureData]; +} + /** * Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead @@ -1017,6 +1184,125 @@ function getMoneyRequestInformation( }; } +/** + * Gathers all the data needed to make an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then + * it creates optimistic versions of them and uses those instead + */ +function getTrackExpenseInformation( + parentChatReport: OnyxEntry | EmptyObject, + participant: Participant, + comment: string, + amount: number, + currency: string, + created: string, + merchant: string, + receipt: Receipt | undefined, + payeeEmail = currentUserEmail, +): TrackExpenseInformation | EmptyObject { + // STEP 1: Get existing chat report + let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; + + // The chatReport always exist and we can get it from Onyx if chatReport is null. + if (!chatReport) { + chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null; + } + + // If we still don't have a report, it likely doens't exist and we will early return here as it should not happen + // Maybe later, we can build an optimistic selfDM chat. + if (!chatReport) { + return {}; + } + + // STEP 2: Get the money request report. + // TODO: This is deferred to later as we are not sure if we create iouReport at all in future. + // We can build an optimistic iouReport here if needed. + + // STEP 3: Build optimistic receipt and transaction + const receiptObject: Receipt = {}; + let filename; + if (receipt?.source) { + receiptObject.source = receipt.source; + receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; + filename = receipt.name; + } + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; + let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( + amount, + currency, + chatReport.reportID, + comment, + created, + '', + '', + merchant, + receiptObject, + filename, + null, + '', + '', + false, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + ); + + // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction + // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction + // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. + // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 + // to remind me to do this. + if (isDistanceRequest) { + optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false); + } + + // STEP 4: Build optimistic reportActions. We need: + // 1. IOU action for the chatReport + // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread + const currentTime = DateUtils.getDBTime(); + const iouAction = ReportUtils.buildOptimisticIOUReportAction( + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount, + currency, + comment, + [participant], + optimisticTransaction.transactionID, + undefined, + chatReport.reportID, + false, + false, + receiptObject, + false, + currentTime, + ); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); + + // STEP 5: Build Onyx Data + const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense( + chatReport, + optimisticTransaction, + iouAction, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, + ); + + return { + chatReport, + iouReport: undefined, + transaction: optimisticTransaction, + iouAction, + createdChatReportActionID: '0', + createdExpenseReportActionID: '0', + reportPreviewAction: undefined, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + onyxData: { + optimisticData, + successData, + failureData, + }, + }; +} + /** Requests money based on a distance (eg. mileage from a map) */ function createDistanceRequest( report: OnyxTypes.Report, @@ -1635,6 +1921,62 @@ function requestMoney( Report.notifyNewAction(activeReportID, payeeAccountID); } +/** + * Track an expense + */ +function trackExpense( + report: OnyxTypes.Report, + amount: number, + currency: string, + created: string, + merchant: string, + payeeEmail: string, + payeeAccountID: number, + participant: Participant, + comment: string, + receipt: Receipt, +) { + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); + const { + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdExpenseReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + onyxData, + } = getTrackExpenseInformation(report, participant, comment, amount, currency, currentCreated, merchant, receipt, payeeEmail); + const activeReportID = report.reportID; + + const parameters: TrackExpenseParams = { + amount, + currency, + comment, + created: currentCreated, + merchant, + iouReportID: iouReport?.reportID ?? '0', + chatReportID: chatReport.reportID, + transactionID: transaction.transactionID, + reportActionID: iouAction.reportActionID, + createdChatReportActionID, + createdExpenseReportActionID, + reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '0', + receipt, + receiptState: receipt?.state, + tag: '', + transactionThreadReportID, + createdReportActionIDForThread, + }; + + API.write(WRITE_COMMANDS.TRACK_EXPENSE, parameters, onyxData); + resetMoneyRequestInfo(); + Navigation.dismissModal(activeReportID); + Report.notifyNewAction(activeReportID, payeeAccountID); +} + /** * Build the Onyx data and IOU split necessary for splitting a bill with 3+ users. * 1. Build the optimistic Onyx data for the group chat, i.e. chatReport and iouReportAction creating the former if it doesn't yet exist. @@ -4347,4 +4689,5 @@ export { cancelPayment, navigateToStartStepIfScanFileCannotBeRead, savePreferredPaymentMethod, + trackExpense, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6a2a5dc6f70f..e518a2ac4616 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -218,6 +218,29 @@ function IOURequestStepConfirmation({ [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], ); + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + * @param {File} [receiptObj] + */ + const trackExpense = useCallback( + (selectedParticipants, trimmedComment, receiptObj) => { + IOU.trackExpense( + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + selectedParticipants[0], + trimmedComment, + receiptObj, + ); + }, + [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], + ); + /** * @param {Array} selectedParticipants * @param {String} trimmedComment @@ -309,6 +332,11 @@ function IOURequestStepConfirmation({ return; } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } + if (receiptFile) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { @@ -347,7 +375,18 @@ function IOURequestStepConfirmation({ requestMoney(selectedParticipants, trimmedComment); }, - [iouType, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, requestType, createDistanceRequest, requestMoney, receiptFile], + [ + transaction, + iouType, + receiptFile, + requestType, + requestMoney, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + report.reportID, + trackExpense, + createDistanceRequest, + ], ); /** From 2a425ad50f68f544c5cf513c7b1176f48904dc00 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 8 Mar 2024 17:22:35 +0530 Subject: [PATCH 11/30] Added gpsPoints in endpoint --- src/libs/API/parameters/TrackExpenseParams.ts | 1 + src/libs/actions/IOU.ts | 11 ++++--- .../step/IOURequestStepConfirmation.js | 33 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 0e17a316bb9f..9965463235cc 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -18,6 +18,7 @@ type TrackExpenseParams = { receipt: Receipt; receiptState?: ValueOf; tag?: string; + gpsPoints?: string; transactionThreadReportID: string; createdReportActionIDForThread: string; }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 26e78ee4cca8..8342d2d466f8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -91,7 +91,7 @@ type TrackExpenseInformation = { transaction: OnyxTypes.Transaction; iouAction: OptimisticIOUReportAction; createdChatReportActionID: string; - createdExpenseReportActionID: string; + createdExpenseReportActionID?: string; reportPreviewAction?: OnyxTypes.ReportAction; transactionThreadReportID: string; createdReportActionIDForThread: string; @@ -1291,7 +1291,7 @@ function getTrackExpenseInformation( transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdExpenseReportActionID: '0', + createdExpenseReportActionID: undefined, reportPreviewAction: undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, @@ -1935,6 +1935,7 @@ function trackExpense( participant: Participant, comment: string, receipt: Receipt, + gpsPoints = undefined, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { @@ -1957,16 +1958,18 @@ function trackExpense( comment, created: currentCreated, merchant, - iouReportID: iouReport?.reportID ?? '0', + iouReportID: iouReport?.reportID, chatReportID: chatReport.reportID, transactionID: transaction.transactionID, reportActionID: iouAction.reportActionID, createdChatReportActionID, createdExpenseReportActionID, - reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '0', + reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, receiptState: receipt?.state, tag: '', + // This needs to be a string of JSON because of limitations with the fetch() API and nested objects + gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, transactionThreadReportID, createdReportActionIDForThread, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index e518a2ac4616..6285fd1c4e23 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -224,7 +224,7 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const trackExpense = useCallback( - (selectedParticipants, trimmedComment, receiptObj) => { + (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { IOU.trackExpense( report, transaction.amount, @@ -236,6 +236,7 @@ function IOURequestStepConfirmation({ selectedParticipants[0], trimmedComment, receiptObj, + gpsPoints, ); }, [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], @@ -333,6 +334,36 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + if (receiptFile) { + // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. + if (transaction.amount === 0) { + getCurrentPosition( + (successData) => { + trackExpense(selectedParticipants, trimmedComment, receiptFile, { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }); + }, + (errorData) => { + Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); + // When there is an error, the money can still be requested, it just won't include the GPS coordinates + trackExpense(selectedParticipants, trimmedComment, receiptFile); + }, + { + // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in + maximumAge: 1000 * 60 * 60, + + // 15 seconds, don't wait too long because the server can always fall back to using the IP address + timeout: 15000, + }, + ); + return; + } + + // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } trackExpense(selectedParticipants, trimmedComment, receiptFile); return; } From 92f803ae9a0bcfcee4a84cf312e935a833656664 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 9 Mar 2024 18:30:21 +0530 Subject: [PATCH 12/30] Fixing on which room we should allow track expense --- src/libs/API/parameters/TrackExpenseParams.ts | 6 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU.ts | 78 ++++++++++++++++--- .../iou/request/step/IOURequestStepAmount.js | 2 +- .../step/IOURequestStepConfirmation.js | 26 ++++++- .../request/step/IOURequestStepDistance.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- 8 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 9965463235cc..f48c8666f109 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -13,11 +13,15 @@ type TrackExpenseParams = { transactionID: string; reportActionID: string; createdChatReportActionID: string; - createdExpenseReportActionID?: string; + createdIOUReportActionID?: string; reportPreviewReportActionID?: string; receipt: Receipt; receiptState?: ValueOf; + category?: string; tag?: string; + taxCode: string; + taxAmount: number; + billable?: boolean; gpsPoints?: string; transactionThreadReportID: string; createdReportActionIDForThread: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 45d95a6f47be..0f5cc3507ed4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4498,7 +4498,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const isScanRequest = TransactionUtils.isScanRequest(transaction); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); @@ -958,6 +961,22 @@ function buildOnyxDataForTrackExpense( }, ]; + // We don't need to compute violations unless we're on a paid policy + if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { + return [optimisticData, successData, failureData]; + } + + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + + if (violationsOnyxData) { + optimisticData.push(violationsOnyxData); + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: [], + }); + } + return [optimisticData, successData, failureData]; } @@ -1186,6 +1205,12 @@ function getTrackExpenseInformation( created: string, merchant: string, receipt: Receipt | undefined, + category: string | undefined, + tag: string | undefined, + billable: boolean | undefined, + policy: OnyxEntry | undefined, + policyTagList: OnyxEntry | undefined, + policyCategories: OnyxEntry | undefined, payeeEmail = currentUserEmail, ): TrackExpenseInformation | EmptyObject { // STEP 1: Get existing chat report @@ -1228,9 +1253,9 @@ function getTrackExpenseInformation( receiptObject, filename, null, - '', - '', - false, + category, + tag, + billable, isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, ); @@ -1272,6 +1297,9 @@ function getTrackExpenseInformation( iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread, + policy, + policyTagList, + policyCategories, ); return { @@ -1280,7 +1308,7 @@ function getTrackExpenseInformation( transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdExpenseReportActionID: undefined, + createdIOUReportActionID: undefined, reportPreviewAction: undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, @@ -1924,6 +1952,14 @@ function trackExpense( participant: Participant, comment: string, receipt: Receipt, + category?: string, + tag?: string, + taxCode = '', + taxAmount = 0, + billable?: boolean, + policy?: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, gpsPoints = undefined, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); @@ -1933,12 +1969,28 @@ function trackExpense( transaction, iouAction, createdChatReportActionID, - createdExpenseReportActionID, + createdIOUReportActionID, reportPreviewAction, transactionThreadReportID, createdReportActionIDForThread, onyxData, - } = getTrackExpenseInformation(report, participant, comment, amount, currency, currentCreated, merchant, receipt, payeeEmail); + } = getTrackExpenseInformation( + report, + participant, + comment, + amount, + currency, + currentCreated, + merchant, + receipt, + category, + tag, + billable, + policy, + policyTagList, + policyCategories, + payeeEmail, + ); const activeReportID = report.reportID; const parameters: TrackExpenseParams = { @@ -1952,11 +2004,15 @@ function trackExpense( transactionID: transaction.transactionID, reportActionID: iouAction.reportActionID, createdChatReportActionID, - createdExpenseReportActionID, + createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, receiptState: receipt?.state, - tag: '', + category, + tag, + taxCode, + taxAmount, + billable, // This needs to be a string of JSON because of limitations with the fetch() API and nested objects gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, transactionThreadReportID, @@ -4464,11 +4520,11 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param transactionID of the transaction to set the participants of * @param report attached to the transaction */ -function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report, iouType: ValueOf) { +function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; - const shouldAddAsReport = iouType === CONST.IOU.TYPE.TRACK_EXPENSE && !isEmptyObject(chatReport) && (ReportUtils.isSelfDM(chatReport) || ReportUtils.isAdminRoom(chatReport)); + const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 07882e95a9ae..9fdd2bea24f4 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -144,7 +144,7 @@ function IOURequestStepAmount({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6285fd1c4e23..b7c669dcebc9 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -236,10 +236,34 @@ function IOURequestStepConfirmation({ selectedParticipants[0], trimmedComment, receiptObj, + transaction.category, + transaction.tag, + transactionTaxCode, + transactionTaxAmount, + transaction.billable, + policy, + policyTags, + policyCategories, gpsPoints, ); }, - [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], + [ + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + transaction.category, + transaction.tag, + transaction.billable, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transactionTaxCode, + transactionTaxAmount, + policy, + policyTags, + policyCategories, + ], ); /** diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 7df5df4cb203..320359192c8d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -127,7 +127,7 @@ function IOURequestStepDistance({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 05961bd6c4c3..7de121af52b4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -129,7 +129,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index 2ef49af80441..f421417b53f6 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -189,7 +189,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); From c47d0e0605b519a09fe0bc71aabca7854ec7d0fc Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 13 Mar 2024 21:09:03 +0530 Subject: [PATCH 13/30] Fix types --- src/CONST.ts | 1 + src/libs/actions/IOU.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fb02dae94c48..4872f51889e4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1302,6 +1302,7 @@ const CONST = { CANCEL: 'cancel', DELETE: 'delete', APPROVE: 'approve', + TRACK: 'track', }, AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 227ed2f9e1b2..23b695b75ee0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1269,7 +1269,7 @@ function getTrackExpenseInformation( // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread const currentTime = DateUtils.getDBTime(); const iouAction = ReportUtils.buildOptimisticIOUReportAction( - CONST.IOU.REPORT_ACTION_TYPE.CREATE, + CONST.IOU.REPORT_ACTION_TYPE.TRACK, amount, currency, comment, @@ -1283,7 +1283,7 @@ function getTrackExpenseInformation( false, currentTime, ); - const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport.reportID); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport); const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); // STEP 5: Build Onyx Data From 88e0bf3d4da6ee603acd017ec96c68e22ec1d9bd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 14 Mar 2024 19:13:16 +0530 Subject: [PATCH 14/30] Fixed bunch of bugs --- .../ReportActionItem/MoneyRequestAction.tsx | 2 ++ .../MoneyRequestPreview/index.tsx | 2 +- .../MoneyRequestPreview/types.ts | 3 +++ src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/libs/ReportActionsUtils.ts | 8 ++++++- src/libs/ReportUtils.ts | 23 ++++++++++++++++++- src/libs/actions/IOU.ts | 5 +++- src/pages/home/report/ReportActionItem.tsx | 5 +++- 9 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 05891311ba6d..1bbb60376e26 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -81,6 +81,7 @@ function MoneyRequestAction({ const {isOffline} = useNetwork(); const isSplitBillAction = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const isTrackExpenseAction = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { @@ -116,6 +117,7 @@ function MoneyRequestAction({ chatReportID={chatReportID} reportID={reportID} isBillSplit={isSplitBillAction} + isTrackExpense={isTrackExpenseAction} action={action} contextMenuAnchor={contextMenuAnchor} checkIfContextMenuActive={checkIfContextMenuActive} diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx index 1c502c53f99e..5d5394d79777 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx @@ -10,7 +10,7 @@ function MoneyRequestPreview(props: MoneyRequestPreviewProps) { // We should not render the component if there is no iouReport and it's not a split. // Moved outside of the component scope to allow for easier use of hooks in the main component. // eslint-disable-next-line react/jsx-props-no-spreading - return lodashIsEmpty(props.iouReport) && !props.isBillSplit ? null : ; + return lodashIsEmpty(props.iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ; } MoneyRequestPreview.displayName = 'MoneyRequestPreview'; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index 17dd42b2f794..b569e8b0f382 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -56,6 +56,9 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { /** True if this is this IOU is a split instead of a 1:1 request */ isBillSplit: boolean; + /** True if this is this IOU is a track expense */ + isTrackExpense: boolean; + /** True if the IOU Preview card is hovered */ isHovered?: boolean; diff --git a/src/languages/en.ts b/src/languages/en.ts index 631fb9463661..da4da91ffc03 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -628,6 +628,7 @@ export default { finished: 'Finished', requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracked ${formattedAmount}${comment ? ` for ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} each`, @@ -659,6 +660,7 @@ export default { updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'request'}`, + threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, tagSelection: ({tagName}: TagSelectionParams) => `Select a ${tagName} to add additional organization to your money.`, categorySelection: 'Select a category to add additional organization to your money.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0b57cd153cab..67848ce3e1f5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -621,6 +621,7 @@ export default { finished: 'Finalizado', requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `rastreado ${formattedAmount}${comment ? ` para ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, @@ -654,6 +655,7 @@ export default { updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Solicitud de ${formattedAmount}`}`, + threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`, threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero.`, categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c158a0c2972c..10575b35040d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -209,6 +209,7 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty return ( parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || + parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK || (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!parentReportAction.originalMessage.IOUDetails)) ); } @@ -649,7 +650,7 @@ function getReportPreviewAction(chatReportID: string, iouReportID: string): Onyx * Get the iouReportID for a given report action. */ function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string { - return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? reportAction.originalMessage.linkedReportID : ''; + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? reportAction.originalMessage.linkedReportID : '0'; } function isCreatedTaskReportAction(reportAction: OnyxEntry): boolean { @@ -674,6 +675,10 @@ function isSplitBillAction(reportAction: OnyxEntry): boolean { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } +function isTrackExpenseAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (reportAction.originalMessage as IOUMessage).type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; +} + function isTaskAction(reportAction: OnyxEntry): boolean { const reportActionName = reportAction?.actionName; return ( @@ -955,6 +960,7 @@ export { isReportPreviewAction, isSentMoneyReportAction, isSplitBillAction, + isTrackExpenseAction, isTaskAction, doesReportHaveVisibleActions, isThreadParentMessage, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 320915902451..fbc6e4958c19 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2367,6 +2367,18 @@ function getTransactionReportName(reportAction: OnyxEntry) amount: formattedAmount, }); } - translationKey = ReportActionsUtils.isSplitBillAction(reportAction) ? 'iou.didSplitAmount' : 'iou.requestedAmount'; + if (ReportActionsUtils.isSplitBillAction(reportAction)) { + translationKey = 'iou.didSplitAmount'; + } else if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { + translationKey = 'iou.trackedAmount'; + } else { + translationKey = 'iou.requestedAmount'; + } return Localize.translateLocal(translationKey, { formattedAmount, comment: transactionDetails?.comment ?? '', diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 23b695b75ee0..114edf5274a1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1276,7 +1276,7 @@ function getTrackExpenseInformation( [participant], optimisticTransaction.transactionID, undefined, - chatReport.reportID, + '0', false, false, receiptObject, @@ -1286,6 +1286,9 @@ function getTrackExpenseInformation( const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport); const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); + // The IOU action and the transactionThread are co-dependent as parent-child, so we need to link them together + iouAction.childReportID = optimisticTransactionThread.reportID; + // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense( chatReport, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 7216f4238621..89702d461e91 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -414,7 +414,10 @@ function ReportActionItem({ isIOUReport(action) && action.originalMessage && // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message - (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) + (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || + action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || + action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK || + isSendingMoney) ) { // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0'; From cccfbf9803ae1d5d94e717dd5e5f4579e9b2b769 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 14 Mar 2024 19:17:41 +0530 Subject: [PATCH 15/30] Disabled track expense in policy expense chats --- src/libs/ReportUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fbc6e4958c19..8bf88ebe5dfd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4577,9 +4577,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry Date: Sun, 17 Mar 2024 11:01:40 +0530 Subject: [PATCH 16/30] Fixed report header for track expense --- src/components/AvatarWithDisplayName.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 3 ++- src/libs/ReportUtils.ts | 13 +++++++++++++ src/pages/home/ReportScreen.tsx | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 16f31b9c7eba..396c10151fbf 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -60,7 +60,7 @@ function AvatarWithDisplayName({ const title = ReportUtils.getReportName(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); - const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report); + const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index a9304b9c3138..b70c6a785177 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -111,7 +111,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, if (canHoldOrUnholdRequest) { const isRequestIOU = parentReport?.type === 'iou'; const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU; - const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover; + const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(report); + const canModifyStatus = !isTrackExpenseReport && (isPolicyAdmin || isActionOwner || isApprover); if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2038b708ae3..31d7e9de863c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1233,6 +1233,18 @@ function isIOURequest(report: OnyxEntry): boolean { return false; } +/** + * A Track Expense Report is a thread where the parent the parentReportAction is a transaction, and + * parentReportAction has type of track. + */ +function isTrackExpenseReport(report: OnyxEntry): boolean { + if (isThread(report)) { + const parentReportAction = ReportActionsUtils.getParentReportAction(report); + return !isEmptyObject(parentReportAction) && ReportActionsUtils.isTrackExpenseAction(parentReportAction); + } + return false; +} + /** * Checks if a report is an IOU or expense request. */ @@ -5565,6 +5577,7 @@ export { isJoinRequestInAdminRoom, canAddOrDeleteTransactions, shouldCreateNewMoneyRequestReport, + isTrackExpenseReport, }; export type { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 4e12c75248d3..ac362a1263c3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -241,7 +241,7 @@ function ReportScreen({ : null, [reportActions, parentReportAction], ); - const isSingleTransactionView = ReportUtils.isMoneyRequest(report); + const isSingleTransactionView = ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] ?? null; const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); From d63b371f17f42bbe6cefe00d52afda75cb2e4d56 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 18 Mar 2024 09:54:53 +0530 Subject: [PATCH 17/30] Disabled distance track expense as it is not implemented yet --- src/pages/iou/request/IOURequestStartPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index f4e1c6261b04..5181ce974001 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -110,7 +110,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = iouType !== CONST.IOU.TYPE.TRACK_EXPENSE && (canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate); // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); From c41cc0d16c8f24e70a43357a9f48fae156f2ff49 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 06:16:26 +0530 Subject: [PATCH 18/30] Fixing bugs after merge --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.js | 2 +- src/pages/iou/request/IOURequestStartPage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index d818770ffd17..a152d763a193 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -196,7 +196,7 @@ function FloatingActionButtonAndPopover(props) { text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => - IOU.startMoneyRequest_temporaryForRefactor( + IOU.startMoneyRequest( CONST.IOU.TYPE.TRACK_EXPENSE, // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index c0581618933b..cb078fac133c 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -158,7 +158,7 @@ function IOURequestStartPage({ title={tabTitles[iouType]} onBackButtonPress={navigateBack} /> - {iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.SPLIT ? ( + {iouType !== CONST.IOU.TYPE.SEND ? ( Date: Tue, 19 Mar 2024 06:21:33 +0530 Subject: [PATCH 19/30] Fixed another bug after merge --- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index c2b54409b578..54ea6b2a3b77 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -141,7 +141,7 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.TRACK_EXPENSE]: { icon: Expensicons.TrackExpense, text: translate('iou.trackExpense'), - onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), }, }; From 9d505da5ad4eee8d562a2a5a8910b279b847293f Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 08:19:08 +0530 Subject: [PATCH 20/30] Fixed delete track expense --- src/components/MoneyRequestHeader.tsx | 8 +- src/libs/actions/IOU.ts | 162 ++++++++++++++++++ .../PopoverReportActionContextMenu.tsx | 9 +- 3 files changed, 174 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index b70c6a785177..845e44f047d9 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -73,11 +73,15 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const deleteTransaction = useCallback(() => { if (parentReportAction) { const iouTransactionID = parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) { + IOU.deleteTrackExpense(parentReport?.reportID ?? '', iouTransactionID, parentReportAction, true); + return; + } IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true); } setIsDeleteModalVisible(false); - }, [parentReportAction, setIsDeleteModalVisible]); + }, [parentReport?.reportID, parentReportAction, setIsDeleteModalVisible]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); @@ -86,7 +90,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction; // If the report supports adding transactions to it, then it also supports deleting transactions from it. - const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; + const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(report)) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { const iouTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bffeae2efdf3..956ab5a6b82e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3596,6 +3596,167 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor } } +function deleteTrackExpense(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { + // STEP 1: Get all collections we're updating + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const transactionThreadID = reportAction.childReportID; + let transactionThread = null; + if (transactionThreadID) { + transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null; + } + + // STEP 2: Decide if we need to: + // 1. Delete the transactionThread - delete if there are no visible comments in the thread + // 2. Update the moneyRequestPreview to show [Deleted request] - update if the transactionThread exists AND it isn't being deleted + const shouldDeleteTransactionThread = transactionThreadID ? (reportAction?.childVisibleActionCount ?? 0) === 0 : false; + const shouldShowDeletedRequestMessage = !!transactionThreadID && !shouldDeleteTransactionThread; + + // STEP 3: Update the IOU reportAction and decide if the iouReport should be deleted. We delete the iouReport if there are no visible comments left in the report. + const updatedReportAction = { + [reportAction.reportActionID]: { + pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + previousMessage: reportAction.message, + message: [ + { + type: 'COMMENT', + html: '', + text: '', + isEdited: true, + isDeletedParentAction: shouldShowDeletedRequestMessage, + }, + ], + originalMessage: { + IOUTransactionID: null, + }, + errors: undefined, + }, + } as OnyxTypes.ReportActions; + + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(chatReport?.reportID ?? '', updatedReportAction); + const reportLastMessageText = ReportActionsUtils.getLastVisibleMessage(chatReport?.reportID ?? '', updatedReportAction).lastMessageText; + + // STEP 4: Build Onyx data + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: null, + }, + ]; + + if (Permissions.canUseViolations(betas)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: null, + }); + } + + if (shouldDeleteTransactionThread) { + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, + value: null, + }, + ); + } + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: updatedReportAction, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + lastMessageText: reportLastMessageText, + lastVisibleActionCreated: lastVisibleAction?.created, + }, + }, + ); + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: transaction, + }, + ]; + + if (Permissions.canUseViolations(betas)) { + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: transactionViolations, + }); + } + + if (shouldDeleteTransactionThread) { + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: transactionThread, + }); + } + + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportAction.reportActionID]: { + ...reportAction, + pendingAction: null, + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: chatReport, + }, + ); + + const parameters: DeleteMoneyRequestParams = { + transactionID, + reportActionID: reportAction.reportActionID, + }; + + // STEP 6: Make the API request + API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); + CachedPDFPaths.clearByKey(transactionID); + + // STEP 7: Navigate the user depending on which page they are on and which resources were deleted + if (isSingleTransactionView && shouldDeleteTransactionThread) { + // Pop the deleted report screen before navigating. This prevents navigating to the Concierge chat due to the missing report. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID ?? '')); + } +} + /** * @param managerID - Account ID of the person sending the money * @param recipient - The user receiving the money @@ -4735,6 +4896,7 @@ export { setMoneyRequestParticipants, createDistanceRequest, deleteMoneyRequest, + deleteTrackExpense, splitBill, splitBillAndOpenReport, setDraftSplitTransaction, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 793fbf9b1e7e..9bf32bb92b35 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -12,7 +12,6 @@ import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; -import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import type {ContextMenuAction} from './ContextMenuActions'; @@ -256,8 +255,12 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = () => (onComfirmDeleteModal.current = runAndResetCallback(onComfirmDeleteModal.current)); const reportAction = reportActionRef.current; - if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - IOU.deleteMoneyRequest(reportAction?.originalMessage?.IOUTransactionID ?? '', reportAction); + if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { + IOU.deleteTrackExpense(reportIDRef.current, reportAction?.originalMessage?.IOUTransactionID ?? '', reportAction); + } else { + IOU.deleteMoneyRequest(reportAction?.originalMessage?.IOUTransactionID ?? '', reportAction); + } } else if (reportAction) { Report.deleteReportComment(reportIDRef.current, reportAction); } From 0c748ce43ed2acd223a3737719a6a769e5d13a46 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 19:11:38 +0530 Subject: [PATCH 21/30] Fixed delete track expense message --- .../ReportActionItem/MoneyRequestAction.tsx | 20 ++++++++++++++----- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportUtils.ts | 3 +++ src/libs/actions/IOU.ts | 2 +- src/pages/home/report/ReportActionItem.tsx | 13 +++++++++--- 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 1bbb60376e26..7d9ba2697c7a 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -12,6 +12,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -80,8 +81,8 @@ function MoneyRequestAction({ const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const isSplitBillAction = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - const isTrackExpenseAction = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; + const isSplitBillAction = ReportActionsUtils.isSplitBillAction(action); + const isTrackExpenseAction = ReportActionsUtils.isTrackExpenseAction(action); const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { @@ -109,9 +110,18 @@ function MoneyRequestAction({ shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } - return isDeletedParentAction || isReversedTransaction ? ( - ${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} /> - ) : ( + if (isDeletedParentAction || isReversedTransaction) { + let message: TranslationPaths; + if (isReversedTransaction) { + message = 'parentReportAction.reversedTransaction'; + } else if (isTrackExpenseAction) { + message = 'parentReportAction.deletedExpense'; + } else { + message = 'parentReportAction.deletedRequest'; + } + return ${translate(message)}`} />; + } + return ( @@ -680,9 +689,7 @@ function ReportActionItem({ showHeader report={report} > - ${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} - /> + ${translate(message)}`} /> From 9edbe166c422adff26386b5f04eaa05b11f4c6ae Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 19:35:55 +0530 Subject: [PATCH 22/30] Fixed translations --- src/languages/en.ts | 13 +++++++++---- src/languages/es.ts | 16 ++++++++++++---- src/libs/ReportUtils.ts | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 99f86e937aa0..47285af2515f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -463,9 +463,14 @@ export default { copyEmailToClipboard: 'Copy email to clipboard', markAsUnread: 'Mark as unread', markAsRead: 'Mark as read', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`, - deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`, - deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}?`, + editAction: ({action}: EditActionParams) => + `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`, + deleteAction: ({action}: DeleteActionParams) => + `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`, + deleteConfirmation: ({action}: DeleteConfirmationParams) => + `Are you sure you want to delete this ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment' + }?`, onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', joinThread: 'Join thread', @@ -628,7 +633,7 @@ export default { finished: 'Finished', requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`, - trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracked ${formattedAmount}${comment ? ` for ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} each`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 4f10fdb8451e..56e9702edca9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -454,10 +454,18 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, - deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + editAction: ({action}: EditActionParams) => + `Editar ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' + }`, + deleteAction: ({action}: DeleteActionParams) => + `Eliminar ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' + }`, deleteConfirmation: ({action}: DeleteConfirmationParams) => - `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + `¿Estás seguro de que quieres eliminar esta ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' + }`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', @@ -621,7 +629,7 @@ export default { finished: 'Finalizado', requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`, - trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `rastreado ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `seguimiento ${formattedAmount}${comment ? ` para ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4471f2be9f17..62e39c38efe0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3197,7 +3197,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num iouMessage = `requested ${amount}${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.TRACK: - iouMessage = `tracked ${amount}${comment && ` for ${comment}`}`; + iouMessage = `tracking ${amount}${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.SPLIT: iouMessage = `split ${amount}${comment && ` for ${comment}`}`; From f393dd2927e4b6bde27703aa59808b41b9442544 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:40:07 +0530 Subject: [PATCH 23/30] Apply suggestions from code review Co-authored-by: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> --- src/components/ReportActionItem/MoneyRequestPreview/index.tsx | 2 +- src/components/ReportActionItem/MoneyRequestPreview/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx index 5d5394d79777..b46e052f3420 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx @@ -7,7 +7,7 @@ import MoneyRequestPreviewContent from './MoneyRequestPreviewContent'; import type {MoneyRequestPreviewOnyxProps, MoneyRequestPreviewProps} from './types'; function MoneyRequestPreview(props: MoneyRequestPreviewProps) { - // We should not render the component if there is no iouReport and it's not a split. + // We should not render the component if there is no iouReport and it's not a split or track expense. // Moved outside of the component scope to allow for easier use of hooks in the main component. // eslint-disable-next-line react/jsx-props-no-spreading return lodashIsEmpty(props.iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index b569e8b0f382..3b3eda4ec30a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -56,7 +56,7 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { /** True if this is this IOU is a split instead of a 1:1 request */ isBillSplit: boolean; - /** True if this is this IOU is a track expense */ + /** Whether this IOU is a track expense */ isTrackExpense: boolean; /** True if the IOU Preview card is hovered */ From 13a8c951671c85e58d7a049d5b2458e153f3345e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 22:39:01 +0530 Subject: [PATCH 24/30] Fixed LHN alternative text --- src/libs/ReportUtils.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 62e39c38efe0..f45c65f8668a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2490,6 +2490,28 @@ function getReportPreviewMessage( } } + if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isTrackExpenseAction(reportAction)) { + // This covers group chats where the last action is a track expense action + const linkedTransaction = getLinkedTransaction(reportAction); + if (isEmptyObject(linkedTransaction)) { + return reportActionMessage; + } + + if (!isEmptyObject(linkedTransaction)) { + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } + + if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); + } + + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.trackedAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + } + } + const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const policyName = getPolicyName(report, false, policy); From 6738aedbbff95a33432d78983e687f804e0f2462 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 05:44:13 +0530 Subject: [PATCH 25/30] Fixed bug after merge --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bbd31c8b2468..2f0ecb4f9fb6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2494,9 +2494,9 @@ function getReportPreviewMessage( } } - if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isTrackExpenseAction(reportAction)) { + if (!isEmptyObject(iouReportAction) && !isIOUReport(report) && iouReportAction && ReportActionsUtils.isTrackExpenseAction(iouReportAction)) { // This covers group chats where the last action is a track expense action - const linkedTransaction = getLinkedTransaction(reportAction); + const linkedTransaction = getLinkedTransaction(iouReportAction); if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } From cc425d7c32bf13c9c7fda8408d21eb050de192d0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 05:53:28 +0530 Subject: [PATCH 26/30] Fixed svg --- assets/images/document-plus.svg | 5 +++++ assets/images/track-expense.svg | 9 --------- src/components/Icon/Expensicons.ts | 4 ++-- .../AttachmentPickerWithMenuItems.tsx | 2 +- .../SidebarScreen/FloatingActionButtonAndPopover.js | 2 +- 5 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 assets/images/document-plus.svg delete mode 100644 assets/images/track-expense.svg diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg new file mode 100644 index 000000000000..bb50afc63c46 --- /dev/null +++ b/assets/images/document-plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg deleted file mode 100644 index c15f28b72dd7..000000000000 --- a/assets/images/track-expense.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 9fb20bd2ea91..6087e83603dd 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -44,6 +44,7 @@ import Copy from '@assets/images/copy.svg'; import CreditCard from '@assets/images/creditcard.svg'; import DocumentSlash from '@assets/images/document-slash.svg'; import Document from '@assets/images/document.svg'; +import DocumentPlus from '@assets/images/document-plus.svg'; import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg'; import DotIndicator from '@assets/images/dot-indicator.svg'; import DownArrow from '@assets/images/down.svg'; @@ -141,7 +142,6 @@ import Task from '@assets/images/task.svg'; import Tax from '@assets/images/tax.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; -import TrackExpense from '@assets/images/track-expense.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -315,5 +315,5 @@ export { ChatBubbleUnread, ChatBubbleReply, Lightbulb, - TrackExpense, + DocumentPlus, }; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 54ea6b2a3b77..95533db02f06 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -139,7 +139,7 @@ function AttachmentPickerWithMenuItems({ onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID ?? ''), }, [CONST.IOU.TYPE.TRACK_EXPENSE]: { - icon: Expensicons.TrackExpense, + icon: Expensicons.DocumentPlus, text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), }, diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index a152d763a193..abf932eff96d 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -192,7 +192,7 @@ function FloatingActionButtonAndPopover(props) { ...(canUseTrackExpense ? [ { - icon: Expensicons.TrackExpense, + icon: Expensicons.DocumentPlus, text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => From 8a59e755b50ab17a4ab1d4cf94a8679deee3e601 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 06:13:12 +0530 Subject: [PATCH 27/30] Fixed lint --- src/components/Icon/Expensicons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 6087e83603dd..7116ba2aab67 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -42,9 +42,9 @@ import Concierge from '@assets/images/concierge.svg'; import Connect from '@assets/images/connect.svg'; import Copy from '@assets/images/copy.svg'; import CreditCard from '@assets/images/creditcard.svg'; +import DocumentPlus from '@assets/images/document-plus.svg'; import DocumentSlash from '@assets/images/document-slash.svg'; import Document from '@assets/images/document.svg'; -import DocumentPlus from '@assets/images/document-plus.svg'; import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg'; import DotIndicator from '@assets/images/dot-indicator.svg'; import DownArrow from '@assets/images/down.svg'; From fb070033aec7b2573495f4eef545cc34cbcee84b Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 06:19:30 +0530 Subject: [PATCH 28/30] Fixed svg fill --- assets/images/document-plus.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg index bb50afc63c46..cce2e3027cea 100644 --- a/assets/images/document-plus.svg +++ b/assets/images/document-plus.svg @@ -1,5 +1,5 @@ - - - - + + + + From edc7d4d37748c596c3e403170c1332d325261662 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 07:07:15 +0530 Subject: [PATCH 29/30] Fixed style on confirm step --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 545a34b10e5c..138bfc937926 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -449,7 +449,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } else { const formattedSelectedParticipants = _.map(selectedParticipants, (participant) => ({ ...participant, - isDisabled: !participant.isPolicyExpenseChat && ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID), })); sections.push({ title: translate('common.to'), @@ -541,6 +541,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const navigateToReportOrUserDetail = (option) => { const activeRoute = Navigation.getActiveRouteWithoutParams(); + if (option.isSelfDM) { + Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); + return; + } + if (option.accountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { From e0b531f56739d6b2d181849cda712eb3aff30dbf Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 10:17:23 +0530 Subject: [PATCH 30/30] Implemented edit track expense, but doesn't work due to BE problems --- src/libs/ReportUtils.ts | 5 + src/libs/actions/IOU.ts | 215 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 216 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2f0ecb4f9fb6..4d7d19333d72 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2232,6 +2232,11 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { return true; } + // TODO: Uncomment this line when BE starts working properly (Editing Track Expense) + // if (reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK) { + // return true; + // } + if (reportAction.originalMessage.type !== CONST.IOU.REPORT_ACTION_TYPE.CREATE) { return false; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index eb245f218285..9d351435a096 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1741,6 +1741,185 @@ function getUpdateMoneyRequestParams( }; } +/** + * @param transactionID + * @param transactionThreadReportID + * @param transactionChanges + * @param [transactionChanges.created] Present when updated the date field + * @param onlyIncludeChangedFields + * When 'true', then the returned params will only include the transaction details for the fields that were changed. + * When `false`, then the returned params will include all the transaction details, regardless of which fields were changed. + * This setting is necessary while the UpdateDistanceRequest API is refactored to be fully 1:1:1 in https://github.com/Expensify/App/issues/28358 + */ +function getUpdateTrackExpenseParams( + transactionID: string, + transactionThreadReportID: string, + transactionChanges: TransactionChanges, + onlyIncludeChangedFields: boolean, +): UpdateMoneyRequestData { + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData + const pendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); + const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null])); + const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); + + // Step 2: Get all the collections being updated + const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread?.parentReportID}`] ?? null; + const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + let updatedTransaction = transaction ? TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, false) : null; + const transactionDetails = ReportUtils.getTransactionDetails(updatedTransaction); + + if (transactionDetails?.waypoints) { + // This needs to be a JSON string since we're sending this to the MapBox API + transactionDetails.waypoints = JSON.stringify(transactionDetails.waypoints); + } + + const dataToIncludeInParams: Partial | undefined = onlyIncludeChangedFields + ? Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => Object.keys(transactionChanges).includes(key))) + : transactionDetails; + + const params: UpdateMoneyRequestParams = { + ...dataToIncludeInParams, + reportID: chatReport?.reportID, + transactionID, + }; + + const hasPendingWaypoints = 'waypoints' in transactionChanges; + if (transaction && updatedTransaction && hasPendingWaypoints) { + updatedTransaction = { + ...updatedTransaction, + amount: CONST.IOU.DEFAULT_AMOUNT, + modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, + modifiedMerchant: Localize.translateLocal('iou.routePending'), + }; + + // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors + successData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, + value: null, + }); + + // Revert the transaction's amount to the original value on failure. + // The IOU Report will be fully reverted in the failureData further below. + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: transaction.amount, + modifiedAmount: transaction.modifiedAmount, + modifiedMerchant: transaction.modifiedMerchant, + }, + }); + } + + // Step 3: Build the modified expense report actions + // We don't create a modified report action if we're updating the waypoints, + // since there isn't actually any optimistic data we can create for them and the report action is created on the server + // with the response from the MapBox API + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false); + if (!hasPendingWaypoints) { + params.reportActionID = updatedReportAction.reportActionID; + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + value: { + [updatedReportAction.reportActionID]: updatedReportAction as OnyxTypes.ReportAction, + }, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + value: { + [updatedReportAction.reportActionID]: {pendingAction: null}, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + value: { + [updatedReportAction.reportActionID]: { + ...(updatedReportAction as OnyxTypes.ReportAction), + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'), + }, + }, + }); + } + + // Step 4: Update the report preview message (and report header) so LHN amount tracked is correct. + // Optimistically modify the transaction and the transaction thread + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + ...updatedTransaction, + pendingFields, + isLoading: hasPendingWaypoints, + errorFields: null, + }, + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, + value: { + lastActorAccountID: updatedReportAction.actorAccountID, + }, + }); + + if (isScanning && ('amount' in transactionChanges || 'currency' in transactionChanges)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [transactionThread?.parentReportActionID ?? '']: { + whisperedToAccountIDs: [], + }, + }, + }); + } + + // Clear out the error fields and loading states on success + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingFields: clearedPendingFields, + isLoading: false, + errorFields: null, + }, + }); + + // Clear out loading states, pending fields, and add the error fields + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingFields: clearedPendingFields, + isLoading: false, + errorFields, + }, + }); + + // Reset the transaction thread to its original state + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, + value: transactionThread, + }); + + return { + params, + onyxData: {optimisticData, successData, failureData}, + }; +} + /** Updates the created date of a money request */ function updateMoneyRequestDate( transactionID: string, @@ -1753,7 +1932,14 @@ function updateMoneyRequestDate( const transactionChanges: TransactionChanges = { created: value, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); } @@ -1785,7 +1971,14 @@ function updateMoneyRequestMerchant( const transactionChanges: TransactionChanges = { merchant: value, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT, params, onyxData); } @@ -1817,7 +2010,14 @@ function updateMoneyRequestDistance( const transactionChanges: TransactionChanges = { waypoints, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE, params, onyxData); } @@ -1849,7 +2049,14 @@ function updateMoneyRequestDescription( const transactionChanges: TransactionChanges = { comment, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION, params, onyxData); }