diff --git a/src/components/TransactionItemRow/DataCells/TotalCell.tsx b/src/components/TransactionItemRow/DataCells/TotalCell.tsx index cc5fcb2b6e0e..e574cd0c010b 100644 --- a/src/components/TransactionItemRow/DataCells/TotalCell.tsx +++ b/src/components/TransactionItemRow/DataCells/TotalCell.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import TextWithTooltip from '@components/TextWithTooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; -import {getTransactionDetails} from '@libs/ReportUtils'; +import {getTransactionDetailsAmount} from '@libs/ReportUtils'; import {getCurrency as getTransactionCurrency, isScanning} from '@libs/TransactionUtils'; import type TransactionDataCellProps from './TransactionDataCellProps'; @@ -12,7 +12,11 @@ function TotalCell({shouldShowTooltip, transactionItem}: TransactionDataCellProp const {translate} = useLocalize(); const currency = getTransactionCurrency(transactionItem); - const amount = getTransactionDetails(transactionItem)?.amount; + const amount = useMemo(() => { + return getTransactionDetailsAmount(transactionItem); + // We want this memo to update only when the amount of transactionItem changes + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [transactionItem.amount, transactionItem.modifiedAmount]); let amountToDisplay = convertToDisplayString(amount, currency); if (isScanning(transactionItem)) { amountToDisplay = translate('iou.receiptStatusTitle'); diff --git a/src/languages/de.ts b/src/languages/de.ts index 94136fb755a5..69071e9d6443 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1023,6 +1023,7 @@ const translations = { share: 'Teilen', participants: 'Teilnehmer', createExpense: 'Ausgabe erstellen', + trackDistance: 'Entfernung verfolgen', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Erstelle ${expensesNumber} Ausgaben`, addExpense: 'Ausgabe hinzufügen', chooseRecipient: 'Empfänger auswählen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 3f283b492eab..715e9645ad23 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1008,6 +1008,7 @@ const translations = { share: 'Share', participants: 'Participants', createExpense: 'Create expense', + trackDistance: 'Track distance', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Create ${expensesNumber} expenses`, addExpense: 'Add expense', chooseRecipient: 'Choose recipient', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1892e66a7b4d..8f9e8e77ca1a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1003,6 +1003,7 @@ const translations = { share: 'Compartir', participants: 'Participantes', createExpense: 'Crear gasto', + trackDistance: 'Gasto de distancia', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Crear ${expensesNumber} gastos`, paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar a ${name ?? 'alguien'}`, chooseRecipient: 'Elige destinatario', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 487181878690..14ae499d4fe7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1023,6 +1023,7 @@ const translations = { share: 'Partager', participants: 'Participants', createExpense: 'Créer une dépense', + trackDistance: 'Suivre la distance', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Créer ${expensesNumber} dépenses`, addExpense: 'Ajouter une dépense', chooseRecipient: 'Choisir le destinataire', diff --git a/src/languages/it.ts b/src/languages/it.ts index 919e867634e8..acfe0ae636f0 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1019,6 +1019,7 @@ const translations = { share: 'Condividi', participants: 'Partecipanti', createExpense: 'Crea spesa', + trackDistance: 'Traccia distanza', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Crea ${expensesNumber} spese`, addExpense: 'Aggiungi spesa', chooseRecipient: 'Scegli destinatario', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1736c282a6b9..99c18f17a397 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1022,6 +1022,7 @@ const translations = { share: '共有', participants: '参加者', createExpense: '経費を作成', + trackDistance: '距離を追跡する', createExpenses: ({expensesNumber}: CreateExpensesParams) => `${expensesNumber}件の経費を作成`, addExpense: '経費を追加', chooseRecipient: '受取人を選択', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index f479091dd542..4d8a7d7e8af7 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1020,6 +1020,7 @@ const translations = { share: 'Delen', participants: 'Deelnemers', createExpense: 'Uitgave aanmaken', + trackDistance: 'Afstand bijhouden', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Maak ${expensesNumber} uitgaven aan`, addExpense: 'Uitgave toevoegen', chooseRecipient: 'Kies ontvanger', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fce5195e1eb5..ad173df056a0 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1018,6 +1018,7 @@ const translations = { share: 'Udostępnij', participants: 'Uczestnicy', createExpense: 'Utwórz wydatek', + trackDistance: 'Śledź odległość', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Utwórz ${expensesNumber} wydatki`, addExpense: 'Dodaj wydatek', chooseRecipient: 'Wybierz odbiorcę', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index cdc88b4e1ad1..b01ff678c71f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1020,6 +1020,7 @@ const translations = { share: 'Compartilhar', participants: 'Participantes', createExpense: 'Criar despesa', + trackDistance: 'Rastrear distância', createExpenses: ({expensesNumber}: CreateExpensesParams) => `Criar ${expensesNumber} despesas`, addExpense: 'Adicionar despesa', chooseRecipient: 'Escolher destinatário', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 90e85b22c2ee..29579790c59d 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1013,6 +1013,7 @@ const translations = { share: '分享', participants: '参与者', createExpense: '创建报销单', + trackDistance: '跟踪距离', createExpenses: ({expensesNumber}: CreateExpensesParams) => `创建${expensesNumber}笔费用`, addExpense: '添加费用', chooseRecipient: '选择收件人', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7b474d7e222e..bf283f68ff9c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3951,7 +3951,6 @@ function getMoneyRequestReportName({ * Gets transaction created, amount, currency, comment, and waypoints (for distance expense) * into a flat object. Used for displaying transactions and sending them in API commands */ - function getTransactionDetails( transaction: OnyxInputOrEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING, @@ -3984,6 +3983,18 @@ function getTransactionDetails( }; } +/** + * Get the amount of a transaction with proper report context handling. + * This is a lightweight alternative to getTransactionDetails when only the amount is needed. + */ +function getTransactionDetailsAmount(transaction: OnyxInputOrEntry): number | undefined { + if (!transaction) { + return; + } + const report = getReportOrDraftReport(transaction?.reportID); + return getTransactionAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)); +} + function getTransactionCommentObject(transaction: OnyxEntry): Comment { return { ...transaction?.comment, @@ -11517,6 +11528,7 @@ export { isOneTransactionReport, isWorkspaceTaskReport, isWorkspaceThread, + getTransactionDetailsAmount }; export type { diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index c4a293952fae..b3a68e794885 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -13,6 +13,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; @@ -36,7 +37,7 @@ import type * as OnyxTypes from '@src/types/onyx'; type MoneyRequestOptions = Record< Exclude, - PopoverMenuItem + PopoverMenuItem[] >; type AttachmentPickerWithMenuItemsProps = { @@ -131,6 +132,9 @@ function AttachmentPickerWithMenuItems({ const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); const {isProduction} = useEnvironment(); + const {isBetaEnabled} = usePermissions(); + + const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); const selectOption = useCallback( (onSelected: () => void, shouldRestrictAction: boolean) => { @@ -152,52 +156,80 @@ function AttachmentPickerWithMenuItems({ */ const moneyRequestOptions = useMemo(() => { const options: MoneyRequestOptions = { - [CONST.IOU.TYPE.SPLIT]: { - icon: Expensicons.Transfer, - text: translate('iou.splitExpense'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), true), - }, - [CONST.IOU.TYPE.SUBMIT]: { - icon: getIconForAction(CONST.IOU.TYPE.CREATE), - text: translate('iou.createExpense'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), true), - }, - [CONST.IOU.TYPE.PAY]: { - icon: getIconForAction(CONST.IOU.TYPE.SEND), - text: translate('iou.paySomeone', {name: getPayeeName(report)}), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => { - if (isDelegateAccessRestricted) { - close(() => { - showDelegateNoAccessModal(); - }); - return; - } - selectOption(() => startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), false); + [CONST.IOU.TYPE.SPLIT]: [ + { + icon: Expensicons.Transfer, + text: translate('iou.splitExpense'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), true), }, - }, - [CONST.IOU.TYPE.TRACK]: { - icon: getIconForAction(CONST.IOU.TYPE.CREATE), - text: translate('iou.createExpense'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), true), - }, - [CONST.IOU.TYPE.INVOICE]: { - icon: Expensicons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldUseNarrowLayout, - onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), false), - }, + ], + [CONST.IOU.TYPE.SUBMIT]: [ + { + icon: getIconForAction(CONST.IOU.TYPE.CREATE), + text: translate('iou.createExpense'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), true), + }, + ...(isManualDistanceTrackingEnabled + ? [ + { + icon: Expensicons.Location, + text: translate('quickAction.recordDistance'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => selectOption(() => null, true), + }, + ] + : []), + ], + [CONST.IOU.TYPE.PAY]: [ + { + icon: getIconForAction(CONST.IOU.TYPE.SEND), + text: translate('iou.paySomeone', {name: getPayeeName(report)}), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => { + if (isDelegateAccessRestricted) { + close(() => { + showDelegateNoAccessModal(); + }); + return; + } + selectOption(() => startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), false); + }, + }, + ], + [CONST.IOU.TYPE.TRACK]: [ + { + icon: getIconForAction(CONST.IOU.TYPE.CREATE), + text: translate('iou.createExpense'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), true), + }, + ...(isManualDistanceTrackingEnabled + ? [ + { + icon: Expensicons.Location, + text: translate('iou.trackDistance'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => selectOption(() => null, true), + }, + ] + : []), + ], + [CONST.IOU.TYPE.INVOICE]: [ + { + icon: Expensicons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => selectOption(() => startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)), false), + }, + ], }; - const moneyRequestOptionsList = temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({ - ...options[option], - })); + const moneyRequestOptionsList = temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => options[option]); - return moneyRequestOptionsList.filter((item, index, self) => index === self.findIndex((t) => t.text === item.text)); - }, [translate, shouldUseNarrowLayout, report, policy, reportParticipantIDs, selectOption, isDelegateAccessRestricted, showDelegateNoAccessModal]); + return moneyRequestOptionsList.flat().filter((item, index, self) => index === self.findIndex((t) => t.text === item.text)); + }, [translate, shouldUseNarrowLayout, report, policy, reportParticipantIDs, selectOption, isDelegateAccessRestricted, showDelegateNoAccessModal, isManualDistanceTrackingEnabled]); const createReportOption: PopoverMenuItem[] = useMemo(() => { if (!isPolicyExpenseChat(report) || !isPaidGroupPolicy(report) || !isReportOwner(report)) { diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 7f89c388eb37..cfd009a63f82 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -118,7 +118,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const isFocused = useIsFocused(); const prevIsFocused = usePrevious(isFocused); const {isOffline} = useNetwork(); - const {isBlockedFromSpotnanaTravel} = usePermissions(); + const {isBlockedFromSpotnanaTravel, isBetaEnabled} = usePermissions(); + const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.primaryLogin, canBeMissing: true}); const primaryContactMethod = primaryLogin ?? session?.email ?? ''; const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); @@ -401,6 +402,25 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const menuItems = [ ...expenseMenuItems, + ...(isManualDistanceTrackingEnabled + ? [ + { + icon: Expensicons.Location, + text: translate('iou.trackDistance'), + shouldCallAfterModalHide: shouldUseNarrowLayout, + onSelected: () => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + setModalVisible(true); + return; + } + // Start the flow to start tracking a distance request + return null; + }); + }, + }, + ] + : []), ...(shouldShowCreateReportOption ? [ { diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 8d67ea531a64..8da9d0629c21 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -150,6 +150,7 @@ function IOURequestStartPage({ }, [headerWithBackBtnContainerElement, tabBarContainerElement, activeTabContainerElement]); const {isBetaEnabled} = usePermissions(); + const manualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); const setTestReceiptAndNavigateRef = useRef<() => void>(() => {}); const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useProductTrainingContext( CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, @@ -234,16 +235,18 @@ function IOURequestStartPage({ )} - - {() => ( - - - - )} - + {!manualDistanceTrackingEnabled && ( + + {() => ( + + + + )} + + )} {!!shouldShowPerDiemOption && ( {() => (