Skip to content

Commit 567bd21

Browse files
Merge pull request #51517 from paultsimura/fix/46897-distance-accountant
fix: distance - share with accountant
2 parents f4f258d + edfb777 commit 567bd21

12 files changed

+192
-36
lines changed

src/components/MoneyRequestConfirmationList.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,8 @@ function MoneyRequestConfirmationList({
211211
}
212212

213213
const defaultRate = defaultMileageRate?.customUnitRateID ?? '';
214-
const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate;
215-
const rateID = lastSelectedRate;
216-
IOU.setCustomUnitRateID(transactionID, rateID);
214+
const lastSelectedRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate;
215+
IOU.setCustomUnitRateID(transactionID, lastSelectedRateID);
217216
}, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, transactionID, isDistanceRequest]);
218217

219218
const mileageRate = DistanceRequestUtils.getRate({transaction, policy, policyDraft});
@@ -278,6 +277,18 @@ function MoneyRequestConfirmationList({
278277
const [didConfirm, setDidConfirm] = useState(isConfirmed);
279278
const [didConfirmSplit, setDidConfirmSplit] = useState(false);
280279

280+
// Clear the form error if it's set to one among the list passed as an argument
281+
const clearFormErrors = useCallback(
282+
(errors: string[]) => {
283+
if (!errors.includes(formError)) {
284+
return;
285+
}
286+
287+
setFormError('');
288+
},
289+
[formError, setFormError],
290+
);
291+
281292
const shouldDisplayFieldError: boolean = useMemo(() => {
282293
if (!isEditingSplitBill) {
283294
return false;
@@ -305,6 +316,32 @@ function MoneyRequestConfirmationList({
305316

306317
const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0);
307318

319+
useEffect(() => {
320+
// We want this effect to run only when the transaction is moving from Self DM to a workspace chat
321+
if (!isDistanceRequest || !isMovingTransactionFromTrackExpense || !isPolicyExpenseChat) {
322+
return;
323+
}
324+
325+
const errorKey = 'iou.error.invalidRate';
326+
const policyRates = DistanceRequestUtils.getMileageRates(policy);
327+
328+
// If the selected rate belongs to the policy, clear the error
329+
if (Object.keys(policyRates).includes(customUnitRateID)) {
330+
clearFormErrors([errorKey]);
331+
return;
332+
}
333+
334+
// If there is a distance rate in the policy that matches the rate and unit of the currently selected mileage rate, select it automatically
335+
const matchingRate = Object.values(policyRates).find((policyRate) => policyRate.rate === mileageRate.rate && policyRate.unit === mileageRate.unit);
336+
if (matchingRate?.customUnitRateID) {
337+
IOU.setCustomUnitRateID(transactionID, matchingRate.customUnitRateID);
338+
return;
339+
}
340+
341+
// If none of the above conditions are met, display the rate error
342+
setFormError(errorKey);
343+
}, [isDistanceRequest, isPolicyExpenseChat, transactionID, mileageRate, customUnitRateID, policy, isMovingTransactionFromTrackExpense, setFormError, clearFormErrors]);
344+
308345
useEffect(() => {
309346
if (shouldDisplayFieldError && didConfirmSplit) {
310347
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -315,7 +352,7 @@ function MoneyRequestConfirmationList({
315352
return;
316353
}
317354
// reset the form error whenever the screen gains or loses focus
318-
setFormError('');
355+
clearFormErrors(['iou.error.genericSmartscanFailureMessage', 'iou.receiptScanningFailed']);
319356

320357
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
321358
}, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
@@ -470,8 +507,8 @@ function MoneyRequestConfirmationList({
470507
return;
471508
}
472509

473-
setFormError('');
474-
}, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate]);
510+
clearFormErrors(['iou.error.invalidSplit', 'iou.error.invalidSplitParticipants', 'iou.error.invalidSplitYourself']);
511+
}, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate, clearFormErrors]);
475512

476513
useEffect(() => {
477514
if (!isTypeSplit || !transaction?.splitShares) {
@@ -638,7 +675,9 @@ function MoneyRequestConfirmationList({
638675
}, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]);
639676

640677
useEffect(() => {
641-
if (!isDistanceRequest || isMovingTransactionFromTrackExpense) {
678+
if (!isDistanceRequest || (isMovingTransactionFromTrackExpense && !isPolicyExpenseChat)) {
679+
// We don't want to recalculate the distance merchant when moving a transaction from Track Expense to a 1:1 chat, because the distance rate will be the same default P2P rate.
680+
// When moving to a policy chat (e.g. sharing with an accountant), we should recalculate the distance merchant with the policy's rate.
642681
return;
643682
}
644683

@@ -661,6 +700,7 @@ function MoneyRequestConfirmationList({
661700
translate,
662701
toLocaleDigit,
663702
isDistanceRequest,
703+
isPolicyExpenseChat,
664704
transaction,
665705
transactionID,
666706
action,

src/components/MoneyRequestConfirmationListFooter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ function MoneyRequestConfirmationListFooter({
259259
const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
260260
// Determine if the merchant error should be displayed
261261
const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty;
262+
const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate';
262263
// The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
263264
const shouldShowReceiptEmptyState = iouType === CONST.IOU.TYPE.SUBMIT && PolicyUtils.isPaidGroupPolicy(policy);
264265
const {
@@ -369,6 +370,7 @@ function MoneyRequestConfirmationListFooter({
369370
style={[styles.moneyRequestMenuItem]}
370371
titleStyle={styles.flex1}
371372
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
373+
brickRoadIndicator={shouldDisplayDistanceRateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
372374
disabled={didConfirm}
373375
interactive={!!rate && !isReadOnly && isPolicyExpenseChat}
374376
/>

src/languages/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import type {
9898
MarkedReimbursedParams,
9999
MarkReimbursedFromIntegrationParams,
100100
MissingPropertyParams,
101+
MovedFromSelfDMParams,
101102
NoLongerHaveAccessParams,
102103
NotAllowedExtensionParams,
103104
NotYouParams,
@@ -979,6 +980,7 @@ const translations = {
979980
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`,
980981
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`,
981982
threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
983+
movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `moved expense from self DM to ${workspaceName ?? `chat with ${reportName}`}`,
982984
tagSelection: 'Select a tag to better organize your spend.',
983985
categorySelection: 'Select a category to better organize your spend.',
984986
error: {
@@ -1008,6 +1010,7 @@ const translations = {
10081010
splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.',
10091011
invalidMerchant: 'Please enter a correct merchant.',
10101012
atLeastOneAttendee: 'At least one attendee must be selected',
1013+
invalidRate: 'Rate not valid for this workspace. Please select an available rate from the workspace.',
10111014
},
10121015
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
10131016
enableWallet: 'Enable wallet',

src/languages/es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import type {
9797
MarkedReimbursedParams,
9898
MarkReimbursedFromIntegrationParams,
9999
MissingPropertyParams,
100+
MovedFromSelfDMParams,
100101
NoLongerHaveAccessParams,
101102
NotAllowedExtensionParams,
102103
NotYouParams,
@@ -977,6 +978,7 @@ const translations = {
977978
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`,
978979
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`,
979980
threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
981+
movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `movió el gasto desde su propio mensaje directo a ${workspaceName ?? `un chat con ${reportName}`}`,
980982
tagSelection: 'Selecciona una etiqueta para organizar mejor tus gastos.',
981983
categorySelection: 'Selecciona una categoría para organizar mejor tus gastos.',
982984
error: {
@@ -1006,6 +1008,7 @@ const translations = {
10061008
splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.',
10071009
invalidMerchant: 'Por favor, introduce un comerciante correcto.',
10081010
atLeastOneAttendee: 'Debe seleccionarse al menos un asistente',
1011+
invalidRate: 'Tasa no válida para este espacio de trabajo. Por favor, selecciona una tasa disponible en el espacio de trabajo.',
10091012
},
10101013
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`,
10111014
enableWallet: 'Habilitar billetera',

src/languages/params.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ type ThreadRequestReportNameParams = {formattedAmount: string; comment: string};
165165

166166
type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string};
167167

168+
type MovedFromSelfDMParams = {workspaceName?: string; reportName?: string};
169+
168170
type SizeExceededParams = {maxUploadSizeInMB: number};
169171

170172
type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number};
@@ -667,7 +669,7 @@ export type {
667669
LoggedInAsParams,
668670
ManagerApprovedAmountParams,
669671
ManagerApprovedParams,
670-
SignUpNewFaceCodeParams,
672+
MovedFromSelfDMParams,
671673
NoLongerHaveAccessParams,
672674
NotAllowedExtensionParams,
673675
NotYouParams,
@@ -701,6 +703,7 @@ export type {
701703
SetTheRequestParams,
702704
SettleExpensifyCardParams,
703705
SettledAfterAddedBankAccountParams,
706+
SignUpNewFaceCodeParams,
704707
SizeExceededParams,
705708
SplitAmountParams,
706709
StepCounterParams,

src/libs/API/parameters/CategorizeTrackedExpenseParams.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ type CategorizeTrackedExpenseParams = {
2020
taxCode: string;
2121
taxAmount: number;
2222
billable?: boolean;
23+
waypoints?: string;
24+
customUnitRateID?: string;
25+
policyExpenseChatReportID?: string;
26+
policyExpenseCreatedReportActionID?: string;
27+
adminsChatReportID?: string;
28+
adminsCreatedReportActionID?: string;
2329
};
2430

2531
export default CategorizeTrackedExpenseParams;

src/libs/API/parameters/ShareTrackedExpenseParams.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ type ShareTrackedExpenseParams = {
2020
taxCode: string;
2121
taxAmount: number;
2222
billable?: boolean;
23+
waypoints?: string;
24+
customUnitRateID?: string;
25+
policyExpenseChatReportID?: string;
26+
policyExpenseCreatedReportActionID?: string;
27+
adminsChatReportID?: string;
28+
adminsCreatedReportActionID?: string;
2329
};
2430

2531
export default ShareTrackedExpenseParams;

src/libs/ModifiedExpenseMessage.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
import isEmpty from 'lodash/isEmpty';
12
import Onyx from 'react-native-onyx';
23
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
34
import CONST from '@src/CONST';
45
import ONYXKEYS from '@src/ONYXKEYS';
5-
import type {PolicyTagLists, ReportAction} from '@src/types/onyx';
6+
import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx';
67
import * as CurrencyUtils from './CurrencyUtils';
78
import DateUtils from './DateUtils';
89
import * as Localize from './Localize';
910
import Log from './Log';
1011
import * as PolicyUtils from './PolicyUtils';
1112
import * as ReportActionsUtils from './ReportActionsUtils';
12-
import * as ReportConnection from './ReportConnection';
13+
// eslint-disable-next-line import/no-cycle
14+
import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicyName, getRootParentReport, isPolicyExpenseChat} from './ReportUtils';
1315
import * as TransactionUtils from './TransactionUtils';
1416

1517
let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
@@ -25,6 +27,13 @@ Onyx.connect({
2527
},
2628
});
2729

30+
let allReports: OnyxCollection<Report>;
31+
Onyx.connect({
32+
key: ONYXKEYS.COLLECTION.REPORT,
33+
waitForCollectionCallback: true,
34+
callback: (value) => (allReports = value),
35+
});
36+
2837
/**
2938
* Utility to get message based on boolean literal value.
3039
*/
@@ -126,6 +135,20 @@ function getForDistanceRequest(newMerchant: string, oldMerchant: string, newAmou
126135
});
127136
}
128137

138+
function getForExpenseMovedFromSelfDM(destinationReportID: string) {
139+
const destinationReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`];
140+
const rootParentReport = getRootParentReport(destinationReport);
141+
142+
// The "Move report" flow only supports moving expenses to a policy expense chat or a 1:1 DM.
143+
const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName(rootParentReport) : buildReportNameFromParticipantNames({report: rootParentReport});
144+
const policyName = getPolicyName(rootParentReport, true);
145+
146+
return Localize.translateLocal('iou.movedFromSelfDM', {
147+
reportName,
148+
workspaceName: !isEmpty(policyName) ? policyName : undefined,
149+
});
150+
}
151+
129152
/**
130153
* Get the report action message when expense has been modified.
131154
*
@@ -136,8 +159,13 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
136159
if (!ReportActionsUtils.isModifiedExpenseAction(reportAction)) {
137160
return '';
138161
}
162+
139163
const reportActionOriginalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
140-
const policyID = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';
164+
const policyID = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';
165+
166+
if (reportActionOriginalMessage?.movedToReportID) {
167+
return getForExpenseMovedFromSelfDM(reportActionOriginalMessage.movedToReportID);
168+
}
141169

142170
const removalFragments: string[] = [];
143171
const setFragments: string[] = [];

src/libs/ReportUtils.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import * as LocalePhoneNumber from './LocalePhoneNumber';
7373
import * as Localize from './Localize';
7474
import Log from './Log';
7575
import {isEmailPublicDomain} from './LoginUtils';
76+
// eslint-disable-next-line import/no-cycle
7677
import ModifiedExpenseMessage from './ModifiedExpenseMessage';
7778
import linkingConfig from './Navigation/linkingConfig';
7879
import Navigation from './Navigation/Navigation';
@@ -3915,6 +3916,21 @@ const reportNameCache = new Map<string, {lastVisibleActionCreated: string; repor
39153916
*/
39163917
const getCacheKey = (report: OnyxEntry<Report>): string => `${report?.reportID}-${report?.lastVisibleActionCreated}-${report?.reportName}`;
39173918

3919+
/**
3920+
* Get the title for a report using only participant names. This may be used for 1:1 DMs and other non-categorized chats.
3921+
*/
3922+
function buildReportNameFromParticipantNames({report, personalDetails}: {report: OnyxEntry<Report>; personalDetails?: Partial<PersonalDetailsList>}) {
3923+
const participantsWithoutCurrentUser: number[] = [];
3924+
Object.keys(report?.participants ?? {}).forEach((accountID) => {
3925+
const accID = Number(accountID);
3926+
if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) {
3927+
participantsWithoutCurrentUser.push(accID);
3928+
}
3929+
});
3930+
const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
3931+
return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', ');
3932+
}
3933+
39183934
/**
39193935
* Get the title for a report.
39203936
*/
@@ -4078,16 +4094,7 @@ function getReportName(
40784094
}
40794095

40804096
// Not a room or PolicyExpenseChat, generate title from first 5 other participants
4081-
const participantsWithoutCurrentUser: number[] = [];
4082-
Object.keys(report?.participants ?? {}).forEach((accountID) => {
4083-
const accID = Number(accountID);
4084-
if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) {
4085-
participantsWithoutCurrentUser.push(accID);
4086-
}
4087-
});
4088-
const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
4089-
const participantNames = participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', ');
4090-
formattedName = participantNames;
4097+
formattedName = buildReportNameFromParticipantNames({report, personalDetails});
40914098

40924099
if (reportID) {
40934100
reportNameCache.set(cacheKey, {lastVisibleActionCreated: report?.lastVisibleActionCreated ?? '', reportName: formattedName});
@@ -8523,6 +8530,7 @@ export {
85238530
buildOptimisticWorkspaceChats,
85248531
buildOptimisticCardAssignedReportAction,
85258532
buildParticipantsFromAccountIDs,
8533+
buildReportNameFromParticipantNames,
85268534
buildTransactionThread,
85278535
canAccessReport,
85288536
isReportNotFound,
@@ -8610,6 +8618,7 @@ export {
86108618
getPersonalDetailsForAccountID,
86118619
getPolicyDescriptionText,
86128620
getPolicyExpenseChat,
8621+
getPolicyExpenseChatName,
86138622
getPolicyName,
86148623
getPolicyType,
86158624
getReimbursementDeQueuedActionMessage,

0 commit comments

Comments
 (0)