Skip to content

Commit d17cada

Browse files
authored
Merge pull request #48870 from software-mansion-labs/attendee-edit
Create Attendees item row for editing requests
2 parents 9f18d1a + 6bd365b commit d17cada

File tree

10 files changed

+106
-14
lines changed

10 files changed

+106
-14
lines changed

src/CONST.ts

+3
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ const CONST = {
278278
// Regex to get link in href prop inside of <a/> component
279279
REGEX_LINK_IN_ANCHOR: /<a\s+(?:[^>]*?\s+)?href="([^"]*)"/gi,
280280

281+
// Regex to read violation value from string given by backend
282+
VIOLATION_LIMIT_REGEX: /[^0-9]+/g,
283+
281284
MERCHANT_NAME_MAX_LENGTH: 255,
282285

283286
MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',

src/components/MoneyRequestConfirmationListFooter.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function MoneyRequestConfirmationListFooter({
233233

234234
const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
235235
const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]);
236-
const shouldShowAttendees = useMemo(() => !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM), [policy?.id, policy?.type]);
236+
const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
237237

238238
const senderWorkspace = useMemo(() => {
239239
const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender);
@@ -516,7 +516,7 @@ function MoneyRequestConfirmationListFooter({
516516
<MenuItemWithTopDescription
517517
key="attendees"
518518
shouldShowRightIcon
519-
title={iouAttendees?.map((item) => item.displayName ?? item.login).join(', ')}
519+
title={iouAttendees?.map((item) => item?.displayName ?? item?.login).join(', ')}
520520
description={`${translate('iou.attendees')} ${
521521
iouAttendees?.length && iouAttendees.length > 1 ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
522522
}`}

src/components/ReportActionItem/MoneyRequestView.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
126126
const {
127127
created: transactionDate,
128128
amount: transactionAmount,
129+
attendees: transactionAttendees,
129130
taxAmount: transactionTaxAmount,
130131
currency: transactionCurrency,
131132
comment: transactionDescription,
@@ -139,6 +140,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
139140
const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
140141
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
141142
const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : '';
143+
const formattedPerAttendeeAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : '';
142144
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
143145
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
144146
const cardProgramName = TransactionUtils.getCardName(transaction);
@@ -191,6 +193,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
191193
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
192194
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
193195
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
196+
const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
194197

195198
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest);
196199
const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport);
@@ -637,6 +640,25 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
637640
}}
638641
/>
639642
)}
643+
{shouldShowAttendees && (
644+
<OfflineWithFeedback pendingAction={getPendingFieldAction('attendees')}>
645+
<MenuItemWithTopDescription
646+
key="attendees"
647+
shouldShowRightIcon
648+
title={transactionAttendees?.map((item) => item?.displayName ?? item?.login).join(', ')}
649+
description={`${translate('iou.attendees')} ${
650+
transactionAttendees?.length && transactionAttendees.length > 1 ? `${formattedPerAttendeeAmount} ${translate('common.perPerson')}` : ''
651+
}`}
652+
style={[styles.moneyRequestMenuItem]}
653+
titleStyle={styles.flex1}
654+
onPress={() =>
655+
Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
656+
}
657+
interactive
658+
shouldRenderAsHTML
659+
/>
660+
</OfflineWithFeedback>
661+
)}
640662
{shouldShowBillable && (
641663
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
642664
<View>

src/libs/ModifiedExpenseMessage.ts

+13
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,19 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
302302
);
303303
}
304304

305+
const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'attendees' in reportActionOriginalMessage;
306+
if (hasModifiedAttendees) {
307+
buildMessageFragmentForValue(
308+
reportActionOriginalMessage.oldAttendees ?? '',
309+
reportActionOriginalMessage.attendees ?? '',
310+
Localize.translateLocal('iou.attendees'),
311+
false,
312+
setFragments,
313+
removalFragments,
314+
changeFragments,
315+
);
316+
}
317+
305318
const message =
306319
getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) +
307320
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +

