diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 6b2b4e16db65..49681f396181 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -62,7 +62,7 @@ const defaultProps = { function MoneyReportHeader({session, personalDetails, policy, chatReport, report: moneyRequestReport, isSmallScreenWidth}) { const {translate} = useLocalize(); - const reportTotal = ReportUtils.getMoneyRequestTotal(moneyRequestReport); + const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); @@ -71,8 +71,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, report const isPayer = policyType === CONST.POLICY.TYPE.CORPORATE ? isPolicyAdmin && isApproved : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isReportDraft(moneyRequestReport); const shouldShowSettlementButton = useMemo( - () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reportTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), - [isPayer, isDraft, isSettled, moneyRequestReport, reportTotal, chatReport], + () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), + [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { if (policyType !== CONST.POLICY.TYPE.CORPORATE) { @@ -80,10 +80,10 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, report } return isManager && !isDraft && !isApproved && !isSettled; }, [policyType, isManager, isDraft, isApproved, isSettled]); - const shouldShowSubmitButton = isDraft && reportTotal !== 0; + const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reportTotal, moneyRequestReport.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableTotal, moneyRequestReport.currency); return ( diff --git a/src/components/ReportActionItem/MoneyReportView.js b/src/components/ReportActionItem/MoneyReportView.js index bfdcc59bf89f..f4be86cf12bd 100644 --- a/src/components/ReportActionItem/MoneyReportView.js +++ b/src/components/ReportActionItem/MoneyReportView.js @@ -28,19 +28,27 @@ const propTypes = { }; function MoneyReportView(props) { - const formattedAmount = CurrencyUtils.convertToDisplayString(ReportUtils.getMoneyRequestTotal(props.report), props.report.currency); - const isSettled = ReportUtils.isSettled(props.report.reportID); const {translate} = useLocalize(); + const isSettled = ReportUtils.isSettled(props.report.reportID); + + const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.report); + + const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend; + const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.report.currency); + const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, props.report.currency); + const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, props.report.currency); + + const subAmountTextStyles = [styles.taskTitleMenuItem, styles.alignSelfCenter, StyleUtils.getFontSizeStyle(variables.fontSizeh1), StyleUtils.getColorStyle(themeColors.textSupporting)]; return ( - + {translate('common.total')} @@ -59,10 +67,50 @@ function MoneyReportView(props) { numberOfLines={1} style={[styles.taskTitleMenuItem, styles.alignSelfCenter]} > - {formattedAmount} + {formattedTotalAmount} + {shouldShowBreakdown ? ( + <> + + + + {translate('cardTransactions.outOfPocket')} + + + + + {formattedOutOfPocketAmount} + + + + + + + {translate('cardTransactions.companySpend')} + + + + + {formattedCompanySpendAmount} + + + + + ) : undefined} { - if (reportTotal) { - return CurrencyUtils.convertToDisplayString(reportTotal, props.iouReport.currency); + if (totalDisplaySpend) { + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); } if (isScanning) { return props.translate('iou.receiptScanning'); @@ -176,7 +176,7 @@ function ReportPreview(props) { const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowSettlementButton = ReportUtils.isControlPolicyExpenseChat(props.chatReport) ? props.policy.role === CONST.POLICY.ROLE.ADMIN && ReportUtils.isReportApproved(props.iouReport) && !iouSettled && !iouCanceled - : !_.isEmpty(props.iouReport) && isCurrentUserManager && !isReportDraft && !iouSettled && !iouCanceled && !props.iouReport.isWaitingOnBankAccount && reportTotal !== 0; + : !_.isEmpty(props.iouReport) && isCurrentUserManager && !isReportDraft && !iouSettled && !iouCanceled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0; return ( diff --git a/src/languages/en.ts b/src/languages/en.ts index b321903a9781..78ec12d3e618 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1843,7 +1843,7 @@ export default { }, cardTransactions: { notActivated: 'Not activated', - outOfPocketSpend: 'Out-of-pocket spend', + outOfPocket: 'Out of pocket', companySpend: 'Company spend', }, distance: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 51d9923a570b..46fe135b448d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2328,7 +2328,7 @@ export default { }, cardTransactions: { notActivated: 'No activado', - outOfPocketSpend: 'Gastos por cuenta propia', + outOfPocket: 'Por cuenta propia', companySpend: 'Gastos de empresa', }, distance: { diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 051c19312f09..60631521ba82 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -521,7 +521,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); if (!hasMultipleParticipants) { result.login = personalDetail.login; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 0b7bbfd61461..ab19a622aa04 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1283,7 +1283,7 @@ function hasNonReimbursableTransactions(iouReportID) { * @param {Object} allReportsDict * @returns {Number} */ -function getMoneyRequestTotal(report, allReportsDict = null) { +function getMoneyRequestReimbursableTotal(report, allReportsDict = null) { const allAvailableReports = allReportsDict || allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { @@ -1294,7 +1294,6 @@ function getMoneyRequestTotal(report, allReportsDict = null) { } if (moneyRequestReport) { const total = lodashGet(moneyRequestReport, 'total', 0); - if (total !== 0) { // There is a possibility that if the Expense report has a negative total. // This is because there are instances where you can get a credit back on your card, @@ -1305,6 +1304,45 @@ function getMoneyRequestTotal(report, allReportsDict = null) { return 0; } +/** + * @param {Object} report + * @param {Object} allReportsDict + * @returns {Object} + */ +function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { + const allAvailableReports = allReportsDict || allReports; + let moneyRequestReport; + if (isMoneyRequestReport(report)) { + moneyRequestReport = report; + } + if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + } + if (moneyRequestReport) { + let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0); + let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0); + + if (nonReimbursableSpend + reimbursableSpend !== 0) { + // There is a possibility that if the Expense report has a negative total. + // This is because there are instances where you can get a credit back on your card, + // or you enter a negative expense to “offset” future expenses + nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend); + reimbursableSpend = isExpenseReport(moneyRequestReport) ? reimbursableSpend * -1 : Math.abs(reimbursableSpend); + const totalDisplaySpend = nonReimbursableSpend + reimbursableSpend; + return { + nonReimbursableSpend, + reimbursableSpend, + totalDisplaySpend, + }; + } + } + return { + nonReimbursableSpend: 0, + reimbursableSpend: 0, + totalDisplaySpend: 0, + }; +} + /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report * @@ -1344,7 +1382,7 @@ function getPolicyExpenseChatName(report, policy = undefined) { * @returns {String} */ function getMoneyRequestReportName(report, policy = undefined) { - const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(report), report.currency); const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID); const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerName, @@ -1550,7 +1588,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment}); } - const totalAmount = getMoneyRequestTotal(report); + const totalAmount = getMoneyRequestReimbursableTotal(report); const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true); const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency); @@ -2219,7 +2257,7 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to function getIOUReportActionMessage(iouReportID, type, total, comment, currency, paymentType = '', isSettlingUp = false) { const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(getReport(iouReportID)), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(getReport(iouReportID)), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -3903,7 +3941,8 @@ export { hasExpensifyGuidesEmails, isWaitingForIOUActionFromCurrentUser, isIOUOwnedByCurrentUser, - getMoneyRequestTotal, + getMoneyRequestReimbursableTotal, + getMoneyRequestSpendBreakdown, canShowReportRecipientLocalTime, formatReportLastMessageText, chatIncludesConcierge, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 314a1d63760e..dd6db33902fb 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -158,7 +158,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict); + report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReportsDict); }); // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: @@ -384,7 +384,7 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d0e84499a443..f5ca7080249c 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -730,6 +730,7 @@ export default compose( prevProps.report.managerEmail === nextProps.report.managerEmail && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && + lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && prevProps.linkedReportActionID === nextProps.linkedReportActionID, ), ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f58c6644cd47..a3671faf194c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -278,6 +278,10 @@ function arePropsEqual(oldProps, newProps) { return false; } + if (lodashGet(newProps, 'report.nonReimbursableTotal') !== lodashGet(oldProps, 'report.nonReimbursableTotal')) { + return false; + } + if (lodashGet(newProps, 'report.writeCapability') !== lodashGet(oldProps, 'report.writeCapability')) { return false; } diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 9dbdde14c50d..394f6c5ddc5a 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -141,6 +141,7 @@ const chatReportSelector = (report) => lastVisibleActionCreated: report.lastVisibleActionCreated, iouReportID: report.iouReportID, total: report.total, + nonReimbursableTotal: report.nonReimbursableTotal, hasOutstandingIOU: report.hasOutstandingIOU, isWaitingOnBankAccount: report.isWaitingOnBankAccount, statusNum: report.statusNum,