Skip to content

Commit 6943615

Browse files
authored
Merge pull request #59209 from daledah/fix/58588
feat: add reimbursable toggle to create expense flow
2 parents 49940a8 + 6362c64 commit 6943615

18 files changed

+194
-19
lines changed

src/CONST.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3718,6 +3718,7 @@ const CONST = {
37183718
TAG: 'tag',
37193719
TAX_RATE: 'taxRate',
37203720
TAX_AMOUNT: 'taxAmount',
3721+
REIMBURSABLE: 'reimbursable',
37213722
REPORT: 'report',
37223723
},
37233724
FOOTER: {

src/components/MoneyRequestConfirmationList.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ type MoneyRequestConfirmationListProps = {
183183

184184
/** The PDF password callback */
185185
onPDFPassword?: () => void;
186+
187+
/** Function to toggle reimbursable */
188+
onToggleReimbursable?: (isOn: boolean) => void;
189+
190+
/** Flag indicating if the IOU is reimbursable */
191+
iouIsReimbursable?: boolean;
186192
};
187193

188194
type MoneyRequestConfirmationListItem = Participant | OptionData;
@@ -224,6 +230,8 @@ function MoneyRequestConfirmationList({
224230
isConfirming,
225231
onPDFLoadError,
226232
onPDFPassword,
233+
iouIsReimbursable = true,
234+
onToggleReimbursable,
227235
}: MoneyRequestConfirmationListProps) {
228236
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true});
229237
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true});
@@ -1089,6 +1097,8 @@ function MoneyRequestConfirmationList({
10891097
unit={unit}
10901098
onPDFLoadError={onPDFLoadError}
10911099
onPDFPassword={onPDFPassword}
1100+
iouIsReimbursable={iouIsReimbursable}
1101+
onToggleReimbursable={onToggleReimbursable}
10921102
isReceiptEditable={isReceiptEditable}
10931103
/>
10941104
);

src/components/MoneyRequestConfirmationListFooter.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
getTaxAmount,
2626
getTaxName,
2727
isAmountMissing,
28+
isCardTransaction,
2829
isCreatedMissing,
2930
isFetchingWaypointsFromServer,
3031
shouldShowAttendees as shouldShowAttendeesTransactionUtils,
@@ -196,6 +197,12 @@ type MoneyRequestConfirmationListFooterProps = {
196197

197198
/** The PDF password callback */
198199
onPDFPassword?: () => void;
200+
201+
/** Function to toggle reimbursable */
202+
onToggleReimbursable?: (isOn: boolean) => void;
203+
204+
/** Flag indicating if the IOU is reimbursable */
205+
iouIsReimbursable: boolean;
199206
};
200207

201208
function MoneyRequestConfirmationListFooter({
@@ -246,6 +253,8 @@ function MoneyRequestConfirmationListFooter({
246253
unit,
247254
onPDFLoadError,
248255
onPDFPassword,
256+
iouIsReimbursable,
257+
onToggleReimbursable,
249258
isReceiptEditable = false,
250259
}: MoneyRequestConfirmationListFooterProps) {
251260
const styles = useThemeStyles();
@@ -312,6 +321,7 @@ function MoneyRequestConfirmationListFooter({
312321
const canModifyTaxFields = !isReadOnly && !isDistanceRequest && !isPerDiemRequest;
313322
// A flag for showing the billable field
314323
const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
324+
const shouldShowReimbursable = policy?.disabledFields?.reimbursable === false && !isCardTransaction(transaction);
315325
// Do not hide fields in case of paying someone
316326
const shouldShowAllFields = !!isPerDiemRequest || !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill;
317327
// Calculate the formatted tax amount based on the transaction's tax amount and the IOU currency code
@@ -636,6 +646,25 @@ function MoneyRequestConfirmationListFooter({
636646
shouldShow: shouldShowAttendees,
637647
isSupplementary: true,
638648
},
649+
{
650+
item: (
651+
<View
652+
key={Str.UCFirst(translate('iou.reimbursable'))}
653+
style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8, styles.optionRow]}
654+
>
655+
<ToggleSettingOptionRow
656+
switchAccessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
657+
title={Str.UCFirst(translate('iou.reimbursable'))}
658+
onToggle={(isOn) => onToggleReimbursable?.(isOn)}
659+
isActive={iouIsReimbursable}
660+
disabled={isReadOnly}
661+
wrapperStyle={styles.flex1}
662+
/>
663+
</View>
664+
),
665+
shouldShow: shouldShowReimbursable,
666+
isSupplementary: true,
667+
},
639668
{
640669
item: (
641670
<View

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {Str} from 'expensify-common';
12
import mapValues from 'lodash/mapValues';
23
import React, {useCallback, useMemo, useState} from 'react';
34
import {View} from 'react-native';
@@ -51,6 +52,7 @@ import {
5152
getBillable,
5253
getDescription,
5354
getDistanceInMeters,
55+
getReimbursable,
5456
getTagForDisplay,
5557
getTaxName,
5658
hasMissingSmartscanFields,
@@ -66,7 +68,7 @@ import {
6668
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
6769
import Navigation from '@navigation/Navigation';
6870
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
69-
import {cleanUpMoneyRequest, updateMoneyRequestBillable} from '@userActions/IOU';
71+
import {cleanUpMoneyRequest, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU';
7072
import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
7173
import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions';
7274
import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction';
@@ -153,6 +155,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
153155
currency: transactionCurrency,
154156
comment: transactionDescription,
155157
merchant: transactionMerchant,
158+
reimbursable: transactionReimbursable,
156159
billable: transactionBillable,
157160
category: transactionCategory,
158161
tag: transactionTag,
@@ -230,6 +233,9 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
230233
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
231234
const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists));
232235
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
236+
const shouldShowReimbursable =
237+
isPolicyExpenseChat && (!!transactionReimbursable || !(policy?.disabledFields?.reimbursable ?? true) || !!updatedTransaction?.reimbursable) && !isCardTransaction;
238+
const canEditReimbursable = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE);
233239
const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]);
234240

235241
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
@@ -279,6 +285,17 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
279285
[transaction, report, policy, policyTagList, policyCategories],
280286
);
281287

288+
const saveReimbursable = useCallback(
289+
(newReimbursable: boolean) => {
290+
// If the value hasn't changed, don't request to save changes on the server and just close the modal
291+
if (newReimbursable === getReimbursable(transaction) || !transaction?.transactionID || !report?.reportID) {
292+
return;
293+
}
294+
updateMoneyRequestReimbursable(transaction.transactionID, report?.reportID, newReimbursable, policy, policyTagList, policyCategories);
295+
},
296+
[transaction, report, policy, policyTagList, policyCategories],
297+
);
298+
282299
if (isCardTransaction) {
283300
if (transactionPostedDate) {
284301
dateDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.posted')} ${transactionPostedDate}`;
@@ -800,6 +817,19 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
800817
/>
801818
</OfflineWithFeedback>
802819
)}
820+
{shouldShowReimbursable && (
821+
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
822+
<View>
823+
<Text>{Str.UCFirst(translate('iou.reimbursable'))}</Text>
824+
</View>
825+
<Switch
826+
accessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
827+
isOn={updatedTransaction?.reimbursable ?? !!transactionReimbursable}
828+
onToggle={saveReimbursable}
829+
disabled={!canEditReimbursable}
830+
/>
831+
</View>
832+
)}
803833
{/* Note: "Billable" toggle and "View trip details" should be always the last two items */}
804834
{shouldShowBillable && (
805835
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>

src/libs/API/parameters/CreateDistanceRequestParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type CreateDistanceRequestParams = {
1515
taxCode?: string;
1616
taxAmount?: number;
1717
billable?: boolean;
18+
reimbursable?: boolean;
1819
transactionThreadReportID?: string;
1920
createdReportActionIDForThread?: string;
2021
payerEmail?: string;

src/libs/API/parameters/CreatePerDiemRequestParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type CreatePerDiemRequestParams = {
2020
transactionThreadReportID: string;
2121
createdReportActionIDForThread: string | undefined;
2222
billable?: boolean;
23+
reimbursable?: boolean;
2324
attendees?: string;
2425
};
2526

src/libs/API/parameters/RequestMoneyParams.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type RequestMoneyParams = {
2727
receiptGpsPoints?: string;
2828
transactionThreadReportID: string;
2929
createdReportActionIDForThread: string | undefined;
30-
reimbursible?: boolean;
30+
reimbursable?: boolean;
3131
description?: string;
3232
attendees?: string;
3333
};

src/libs/API/parameters/SplitBillParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type SplitBillParams = {
99
category: string;
1010
tag: string;
1111
billable: boolean;
12+
reimbursable: boolean;
1213
transactionID: string;
1314
reportActionID: string;
1415
createdReportActionID?: string;

src/libs/API/parameters/StartSplitBillParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type StartSplitBillParams = {
1313
isFromGroupDM: boolean;
1414
createdReportActionID?: string;
1515
billable: boolean;
16+
reimbursable: boolean;
1617
chatType?: string;
1718
taxCode?: string;
1819
taxAmount?: number;

src/libs/API/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ const WRITE_COMMANDS = {
188188
COMPLETE_SPLIT_BILL: 'CompleteSplitBill',
189189
UPDATE_MONEY_REQUEST_ATTENDEES: 'UpdateMoneyRequestAttendees',
190190
UPDATE_MONEY_REQUEST_DATE: 'UpdateMoneyRequestDate',
191+
UPDATE_MONEY_REQUEST_REIMBURSABLE: 'UpdateMoneyRequestReimbursable',
191192
UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable',
192193
UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant',
193194
UPDATE_MONEY_REQUEST_TAG: 'UpdateMoneyRequestTag',
@@ -655,6 +656,7 @@ type WriteCommandParameters = {
655656
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES]: Parameters.UpdateMoneyRequestParams;
656657
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE]: Parameters.UpdateMoneyRequestParams;
657658
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams;
659+
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_REIMBURSABLE]: Parameters.UpdateMoneyRequestParams;
658660
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams;
659661
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG]: Parameters.UpdateMoneyRequestParams;
660662
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_AMOUNT]: Parameters.UpdateMoneyRequestParams;

src/libs/ModifiedExpenseMessage.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,6 @@ Onyx.connect({
3636
callback: (value) => (allReports = value),
3737
});
3838

39-
/**
40-
* Utility to get message based on boolean literal value.
41-
*/
42-
function getBooleanLiteralMessage(value: string | undefined, truthyMessage: string, falsyMessage: string): string {
43-
return value === 'true' ? truthyMessage : falsyMessage;
44-
}
45-
4639
/**
4740
* Builds the partial message fragment for a modified field on the expense.
4841
*/
@@ -334,8 +327,8 @@ function getForReportAction({
334327
const hasModifiedReimbursable = isReportActionOriginalMessageAnObject && 'oldReimbursable' in reportActionOriginalMessage && 'reimbursable' in reportActionOriginalMessage;
335328
if (hasModifiedReimbursable) {
336329
buildMessageFragmentForValue(
337-
getBooleanLiteralMessage(reportActionOriginalMessage?.reimbursable, translateLocal('iou.reimbursable'), translateLocal('iou.nonReimbursable')),
338-
getBooleanLiteralMessage(reportActionOriginalMessage?.oldReimbursable, translateLocal('iou.reimbursable'), translateLocal('iou.nonReimbursable')),
330+
reportActionOriginalMessage?.reimbursable ?? '',
331+
reportActionOriginalMessage?.oldReimbursable ?? '',
339332
translateLocal('iou.expense'),
340333
true,
341334
setFragments,

src/libs/ReportUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ type TransactionDetails = {
660660
customUnitRateID?: string;
661661
comment: string;
662662
category: string;
663+
reimbursable: boolean;
663664
billable: boolean;
664665
tag: string;
665666
mccGroup?: ValueOf<typeof CONST.MCC_GROUPS>;
@@ -3734,6 +3735,7 @@ function getTransactionDetails(
37343735
waypoints: getWaypoints(transaction),
37353736
customUnitRateID: getRateID(transaction),
37363737
category: getCategory(transaction),
3738+
reimbursable: getReimbursable(transaction),
37373739
billable: getBillable(transaction),
37383740
tag: getTag(transaction),
37393741
mccGroup: getMCCGroup(transaction),
@@ -3837,6 +3839,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxInputOrEntry<ReportAction>
38373839
CONST.EDIT_REQUEST_FIELD.RECEIPT,
38383840
CONST.EDIT_REQUEST_FIELD.DISTANCE,
38393841
CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE,
3842+
CONST.EDIT_REQUEST_FIELD.REIMBURSABLE,
38403843
CONST.EDIT_REQUEST_FIELD.REPORT,
38413844
];
38423845

@@ -4378,6 +4381,12 @@ function getModifiedExpenseOriginalMessage(
43784381
originalMessage.currency = getCurrency(oldTransaction);
43794382
}
43804383

4384+
if ('reimbursable' in transactionChanges) {
4385+
const oldReimbursable = getReimbursable(oldTransaction);
4386+
originalMessage.oldReimbursable = oldReimbursable ? translateLocal('common.reimbursable').toLowerCase() : translateLocal('iou.nonReimbursable').toLowerCase();
4387+
originalMessage.reimbursable = transactionChanges?.reimbursable ? translateLocal('common.reimbursable').toLowerCase() : translateLocal('iou.nonReimbursable').toLowerCase();
4388+
}
4389+
43814390
if ('billable' in transactionChanges) {
43824391
const oldBillable = getBillable(oldTransaction);
43834392
originalMessage.oldBillable = oldBillable ? translateLocal('common.billable').toLowerCase() : translateLocal('common.nonBillable').toLowerCase();

src/libs/TransactionUtils/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,10 @@ function getUpdatedTransaction({
486486
updatedTransaction.taxCode = transactionChanges.taxCode;
487487
}
488488

489+
if (Object.hasOwn(transactionChanges, 'reimbursable') && typeof transactionChanges.reimbursable === 'boolean') {
490+
updatedTransaction.reimbursable = transactionChanges.reimbursable;
491+
}
492+
489493
if (Object.hasOwn(transactionChanges, 'billable') && typeof transactionChanges.billable === 'boolean') {
490494
updatedTransaction.billable = transactionChanges.billable;
491495
}
@@ -526,6 +530,7 @@ function getUpdatedTransaction({
526530
...(Object.hasOwn(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
527531
...(Object.hasOwn(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
528532
...(Object.hasOwn(transactionChanges, 'waypoints') && {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
533+
...(Object.hasOwn(transactionChanges, 'reimbursable') && {reimbursable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
529534
...(Object.hasOwn(transactionChanges, 'billable') && {billable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
530535
...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
531536
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
@@ -684,7 +689,7 @@ function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Atten
684689
/**
685690
* Return the reimbursable value. Defaults to true to match BE logic.
686691
*/
687-
function getReimbursable(transaction: Transaction): boolean {
692+
function getReimbursable(transaction: OnyxInputOrEntry<Transaction>): boolean {
688693
return transaction?.reimbursable ?? true;
689694
}
690695

0 commit comments

Comments
 (0)