src/libs/ReportUtils.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import type {
4242
TransactionViolation,
4343
UserWallet,
4444
} from '@src/types/onyx';
45-
import type {Participant} from '@src/types/onyx/IOU';
45+
import type {Attendee, Participant} from '@src/types/onyx/IOU';
4646
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
4747
import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction';
4848
import type Onboarding from '@src/types/onyx/Onboarding';
@@ -362,6 +362,7 @@ type OptimisticTaskReport = Pick<
362362
type TransactionDetails = {
363363
created: string;
364364
amount: number;
365+
attendees: Attendee[];
365366
taxAmount?: number;
366367
taxCode?: string;
367368
currency: string;
@@ -2911,6 +2912,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry<Transaction>, creat
29112912
return {
29122913
created: TransactionUtils.getFormattedCreated(transaction, createdDateFormat),
29132914
amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
2915+
attendees: TransactionUtils.getAttendees(transaction),
29142916
taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
29152917
taxCode: TransactionUtils.getTaxCode(transaction),
29162918
currency: TransactionUtils.getCurrency(transaction),
@@ -3469,7 +3471,7 @@ function getModifiedExpenseOriginalMessage(
34693471
originalMessage.merchant = transactionChanges?.merchant;
34703472
}
34713473
if ('attendees' in transactionChanges) {
3472-
[originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(oldTransaction?.modifiedAttendees, oldTransaction?.attendees);
3474+
[originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(transactionChanges?.attendees, TransactionUtils.getAttendees(oldTransaction));
34733475
}
34743476

34753477
// The amount is always a combination of the currency and the number value so when one changes we need to store both

src/libs/TransactionUtils/index.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as ReportConnection from '@libs/ReportConnection';
1919
import * as ReportUtils from '@libs/ReportUtils';
2020
import type {IOURequestType} from '@userActions/IOU';
2121
import CONST from '@src/CONST';
22+
import type {IOUType} from '@src/CONST';
2223
import ONYXKEYS from '@src/ONYXKEYS';
2324
import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
2425
import type {Attendee} from '@src/types/onyx/IOU';
@@ -126,6 +127,7 @@ function buildOptimisticTransaction(
126127
currency: string,
127128
reportID: string,
128129
comment = '',
130+
attendees: Attendee[] = [],
129131
created = '',
130132
source = '',
131133
originalTransactionID = '',
@@ -171,6 +173,7 @@ function buildOptimisticTransaction(
171173
taxAmount,
172174
billable,
173175
reimbursable,
176+
attendees,
174177
};
175178
}
176179

@@ -199,6 +202,10 @@ function isMerchantMissing(transaction: OnyxEntry<Transaction>) {
199202
return isMerchantEmpty;
200203
}
201204

205+
function shouldShowAttendees(iouType: IOUType, policy: OnyxEntry<Policy>): boolean {
206+
return iouType === CONST.IOU.TYPE.SUBMIT && !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM);
207+
}
208+
202209
/**
203210
* Check if the merchant is partial i.e. `(none)`
204211
*/
@@ -293,6 +300,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
293300
updatedTransaction.tag = transactionChanges.tag;
294301
}
295302

303+
if (Object.hasOwn(transactionChanges, 'attendees')) {
304+
updatedTransaction.modifiedAttendees = transactionChanges?.attendees;
305+
}
306+
296307
if (
297308
shouldUpdateReceiptState &&
298309
shouldStopSmartscan &&
@@ -316,6 +327,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
316327
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
317328
...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
318329
...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
330+
...(Object.hasOwn(transactionChanges, 'attendees') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
319331
};
320332

321333
return updatedTransaction;
@@ -418,7 +430,14 @@ function getMerchant(transaction: OnyxInputOrEntry<Transaction>): string {
418430
}
419431

420432
/**
421-
* Return the merchant field from the transaction, return the modifiedMerchant if present.
433+
* Return the list of modified attendees if present otherwise list of attendees
434+
*/
435+
function getAttendees(transaction: OnyxInputOrEntry<Transaction>): Attendee[] {
436+
return transaction?.modifiedAttendees ? transaction.modifiedAttendees : transaction?.attendees ?? [];
437+
}
438+
439+
/**
440+
* Return the list of attendees as a string and modified list of attendees as a string if present.
422441
*/
423442
function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] {
424443
const oldAttendees = modifiedAttendees ?? [];
@@ -1096,6 +1115,7 @@ export {
10961115
isManualRequest,
10971116
isScanRequest,
10981117
getAmount,
1118+
getAttendees,
10991119
getTaxAmount,
11001120
getTaxCode,
11011121
getCurrency,
@@ -1160,6 +1180,7 @@ export {
11601180
removeSettledAndApprovedTransactions,
11611181
getCardName,
11621182
hasReceiptSource,
1183+
shouldShowAttendees,
11631184
};
11641185

11651186
export type {TransactionChanges};

src/libs/Violations/ViolationsUtils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,11 @@ const ViolationsUtils = {
345345
return violation.name as never;
346346
}
347347
},
348+
349+
// We have to use regex, because Violation limit is given in a inconvenient form: "$2,000.00"
350+
getViolationAmountLimit(violation: TransactionViolation): number {
351+
return Number(violation.data?.formattedLimit?.replace(CONST.VIOLATION_LIMIT_REGEX, ''));
352+
},
348353
};
349354

350355
export default ViolationsUtils;

src/libs/actions/IOU.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,7 @@ function getSendInvoiceInformation(
19011901
currency,
19021902
optimisticInvoiceReport.reportID,
19031903
trimmedComment,
1904+
[],
19041905
created,
19051906
'',
19061907
'',
@@ -2022,6 +2023,7 @@ function getMoneyRequestInformation(
20222023
payeeEmail = currentUserEmail,
20232024
moneyRequestReportID = '',
20242025
linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction,
2026+
attendees?: Attendee[],
20252027
): MoneyRequestInformation {
20262028
const payerEmail = PhoneNumber.addSMSDomainIfPhoneNumber(participant.login ?? '');
20272029
const payerAccountID = Number(participant.accountID);
@@ -2081,6 +2083,7 @@ function getMoneyRequestInformation(
20812083
currency,
20822084
iouReport.reportID,
20832085
comment,
2086+
attendees,
20842087
created,
20852088
'',
20862089
'',
@@ -2318,6 +2321,7 @@ function getTrackExpenseInformation(
23182321
currency,
23192322
shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1',
23202323
comment,
2324+
[],
23212325
created,
23222326
'',
23232327
'',
@@ -2478,6 +2482,7 @@ function getUpdateMoneyRequestParams(
24782482
policyTagList: OnyxTypes.OnyxInputOrEntry<OnyxTypes.PolicyTagLists>,
24792483
policyCategories: OnyxTypes.OnyxInputOrEntry<OnyxTypes.PolicyCategories>,
24802484
onlyIncludeChangedFields: boolean,
2485+
violations?: OnyxEntry<OnyxTypes.TransactionViolations>,
24812486
): UpdateMoneyRequestData {
24822487
const optimisticData: OnyxUpdate[] = [];
24832488
const successData: OnyxUpdate[] = [];
@@ -2721,7 +2726,19 @@ function getUpdateMoneyRequestParams(
27212726
}
27222727
}
27232728

2724-
// Clear out the error fields and loading states on success
2729+
const overLimitViolation = violations?.find((violation) => violation.name === 'overLimit');
2730+
// 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
2731+
if ('attendees' in transactionChanges && !!overLimitViolation) {
2732+
const limitForSingleAttendee = ViolationsUtils.getViolationAmountLimit(overLimitViolation);
2733+
if (limitForSingleAttendee * (transactionChanges?.attendees?.length ?? 1) > Math.abs(TransactionUtils.getAmount(transaction))) {
2734+
optimisticData.push({
2735+
onyxMethod: Onyx.METHOD.MERGE,
2736+
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
2737+
value: violations?.filter((violation) => violation.name !== 'overLimit') ?? [],
2738+
});
2739+
}
2740+
}
2741+
27252742
successData.push({
27262743
onyxMethod: Onyx.METHOD.MERGE,
27272744
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
@@ -3045,11 +3062,12 @@ function updateMoneyRequestAttendees(
30453062
policy: OnyxEntry<OnyxTypes.Policy>,
30463063
policyTagList: OnyxEntry<OnyxTypes.PolicyTagLists>,
30473064
policyCategories: OnyxEntry<OnyxTypes.PolicyCategories>,
3065+
violations: OnyxEntry<OnyxTypes.TransactionViolations>,
30483066
) {
30493067
const transactionChanges: TransactionChanges = {
30503068
attendees,
30513069
};
3052-
const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
3070+
const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true, violations);
30533071
const {params, onyxData} = data;
30543072
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES, params, onyxData);
30553073
}
@@ -3572,6 +3590,7 @@ function requestMoney(
35723590
payeeEmail,
35733591
moneyRequestReportID,
35743592
linkedTrackedExpenseReportAction,
3593+
attendees,
35753594
);
35763595
const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport.reportID;
35773596

@@ -3980,6 +3999,7 @@ function createSplitsAndOnyxData(
39803999
currency,
39814000
CONST.REPORT.SPLIT_REPORTID,
39824001
comment,
4002+
[],
39834003
created,
39844004
'',
39854005
'',
@@ -4217,6 +4237,7 @@ function createSplitsAndOnyxData(
42174237
currency,
42184238
oneOnOneIOUReport.reportID,
42194239
comment,
4240+
[],
42204241
created,
42214242
CONST.IOU.TYPE.SPLIT,
42224243
splitTransaction.transactionID,
@@ -4564,6 +4585,7 @@ function startSplitBill({
45644585
currency,
45654586
CONST.REPORT.SPLIT_REPORTID,
45664587
comment,
4588+
[],
45674589
'',
45684590
'',
45694591
'',
@@ -4970,6 +4992,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
49704992
currency ?? '',
49714993
oneOnOneIOUReport?.reportID ?? '-1',
49724994
updatedTransaction?.comment?.comment,
4995+
[],
49734996
updatedTransaction?.modifiedCreated,
49744997
CONST.IOU.TYPE.SPLIT,
49754998
transactionID,

src/pages/iou/request/step/IOURequestStepAttendees.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx';
55
import useLocalize from '@hooks/useLocalize';
66
import usePrevious from '@hooks/usePrevious';
77
import Navigation from '@libs/Navigation/Navigation';
8+
import * as TransactionUtils from '@libs/TransactionUtils';
89
import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttendeeSelector';
910
import * as IOU from '@userActions/IOU';
1011
import CONST from '@src/CONST';
@@ -30,19 +31,19 @@ type IOURequestStepAttendeesOnyxProps = {
3031
type IOURequestStepAttendeesProps = IOURequestStepAttendeesOnyxProps & WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_ATTENDEES>;
3132

3233
function IOURequestStepAttendees({
33-
route,
3434
route: {
3535
params: {transactionID, reportID, iouType, backTo, action},
3636
},
3737
policy,
3838
policyTags,
3939
policyCategories,
4040
}: IOURequestStepAttendeesProps) {
41-
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${route?.params.transactionID || -1}`);
42-
const [attendees, setAttendees] = useState<Attendee[]>(transaction?.attendees ?? []);
41+
const isEditing = action === CONST.IOU.ACTION.EDIT;
42+
const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || -1}`);
43+
const [attendees, setAttendees] = useState<Attendee[]>(TransactionUtils.getAttendees(transaction));
4344
const previousAttendees = usePrevious(attendees);
4445
const {translate} = useLocalize();
45-
const isEditing = action === CONST.IOU.ACTION.EDIT;
46+
const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
4647

4748
const saveAttendees = useCallback(() => {
4849
if (attendees.length <= 0) {
@@ -51,12 +52,12 @@ function IOURequestStepAttendees({
5152
if (!lodashIsEqual(previousAttendees, attendees)) {
5253
IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing);
5354
if (isEditing) {
54-
IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories);
55+
IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations);
5556
}
5657
}
5758

5859
Navigation.goBack(backTo);
59-
}, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID]);
60+
}, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID, violations]);
6061

6162
const navigateBack = () => {
6263
Navigation.goBack(backTo);

0 commit comments

Comments
 (0)