Skip to content

Commit

Permalink
Merge pull request #24639 from Expensify/youssef_approve_money_request
Browse files Browse the repository at this point in the history
Implement report approvals
  • Loading branch information
luacmartins authored Aug 28, 2023
2 parents d42967a + 577d9ad commit 0343bfe
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 127 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ const CONST = {
ACTIONS: {
LIMIT: 50,
TYPE: {
APPROVED: 'APPROVED',
ADDCOMMENT: 'ADDCOMMENT',
CLOSED: 'CLOSED',
CREATED: 'CREATED',
Expand Down
8 changes: 4 additions & 4 deletions src/components/AvatarWithDisplayName.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const propTypes = {
/** The report currently being looked at */
report: reportPropTypes,

/** The policies which the user has access to and which the report could be tied to */
policies: PropTypes.shape({
/** The policy which the user has access to and which the report is tied to */
policy: PropTypes.shape({
/** Name of the policy */
name: PropTypes.string,
}),
Expand All @@ -49,7 +49,7 @@ const propTypes = {

const defaultProps = {
personalDetails: {},
policies: {},
policy: {},
report: {},
isAnonymous: false,
size: CONST.AVATAR_SIZE.DEFAULT,
Expand Down Expand Up @@ -85,7 +85,7 @@ function AvatarWithDisplayName(props) {
const subtitle = ReportUtils.getChatRoomSubtitle(props.report);
const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report);
const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(props.report) || ReportUtils.isMoneyRequest(props.report);
const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies, true);
const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policy, true);
const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([props.report.ownerAccountID], props.personalDetails);
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(_.values(ownerPersonalDetails), false);
const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(props.report);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ const propTypes = {
/** Report, if we're showing the details for one and using AvatarWithDisplay */
report: iouReportPropTypes,

/** Policies, if we're showing the details for a report and need info about it for AvatarWithDisplay */
policies: PropTypes.shape({
/** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */
policy: PropTypes.shape({
/** Name of the policy */
name: PropTypes.string,
}),
Expand Down
4 changes: 2 additions & 2 deletions src/components/HeaderWithBackButton/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function HeaderWithBackButton({
onDownloadButtonPress = () => {},
onThreeDotsButtonPress = () => {},
report = null,
policies = {},
policy = {},
personalDetails = {},
shouldShowAvatarWithDisplay = false,
shouldShowBackButton = true,
Expand Down Expand Up @@ -77,7 +77,7 @@ function HeaderWithBackButton({
{shouldShowAvatarWithDisplay && (
<AvatarWithDisplayName
report={report}
policies={policies}
policy={policy}
personalDetails={personalDetails}
/>
)}
Expand Down
107 changes: 74 additions & 33 deletions src/components/MoneyReportHeader.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import useLocalize from '../hooks/useLocalize';
import HeaderWithBackButton from './HeaderWithBackButton';
import iouReportPropTypes from '../pages/iouReportPropTypes';
import * as ReportUtils from '../libs/ReportUtils';
Expand All @@ -12,9 +13,10 @@ import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimen
import compose from '../libs/compose';
import Navigation from '../libs/Navigation/Navigation';
import ROUTES from '../ROUTES';
import SettlementButton from './SettlementButton';
import * as Policy from '../libs/actions/Policy';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
import SettlementButton from './SettlementButton';
import Button from './Button';
import * as IOU from '../libs/actions/IOU';
import * as CurrencyUtils from '../libs/CurrencyUtils';
import reportPropTypes from '../pages/reportPropTypes';
Expand All @@ -23,10 +25,16 @@ const propTypes = {
/** The report currently being looked at */
report: iouReportPropTypes.isRequired,

/** The policies which the user has access to and which the report could be tied to */
policies: PropTypes.shape({
/** The policy tied to the money request report */
policy: PropTypes.shape({
/** Name of the policy */
name: PropTypes.string,

/** Type of the policy */
type: PropTypes.string,

/** The role of the current user in the policy */
role: PropTypes.string,
}).isRequired,

/** The chat report this report is linked to */
Expand All @@ -51,39 +59,50 @@ const defaultProps = {
},
};

function MoneyReportHeader(props) {
const moneyRequestReport = props.report;
function MoneyReportHeader({session, personalDetails, policy, chatReport, report: moneyRequestReport, isSmallScreenWidth}) {
const {translate} = useLocalize();
const reportTotal = ReportUtils.getMoneyRequestTotal(moneyRequestReport);
const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const policy = props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`];
const isPayer =
Policy.isAdminOfFreePolicy([policy]) || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID);
const reportTotal = ReportUtils.getMoneyRequestTotal(props.report);
const shouldShowSettlementButton = !isSettled && isPayer && !moneyRequestReport.isWaitingOnBankAccount && reportTotal !== 0;
const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport);
const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'payPalMeAddress']));
const formattedAmount = CurrencyUtils.convertToDisplayString(reportTotal, props.report.currency);
const policyType = lodashGet(policy, 'type');
const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN;
const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID;
const isPayer = policyType === CONST.POLICY.TYPE.CORPORATE ? isPolicyAdmin && isApproved : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const shouldShowSettlementButton = useMemo(
() => isPayer && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reportTotal !== 0,
[isPayer, isSettled, moneyRequestReport, reportTotal],
);
const shouldShowApproveButton = useMemo(() => {
if (policyType !== CONST.POLICY.TYPE.CORPORATE) {
return false;
}
return isManager && !isApproved && !isSettled;
}, [policyType, isManager, isApproved, isSettled]);
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const shouldShowPaypal = Boolean(lodashGet(personalDetails, [moneyRequestReport.managerID, 'payPalMeAddress']));
const formattedAmount = CurrencyUtils.convertToDisplayString(reportTotal, moneyRequestReport.currency);

return (
<View style={[styles.pt0]}>
<HeaderWithBackButton
shouldShowAvatarWithDisplay
shouldShowPinButton={false}
report={props.report}
policies={props.policies}
personalDetails={props.personalDetails}
shouldShowBackButton={props.isSmallScreenWidth}
report={moneyRequestReport}
policy={policy}
personalDetails={personalDetails}
shouldShowBackButton={isSmallScreenWidth}
onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)}
shouldShowBorderBottom={!shouldShowSettlementButton || !props.isSmallScreenWidth}
shouldShowBorderBottom={!shouldShowSettlementButton || !isSmallScreenWidth}
>
{shouldShowSettlementButton && !props.isSmallScreenWidth && (
{shouldShowSettlementButton && !isSmallScreenWidth && (
<View style={[styles.pv2]}>
<SettlementButton
currency={props.report.currency}
policyID={props.report.policyID}
currency={moneyRequestReport.currency}
policyID={moneyRequestReport.policyID}
shouldShowPaypal={shouldShowPaypal}
chatReportID={props.chatReport.reportID}
iouReport={props.report}
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, props.chatReport, props.report)}
chatReportID={chatReport.reportID}
iouReport={moneyRequestReport}
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
addBankAccountRoute={bankAccountRoute}
shouldShowPaymentOptions
Expand All @@ -92,23 +111,45 @@ function MoneyReportHeader(props) {
/>
</View>
)}
{shouldShowApproveButton && !isSmallScreenWidth && (
<View style={[styles.pv2]}>
<Button
success
medium
text={translate('iou.approve')}
style={[styles.mnw120, styles.pv2, styles.pr0]}
onPress={() => IOU.approveMoneyRequest(moneyRequestReport)}
/>
</View>
)}
</HeaderWithBackButton>
{shouldShowSettlementButton && props.isSmallScreenWidth && (
<View style={[styles.ph5, styles.pb2, props.isSmallScreenWidth && styles.borderBottom]}>
{shouldShowSettlementButton && isSmallScreenWidth && (
<View style={[styles.ph5, styles.pb2, isSmallScreenWidth && styles.borderBottom]}>
<SettlementButton
currency={props.report.currency}
policyID={props.report.policyID}
currency={moneyRequestReport.currency}
policyID={moneyRequestReport.policyID}
shouldShowPaypal={shouldShowPaypal}
chatReportID={props.report.chatReportID}
iouReport={props.report}
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, props.chatReport, props.report)}
chatReportID={moneyRequestReport.chatReportID}
iouReport={moneyRequestReport}
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
addBankAccountRoute={bankAccountRoute}
shouldShowPaymentOptions
formattedAmount={formattedAmount}
/>
</View>
)}
{shouldShowApproveButton && isSmallScreenWidth && (
<View style={[styles.ph5, styles.pb2, isSmallScreenWidth && styles.borderBottom]}>
<Button
success
medium
text={translate('iou.approve')}
style={[styles.w100, styles.pr0]}
onPress={() => IOU.approveMoneyRequest(moneyRequestReport)}
/>
</View>
)}
</View>
);
}
Expand Down
9 changes: 5 additions & 4 deletions src/components/MoneyRequestHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const propTypes = {
/** The expense report or iou report (only will have a value if this is a transaction thread) */
parentReport: iouReportPropTypes,

/** The policies which the user has access to and which the report could be tied to */
policies: PropTypes.shape({
/** The policy which the report is tied to */
policy: PropTypes.shape({
/** Name of the policy */
name: PropTypes.string,
}).isRequired,
Expand Down Expand Up @@ -58,6 +58,7 @@ function MoneyRequestHeader(props) {
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const moneyRequestReport = props.parentReport;
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);

const parentReportAction = ReportActionsUtils.getParentReportAction(props.report);

// Only the requestor can take delete the request, admins can only edit it.
Expand Down Expand Up @@ -90,7 +91,7 @@ function MoneyRequestHeader(props) {
]}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(props.windowWidth)}
report={report}
policies={props.policies}
policy={props.policy}
personalDetails={props.personalDetails}
shouldShowBackButton={props.isSmallScreenWidth}
onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)}
Expand Down Expand Up @@ -122,7 +123,7 @@ export default compose(
key: ONYXKEYS.SESSION,
},
parentReport: {
key: (props) => `${ONYXKEYS.COLLECTION.REPORT}${props.report.parentReportID}`,
key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`,
},
}),
)(MoneyRequestHeader);
4 changes: 3 additions & 1 deletion src/components/ReportActionItem/MoneyRequestPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ function MoneyRequestPreview(props) {
}

let message = props.translate('iou.cash');
if (props.iouReport.isWaitingOnBankAccount) {
if (ReportUtils.isControlPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) {
message += ` • ${props.translate('iou.approved')}`;
} else if (props.iouReport.isWaitingOnBankAccount) {
message += ` • ${props.translate('iou.pending')}`;
} else if (ReportUtils.isSettled(props.iouReport.reportID)) {
message += ` • ${props.translate('iou.settledExpensify')}`;
Expand Down
18 changes: 15 additions & 3 deletions src/components/ReportActionItem/ReportPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import styles from '../../styles/styles';
import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import compose from '../../libs/compose';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import ControlSelection from '../../libs/ControlSelection';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
Expand Down Expand Up @@ -41,6 +42,9 @@ const propTypes = {
// eslint-disable-next-line react/no-unused-prop-types
iouReportID: PropTypes.string.isRequired,

/** The report's policyID, used for Onyx subscription */
policyID: PropTypes.string.isRequired,

/* Onyx Props */
/** chatReport associated with iouReport */
chatReport: reportPropTypes,
Expand Down Expand Up @@ -140,15 +144,20 @@ function ReportPreview(props) {
};

const getPreviewMessage = () => {
const managerName = ReportUtils.isPolicyExpenseChat(props.chatReport) ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
if (isScanning) {
return props.translate('common.receipt');
}
if (ReportUtils.isControlPolicyExpenseChat(props.chatReport) && ReportUtils.isReportApproved(props.iouReport)) {
return props.translate('iou.managerApproved', {manager: ReportUtils.getDisplayNameForParticipant(managerID, true)});
}
const managerName = ReportUtils.isPolicyExpenseChat(props.chatReport) ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
return props.translate(iouSettled || props.iouReport.isWaitingOnBankAccount ? 'iou.payerPaid' : 'iou.payerOwes', {payer: managerName});
};

const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport);
const shouldShowSettlementButton = !_.isEmpty(props.iouReport) && isCurrentUserManager && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reportTotal !== 0;
const shouldShowSettlementButton = ReportUtils.isControlPolicyExpenseChat(props.chatReport)
? props.policy.role === CONST.POLICY.ROLE.ADMIN && ReportUtils.isReportApproved(props.iouReport) && !iouSettled
: !_.isEmpty(props.iouReport) && isCurrentUserManager && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reportTotal !== 0;

return (
<View style={[styles.chatItemMessage, ...props.containerStyles]}>
Expand Down Expand Up @@ -202,7 +211,7 @@ function ReportPreview(props) {
{shouldShowSettlementButton && (
<SettlementButton
currency={props.iouReport.currency}
policyID={props.iouReport.policyID}
policyID={props.policyID}
chatReportID={props.chatReportID}
iouReport={props.iouReport}
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)}
Expand All @@ -225,6 +234,9 @@ ReportPreview.displayName = 'ReportPreview';
export default compose(
withLocalize,
withOnyx({
policy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
},
chatReport: {
key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
},
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ export default {
},
iou: {
amount: 'Amount',
approve: 'Approve',
approved: 'Approved',
cash: 'Cash',
split: 'Split',
request: 'Request',
Expand Down Expand Up @@ -426,6 +428,7 @@ export default {
payerOwes: ({payer}) => `${payer} owes: `,
payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`,
payerPaid: ({payer}) => `${payer} paid: `,
managerApproved: ({manager}) => `${manager} approved:`,
payerSettled: ({amount}) => `paid ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`,
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ export default {
},
iou: {
amount: 'Importe',
approve: 'Aprobar',
approved: 'Aprobado',
cash: 'Efectivo',
split: 'Dividir',
request: 'Solicitar',
Expand Down Expand Up @@ -425,6 +427,7 @@ export default {
payerOwes: ({payer}) => `${payer} debe: `,
payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`,
payerPaid: ({payer}) => `${payer} pagó: `,
managerApproved: ({manager}) => `${manager} aprobó:`,
payerSettled: ({amount}) => `pagó ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`,
Expand Down
Loading

0 comments on commit 0343bfe

Please sign in to comment.