From 6b69c5cf75a52d41fe6cc8997831cb427935858d Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 10 Jan 2025 22:03:06 +0530 Subject: [PATCH] Fix eReceipts --- .../Attachments/AttachmentView/index.tsx | 5 + src/components/EReceiptThumbnail.tsx | 39 +++--- src/components/PerDiemEReceipt.tsx | 120 ++++++++++++++++++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/actions/IOU.ts | 16 ++- src/libs/actions/Policy/PerDiem.ts | 21 ++- src/stories/EReceiptThumbail.stories.tsx | 4 +- 8 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/components/PerDiemEReceipt.tsx diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 17596bea9af0..bda6f10e4f4f 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -9,6 +9,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import PerDiemEReceipt from '@components/PerDiemEReceipt'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; @@ -215,6 +216,10 @@ function AttachmentView({ ); } + if (TransactionUtils.isPerDiemRequest(transaction) && transaction) { + return ; + } + if (TransactionUtils.isDistanceRequest(transaction) && transaction) { return ; } diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 981c4d46e212..25b847b21dce 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -1,16 +1,15 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; import * as TripReservationUtils from '@libs/TripReservationUtils'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; import * as Expensicons from './Icon/Expensicons'; @@ -18,15 +17,10 @@ import * as MCCIcons from './Icon/MCCIcons'; import Image from './Image'; import Text from './Text'; -type EReceiptThumbnailOnyxProps = { - transaction: OnyxEntry; -}; - type IconSize = 'x-small' | 'small' | 'medium' | 'large'; -type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { - /** TransactionID of the transaction this EReceipt corresponds to. It's used by withOnyx HOC */ - // eslint-disable-next-line react/no-unused-prop-types +type EReceiptThumbnailProps = { + /** TransactionID of the transaction this EReceipt corresponds to. */ transactionID: string; /** Border radius to be applied on the parent view. */ @@ -54,9 +48,10 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transactionID, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); @@ -68,6 +63,7 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT const transactionMCCGroup = transactionDetails?.mccGroup; const MCCIcon = transactionMCCGroup ? MCCIcons[`${transactionMCCGroup}`] : undefined; const tripIcon = TripReservationUtils.getTripEReceiptIcon(transaction); + const isPerDiemRequest = TransactionUtils.isPerDiemRequest(transaction); let receiptIconWidth: number = variables.eReceiptIconWidth; let receiptIconHeight: number = variables.eReceiptIconHeight; @@ -135,7 +131,15 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT {fileExtension.toUpperCase()} )} - {MCCIcon && !isReceiptThumbnail ? ( + {isPerDiemRequest ? ( + + ) : null} + {!isPerDiemRequest && MCCIcon && !isReceiptThumbnail ? ( ) : null} - {!MCCIcon && tripIcon ? ( + {!isPerDiemRequest && !MCCIcon && tripIcon ? ( ({ - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - }, -})(EReceiptThumbnail); -export type {IconSize, EReceiptThumbnailProps, EReceiptThumbnailOnyxProps}; +export default EReceiptThumbnail; + +export type {IconSize, EReceiptThumbnailProps}; diff --git a/src/components/PerDiemEReceipt.tsx b/src/components/PerDiemEReceipt.tsx new file mode 100644 index 000000000000..f3dca29bfdbc --- /dev/null +++ b/src/components/PerDiemEReceipt.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {TransactionCustomUnit} from '@src/types/onyx/Transaction'; +import EReceiptThumbnail from './EReceiptThumbnail'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +type PerDiemEReceiptProps = { + /* TransactionID of the transaction this EReceipt corresponds to */ + transactionID: string; +}; + +function computeDefaultPerDiemExpenseRates(customUnit: TransactionCustomUnit, currency: string) { + const subRates = customUnit.subRates ?? []; + const subRateComments = subRates.map((subRate) => { + const rate = subRate.rate ?? 0; + const rateComment = subRate.name ?? ''; + const quantity = subRate.quantity ?? 0; + return `${quantity}x ${rateComment} @ ${CurrencyUtils.convertAmountToDisplayString(rate, currency)}`; + }); + return subRateComments.join(', '); +} + +function getPerDiemDestination(merchant: string) { + const merchantParts = merchant.split(', '); + if (merchantParts.length < 1) { + return ''; + } + return merchantParts.slice(0, merchantParts.length - 1).join(', '); +} + +function getPerDiemDates(merchant: string) { + const merchantParts = merchant.split(', '); + if (merchantParts.length < 1) { + return merchantParts.at(0) ?? ''; + } + return merchantParts.at(-1); +} + +function PerDiemEReceipt({transactionID}: PerDiemEReceiptProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + + // Get receipt colorway, or default to Yellow. + const {backgroundColor: primaryColor, color: secondaryColor} = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)) ?? {}; + + const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant} = ReportUtils.getTransactionDetails(transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT) ?? {}; + const ratesDescription = computeDefaultPerDiemExpenseRates(transaction?.comment?.customUnit ?? {}, transactionCurrency ?? ''); + const datesDescription = getPerDiemDates(transactionMerchant ?? ''); + const destination = getPerDiemDestination(transactionMerchant ?? ''); + const formattedAmount = CurrencyUtils.convertToDisplayStringWithoutCurrency(transactionAmount ?? 0, transactionCurrency); + const currency = CurrencyUtils.getCurrencySymbol(transactionCurrency ?? ''); + + const secondaryTextColorStyle = secondaryColor ? StyleUtils.getColorStyle(secondaryColor) : undefined; + + return ( + + + + + + + + + + + + {currency} + + + {formattedAmount} + + + {`${destination} ${translate('common.perDiem').toLowerCase()}`} + + + + {translate('iou.dates')} + {datesDescription} + + + {translate('iou.rates')} + {ratesDescription} + + + + + {translate('eReceipt.guaranteed')} + + + + ); +} + +PerDiemEReceipt.displayName = 'PerDiemEReceipt'; + +export default PerDiemEReceipt; diff --git a/src/languages/en.ts b/src/languages/en.ts index fc182fbf490f..d29a22212a35 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1101,6 +1101,8 @@ const translations = { one: `Trip: 1 full day`, other: (count: number) => `Trip: ${count} full days`, }), + dates: 'Dates', + rates: 'Rates', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index f3c2c5bb3032..c3092ea7dc95 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1099,6 +1099,8 @@ const translations = { one: `Viaje: 1 día completo`, other: (count: number) => `Viaje: ${count} días completos`, }), + dates: 'Dates', + rates: 'Rates', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 484425020ddd..9f2e87ce90da 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -76,6 +76,7 @@ import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionCh import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; import * as Category from './Policy/Category'; +import * as PerDiem from './Policy/PerDiem'; import * as Policy from './Policy/Policy'; import * as Tag from './Policy/Tag'; import * as Report from './Report'; @@ -298,6 +299,7 @@ type MoneyRequestOptimisticParams = { categories?: string[]; tags?: OnyxTypes.RecentlyUsedTags; currencies?: string[]; + destinations?: string[]; }; personalDetailListAction?: OnyxTypes.PersonalDetailsList; nextStep?: OnyxTypes.ReportNextStep | null; @@ -854,6 +856,7 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR } = optimisticParams; const isScanRequest = TransactionUtils.isScanRequest(transaction); + const isPerDiemRequest = TransactionUtils.isPerDiemRequest(transaction); const outstandingChildRequest = ReportUtils.getOutstandingChildRequest(iou.report); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = []; @@ -974,6 +977,14 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR }); } + if (policyRecentlyUsed.destinations?.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS}${iou.report.policyID}`, + value: policyRecentlyUsed.destinations, + }); + } + const redundantParticipants: Record = {}; if (!isEmptyObject(personalDetailListAction)) { const successPersonalDetailListAction: Record = {}; @@ -1200,7 +1211,7 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR }, ]; - if (!isOneOnOneSplit) { + if (!isOneOnOneSplit && !isPerDiemRequest) { optimisticData.push({ onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, @@ -2734,10 +2745,12 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI }); // This is to differentiate between a normal expense and a per diem expense optimisticTransaction.iouRequestType = CONST.IOU.REQUEST_TYPE.PER_DIEM; + optimisticTransaction.hasEReceipt = true; const optimisticPolicyRecentlyUsedCategories = Category.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); const optimisticPolicyRecentlyUsedCurrencies = Policy.buildOptimisticRecentlyUsedCurrencies(currency); + const optimisticPolicyRecentlyUsedDestinations = PerDiem.buildOptimisticPolicyRecentlyUsedDestinations(iouReport.policyID, customUnit.customUnitRateID); // STEP 4: Build optimistic reportActions. We need: // 1. CREATED action for the chatReport @@ -2822,6 +2835,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI categories: optimisticPolicyRecentlyUsedCategories, tags: optimisticPolicyRecentlyUsedTags, currencies: optimisticPolicyRecentlyUsedCurrencies, + destinations: optimisticPolicyRecentlyUsedDestinations, }, personalDetailListAction: optimisticPersonalDetailListAction, nextStep: optimisticNextStep, diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts index 323045e49821..f7b66dee0f8e 100644 --- a/src/libs/actions/Policy/PerDiem.ts +++ b/src/libs/actions/Policy/PerDiem.ts @@ -1,4 +1,5 @@ import lodashDeepClone from 'lodash/cloneDeep'; +import lodashUnion from 'lodash/union'; import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -13,7 +14,7 @@ import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report} from '@src/types/onyx'; +import type {Policy, RecentlyUsedCategories, Report} from '@src/types/onyx'; import type {ErrorFields, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {CustomUnit, Rate} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -397,6 +398,23 @@ function editPerDiemRateCurrency(policyID: string, rateID: string, customUnit: C API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData); } +let allRecentlyUsedDestination: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS, + waitForCollectionCallback: true, + callback: (val) => (allRecentlyUsedDestination = val), +}); + +function buildOptimisticPolicyRecentlyUsedDestinations(policyID: string | undefined, destination: string | undefined) { + if (!policyID || !destination) { + return []; + } + + const policyRecentlyUsedDestinations = allRecentlyUsedDestination?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS}${policyID}`] ?? []; + + return lodashUnion([destination], policyRecentlyUsedDestinations); +} + export { generateCustomUnitID, enablePerDiem, @@ -409,4 +427,5 @@ export { editPerDiemRateSubrate, editPerDiemRateAmount, editPerDiemRateCurrency, + buildOptimisticPolicyRecentlyUsedDestinations, }; diff --git a/src/stories/EReceiptThumbail.stories.tsx b/src/stories/EReceiptThumbail.stories.tsx index 6dbbfb974e85..6ac4413a083f 100644 --- a/src/stories/EReceiptThumbail.stories.tsx +++ b/src/stories/EReceiptThumbail.stories.tsx @@ -2,7 +2,7 @@ import type {Meta, StoryFn} from '@storybook/react'; import React from 'react'; import {View} from 'react-native'; -import type {EReceiptThumbnailOnyxProps, EReceiptThumbnailProps} from '@components/EReceiptThumbnail'; +import type {EReceiptThumbnailProps} from '@components/EReceiptThumbnail'; import EReceiptThumbnail from '@components/EReceiptThumbnail'; type EReceiptThumbnailStory = StoryFn; @@ -17,7 +17,7 @@ const story: Meta = { component: EReceiptThumbnail, }; -function Template(props: Omit) { +function Template(props: EReceiptThumbnailProps) { return (