Skip to content

Commit

Permalink
Merge pull request #3881 from rushatgabhane/split-deselect-attendees
Browse files Browse the repository at this point in the history
Deselect attendees when splitting bill
  • Loading branch information
Luke9389 authored Aug 3, 2021
2 parents e511b1f + ecb0d48 commit 22e7f0a
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 39 deletions.
1 change: 1 addition & 0 deletions src/CONST.js

Large diffs are not rendered by default.

159 changes: 121 additions & 38 deletions src/components/IOUConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import {ScrollView, TextInput} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
import {withSafeAreaInsets} from 'react-native-safe-area-context';
import _ from 'underscore';
import styles from '../styles/styles';
import Text from './Text';
import themeColors from '../styles/themes/default';
Expand Down Expand Up @@ -89,6 +90,11 @@ const propTypes = {
/** Is the network currently offline or not */
isOffline: PropTypes.bool,
}),

/** Current user session */
session: PropTypes.shape({
email: PropTypes.string.isRequired,
}).isRequired,
};

const defaultProps = {
Expand All @@ -106,41 +112,97 @@ const defaultProps = {
const MINIMUM_BOTTOM_OFFSET = 240;

class IOUConfirmationList extends Component {
constructor(props) {
super(props);

this.toggleOption = this.toggleOption.bind(this);

const formattedParticipants = _.map(this.getParticipantsWithAmount(this.props.participants), participant => ({
...participant, selected: true,
}));

this.state = {
participants: formattedParticipants,
};
}

/**
* Get selected participants
* @returns {Array}
*/
getSelectedParticipants() {
return _.filter(this.state.participants, participant => participant.selected);
}

/**
* Get unselected participants
* @returns {Array}
*/
getUnselectedParticipants() {
return _.filter(this.state.participants, participant => !participant.selected);
}

/**
* Returns the participants with amount
* @param {Array} participants
* @returns {Array}
*/
getParticipantsWithAmount(participants) {
return getIOUConfirmationOptionsFromParticipants(
participants,
this.props.numberFormat(this.calculateAmount(participants) / 100, {
style: 'currency',
currency: this.props.iou.selectedCurrencyCode,
}),
);
}

/**
* Returns the participants without amount
* @param {Array} participants
* @returns {Array}
*/
getParticipantsWithoutAmount(participants) {
return _.map(participants, option => _.omit(option, 'descriptiveText'));
}

/**
* Returns the sections needed for the OptionsSelector
*
* @param {Boolean} maxParticipantsReached
* @returns {Array}
*/
getSections() {
const sections = [];

if (this.props.hasMultipleParticipants) {
const selectedParticipants = this.getSelectedParticipants();
const unselectedParticipants = this.getUnselectedParticipants();

const formattedSelectedParticipants = this.getParticipantsWithAmount(selectedParticipants);
const formattedUnselectedParticipants = this.getParticipantsWithoutAmount(unselectedParticipants);

const formattedMyPersonalDetails = getIOUConfirmationOptionsFromMyPersonalDetail(
this.props.myPersonalDetails,
this.props.numberFormat(this.calculateAmount() / 100, {
this.props.numberFormat(this.calculateAmount(selectedParticipants, true) / 100, {
style: 'currency',
currency: this.props.iou.selectedCurrencyCode,
}),
);

const formattedParticipants = getIOUConfirmationOptionsFromParticipants(this.props.participants,
this.props.numberFormat(this.calculateAmount() / 100, {
style: 'currency',
currency: this.props.iou.selectedCurrencyCode,
}));

sections.push({
title: this.props.translate('iOUConfirmationList.whoPaid'),
data: [formattedMyPersonalDetails],
shouldShow: true,
indexOffset: 0,
});
sections.push({
}, {
title: this.props.translate('iOUConfirmationList.whoWasThere'),
data: formattedParticipants,
data: formattedSelectedParticipants,
shouldShow: true,
indexOffset: 0,
}, {
title: undefined,
data: formattedUnselectedParticipants,
shouldShow: !_.isEmpty(formattedUnselectedParticipants),
indexOffset: 0,
});
} else {
const formattedParticipants = getIOUConfirmationOptionsFromParticipants(this.props.participants,
Expand All @@ -161,7 +223,6 @@ class IOUConfirmationList extends Component {

/**
* Gets splits for the transaction
*
* @returns {Array|null}
*/
getSplits() {
Expand All @@ -170,63 +231,54 @@ class IOUConfirmationList extends Component {
if (!this.props.hasMultipleParticipants) {
return null;
}

const splits = this.props.participants.map(participant => ({
const selectedParticipants = this.getSelectedParticipants();
const splits = _.map(selectedParticipants, participant => ({
email: participant.login,

// We should send in cents to API
// Cents is temporary and there must be support for other currencies in the future
amount: this.calculateAmount(),
amount: this.calculateAmount(selectedParticipants),
}));

splits.push({
email: this.props.myPersonalDetails.login,

// The user is default and we should send in cents to API
// USD is temporary and there must be support for other currencies in the future
amount: this.calculateAmount(true),
amount: this.calculateAmount(selectedParticipants, true),
});
return splits;
}

/**
* Gets participants list for a report
*
* @returns {Array}
*/
getParticipants() {
const participants = this.props.participants.map(participant => participant.login);
participants.push(this.props.myPersonalDetails.login);
return participants;
}

/**
* Returns selected options with all participant logins -- there is checkmark for every row in List for split flow
* Returns selected options -- there is checkmark for every row in List for split flow
* @returns {Array}
*/
getAllOptionsAsSelected() {
getSelectedOptions() {
if (!this.props.hasMultipleParticipants) {
return [];
}
const selectedParticipants = this.getSelectedParticipants();
return [
...this.props.participants,
...selectedParticipants,
getIOUConfirmationOptionsFromMyPersonalDetail(this.props.myPersonalDetails),
];
}

/**
* Calculates the amount per user
* Calculates the amount per user given a list of participants
* @param {Array} participants
* @param {Boolean} isDefaultUser
* @returns {Number}
*/
calculateAmount(isDefaultUser = false) {
calculateAmount(participants, isDefaultUser = false) {
// Convert to cents before working with iouAmount to avoid
// javascript subtraction with decimal problem -- when dealing with decimals,
// because they are encoded as IEEE 754 floating point numbers, some of the decimal
// numbers cannot be represented with perfect accuracy.
// Cents is temporary and there must be support for other currencies in the future
const iouAmount = Math.round(parseFloat(this.props.iouAmount * 100));
const totalParticipants = this.props.participants.length + 1;
const totalParticipants = participants.length + 1;
const amountPerPerson = Math.round(iouAmount / totalParticipants);

if (!isDefaultUser) { return amountPerPerson; }
Expand All @@ -237,6 +289,29 @@ class IOUConfirmationList extends Component {
return iouAmount !== sumAmount ? (amountPerPerson + difference) : amountPerPerson;
}

/**
* Toggle selected option's selected prop.
* @param {Object} option
*/
toggleOption(option) {
// Return early if selected option is currently logged in user.
if (option.login === this.props.session.email) {
return;
}

this.setState((prevState) => {
const newParticipants = _.reject(prevState.participants, participant => (
participant.login === option.login
));

newParticipants.push({
...option,
selected: !option.selected,
});
return {participants: newParticipants};
});
}

render() {
const buttonText = this.props.translate(
this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', {
Expand All @@ -246,6 +321,9 @@ class IOUConfirmationList extends Component {
),
},
);
const hoverStyle = this.props.hasMultipleParticipants ? styles.hoveredComponentBG : {};
const toggleOption = this.props.hasMultipleParticipants ? this.toggleOption : undefined;
const selectedParticipants = this.getSelectedParticipants();
return (
<>
<ScrollView style={[styles.flex1, styles.w100]}>
Expand All @@ -258,12 +336,14 @@ class IOUConfirmationList extends Component {
}]}
sections={this.getSections()}
disableArrowKeysActions
disableRowInteractivity
disableFocusOptions
hideAdditionalOptionStates
forceTextUnreadStyle
canSelectMultipleOptions={this.props.hasMultipleParticipants}
disableFocusOptions
selectedOptions={this.getAllOptionsAsSelected()}
selectedOptions={this.getSelectedOptions()}
onSelectRow={toggleOption}
disableRowInteractivity={!this.props.hasMultipleParticipants}
optionHoveredStyle={hoverStyle}
/>
<Text style={[styles.p5, styles.textMicroBold, styles.colorHeading]}>
{this.props.translate('iOUConfirmationList.whatsItFor')}
Expand All @@ -287,9 +367,9 @@ class IOUConfirmationList extends Component {
)}
<Button
success
isDisabled={this.props.network.isOffline}
style={[styles.w100]}
isLoading={this.props.iou.loading && !this.props.network.isOffline}
isDisabled={selectedParticipants.length === 0 || this.props.network.isOffline}
text={buttonText}
onPress={() => this.props.onConfirm(this.getSplits())}
pressOnEnter
Expand All @@ -313,6 +393,9 @@ export default compose(
myPersonalDetails: {
key: ONYXKEYS.MY_PERSONAL_DETAILS,
},
session: {
key: ONYXKEYS.SESSION,
},
network: {
key: ONYXKEYS.NETWORK,
},
Expand Down
2 changes: 2 additions & 0 deletions src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ function getIOUConfirmationOptionsFromMyPersonalDetail(myPersonalDetail, amountT
alternateText: myPersonalDetail.login,
icons: [myPersonalDetail.avatar],
descriptiveText: amountText,
login: myPersonalDetail.login,
};
}

Expand Down Expand Up @@ -614,6 +615,7 @@ function getNewGroupOptions(

/**
* Build the options for the Sidebar a.k.a. LHN
*
* @param {Object} reports
* @param {Object} personalDetails
* @param {Object} draftComments
Expand Down
21 changes: 21 additions & 0 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ function createIOUSplit(params) {
});
}

/**
* Creates IOUSplit Transaction for Group DM
* @param {Object} params
* @param {Array} params.splits
* @param {String} params.comment
* @param {Number} params.amount
* @param {String} params.currency
* @param {String} params.reportID
*/
function createIOUSplitGroup(params) {
Onyx.merge(ONYXKEYS.IOU, {loading: true, creatingIOUTransaction: true, error: false});

API.CreateIOUSplit({
...params,
splits: JSON.stringify(params.splits),
})
.then(() => Onyx.merge(ONYXKEYS.IOU, {loading: false, creatingIOUTransaction: false}))
.catch(() => Onyx.merge(ONYXKEYS.IOU, {error: true}));
}

/**
* Reject an iouReport transaction. Declining and cancelling transactions are done via the same Auth command.
*
Expand Down Expand Up @@ -246,6 +266,7 @@ function payIOUReport({
export {
createIOUTransaction,
createIOUSplit,
createIOUSplitGroup,
rejectTransaction,
payIOUReport,
setIOUSelectedCurrency,
Expand Down
20 changes: 19 additions & 1 deletion src/pages/iou/IOUModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import IOUConfirmPage from './steps/IOUConfirmPage';
import Header from '../../components/Header';
import styles from '../../styles/styles';
import Icon from '../../components/Icon';
import {createIOUSplit, createIOUTransaction, setIOUSelectedCurrency} from '../../libs/actions/IOU';
import {
createIOUSplit, createIOUTransaction, createIOUSplitGroup, setIOUSelectedCurrency,
} from '../../libs/actions/IOU';
import {Close, BackArrow} from '../../components/Icon/Expensicons';
import Navigation from '../../libs/Navigation/Navigation';
import ONYXKEYS from '../../ONYXKEYS';
Expand Down Expand Up @@ -226,6 +228,22 @@ class IOUModal extends Component {
* @param {Array} [splits]
*/
createTransaction(splits) {
const reportID = lodashGet(this.props, 'route.params.reportID', '');

// Only splits from a group DM has a reportID
// Check if reportID is a number
if (splits && CONST.REGEX.NUMBER.test(reportID)) {
createIOUSplitGroup({
comment: this.state.comment,

// should send in cents to API
amount: Math.round(this.state.amount * 100),
currency: this.props.iou.selectedCurrencyCode,
splits,
reportID,
});
return;
}
if (splits) {
createIOUSplit({
comment: this.state.comment,
Expand Down

0 comments on commit 22e7f0a

Please sign in to comment.