Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Attendees item row for editing requests #48870

Merged
7 changes: 5 additions & 2 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,10 @@ function MoneyRequestConfirmationListFooter({

const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]);
const shouldShowAttendees = useMemo(() => !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM), [policy?.id, policy?.type]);
const shouldShowAttendees = useMemo(
() => iouType === CONST.IOU.TYPE.SUBMIT && !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can create a utility for shouldShowAttendees
Since this condition is used in several places

[iouType, policy?.id, policy?.type],
);

const senderWorkspace = useMemo(() => {
const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender);
Expand Down Expand Up @@ -516,7 +519,7 @@ function MoneyRequestConfirmationListFooter({
<MenuItemWithTopDescription
key="attendees"
shouldShowRightIcon
title={iouAttendees?.map((item) => item.displayName ?? item.login).join(', ')}
title={iouAttendees?.map((item) => item?.displayName ?? item?.login).join(', ')}
description={`${translate('iou.attendees')} ${
iouAttendees?.length && iouAttendees.length > 1 ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
}`}
Expand Down
22 changes: 22 additions & 0 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function MoneyRequestView({
const {
created: transactionDate,
amount: transactionAmount,
attendees: transactionAttendees,
taxAmount: transactionTaxAmount,
currency: transactionCurrency,
comment: transactionDescription,
Expand All @@ -166,6 +167,7 @@ function MoneyRequestView({
const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : '';
const formattedPerAttendeeAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : '';
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
const cardProgramName = TransactionUtils.getCardName(transaction);
Expand Down Expand Up @@ -218,6 +220,7 @@ function MoneyRequestView({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
const shouldShowAttendees = iouType === CONST.IOU.TYPE.SUBMIT && !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM);

const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest);
const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport);
Expand Down Expand Up @@ -660,6 +663,25 @@ function MoneyRequestView({
}}
/>
)}
{shouldShowAttendees && (
<OfflineWithFeedback pendingAction={getPendingFieldAction('attendees')}>
<MenuItemWithTopDescription
key="attendees"
shouldShowRightIcon
title={transactionAttendees?.map((item) => item?.displayName ?? item?.login).join(', ')}
description={`${translate('iou.attendees')} ${
transactionAttendees?.length && transactionAttendees.length > 1 ? `${formattedPerAttendeeAmount} ${translate('common.perPerson')}` : ''
}`}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() =>
Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
}
interactive
shouldRenderAsHTML
/>
</OfflineWithFeedback>
)}
{shouldShowBillable && (
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
<View>
Expand Down
13 changes: 13 additions & 0 deletions src/libs/ModifiedExpenseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,19 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
);
}

const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'attendees' in reportActionOriginalMessage;
if (hasModifiedAttendees) {
buildMessageFragmentForValue(
reportActionOriginalMessage.oldAttendees ?? '',
reportActionOriginalMessage.attendees ?? '',
Localize.translateLocal('iou.attendees'),
false,
setFragments,
removalFragments,
changeFragments,
);
}

const message =
getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) +
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +
Expand Down
6 changes: 4 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import type {
TransactionViolation,
UserWallet,
} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction';
import type Onboarding from '@src/types/onyx/Onboarding';
Expand Down Expand Up @@ -365,6 +365,7 @@ type OptimisticTaskReport = Pick<
type TransactionDetails = {
created: string;
amount: number;
attendees: Attendee[];
taxAmount?: number;
taxCode?: string;
currency: string;
Expand Down Expand Up @@ -2876,6 +2877,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry<Transaction>, creat
return {
created: TransactionUtils.getFormattedCreated(transaction, createdDateFormat),
amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
attendees: TransactionUtils.getAttendees(transaction),
taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
taxCode: TransactionUtils.getTaxCode(transaction),
currency: TransactionUtils.getCurrency(transaction),
Expand Down Expand Up @@ -3433,7 +3435,7 @@ function getModifiedExpenseOriginalMessage(
originalMessage.merchant = transactionChanges?.merchant;
}
if ('attendees' in transactionChanges) {
[originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(oldTransaction?.modifiedAttendees, oldTransaction?.attendees);
[originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(transactionChanges?.attendees, TransactionUtils.getAttendees(oldTransaction));
}

// The amount is always a combination of the currency and the number value so when one changes we need to store both
Expand Down
14 changes: 13 additions & 1 deletion src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ function buildOptimisticTransaction(
currency: string,
reportID: string,
comment = '',
attendees: Attendee[] = [],
created = '',
source = '',
originalTransactionID = '',
Expand Down Expand Up @@ -171,6 +172,7 @@ function buildOptimisticTransaction(
taxAmount,
billable,
reimbursable,
attendees,
};
}

Expand Down Expand Up @@ -281,6 +283,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
updatedTransaction.tag = transactionChanges.tag;
}

if (Object.hasOwn(transactionChanges, 'attendees')) {
updatedTransaction.modifiedAttendees = transactionChanges?.attendees;
}

if (
shouldUpdateReceiptState &&
shouldStopSmartscan &&
Expand All @@ -304,6 +310,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'attendees') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
};

return updatedTransaction;
Expand Down Expand Up @@ -405,8 +412,12 @@ function getMerchant(transaction: OnyxInputOrEntry<Transaction>): string {
return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? '';
}

function getAttendees(transaction: OnyxInputOrEntry<Transaction>): Attendee[] {
return transaction?.modifiedAttendees ? transaction.modifiedAttendees : transaction?.attendees ?? [];
}

/**
* Return the merchant field from the transaction, return the modifiedMerchant if present.
* Return the list of attendees as a string and modified list of attendees as a string if present.
*/
function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] {
const oldAttendees = modifiedAttendees ?? [];
Expand Down Expand Up @@ -1084,6 +1095,7 @@ export {
isManualRequest,
isScanRequest,
getAmount,
getAttendees,
getTaxAmount,
getTaxCode,
getCurrency,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Violations/ViolationsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ const ViolationsUtils = {
return violation.name as never;
}
},

// We have to use regex, because Violation limit is given in a inconvenient form: "$2,000.00"
getViolationAmountLimit(violation: TransactionViolation): number {
return Number(violation.data?.formattedLimit?.replace(/[^0-9]+/g, ''));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/[^0-9]+/g
We use this regex in several places
Let's create constant for this !

},
};

export default ViolationsUtils;
27 changes: 25 additions & 2 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,7 @@ function getSendInvoiceInformation(
currency,
optimisticInvoiceReport.reportID,
trimmedComment,
[],
created,
'',
'',
Expand Down Expand Up @@ -1998,6 +1999,7 @@ function getMoneyRequestInformation(
payeeEmail = currentUserEmail,
moneyRequestReportID = '',
linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction,
attendees?: Attendee[],
): MoneyRequestInformation {
const payerEmail = PhoneNumber.addSMSDomainIfPhoneNumber(participant.login ?? '');
const payerAccountID = Number(participant.accountID);
Expand Down Expand Up @@ -2057,6 +2059,7 @@ function getMoneyRequestInformation(
currency,
iouReport.reportID,
comment,
attendees,
created,
'',
'',
Expand Down Expand Up @@ -2290,6 +2293,7 @@ function getTrackExpenseInformation(
currency,
shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1',
comment,
[],
created,
'',
'',
Expand Down Expand Up @@ -2450,6 +2454,7 @@ function getUpdateMoneyRequestParams(
policyTagList: OnyxTypes.OnyxInputOrEntry<OnyxTypes.PolicyTagLists>,
policyCategories: OnyxTypes.OnyxInputOrEntry<OnyxTypes.PolicyCategories>,
onlyIncludeChangedFields: boolean,
violations?: OnyxEntry<OnyxTypes.TransactionViolations>,
): UpdateMoneyRequestData {
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
Expand Down Expand Up @@ -2677,7 +2682,19 @@ function getUpdateMoneyRequestParams(
}
}

// Clear out the error fields and loading states on success
const overLimitViolation = violations?.find((violation) => violation.name === 'overLimit');
// Update violation limit, if we modify attendees. The given limit value is for a single attendee, if we have multiple attendees we should multpiply limit by attende count
if ('attendees' in transactionChanges && !!overLimitViolation) {
const limitForSingleAttendee = Number(overLimitViolation.data?.formattedLimit?.replace(/[^0-9]+/g, ''));
if (limitForSingleAttendee * (transactionChanges?.attendees?.length ?? 1) > Math.abs(TransactionUtils.getAmount(transaction))) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
value: violations?.filter((violation) => violation.name !== 'overLimit') ?? [],
});
}
}

successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
Expand Down Expand Up @@ -3002,11 +3019,12 @@ function updateMoneyRequestAttendees(
policy: OnyxEntry<OnyxTypes.Policy>,
policyTagList: OnyxEntry<OnyxTypes.PolicyTagLists>,
policyCategories: OnyxEntry<OnyxTypes.PolicyCategories>,
violations: OnyxEntry<OnyxTypes.TransactionViolations>,
) {
const transactionChanges: TransactionChanges = {
attendees,
};
const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true, violations);
const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES, params, onyxData);
}
Expand Down Expand Up @@ -3529,6 +3547,7 @@ function requestMoney(
payeeEmail,
moneyRequestReportID,
linkedTrackedExpenseReportAction,
attendees,
);
const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport.reportID;

Expand Down Expand Up @@ -3935,6 +3954,7 @@ function createSplitsAndOnyxData(
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
[],
created,
'',
'',
Expand Down Expand Up @@ -4180,6 +4200,7 @@ function createSplitsAndOnyxData(
currency,
oneOnOneIOUReport.reportID,
comment,
[],
created,
CONST.IOU.TYPE.SPLIT,
splitTransaction.transactionID,
Expand Down Expand Up @@ -4523,6 +4544,7 @@ function startSplitBill({
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
[],
'',
'',
'',
Expand Down Expand Up @@ -4920,6 +4942,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
currency ?? '',
oneOnOneIOUReport?.reportID ?? '-1',
updatedTransaction?.comment?.comment,
[],
updatedTransaction?.modifiedCreated,
CONST.IOU.TYPE.SPLIT,
transactionID,
Expand Down
13 changes: 7 additions & 6 deletions src/pages/iou/request/step/IOURequestStepAttendees.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import Navigation from '@libs/Navigation/Navigation';
import * as TransactionUtils from '@libs/TransactionUtils';
import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttendeeSelector';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
Expand All @@ -30,19 +31,19 @@ type IOURequestStepAttendeesOnyxProps = {
type IOURequestStepAttendeesProps = IOURequestStepAttendeesOnyxProps & WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_ATTENDEES>;

function IOURequestStepAttendees({
route,
route: {
params: {transactionID, reportID, iouType, backTo, action},
},
policy,
policyTags,
policyCategories,
}: IOURequestStepAttendeesProps) {
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${route?.params.transactionID || -1}`);
const [attendees, setAttendees] = useState<Attendee[]>(transaction?.attendees ?? []);
const isEditing = action === CONST.IOU.ACTION.EDIT;
const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || -1}`);
const [attendees, setAttendees] = useState<Attendee[]>(TransactionUtils.getAttendees(transaction));
const previousAttendees = usePrevious(attendees);
const {translate} = useLocalize();
const isEditing = action === CONST.IOU.ACTION.EDIT;
const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);

const saveAttendees = useCallback(() => {
if (attendees.length <= 0) {
Expand All @@ -51,12 +52,12 @@ function IOURequestStepAttendees({
if (!lodashIsEqual(previousAttendees, attendees)) {
IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing);
if (isEditing) {
IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories);
IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations);
}
}

Navigation.goBack(backTo);
}, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID]);
}, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID, violations]);

const navigateBack = () => {
Navigation.goBack(backTo);
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/TransactionUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {Attendee} from '@src/types/onyx/IOU';
import * as TransactionUtils from '../../src/libs/TransactionUtils';
import type {Transaction} from '../../src/types/onyx';

Expand All @@ -6,8 +7,9 @@ function generateTransaction(values: Partial<Transaction> = {}): Transaction {
const amount = 100;
const currency = 'USD';
const comment = '';
const attendees: Attendee[] = [];
const created = '2023-10-01';
const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, created);
const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, attendees, created);

return {...baseValues, ...values};
}
Expand Down
Loading