Skip to content

Commit

Permalink
Fix eReceipts
Browse files Browse the repository at this point in the history
  • Loading branch information
shubham1206agra committed Jan 10, 2025
1 parent eb98559 commit 6b69c5c
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 23 deletions.
5 changes: 5 additions & 0 deletions src/components/Attachments/AttachmentView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -215,6 +216,10 @@ function AttachmentView({
);
}

if (TransactionUtils.isPerDiemRequest(transaction) && transaction) {
return <PerDiemEReceipt transactionID={transaction.transactionID} />;
}

if (TransactionUtils.isDistanceRequest(transaction) && transaction) {
return <DistanceEReceipt transaction={transaction} />;
}
Expand Down
39 changes: 20 additions & 19 deletions src/components/EReceiptThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
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';
import * as MCCIcons from './Icon/MCCIcons';
import Image from './Image';
import Text from './Text';

type EReceiptThumbnailOnyxProps = {
transaction: OnyxEntry<Transaction>;
};

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. */
Expand Down Expand Up @@ -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]);
Expand All @@ -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;
Expand Down Expand Up @@ -135,15 +131,23 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT
{fileExtension.toUpperCase()}
</Text>
)}
{MCCIcon && !isReceiptThumbnail ? (
{isPerDiemRequest ? (
<Icon
src={Expensicons.CalendarSolid}
height={receiptMCCSize}
width={receiptMCCSize}
fill={primaryColor}
/>
) : null}
{!isPerDiemRequest && MCCIcon && !isReceiptThumbnail ? (
<Icon
src={MCCIcon}
height={receiptMCCSize}
width={receiptMCCSize}
fill={primaryColor}
/>
) : null}
{!MCCIcon && tripIcon ? (
{!isPerDiemRequest && !MCCIcon && tripIcon ? (
<Icon
src={tripIcon}
height={receiptMCCSize}
Expand All @@ -158,9 +162,6 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT
}

EReceiptThumbnail.displayName = 'EReceiptThumbnail';
export default withOnyx<EReceiptThumbnailProps, EReceiptThumbnailOnyxProps>({
transaction: {
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
})(EReceiptThumbnail);
export type {IconSize, EReceiptThumbnailProps, EReceiptThumbnailOnyxProps};
export default EReceiptThumbnail;

export type {IconSize, EReceiptThumbnailProps};
120 changes: 120 additions & 0 deletions src/components/PerDiemEReceipt.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.eReceiptContainer, primaryColor ? StyleUtils.getBackgroundColorStyle(primaryColor) : undefined]}>
<View style={styles.fullScreen}>
<EReceiptThumbnail
transactionID={transactionID}
centerIconV={false}
/>
</View>
<View style={[styles.alignItemsCenter, styles.ph8, styles.pb14, styles.pt8]}>
<View style={[StyleUtils.getWidthAndHeightStyle(variables.eReceiptIconWidth, variables.eReceiptIconHeight)]} />
</View>
<View style={[styles.flexColumn, styles.justifyContentBetween, styles.alignItemsCenter, styles.ph9, styles.flex1]}>
<View style={[styles.alignItemsCenter, styles.alignSelfCenter, styles.flexColumn, styles.gap2, styles.mb8]}>
<View style={[styles.flexRow, styles.justifyContentCenter, StyleUtils.getWidthStyle(variables.eReceiptTextContainerWidth)]}>
<View style={[styles.flexColumn, styles.pt1]}>
<Text style={[styles.eReceiptCurrency, secondaryTextColorStyle]}>{currency}</Text>
</View>
<Text
adjustsFontSizeToFit
style={[styles.eReceiptAmountLarge, secondaryTextColorStyle]}
>
{formattedAmount}
</Text>
</View>
<Text style={[styles.eReceiptMerchant, styles.breakWord, styles.textAlignCenter]}>{`${destination} ${translate('common.perDiem').toLowerCase()}`}</Text>
</View>
<View style={[styles.alignSelfStretch, styles.flexColumn, styles.mb8, styles.gap4]}>
<View style={[styles.flexColumn, styles.gap1]}>
<Text style={[styles.eReceiptWaypointTitle, secondaryTextColorStyle]}>{translate('iou.dates')}</Text>
<Text style={[styles.eReceiptWaypointAddress]}>{datesDescription}</Text>
</View>
<View style={[styles.flexColumn, styles.gap1]}>
<Text style={[styles.eReceiptWaypointTitle, secondaryTextColorStyle]}>{translate('iou.rates')}</Text>
<Text style={[styles.eReceiptWaypointAddress]}>{ratesDescription}</Text>
</View>
</View>
<View style={[styles.justifyContentBetween, styles.alignItemsCenter, styles.alignSelfStretch, styles.flexRow, styles.mb8]}>
<Icon
width={variables.eReceiptWordmarkWidth}
height={variables.eReceiptWordmarkHeight}
fill={secondaryColor}
src={Expensicons.ExpensifyWordmark}
/>
<Text style={styles.eReceiptGuaranteed}>{translate('eReceipt.guaranteed')}</Text>
</View>
</View>
</View>
);
}

PerDiemEReceipt.displayName = 'PerDiemEReceipt';

export default PerDiemEReceipt;
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 15 additions & 1 deletion src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -298,6 +299,7 @@ type MoneyRequestOptimisticParams = {
categories?: string[];
tags?: OnyxTypes.RecentlyUsedTags;
currencies?: string[];
destinations?: string[];
};
personalDetailListAction?: OnyxTypes.PersonalDetailsList;
nextStep?: OnyxTypes.ReportNextStep | null;
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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<number, null> = {};
if (!isEmptyObject(personalDetailListAction)) {
const successPersonalDetailListAction: Record<number, null> = {};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2822,6 +2835,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI
categories: optimisticPolicyRecentlyUsedCategories,
tags: optimisticPolicyRecentlyUsedTags,
currencies: optimisticPolicyRecentlyUsedCurrencies,
destinations: optimisticPolicyRecentlyUsedDestinations,
},
personalDetailListAction: optimisticPersonalDetailListAction,
nextStep: optimisticNextStep,
Expand Down
21 changes: 20 additions & 1 deletion src/libs/actions/Policy/PerDiem.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<RecentlyUsedCategories> = {};
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,
Expand All @@ -409,4 +427,5 @@ export {
editPerDiemRateSubrate,
editPerDiemRateAmount,
editPerDiemRateCurrency,
buildOptimisticPolicyRecentlyUsedDestinations,
};
4 changes: 2 additions & 2 deletions src/stories/EReceiptThumbail.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof EReceiptThumbnail>;
Expand All @@ -17,7 +17,7 @@ const story: Meta<typeof EReceiptThumbnail> = {
component: EReceiptThumbnail,
};

function Template(props: Omit<EReceiptThumbnailProps, keyof EReceiptThumbnailOnyxProps>) {
function Template(props: EReceiptThumbnailProps) {
return (
<View style={{display: 'flex', flexDirection: 'column', gap: 12}}>
<View>
Expand Down

0 comments on commit 6b69c5c

Please sign in to comment.