diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d762e6540a5d..8cf31045dd5c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -702,6 +702,8 @@ const CONST = { IS_TRAVEL_VERIFIED: 'isTravelVerified', PLAID_COMPANY_CARDS: 'plaidCompanyCards', TRACK_FLOWS: 'trackFlows', + NEWDOT_REVERT_SPLITS: 'newDotRevertSplits', + NEWDOT_UPDATE_SPLITS: 'newDotUpdateSplits', EXPENSIFY_CARD_EU_UK: 'expensifyCardEuUk', EUR_BILLING: 'eurBilling', NO_OPTIMISTIC_TRANSACTION_THREADS: 'noOptimisticTransactionThreads', @@ -5133,6 +5135,7 @@ const CONST = { DISABLED: 'disabled', }, SPACE_CHARACTER_WIDTH: 4, + CHARACTER_WIDTH: 8, // The attribute used in the SelectionScraper.js helper to query all the DOM elements // that should be removed from the copied contents in the getHTMLOfSelection() method diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index badb1da02674..f4d49e371347 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -15,6 +15,7 @@ import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; import usePaymentAnimations from '@hooks/usePaymentAnimations'; import usePaymentOptions from '@hooks/usePaymentOptions'; +import usePermissions from '@hooks/usePermissions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; @@ -71,6 +72,7 @@ import { import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import { allHavePendingRTERViolation, + getOriginalTransactionWithSplitInfo, hasDuplicateTransactions, isDuplicate, isExpensifyCardTransaction, @@ -198,6 +200,7 @@ function MoneyReportHeader({ const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true}); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const exportTemplates = useMemo(() => getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, policy), [integrationsExportTemplates, csvExportLayouts, policy]); + const {isBetaEnabled} = usePermissions(); const requestParentReportAction = useMemo(() => { if (!reportActions || !transactionThreadReport?.parentReportActionID) { @@ -256,6 +259,7 @@ function MoneyReportHeader({ const {translate} = useLocalize(); const {isOffline} = useNetwork(); const isOnHold = isOnHoldTransactionUtils(transaction); + const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); @@ -880,8 +884,9 @@ function MoneyReportHeader({ reportActions, policies, isChatReportArchived, + isNewDotUpdateSplitsBeta: isBetaEnabled(CONST.BETAS.NEWDOT_UPDATE_SPLITS), }); - }, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, policies, chatReport, isChatReportArchived, currentUserLogin]); + }, [moneyRequestReport, currentUserLogin, chatReport, transactions, violations, policy, reportNameValuePairs, reportActions, policies, isChatReportArchived, isBetaEnabled]); const secondaryExportActions = useMemo(() => { if (!moneyRequestReport) { @@ -994,7 +999,7 @@ function MoneyReportHeader({ }, }, [CONST.REPORT.SECONDARY_ACTIONS.SPLIT]: { - text: translate('iou.split'), + text: isExpenseSplit ? translate('iou.editSplits') : translate('iou.split'), icon: Expensicons.ArrowSplit, value: CONST.REPORT.SECONDARY_ACTIONS.SPLIT, onSelected: () => { diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 5666779d7fc0..f0335a266b02 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -720,7 +720,7 @@ function MoneyRequestConfirmationList({ onFormatAmount={convertToDisplayStringWithoutCurrency} onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? CONST.DEFAULT_NUMBER_ID, Number(value))} maxLength={formattedTotalAmount.length + 1} - contentWidth={(formattedTotalAmount.length + 1) * 8} + contentWidth={(formattedTotalAmount.length + 1) * CONST.CHARACTER_WIDTH} shouldApplyPaddingToContainer shouldUseDefaultLineHeightForPrefix={false} shouldWrapInputInContainer={false} diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 3e69059f0ed6..07b0eb3a5e45 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -9,6 +9,7 @@ import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactio import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; @@ -26,6 +27,7 @@ import {getSecondaryTransactionThreadActions} from '@libs/ReportSecondaryActionU import {changeMoneyRequestHoldStatus, isSelfDM, navigateToDetailsPage, rejectMoneyRequestReason} from '@libs/ReportUtils'; import {getReviewNavigationRoute} from '@libs/TransactionPreviewUtils'; import { + getOriginalTransactionWithSplitInfo, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDuplicate as isDuplicateTransactionUtils, isExpensifyCardTransaction, @@ -110,6 +112,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isDuplicate = isDuplicateTransactionUtils(transaction); const reportID = report?.reportID; const {removeTransaction} = useSearchContext(); + const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; @@ -117,6 +120,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const shouldDisplayTransactionNavigation = !!(reportID && isReportInRHP); const isParentReportArchived = useReportIsArchived(report?.parentReportID); + const {isBetaEnabled} = usePermissions(); + const hasPendingRTERViolation = hasPendingRTERViolationTransactionUtils(transactionViolations); const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(parentReport, policy, transactionViolations); @@ -259,8 +264,16 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre if (!transaction || !reportActions) { return []; } - return getSecondaryTransactionThreadActions(currentUserLogin ?? '', parentReport, transaction, Object.values(reportActions), policy, report); - }, [report, parentReport, policy, transaction, currentUserLogin]); + return getSecondaryTransactionThreadActions( + currentUserLogin ?? '', + parentReport, + transaction, + Object.values(reportActions), + policy, + report, + isBetaEnabled(CONST.BETAS.NEWDOT_UPDATE_SPLITS), + ); + }, [parentReport, transaction, currentUserLogin, policy, report, isBetaEnabled]); const dismissModalAndUpdateUseReject = () => { setIsRejectEducationalModalVisible(false); @@ -301,7 +314,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre }, }, [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.SPLIT]: { - text: translate('iou.split'), + text: isExpenseSplit ? translate('iou.editSplits') : translate('iou.split'), icon: Expensicons.ArrowSplit, value: CONST.REPORT.SECONDARY_ACTIONS.SPLIT, onSelected: () => { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e80ab02f4eec..6a9091866644 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -18,6 +18,7 @@ import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -36,6 +37,7 @@ import {getReportIDForExpense} from '@libs/MergeTransactionUtils'; import {hasEnabledOptions} from '@libs/OptionsListUtils'; import {getLengthOfTag, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isTaxTrackingEnabled} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import { canEditFieldOfMoneyRequest, canEditMoneyRequest, @@ -59,6 +61,7 @@ import { getDescription, getDistanceInMeters, getFormattedCreated, + getOriginalTransactionWithSplitInfo, getReimbursable, getTagForDisplay, getTaxName, @@ -67,7 +70,6 @@ import { hasRoute as hasRouteTransactionUtils, isCardTransaction as isCardTransactionTransactionUtils, isDistanceRequest as isDistanceRequestTransactionUtils, - isExpenseSplit, isExpenseUnreported as isExpenseUnreportedTransactionUtils, isManualDistanceRequest as isManualDistanceRequestTransactionUtils, isPerDiemRequest as isPerDiemRequestTransactionUtils, @@ -77,7 +79,7 @@ import { import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; -import {updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU'; +import {initSplitExpense, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -126,6 +128,7 @@ function MoneyRequestView({ const theme = useTheme(); const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); + const {isBetaEnabled} = usePermissions(); const {translate, toLocaleDigit} = useLocalize(); const {getReportRHPActiveRoute} = useActiveRoute(); @@ -168,9 +171,6 @@ function MoneyRequestView({ const transactionViolations = useTransactionViolations(transaction?.transactionID); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); - const originalTransactionIDFromComment = transaction?.comment?.originalTransactionID; - const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionIDFromComment ?? ''}`, {canBeMissing: true}); - const { created: transactionDate, amount: transactionAmount, @@ -232,9 +232,12 @@ function MoneyRequestView({ const isReportArchived = useReportIsArchived(report?.reportID); const isEditable = !!canUserPerformWriteActionReportUtils(report, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, transaction, isChatReportArchived) && isEditable; + const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction); + const isSplitAvailable = moneyRequestReport && transaction && isSplitAction(moneyRequestReport, [transaction], policy, isBetaEnabled(CONST.BETAS.NEWDOT_UPDATE_SPLITS)); const canEditTaxFields = canEdit && !isDistanceRequest; - const canEditAmount = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT, undefined, isChatReportArchived); + const canEditAmount = + (isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT, undefined, isChatReportArchived)) || (isExpenseSplit && isSplitAvailable); const canEditMerchant = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.MERCHANT, undefined, isChatReportArchived); const canEditDate = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE, undefined, isChatReportArchived); const canEditDistance = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE, undefined, isChatReportArchived); @@ -346,9 +349,6 @@ function MoneyRequestView({ if (formattedOriginalAmount) { amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.original')} ${formattedOriginalAmount}`; } - if (isExpenseSplit(transaction, originalTransaction)) { - amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.split')}`; - } if (isCancelled) { amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`; } @@ -356,9 +356,6 @@ function MoneyRequestView({ if (!isDistanceRequest && !isPerDiemRequest) { amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.cash')}`; } - if (isExpenseSplit(transaction, originalTransaction)) { - amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.split')}`; - } if (isCancelled) { amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`; } else if (isApproved) { @@ -367,6 +364,9 @@ function MoneyRequestView({ amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.settledExpensify')}`; } } + if (isExpenseSplit) { + amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.split')}`; + } const hasErrors = hasMissingSmartscanFields(transaction); const pendingAction = transaction?.pendingAction; @@ -453,6 +453,11 @@ function MoneyRequestView({ return; } + if (isExpenseSplit && isBetaEnabled(CONST.BETAS.NEWDOT_UPDATE_SPLITS)) { + initSplitExpense(transaction); + return; + } + if (isManualDistanceRequest) { Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_DISTANCE_MANUAL.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, report.reportID, getReportRHPActiveRoute()), @@ -478,6 +483,12 @@ function MoneyRequestView({ if (!transaction?.transactionID || !report?.reportID) { return; } + + if (isExpenseSplit && isBetaEnabled(CONST.BETAS.NEWDOT_UPDATE_SPLITS)) { + initSplitExpense(transaction); + return; + } + Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, report.reportID, getReportRHPActiveRoute()), ); @@ -618,6 +629,15 @@ function MoneyRequestView({ interactive={canEditAmount} shouldShowRightIcon={canEditAmount} onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } + + if (isExpenseSplit && isBetaEnabled(CONST.BETAS.NEWDOT_UPDATE_SPLITS)) { + initSplitExpense(transaction); + return; + } + Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, report.reportID, '', '', getReportRHPActiveRoute()), ); diff --git a/src/components/SelectionListWithSections/SplitListItem.tsx b/src/components/SelectionListWithSections/SplitListItem.tsx index 8f7ac13c11bc..c5d89b7d2851 100644 --- a/src/components/SelectionListWithSections/SplitListItem.tsx +++ b/src/components/SelectionListWithSections/SplitListItem.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; import {Folder, Tag} from '@components/Icon/Expensicons'; @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; import type {ListItem, SplitListItemProps, SplitListItemType} from './types'; @@ -41,6 +42,9 @@ function SplitListItem({ const isBottomVisible = !!splitItem.category || !!splitItem.tags?.at(0); + const [prefixCharacterMargin, setPrefixCharacterMargin] = useState(CONST.CHARACTER_WIDTH); + const inputMarginLeft = prefixCharacterMargin + styles.pl1.paddingLeft; + const contentWidth = (formattedOriginalAmount.length + 1) * CONST.CHARACTER_WIDTH; const focusHandler = useCallback(() => { if (!onInputFocus) { return; @@ -55,22 +59,17 @@ function SplitListItem({ return ( ({ - + {!splitItem.isEditable ? ( + + { + if (event.nativeEvent.layout.width === 0 && event.nativeEvent.layout.height === 0) { + return; + } + setPrefixCharacterMargin(event?.nativeEvent?.layout.width); + }} + > + {splitItem.currencySymbol} + + + {convertToDisplayStringWithoutCurrency(splitItem.amount, splitItem.currency)} + + + ) : ( + + )} - - + + {!splitItem.isEditable ? null : ( + + + + )} diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index d89984a74fd9..8413f2f2df03 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -465,8 +465,8 @@ type SplitListItemType = ListItem & /** Original amount before split */ originalAmount: number; - /** Indicates whether a split was opened through this transaction */ - isTransactionLinked: boolean; + /** Indicates whether a split wasn't approved, paid etc. when report.statusNum < CONST.REPORT.STATUS_NUM.CLOSED */ + isEditable: boolean; /** Function for updating amount */ onSplitExpenseAmountChange: (currentItemTransactionID: string, value: number) => void; diff --git a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx index 91539af4e7c5..43e056df1d54 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx @@ -105,7 +105,7 @@ function BaseTextInput({ const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); const [width, setWidth] = useState(null); - const [prefixCharacterPadding, setPrefixCharacterPadding] = useState(8); + const [prefixCharacterPadding, setPrefixCharacterPadding] = useState(CONST.CHARACTER_WIDTH); const [isPrefixCharacterPaddingCalculated, setIsPrefixCharacterPaddingCalculated] = useState(() => !prefixCharacter); const labelScale = useSharedValue(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE); const labelTranslateY = useSharedValue(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y); diff --git a/src/components/TextInput/BaseTextInput/implementation/index.tsx b/src/components/TextInput/BaseTextInput/implementation/index.tsx index dedda705fedc..095586b8fc50 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.tsx @@ -105,7 +105,7 @@ function BaseTextInput({ const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [width, setWidth] = useState(null); - const [prefixCharacterPadding, setPrefixCharacterPadding] = useState(8); + const [prefixCharacterPadding, setPrefixCharacterPadding] = useState(CONST.CHARACTER_WIDTH); const [isPrefixCharacterPaddingCalculated, setIsPrefixCharacterPaddingCalculated] = useState(() => !prefixCharacter); const labelScale = useSharedValue(initialActiveLabel ? ACTIVE_LABEL_SCALE : INACTIVE_LABEL_SCALE); diff --git a/src/languages/de.ts b/src/languages/de.ts index 9774693a77c2..d856e2750223 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1106,11 +1106,15 @@ const translations = { splitExpense: 'Ausgabe aufteilen', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} von ${merchant}`, addSplit: 'Split hinzufügen', + editSplits: 'Splits bearbeiten', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Der Gesamtbetrag ist ${amount} höher als die ursprüngliche Ausgabe.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Der Gesamtbetrag ist ${amount} weniger als die ursprüngliche Ausgabe.`, splitExpenseZeroAmount: 'Bitte geben Sie einen gültigen Betrag ein, bevor Sie fortfahren.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Bearbeiten Sie ${amount} für ${merchant}`, + splitExpenseOneMoreSplit: 'Keine Aufteilungen hinzugefügt. Fügen Sie mindestens eine hinzu, um zu speichern.', removeSplit: 'Aufteilung entfernen', + splitExpenseCannotBeEditedModalTitle: 'Diese Ausgabe kann nicht bearbeitet werden', + splitExpenseCannotBeEditedModalDescription: 'Genehmigte oder bezahlte Ausgaben können nicht bearbeitet werden', paySomeone: ({name}: PaySomeoneParams = {}) => `Zahlen Sie ${name ?? 'jemand'}`, expense: 'Ausgabe', categorize: 'Kategorisieren', diff --git a/src/languages/en.ts b/src/languages/en.ts index b07bebc41c22..ad79dea18efd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1090,11 +1090,15 @@ const translations = { splitExpense: 'Split expense', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} from ${merchant}`, addSplit: 'Add split', + editSplits: 'Edit splits', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Total amount is ${amount} greater than the original expense.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Total amount is ${amount} less than the original expense.`, splitExpenseZeroAmount: 'Please enter a valid amount before continuing.', + splitExpenseOneMoreSplit: 'No splits added. Add at least one to save.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Edit ${amount} for ${merchant}`, removeSplit: 'Remove split', + splitExpenseCannotBeEditedModalTitle: "This expense can't be edited", + splitExpenseCannotBeEditedModalDescription: 'Approved or paid expenses cannot be edited', paySomeone: ({name}: PaySomeoneParams = {}) => `Pay ${name ?? 'someone'}`, expense: 'Expense', categorize: 'Categorize', diff --git a/src/languages/es.ts b/src/languages/es.ts index f892f0dc9206..3211528afcb4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1078,11 +1078,15 @@ const translations = { splitExpense: 'Dividir gasto', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} de ${merchant}`, addSplit: 'Añadir división', + editSplits: 'Editar divisiones', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `El importe total es ${amount} mayor que el gasto original.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `El importe total es ${amount} menor que el gasto original.`, splitExpenseZeroAmount: 'Por favor, introduce un importe válido antes de continuar.', + splitExpenseOneMoreSplit: 'No se han añadido divisiones. Añade al menos una para guardar.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Editar ${amount} para ${merchant}`, removeSplit: 'Eliminar división', + splitExpenseCannotBeEditedModalTitle: 'Este gasto no se puede editar', + splitExpenseCannotBeEditedModalDescription: 'Los gastos aprobados o pagados no se pueden editar', addExpense: 'Agregar gasto', expense: 'Gasto', categorize: 'Categorizar', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 6ab5b72ef8d9..1cb750be8e4a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1104,10 +1104,14 @@ const translations = { splitExpense: 'Fractionner la dépense', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} de ${merchant}`, addSplit: 'Ajouter une répartition', + editSplits: 'Modifier les répartitions', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Le montant total est de ${amount} supérieur à la dépense initiale.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Le montant total est de ${amount} inférieur à la dépense originale.`, splitExpenseZeroAmount: 'Veuillez entrer un montant valide avant de continuer.', + splitExpenseOneMoreSplit: 'Aucun partage ajouté. Ajoutez au moins un pour enregistrer.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Modifier ${amount} pour ${merchant}`, + splitExpenseCannotBeEditedModalTitle: 'Cette dépense ne peut pas être modifiée', + splitExpenseCannotBeEditedModalDescription: 'Les dépenses approuvées ou payées ne peuvent pas être modifiées', removeSplit: 'Supprimer la division', paySomeone: ({name}: PaySomeoneParams = {}) => `Payer ${name ?? "quelqu'un"}`, expense: 'Dépense', diff --git a/src/languages/it.ts b/src/languages/it.ts index 93ca1f414040..cb106583dc65 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1100,11 +1100,15 @@ const translations = { splitExpense: 'Dividi spesa', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} da ${merchant}`, addSplit: 'Aggiungi divisione', + editSplits: 'Modifica suddivisioni', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `L'importo totale è ${amount} maggiore della spesa originale.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `L'importo totale è ${amount} inferiore alla spesa originale.`, splitExpenseZeroAmount: 'Per favore inserisci un importo valido prima di continuare.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Modifica ${amount} per ${merchant}`, + splitExpenseOneMoreSplit: 'Nessuna suddivisione aggiunta. Aggiungine almeno una per salvare.', removeSplit: 'Rimuovi divisione', + splitExpenseCannotBeEditedModalTitle: 'Questa spesa non può essere modificata', + splitExpenseCannotBeEditedModalDescription: 'Le spese approvate o pagate non possono essere modificate', paySomeone: ({name}: PaySomeoneParams = {}) => `Paga ${name ?? 'qualcuno'}`, expense: 'Spesa', categorize: 'Categorizza', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 102c33bd6c47..8beb5f1d4a44 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1102,10 +1102,14 @@ const translations = { splitExpense: '経費を分割', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${merchant}から${amount}`, addSplit: '分割を追加', + editSplits: '分割を編集', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `合計金額は元の経費よりも${amount}多いです。`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `合計金額は元の経費よりも${amount}少なくなっています。`, splitExpenseZeroAmount: '続行する前に有効な金額を入力してください。', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `${merchant}の${amount}を編集`, + splitExpenseOneMoreSplit: '分割が追加されていません。保存するには少なくとも1つ追加してください。', + splitExpenseCannotBeEditedModalTitle: 'この経費は編集できません', + splitExpenseCannotBeEditedModalDescription: '承認済みまたは支払済みの経費は編集できません', removeSplit: '分割を削除', paySomeone: ({name}: PaySomeoneParams = {}) => `${name ?? '誰か'}に支払う`, expense: '経費', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 89d3cc7dae2a..4c1c25fd4bdd 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1100,9 +1100,13 @@ const translations = { splitExpense: 'Uitgave splitsen', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} van ${merchant}`, addSplit: 'Splits toevoegen', + editSplits: 'Splits bewerken', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Het totale bedrag is ${amount} meer dan de oorspronkelijke uitgave.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Het totale bedrag is ${amount} minder dan de oorspronkelijke uitgave.`, splitExpenseZeroAmount: 'Voer een geldig bedrag in voordat u doorgaat.', + splitExpenseOneMoreSplit: 'Geen splitsing toegevoegd. Voeg er ten minste \u00E9\u00E9n toe om op te slaan.', + splitExpenseCannotBeEditedModalTitle: 'Deze uitgave kan niet worden bewerkt', + splitExpenseCannotBeEditedModalDescription: 'Goedgekeurde of betaalde uitgaven kunnen niet worden bewerkt', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Bewerk ${amount} voor ${merchant}`, removeSplit: 'Verwijder splitsing', paySomeone: ({name}: PaySomeoneParams = {}) => `Betaal ${name ?? 'iemand'}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ad4463fd5fdc..c167a4cd52b9 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1099,10 +1099,14 @@ const translations = { splitExpense: 'Podziel wydatek', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} od ${merchant}`, addSplit: 'Dodaj podział', + editSplits: 'Edytuj podziały', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Całkowita kwota jest o ${amount} większa niż pierwotny wydatek.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `Całkowita kwota jest o ${amount} mniejsza niż pierwotny wydatek.`, splitExpenseZeroAmount: 'Proszę wprowadzić prawidłową kwotę przed kontynuowaniem.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Edytuj ${amount} dla ${merchant}`, + splitExpenseOneMoreSplit: 'Nie dodano żadnych podziałów. Dodaj przynajmniej jeden, aby zapisać.', + splitExpenseCannotBeEditedModalTitle: 'Ten wydatek nie może być edytowany', + splitExpenseCannotBeEditedModalDescription: 'Zatwierdzone lub opłacone wydatki nie mogą być edytowane', removeSplit: 'Usuń podział', paySomeone: ({name}: PaySomeoneParams = {}) => `Zapłać ${name ?? 'ktoś'}`, expense: 'Wydatek', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 7a49e983d3c8..6733abb0c0fa 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1101,10 +1101,14 @@ const translations = { splitExpense: 'Dividir despesa', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `${amount} de ${merchant}`, addSplit: 'Adicionar divisão', + editSplits: 'Editar divisões', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `O valor total é ${amount} maior que a despesa original.`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `O valor total é ${amount} a menos que a despesa original.`, splitExpenseZeroAmount: 'Por favor, insira um valor válido antes de continuar.', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `Editar ${amount} para ${merchant}`, + splitExpenseOneMoreSplit: 'Nenhuma divisão adicionada. Adicione pelo menos uma para salvar.', + splitExpenseCannotBeEditedModalTitle: 'Esta despesa não pode ser editada', + splitExpenseCannotBeEditedModalDescription: 'Despesas aprovadas ou pagas não podem ser editadas', removeSplit: 'Remover divisão', paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar ${name ?? 'alguém'}`, expense: 'Despesa', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 19cd5c55084b..19466c779bc1 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1092,10 +1092,14 @@ const translations = { splitExpense: '拆分费用', splitExpenseSubtitle: ({amount, merchant}: SplitExpenseSubtitleParams) => `来自${merchant}的${amount}`, addSplit: '添加分账', + editSplits: '编辑拆分', totalAmountGreaterThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `总金额比原始费用多${amount}。`, totalAmountLessThanOriginal: ({amount}: TotalAmountGreaterOrLessThanOriginalParams) => `总金额比原始费用少 ${amount}。`, splitExpenseZeroAmount: '请在继续之前输入有效金额。', splitExpenseEditTitle: ({amount, merchant}: SplitExpenseEditTitleParams) => `为${merchant}编辑${amount}`, + splitExpenseOneMoreSplit: '没有添加分割。至少添加一个来保存。', + splitExpenseCannotBeEditedModalTitle: '此费用无法编辑', + splitExpenseCannotBeEditedModalDescription: '已批准或已支付的费用无法编辑', removeSplit: '移除拆分', paySomeone: ({name}: PaySomeoneParams = {}) => `支付${name ?? '某人'}`, expense: '费用', diff --git a/src/libs/API/parameters/SplitTransactionParams.ts b/src/libs/API/parameters/SplitTransactionParams.ts index 09c345ff3907..8b611cd8d038 100644 --- a/src/libs/API/parameters/SplitTransactionParams.ts +++ b/src/libs/API/parameters/SplitTransactionParams.ts @@ -1,23 +1,24 @@ +import type {Comment} from '@src/types/onyx/Transaction'; + type SplitTransactionSplitsParam = Array<{ amount: number; + transactionID: string; category?: string; tag?: string; created: string; merchant?: string; - comment?: { - comment?: string; - }; - nameValuePairs?: { - comment?: string; - }; + comment?: Comment; splitReportActionID?: string; transactionThreadReportID?: string; createdReportActionIDForThread?: string; + modifiedExpenseReportActionID?: string; + reimbursable?: boolean; + billable?: boolean; + reportID?: string; }>; type SplitTransactionParams = { transactionID: string; - isReverseSplitOperation: boolean; [key: string]: string | boolean; }; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 2bdedba908d6..cfe596d190ff 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -211,6 +211,7 @@ const WRITE_COMMANDS = { CREATE_PER_DIEM_REQUEST: 'CreatePerDiemRequest', SPLIT_BILL: 'SplitBill', SPLIT_BILL_AND_OPEN_REPORT: 'SplitBillAndOpenReport', + UPDATE_SPLIT_TRANSACTION: 'UpdateSplitTransaction', SPLIT_TRANSACTION: 'Transaction_Split', DELETE_MONEY_REQUEST: 'DeleteMoneyRequest', REJECT_MONEY_REQUEST: 'RejectMoneyRequest', @@ -709,6 +710,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SPLIT_BILL]: Parameters.SplitBillParams; [WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT]: Parameters.SplitBillParams; [WRITE_COMMANDS.SPLIT_TRANSACTION]: Parameters.SplitTransactionParams; + [WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION]: Parameters.SplitTransactionParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST]: Parameters.DeleteMoneyRequestParams; [WRITE_COMMANDS.REJECT_MONEY_REQUEST]: Parameters.RejectMoneyRequestParams; [WRITE_COMMANDS.MARK_TRANSACTION_VIOLATION_AS_RESOLVED]: Parameters.MarkTransactionViolationAsResolvedParams; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index fdef5f08ff76..2ef118b79b4c 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1077,6 +1077,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) accountant: CONST.RED_BRICK_ROAD_PENDING_ACTION, splitExpenses: CONST.RED_BRICK_ROAD_PENDING_ACTION, isDemoTransaction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + splitExpensesTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION, taxValue: CONST.RED_BRICK_ROAD_PENDING_ACTION, }, 'string', @@ -1117,6 +1118,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) dismissedViolations: 'object', splitExpenses: 'array', isDemoTransaction: 'boolean', + splitExpensesTotal: 'number', }); case 'accountant': return validateObject>(value, { diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 0b585a2f6904..7371b38635d9 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -74,7 +74,7 @@ function isAddExpenseAction(report: Report, reportTransactions: Transaction[], i return canAddTransaction(report, isReportArchived); } -function isSplitAction(report: Report, reportTransactions: Transaction[], policy?: Policy): boolean { +function isSplitAction(report: Report, reportTransactions: Transaction[], policy?: Policy, isNewDotUpdateSplitsBeta = true): boolean { if (Number(reportTransactions?.length) !== 1) { return false; } @@ -91,8 +91,12 @@ function isSplitAction(report: Report, reportTransactions: Transaction[], policy return false; } - const {isExpenseSplit, isBillSplit} = getOriginalTransactionWithSplitInfo(reportTransaction); - if (isExpenseSplit || isBillSplit) { + const {isBillSplit, isExpenseSplit} = getOriginalTransactionWithSplitInfo(reportTransaction); + if (isBillSplit) { + return false; + } + + if (isExpenseSplit && !isNewDotUpdateSplitsBeta) { return false; } @@ -100,7 +104,7 @@ function isSplitAction(report: Report, reportTransactions: Transaction[], policy return false; } - if (report.stateNum && report.stateNum >= CONST.REPORT.STATE_NUM.APPROVED) { + if (report.statusNum && report.statusNum >= CONST.REPORT.STATUS_NUM.CLOSED) { return false; } @@ -568,6 +572,7 @@ function getSecondaryReportActions({ reportActions, policies, isChatReportArchived = false, + isNewDotUpdateSplitsBeta, }: { currentUserEmail: string; report: Report; @@ -580,6 +585,7 @@ function getSecondaryReportActions({ policies?: OnyxCollection; canUseNewDotSplits?: boolean; isChatReportArchived?: boolean; + isNewDotUpdateSplitsBeta?: boolean; }): Array> { const options: Array> = []; @@ -639,7 +645,7 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.REJECT); } - if (isSplitAction(report, reportTransactions, policy)) { + if (isSplitAction(report, reportTransactions, policy, isNewDotUpdateSplitsBeta)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.SPLIT); } @@ -695,6 +701,7 @@ function getSecondaryTransactionThreadActions( reportActions: ReportAction[], policy: OnyxEntry, transactionThreadReport?: OnyxEntry, + isNewDotUpdateSplitsBeta?: boolean, ): Array> { const options: Array> = []; @@ -710,7 +717,7 @@ function getSecondaryTransactionThreadActions( options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT); } - if (isSplitAction(parentReport, [reportTransaction], policy)) { + if (isSplitAction(parentReport, [reportTransaction], policy, isNewDotUpdateSplitsBeta)) { options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.SPLIT); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index aa2b62b5ef05..71fcfe2b0acb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -727,6 +727,7 @@ type TransactionDetails = { originalAmount: number; originalCurrency: string; postedDate: string; + transactionID: string; distance?: number; }; @@ -4141,6 +4142,7 @@ function getTransactionDetails( originalAmount: getOriginalAmount(transaction), originalCurrency: getOriginalCurrency(transaction), postedDate: getFormattedPostedDate(transaction), + transactionID: transaction.transactionID, ...(isManualDistanceRequest && {distance: transaction.comment?.customUnit?.quantity ?? undefined}), }; } @@ -6634,7 +6636,7 @@ function buildOptimisticIOUReportAction(params: BuildOptimisticIOUReportActionPa automatic: false, isAttachmentOnly: false, originalMessage, - reportActionID: reportActionID ?? rand64(), + reportActionID: linkedExpenseReportAction?.reportActionID ?? reportActionID ?? rand64(), shouldShow: true, created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index b89f1aec0b3a..1edc6080d994 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -99,7 +99,9 @@ type TransactionParams = { filename?: string; customUnit?: TransactionCustomUnit; splitExpenses?: SplitExpense[]; + splitExpensesTotal?: number; participants?: Participant[]; + pendingAction?: PendingAction; distance?: number; }; @@ -310,7 +312,9 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T filename = '', customUnit, splitExpenses, + splitExpensesTotal, participants, + pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, } = transactionParams; // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -326,10 +330,12 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T if (originalTransactionID) { commentJSON.originalTransactionID = originalTransactionID; } - if (splitExpenses) { commentJSON.splitExpenses = splitExpenses; } + if (splitExpensesTotal) { + commentJSON.splitExpensesTotal = splitExpensesTotal; + } const isMapDistanceTransaction = !!pendingFields?.waypoints; const isManualDistanceTransaction = isManualDistanceRequest(existingTransaction); @@ -354,7 +360,7 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T comment: commentJSON, merchant: merchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, created: created || DateUtils.getDBTime(), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + pendingAction, receipt: receipt?.source ? {source: receipt.source, filename: receipt?.name ?? filename, state: receipt.state ?? CONST.IOU.RECEIPT_STATE.SCAN_READY, isTestDriveReceipt: receipt.isTestDriveReceipt} : {}, @@ -1929,6 +1935,20 @@ function isTransactionPendingDelete(transaction: OnyxEntry): boolea return getTransactionPendingAction(transaction) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } +/** + * Retrieves all “child” transactions associated with a given original transaction + */ +function getChildTransactions(originalTransactionID: string | undefined) { + return Object.values(allTransactions ?? {}).filter((currentTransaction) => { + const currentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${currentTransaction?.reportID}`]; + return ( + currentTransaction?.comment?.originalTransactionID === originalTransactionID && + !!currentReport && + currentTransaction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ); + }); +} + /** * Creates sections data for unreported expenses, marking transactions with DELETE pending action as disabled */ @@ -2063,6 +2083,7 @@ export { getOriginalTransactionWithSplitInfo, getTransactionPendingAction, isTransactionPendingDelete, + getChildTransactions, createUnreportedExpenseSections, isDemoTransaction, shouldShowViolation, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8eb30f894b93..804790681a6d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -208,9 +208,11 @@ import { buildOptimisticTransaction, getAmount, getCategoryTaxCodeAndAmount, + getChildTransactions, getCurrency, getDistanceInMeters, getMerchant, + getOriginalTransactionWithSplitInfo, getUpdatedTransaction, hasAnyTransactionWithoutRTERViolation, hasDuplicateTransactions, @@ -236,8 +238,8 @@ import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {Accountant, Attendee, Participant, Split} from '@src/types/onyx/IOU'; -import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; +import type {Accountant, Attendee, Participant, Split, SplitExpense} from '@src/types/onyx/IOU'; +import type {ErrorFields, Errors, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import type RecentlyUsedTags from '@src/types/onyx/RecentlyUsedTags'; @@ -423,6 +425,8 @@ type RequestMoneyTransactionParams = Omit & { originalTransactionID?: string; isTestDrive?: boolean; source?: string; + pendingAction?: PendingAction; + pendingFields?: PendingFields; distance?: number; isLinkedTrackedExpenseReportArchived?: boolean; }; @@ -494,13 +498,14 @@ type MoneyRequestInformationParams = { existingTransactionID?: string; existingTransaction?: OnyxEntry; retryParams?: StartSplitBilActionParams | CreateTrackExpenseParams | RequestMoneyInformation | ReplaceReceipt; - isSplitExpense?: boolean; + newReportTotal?: number; testDriveCommentReportActionID?: string; optimisticChatReportID?: string; optimisticCreatedReportActionID?: string; optimisticIOUReportID?: string; optimisticReportPreviewActionID?: string; shouldGenerateTransactionThreadReport?: boolean; + isSplitExpense?: boolean; }; type MoneyRequestOptimisticParams = { @@ -3364,13 +3369,14 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma existingTransactionID, moneyRequestReportID = '', retryParams, - isSplitExpense, + newReportTotal, testDriveCommentReportActionID, optimisticChatReportID, optimisticCreatedReportActionID, optimisticIOUReportID, optimisticReportPreviewActionID, shouldGenerateTransactionThreadReport = true, + isSplitExpense, } = moneyRequestInformation; const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams; const {policy, policyCategories, policyTagList} = policyParams; @@ -3392,6 +3398,8 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma billable, reimbursable = true, linkedTrackedExpenseReportAction, + pendingAction, + pendingFields = {}, } = transactionParams; const payerEmail = addSMSDomainIfPhoneNumber(participant.login ?? ''); @@ -3439,21 +3447,26 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma ? buildOptimisticExpenseReport(chatReport.reportID, chatReport.policyID, payeeAccountID, amount, currency, nonReimbursableTotal, undefined, optimisticIOUReportID) : buildOptimisticIOUReport(payeeAccountID, payerAccountID, amount, chatReport.reportID, currency, undefined, undefined, optimisticIOUReportID); } else if (isPolicyExpenseChat) { - // Splitting doesn't affect the amount, so no adjustment is needed - // The amount remains constant after the split - if (!isSplitExpense) { - iouReport = {...iouReport}; - // Because of the Expense reports are stored as negative values, we subtract the total from the amount - if (iouReport?.currency === currency) { - if (!Number.isNaN(iouReport.total) && iouReport.total !== undefined) { + iouReport = {...iouReport}; + // Because of the Expense reports are stored as negative values, we subtract the total from the amount + if (iouReport?.currency === currency) { + if (!Number.isNaN(iouReport.total) && iouReport.total !== undefined) { + // Use newReportTotal in scenarios where the total is based on more than just the current transaction, and we need to override it manually + if (newReportTotal) { + iouReport.total = newReportTotal; + } else { iouReport.total -= amount; } if (!reimbursable) { iouReport.nonReimbursableTotal = (iouReport.nonReimbursableTotal ?? 0) - amount; } - - if (typeof iouReport.unheldTotal === 'number') { + } + if (typeof iouReport.unheldTotal === 'number') { + // Use newReportTotal in scenarios where the total is based on more than just the current transaction amount, and we need to override it manually + if (newReportTotal) { + iouReport.unheldTotal = newReportTotal; + } else { iouReport.unheldTotal -= amount; } } @@ -3491,8 +3504,9 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma source, taxAmount: isExpenseReport(iouReport) ? -(taxAmount ?? 0) : taxAmount, billable, + pendingAction, + pendingFields: isDistanceRequest && !isManualDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, ...pendingFields} : pendingFields, reimbursable: isPolicyExpenseChat ? reimbursable : true, - pendingFields: isDistanceRequest && !isManualDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, }, isDemoTransactionParam: isSelectedManagerMcTest(participant.login) || transactionParams.receipt?.isTestDriveReceipt, }); @@ -4116,6 +4130,7 @@ function calculateDiffAmount( * @param policy May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param policyTagList * @param policyCategories + * @param newTransactionReportID */ function getUpdateMoneyRequestParams( transactionID: string | undefined, @@ -4126,6 +4141,7 @@ function getUpdateMoneyRequestParams( policyCategories: OnyxTypes.OnyxInputOrEntry, violations?: OnyxEntry, hash?: number, + newTransactionReportID?: string, ): UpdateMoneyRequestData { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; @@ -4139,8 +4155,9 @@ function getUpdateMoneyRequestParams( // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const isTransactionOnHold = isOnHold(transaction); - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread?.parentReportID}`] ?? null; + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${newTransactionReportID ?? transactionThread?.parentReportID}`] ?? null; const isFromExpenseReport = isExpenseReport(iouReport); const updatedTransaction: OnyxEntry = transaction ? getUpdatedTransaction({ @@ -4187,6 +4204,7 @@ function getUpdateMoneyRequestParams( modifiedAmount: transaction.modifiedAmount, modifiedMerchant: transaction.modifiedMerchant, modifiedCurrency: transaction.modifiedCurrency, + reportID: transaction.reportID, }, }); } @@ -4319,6 +4337,7 @@ function getUpdateMoneyRequestParams( value: { ...updatedTransaction, errorFields: null, + reportID: newTransactionReportID ?? updatedTransaction?.reportID, }, }); @@ -4453,6 +4472,7 @@ function getUpdateMoneyRequestParams( pendingFields: clearedPendingFields, isLoading: false, errorFields, + reportID: transaction?.reportID, }, }); @@ -12753,89 +12773,80 @@ function markRejectViolationAsResolved(transactionID: string, reportID?: string) notifyNewAction(currentReportID, userAccountID); } +function initSplitExpenseItemData( + transaction: OnyxEntry, + {amount, transactionID, reportID}: {amount?: number; transactionID?: string; reportID?: string} = {}, +): SplitExpense { + const transactionDetails = getTransactionDetails(transaction); + const currentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`]; + + return { + transactionID: transactionID ?? transactionDetails?.transactionID ?? String(CONST.DEFAULT_NUMBER_ID), + amount: amount ?? transactionDetails?.amount ?? 0, + description: transactionDetails?.comment, + category: transactionDetails?.category, + tags: transaction?.tag ? [transaction?.tag] : [], + created: transactionDetails?.created ?? DateUtils.formatWithUTCTimeZone(DateUtils.getDBTime(), CONST.DATE.FNS_FORMAT_STRING), + merchant: transaction?.modifiedMerchant ? transaction.modifiedMerchant : (transaction?.merchant ?? ''), + statusNum: currentReport?.statusNum ?? 0, + reportID: reportID ?? transaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID), + }; +} + /** * Create a draft transaction to set up split expense details for the split expense flow */ -function initSplitExpense(transaction: OnyxEntry, isOpenCreatedSplit?: boolean) { +function initSplitExpense(transaction: OnyxEntry) { if (!transaction) { return; } - const reportID = transaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); + const reportID = transaction.reportID ?? String(CONST.DEFAULT_NUMBER_ID); - if (isOpenCreatedSplit) { + const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction); + if (isExpenseSplit) { const originalTransactionID = transaction.comment?.originalTransactionID; const originalTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; - const relatedTransactions = Object.values(allTransactions).filter((currentTransaction) => { - const currentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${currentTransaction?.reportID}`]; - return currentTransaction?.comment?.originalTransactionID === originalTransactionID && !!currentReport && currentReport?.stateNum !== CONST.REPORT.STATUS_NUM.CLOSED; - }); - + const relatedTransactions = getChildTransactions(originalTransactionID); const transactionDetails = getTransactionDetails(originalTransaction); + const splitExpenses = relatedTransactions.map((currentTransaction) => initSplitExpenseItemData(currentTransaction)); + const draftTransaction = buildOptimisticTransaction({ originalTransactionID, transactionParams: { - splitExpenses: relatedTransactions.map((currentTransaction) => { - const currentTransactionDetails = getTransactionDetails(currentTransaction); - return { - transactionID: currentTransaction?.transactionID ?? String(CONST.DEFAULT_NUMBER_ID), - amount: currentTransactionDetails?.amount ?? 0, - description: currentTransactionDetails?.comment, - category: currentTransactionDetails?.category, - tags: currentTransactionDetails?.tag ? [currentTransactionDetails?.tag] : [], - created: currentTransaction?.created ?? '', - }; - }), + splitExpenses, + splitExpensesTotal: splitExpenses.reduce((total, item) => total + item.amount, 0), amount: transactionDetails?.amount ?? 0, currency: transactionDetails?.currency ?? CONST.CURRENCY.USD, - merchant: transactionDetails?.merchant ?? '', participants: transaction?.participants, + merchant: transaction?.modifiedMerchant ? transaction.modifiedMerchant : (transaction?.merchant ?? ''), attendees: transactionDetails?.attendees as Attendee[], - reportID: originalTransaction?.reportID, + reportID, reimbursable: transactionDetails?.reimbursable, }, }); Onyx.set(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${originalTransactionID}`, draftTransaction); - Navigation.navigate( - ROUTES.SPLIT_EXPENSE.getRoute( - originalTransaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID), - originalTransactionID, - transaction.transactionID, - Navigation.getActiveRouteWithoutParams(), - ), - ); + Navigation.navigate(ROUTES.SPLIT_EXPENSE.getRoute(reportID, originalTransactionID, transaction.transactionID, Navigation.getActiveRoute())); return; } const transactionDetails = getTransactionDetails(transaction); const transactionDetailsAmount = transactionDetails?.amount ?? 0; - const defaultCreated = DateUtils.formatWithUTCTimeZone(DateUtils.getDBTime(), CONST.DATE.FNS_FORMAT_STRING); + + const splitExpenses = [ + initSplitExpenseItemData(transaction, {amount: calculateIOUAmount(1, transactionDetailsAmount, transactionDetails?.currency ?? '', false), transactionID: NumberUtils.rand64()}), + initSplitExpenseItemData(transaction, {amount: calculateIOUAmount(1, transactionDetailsAmount, transactionDetails?.currency ?? '', true), transactionID: NumberUtils.rand64()}), + ]; const draftTransaction = buildOptimisticTransaction({ originalTransactionID: transaction.transactionID, transactionParams: { - splitExpenses: [ - { - transactionID: NumberUtils.rand64(), - amount: calculateIOUAmount(1, transactionDetailsAmount, transactionDetails?.currency ?? '', false), - description: transactionDetails?.comment, - category: transactionDetails?.category, - tags: transaction?.tag ? [transaction?.tag] : [], - created: transactionDetails?.created ?? defaultCreated, - }, - { - transactionID: NumberUtils.rand64(), - amount: calculateIOUAmount(1, transactionDetailsAmount, transactionDetails?.currency ?? '', true), - description: transactionDetails?.comment, - category: transactionDetails?.category, - tags: transaction?.tag ? [transaction?.tag] : [], - created: transactionDetails?.created ?? defaultCreated, - }, - ], + splitExpenses, + splitExpensesTotal: splitExpenses.reduce((total, item) => total + item.amount, 0), amount: transactionDetailsAmount, currency: transactionDetails?.currency ?? CONST.CURRENCY.USD, merchant: transactionDetails?.merchant ?? '', @@ -12848,7 +12859,7 @@ function initSplitExpense(transaction: OnyxEntry, isOpenC Onyx.set(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transaction?.transactionID}`, draftTransaction); - Navigation.navigate(ROUTES.SPLIT_EXPENSE.getRoute(reportID ?? String(CONST.DEFAULT_NUMBER_ID), transaction.transactionID, undefined, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.SPLIT_EXPENSE.getRoute(reportID, transaction.transactionID, undefined, Navigation.getActiveRoute())); } /** @@ -12872,7 +12883,7 @@ function initDraftSplitExpenseDataForEdit(draftTransaction: OnyxEntry, dra return; } - const transactionDetails = getTransactionDetails(transaction); - Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transaction.transactionID}`, { comment: { splitExpenses: [ ...(draftTransaction.comment?.splitExpenses ?? []), - { - transactionID: NumberUtils.rand64(), + initSplitExpenseItemData(transaction, { amount: 0, - description: transactionDetails?.comment, - category: transactionDetails?.category, - tags: transaction?.tag ? [transaction?.tag] : [], - created: transactionDetails?.created ?? DateUtils.formatWithUTCTimeZone(DateUtils.getDBTime(), CONST.DATE.FNS_FORMAT_STRING), - }, + transactionID: NumberUtils.rand64(), + reportID: draftTransaction?.reportID, + }), ], }, }); @@ -13007,12 +13013,24 @@ function saveSplitTransactions( const originalTransactionID = draftTransaction?.comment?.originalTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID; const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; + const originalTransactionDetails = getTransactionDetails(originalTransaction); const iouActions = getIOUActionForTransactions([originalTransactionID], expenseReport?.reportID); const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport); const splitExpenses = draftTransaction?.comment?.splitExpenses ?? []; + // List of all child transactions that have been created after split + const originalChildTransactions = getChildTransactions(originalTransactionID); + const isCreationOfSplits = originalChildTransactions.length === 0; + const processedChildTransactionIDs: string[] = []; + + const reportTotal = transactionReport?.total ?? 0; + const splitExpensesTotal = draftTransaction?.comment?.splitExpensesTotal ?? 0; + + const isReverseSplitOperation = splitExpenses.length === 1 && originalChildTransactions.length > 0; + + let changesInReportTotal = 0; // Validate custom unit rate before proceeding with split const customUnitRateID = originalTransaction?.comment?.customUnit?.customUnitRateID; const isPerDiem = isPerDiemRequestTransactionUtils(originalTransaction); @@ -13033,26 +13051,39 @@ function saveSplitTransactions( const splits: SplitTransactionSplitsParam = splitExpenses.map((split) => { const currentDescription = getParsedComment(Parser.htmlToMarkdown(split.description ?? '')); + changesInReportTotal += split.amount; return { amount: split.amount, category: split.category ?? '', tag: split.tags?.[0] ?? '', created: split.created, - merchant: draftTransaction?.merchant ?? '', + merchant: split?.merchant ?? '', transactionID: split.transactionID, comment: { comment: currentDescription, }, }; }) ?? []; + changesInReportTotal -= splitExpensesTotal; const successData = [] as OnyxUpdate[]; const failureData = [] as OnyxUpdate[]; const optimisticData = [] as OnyxUpdate[]; splitExpenses.forEach((splitExpense, index) => { + const existingTransactionID = isReverseSplitOperation ? originalTransactionID : splitExpense.transactionID; + const splitTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransactionID}`]; + if (splitTransaction) { + processedChildTransactionIDs.push(splitTransaction.transactionID); + } + + const splitReportActions = getAllReportActions(isReverseSplitOperation ? expenseReport?.reportID : splitTransaction?.reportID); + const currentReportAction = Object.values(splitReportActions).find((action) => { + const transactionID = isMoneyRequestAction(action) ? (getOriginalMessage(action)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; + return transactionID === existingTransactionID; + }); + const requestMoneyInformation = { - report: expenseReport, participantParams: { participant: participants.at(0) ?? ({} as Participant), payeeEmail: currentUserPersonalDetails?.login ?? '', @@ -13068,133 +13099,232 @@ function saveSplitTransactions( modifiedAmount: splitExpense.amount ?? 0, currency: draftTransaction?.currency ?? CONST.CURRENCY.USD, created: splitExpense.created, - merchant: draftTransaction?.merchant ?? '', + merchant: splitExpense.merchant ?? '', comment: splitExpense.description, category: splitExpense.category, tag: splitExpense.tags?.[0], originalTransactionID, attendees: draftTransaction?.comment?.attendees, source: CONST.IOU.TYPE.SPLIT, + linkedTrackedExpenseReportAction: currentReportAction, + pendingAction: splitTransaction ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + pendingFields: splitTransaction ? splitTransaction.pendingFields : undefined, reimbursable: draftTransaction?.reimbursable, }, - }; + parentChatReport: getReportOrDraftReport(getReportOrDraftReport(expenseReport?.chatReportID)?.parentReportID), + existingTransaction: originalTransaction, + } as MoneyRequestInformationParams; - const {report, participantParams, policyParams, transactionParams} = requestMoneyInformation; + if (isReverseSplitOperation) { + requestMoneyInformation.transactionParams = { + amount: splitExpense.amount ?? 0, + currency: draftTransaction?.currency ?? CONST.CURRENCY.USD, + created: splitExpense.created, + merchant: splitExpense.merchant ?? '', + comment: splitExpense.description, + category: splitExpense.category, + tag: splitExpense.tags?.[0], + attendees: draftTransaction?.comment?.attendees, + linkedTrackedExpenseReportAction: currentReportAction, + }; + requestMoneyInformation.existingTransaction = undefined; + } + + const {participantParams, policyParams, transactionParams, parentChatReport, existingTransaction} = requestMoneyInformation; const parsedComment = getParsedComment(Parser.htmlToMarkdown(transactionParams.comment ?? '')); transactionParams.comment = parsedComment; - const currentChatReport = getReportOrDraftReport(report?.chatReportID); - const parentChatReport = getReportOrDraftReport(currentChatReport?.parentReportID); - - const existingTransactionID = splitExpense.transactionID; - const {transactionThreadReportID, createdReportActionIDForThread, onyxData, iouAction} = getMoneyRequestInformation({ participantParams, parentChatReport, policyParams, transactionParams, - moneyRequestReportID: report?.reportID, - existingTransaction: originalTransaction, + moneyRequestReportID: splitExpense?.reportID, + existingTransaction, existingTransactionID, + newReportTotal: reportTotal - changesInReportTotal, isSplitExpense: true, }); - const split = splits.at(index); - if (split) { - // For request params we need to have the transactionThreadReportID, createdReportActionIDForThread and splitReportActionID which we get from moneyRequestInformation - split.transactionThreadReportID = transactionThreadReportID; - split.createdReportActionIDForThread = createdReportActionIDForThread; - split.splitReportActionID = iouAction.reportActionID; + let updateMoneyRequestParamsOnyxData: OnyxData = {}; + const currentSplit = splits.at(index); + + // For existing split transactions, update the field change messages + // For new transactions, skip this step + if (splitTransaction) { + const existing = getTransactionDetails(splitTransaction); + const transactionChanges = { + ...currentSplit, + comment: currentSplit?.comment?.comment, + } as TransactionChanges; + + if (currentSplit) { + currentSplit.reimbursable = splitTransaction.reimbursable; + currentSplit.billable = splitTransaction.billable; + } + + Object.keys(transactionChanges).forEach((key) => { + const newValue = transactionChanges[key as keyof typeof transactionChanges]; + const oldValue = existing?.[key as keyof typeof existing]; + if (newValue === oldValue) { + delete transactionChanges[key as keyof typeof transactionChanges]; + // Ensure we pass the currency to getUpdateMoneyRequestParams as well, so the amount message is created correctly + } else if (key === 'amount') { + transactionChanges.currency = originalTransactionDetails?.currency; + } + }); + + if (Object.keys(transactionChanges).length > 0) { + const {onyxData: moneyRequestParamsOnyxData, params} = getUpdateMoneyRequestParams( + existingTransactionID, + isReverseSplitOperation ? splitExpense?.reportID : transactionThreadReportID, + transactionChanges, + policy, + policyTags ?? null, + policyCategories ?? null, + undefined, + undefined, + splitExpense?.reportID, + ); + if (currentSplit) { + currentSplit.modifiedExpenseReportActionID = params.reportActionID; + } + updateMoneyRequestParamsOnyxData = moneyRequestParamsOnyxData; + } + // For new split transactions, set the reportID once the transaction and associated report are created + } else if (currentSplit) { + currentSplit.reportID = splitExpense?.reportID; + } + + if (currentSplit) { + currentSplit.transactionThreadReportID = transactionThreadReportID; + currentSplit.createdReportActionIDForThread = createdReportActionIDForThread; + currentSplit.splitReportActionID = iouAction.reportActionID; } - optimisticData.push(...(onyxData.optimisticData ?? [])); - successData.push(...(onyxData.successData ?? [])); - failureData.push(...(onyxData.failureData ?? [])); + optimisticData.push(...(onyxData.optimisticData ?? []), ...(updateMoneyRequestParamsOnyxData.optimisticData ?? [])); + successData.push(...(onyxData.successData ?? []), ...(updateMoneyRequestParamsOnyxData.successData ?? [])); + failureData.push(...(onyxData.failureData ?? []), ...(updateMoneyRequestParamsOnyxData.failureData ?? [])); }); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, - value: { - ...originalTransaction, - reportID: CONST.REPORT.SPLIT_REPORT_ID, - }, + // All transactions that were deleted in the split list will be marked as deleted in onyx + const undeletedTransactions = originalChildTransactions.filter( + (currentTransaction) => !processedChildTransactionIDs.includes(currentTransaction?.transactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID), + ); + undeletedTransactions.forEach((undeletedTransaction) => { + const splitTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${undeletedTransaction?.transactionID}`]; + const splitReportActions = getAllReportActions(splitTransaction?.reportID); + const currentReportAction = Object.values(splitReportActions).find((action) => { + const transactionID = isMoneyRequestAction(action) ? (getOriginalMessage(action)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; + return transactionID === undeletedTransaction?.transactionID; + }) as ReportAction; + + const { + optimisticData: deleteExpenseOptimisticData, + failureData: deleteExpenseFailureData, + successData: deleteExpenseSuccessData, + } = getDeleteTrackExpenseInformation(splitTransaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID), undeletedTransaction?.transactionID, currentReportAction); + + optimisticData.push(...(deleteExpenseOptimisticData ?? [])); + successData.push(...(deleteExpenseSuccessData ?? [])); + failureData.push(...(deleteExpenseFailureData ?? [])); }); - const firstIOU = iouActions.at(0); - if (firstIOU) { - const {updatedReportAction, iouReport, transactionThread} = prepareToCleanUpMoneyRequest(originalTransactionID, firstIOU); + if (!isReverseSplitOperation) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${firstIOU?.childReportID}`, - value: null, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, + value: { + ...originalTransaction, + reportID: CONST.REPORT.SPLIT_REPORT_ID, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, + value: originalTransaction, }); + + const firstIOU = iouActions.at(0); + if (firstIOU) { + const {updatedReportAction, iouReport, transactionThread} = prepareToCleanUpMoneyRequest(originalTransactionID, firstIOU); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${firstIOU?.childReportID}`, + value: null, + }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: updatedReportAction, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: { + [firstIOU.reportActionID]: { + ...firstIOU, + pendingAction: null, + }, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${firstIOU?.childReportID}`, + value: transactionThread, + }); + } + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: updatedReportAction, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: null, + }, + }, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - [firstIOU.reportActionID]: { - ...firstIOU, - pendingAction: null, + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: originalTransaction, }, }, }); - failureData.push({ + } else { + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${firstIOU?.childReportID}`, - value: transactionThread, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, + value: { + errors: null, + }, }); } - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, - value: originalTransaction, - }); - - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: null, - }, - }, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, - value: { - data: { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: originalTransaction, - }, - }, - }); - // Prepare splitApiParams for the Transaction_Split API call which requires a specific format for the splits - // The format is: splits[0][amount], splits[0][category], splits[0][tag], etc. - const splitApiParams = {} as Record; + // The format is: splits[0][amount], splits[0][category], splits[0][tag] etc. + const splitApiParams = {} as Record; splits.forEach((split, i) => { Object.entries(split).forEach(([key, value]) => { - const formattedValue = value !== null && typeof value === 'object' ? JSON.stringify(value) : value; - splitApiParams[`splits[${i}][${key}]`] = formattedValue; + splitApiParams[`splits[${i}][${key}]`] = value !== null && typeof value === 'object' ? JSON.stringify(value) : value; }); }); - const parameters: SplitTransactionParams = { ...splitApiParams, - isReverseSplitOperation: false, transactionID: originalTransactionID, }; - API.write(WRITE_COMMANDS.SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData}); + if (isCreationOfSplits) { + API.write(WRITE_COMMANDS.SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData}); + } else { + // eslint-disable-next-line rulesdir/no-multiple-api-calls + API.write(WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData}); + } // eslint-disable-next-line deprecation/deprecation InteractionManager.runAfterInteractions(() => removeDraftSplitTransaction(originalTransactionID)); @@ -13460,6 +13590,7 @@ export { setMoneyRequestReimbursable, computePerDiemExpenseAmount, initSplitExpense, + initSplitExpenseItemData, addSplitExpenseField, updateSplitExpenseAmountField, saveSplitTransactions, diff --git a/src/pages/iou/SplitExpenseEditPage.tsx b/src/pages/iou/SplitExpenseEditPage.tsx index 711131862092..e3d678772f07 100644 --- a/src/pages/iou/SplitExpenseEditPage.tsx +++ b/src/pages/iou/SplitExpenseEditPage.tsx @@ -9,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,9 +23,9 @@ import Parser from '@libs/Parser'; import {getTagLists} from '@libs/PolicyUtils'; import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import type {TransactionDetails} from '@libs/ReportUtils'; -import {getParsedComment, getReportOrDraftReport, getTransactionDetails} from '@libs/ReportUtils'; +import {getParsedComment, getReportName, getReportOrDraftReport, getTransactionDetails} from '@libs/ReportUtils'; import {getTagVisibility, hasEnabledTags} from '@libs/TagsOptionsListUtils'; -import {getTag, getTagForDisplay} from '@libs/TransactionUtils'; +import {getChildTransactions, getTag, getTagForDisplay} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -36,11 +37,13 @@ type SplitExpensePageProps = PlatformStackScreenProps>(() => getTransactionDetails(splitExpenseDraftTransaction) ?? {}, [splitExpenseDraftTransaction]); const policy = usePolicy(report?.policyID); @@ -65,6 +68,8 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) { const isSplitAvailable = report && transaction && isSplitAction(report, [transaction], policy); const isCategoryRequired = !!policy?.requiresCategory; + const childTransactions = useMemo(() => getChildTransactions(transactionID), [transactionID]); + const reportName = getReportName(report, policy); const shouldShowTags = !!policy?.areTagsEnabled && !!(transactionTag || hasEnabledTags(policyTagLists)); const tagVisibility = useMemo( @@ -82,7 +87,9 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) { return ( - + + - {Number(splitExpensesList?.length) > 2 && ( + {Number(splitExpensesList?.length) > 1 && (