Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/components/DistanceEReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getTransactionDetails} from '@libs/ReportUtils';
import {getWaypointIndex, hasReceipt, isFetchingWaypointsFromServer} from '@libs/TransactionUtils';
import {getAmount, getCurrency, getFormattedCreated, getMerchant, getWaypointIndex, hasReceipt, isFetchingWaypointsFromServer} from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import type {TranslationPaths} from '@src/languages/types';
import type {Transaction} from '@src/types/onyx';
Expand All @@ -28,7 +27,10 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const thumbnail = hasReceipt(transaction) ? getThumbnailAndImageURIs(transaction).thumbnail : null;
const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = getTransactionDetails(transaction) ?? {};
const transactionAmount = getAmount(transaction, false, false);
const transactionCurrency = getCurrency(transaction);
const transactionMerchant = getMerchant(transaction);
const transactionDate = getFormattedCreated(transaction);
const formattedTransactionAmount = convertToDisplayString(transactionAmount, transactionCurrency);
const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? '');
const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]);
Expand Down
17 changes: 8 additions & 9 deletions src/components/EReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {getCardDescription, getCompanyCardDescription} from '@libs/CardUtils';
import {convertToDisplayString, getCurrencySymbol} from '@libs/CurrencyUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getTransactionDetails} from '@libs/ReportUtils';
import {getAmount, getCardID, getCardName, getCurrency, getFormattedCreated, getMerchant} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -43,14 +43,13 @@ function EReceipt({transactionID, transactionItem, isThumbnail = false}: EReceip

const {primaryColor, secondaryColor, titleColor, MCCIcon, tripIcon, backgroundImage} = useEReceipt(transactionItem ?? transaction);

const {
amount: transactionAmount,
currency: transactionCurrency,
merchant: transactionMerchant,
created: transactionDate,
cardID: transactionCardID,
cardName: transactionCardName,
} = getTransactionDetails(transactionItem ?? transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT) ?? {};
const transactionData = transactionItem ?? transaction;
const transactionAmount = transactionData ? getAmount(transactionData, false, false) : undefined;
const transactionCurrency = transactionData ? getCurrency(transactionData) : undefined;
const transactionMerchant = transactionData ? getMerchant(transactionData) : undefined;
const transactionDate = transactionData ? getFormattedCreated(transactionData, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : undefined;
const transactionCardID = transactionData ? getCardID(transactionData) : undefined;
const transactionCardName = transactionData ? getCardName(transactionData) : undefined;
const formattedAmount = convertToDisplayString(transactionAmount, transactionCurrency);
const currency = getCurrencySymbol(transactionCurrency ?? '');
const amount = currency ? formattedAmount.replace(currency, '') : formattedAmount;
Expand Down
16 changes: 9 additions & 7 deletions src/components/PerDiemEReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import useThemeStyles from '@hooks/useThemeStyles';
import {convertAmountToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencySymbol} from '@libs/CurrencyUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getTransactionDetails} from '@libs/ReportUtils';
import {getAmount, getCurrency, getMerchant} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';

Check failure on line 11 in src/components/PerDiemEReceipt.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

'CONST' is defined but never used

Check failure on line 11 in src/components/PerDiemEReceipt.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'CONST' is defined but never used
import ONYXKEYS from '@src/ONYXKEYS';
import type {TransactionCustomUnit} from '@src/types/onyx/Transaction';
import EReceiptThumbnail from './EReceiptThumbnail';
Expand Down Expand Up @@ -59,12 +59,14 @@
// Get receipt colorway, or default to Yellow.
const {backgroundColor: primaryColor, color: secondaryColor} = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)) ?? {};

const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant} = 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 = convertToDisplayStringWithoutCurrency(transactionAmount ?? 0, transactionCurrency);
const currency = getCurrencySymbol(transactionCurrency ?? '');
const transactionAmount = transaction ? getAmount(transaction, false, false) : 0;
const transactionCurrency = transaction ? getCurrency(transaction) : '';
const transactionMerchant = transaction ? getMerchant(transaction) : '';
const ratesDescription = computeDefaultPerDiemExpenseRates(transaction?.comment?.customUnit ?? {}, transactionCurrency);
const datesDescription = getPerDiemDates(transactionMerchant);
const destination = getPerDiemDestination(transactionMerchant);
const formattedAmount = convertToDisplayStringWithoutCurrency(transactionAmount, transactionCurrency);
const currency = getCurrencySymbol(transactionCurrency);

const secondaryTextColorStyle = secondaryColor ? StyleUtils.getColorStyle(secondaryColor) : undefined;

Expand Down
47 changes: 43 additions & 4 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,14 @@
const isTask = type === CONST.SEARCH.DATA_TYPES.TASK;
const canSelectMultiple = !isChat && !isTask && (!isSmallScreenWidth || isMobileSelectionModeEnabled);
const ListItem = getListItem(type, status, groupBy);

// PERFORMANCE TEST: Toggle this to enable EReceipt duplication for performance testing
const ENABLE_ERECEIPT_PERFORMANCE_TEST = true;
const TARGET_ERECEIPT_COUNT = 100;

const sortedSelectedData = useMemo(
() =>
getSortedSections(type, status, data, localeCompare, sortBy, sortOrder, groupBy).map((item) => {
() => {
let processedData = getSortedSections(type, status, data, localeCompare, sortBy, sortOrder, groupBy).map((item) => {
const baseKey = isChat
? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}`
: `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
Expand All @@ -625,8 +630,42 @@
const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch;

return mapToItemWithAdditionalInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight);
}),
[type, status, data, sortBy, sortOrder, groupBy, isChat, newSearchResultKey, selectedTransactions, canSelectMultiple, localeCompare],
});

// PERFORMANCE TEST: Duplicate EReceipts for performance testing
if (ENABLE_ERECEIPT_PERFORMANCE_TEST && !isChat && !isTask) {
// Filter for transactions with EReceipts
const eReceiptTransactions = processedData.filter(
(item) => isTransactionListItemType(item) && item.hasEReceipt
);

if (eReceiptTransactions.length > 0) {
// Calculate how many duplicates we need per original transaction
const duplicatesPerTransaction = Math.ceil((TARGET_ERECEIPT_COUNT - eReceiptTransactions.length) / eReceiptTransactions.length);

// Duplicate each EReceipt transaction
const duplicatedTransactions = eReceiptTransactions.flatMap((transaction, index) =>
Array.from({length: duplicatesPerTransaction}, (_, dupIndex) => ({
...transaction,
keyForList: `${transaction.keyForList}_perf_${index}_${dupIndex}`,
transactionID: `${(transaction as TransactionListItemType).transactionID}_perf_${index}_${dupIndex}`,
amount: (transaction as TransactionListItemType).amount + (dupIndex * 5), // Vary amount slightly
merchant: `${(transaction as TransactionListItemType).merchant} (Test ${index}-${dupIndex})`,
shouldAnimateInHighlight: false,
}))
);

// Add duplicated transactions to the list
processedData = [...processedData, ...duplicatedTransactions];

// Log for verification
console.log(`[PERF TEST] Created ${duplicatedTransactions.length + eReceiptTransactions.length} EReceipts from ${eReceiptTransactions.length} originals`);

Check failure on line 662 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected console statement

Check failure on line 662 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Unexpected console statement
}
}

return processedData;
},
[type, status, data, sortBy, sortOrder, groupBy, isChat, isTask, newSearchResultKey, selectedTransactions, canSelectMultiple, localeCompare],

Check warning on line 668 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useMemo has a missing dependency: 'ENABLE_ERECEIPT_PERFORMANCE_TEST'. Either include it or remove the dependency array

Check warning on line 668 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useMemo has a missing dependency: 'ENABLE_ERECEIPT_PERFORMANCE_TEST'. Either include it or remove the dependency array
);

const hasErrors = Object.keys(searchResults?.errors ?? {}).length > 0 && !isOffline;
Expand Down
5 changes: 2 additions & 3 deletions src/hooks/useEReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {useMemo} from 'react';
import * as eReceiptBGs from '@components/Icon/EReceiptBGs';
import * as MCCIcons from '@components/Icon/MCCIcons';
import type {TransactionListItemType} from '@components/SelectionList/types';
import {getTransactionDetails} from '@libs/ReportUtils';
import {getMCCGroup} from '@libs/TransactionUtils';
import {getTripEReceiptIcon} from '@libs/TripReservationUtils';
import CONST from '@src/CONST';
import type {Transaction} from '@src/types/onyx';
Expand All @@ -25,8 +25,7 @@ export default function useEReceipt(transactionData: Transaction | TransactionLi
const primaryColor = colorStyles?.backgroundColor;
const secondaryColor = colorStyles?.color;
const titleColor = colorStyles?.titleColor;
const transactionDetails = getTransactionDetails(transactionData);
const transactionMCCGroup = transactionDetails?.mccGroup;
const transactionMCCGroup = transactionData ? getMCCGroup(transactionData) : undefined;
const MCCIcon = transactionMCCGroup ? MCCIcons[`${transactionMCCGroup}`] : undefined;
const tripIcon = getTripEReceiptIcon(transactionData);

Expand Down
91 changes: 86 additions & 5 deletions src/libs/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
const TIMEZONE_UPDATE_THROTTLE_MINUTES = 5;

let currentUserAccountID: number | undefined;
Onyx.connect({

Check warning on line 56 in src/libs/DateUtils.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function

Check warning on line 56 in src/libs/DateUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (val) => {
// When signed out, val is undefined
Expand All @@ -66,7 +66,7 @@
});

let timezone: Required<Timezone> = CONST.DEFAULT_TIME_ZONE;
Onyx.connect({

Check warning on line 69 in src/libs/DateUtils.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function

Check warning on line 69 in src/libs/DateUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (!currentUserAccountID) {
Expand All @@ -85,7 +85,7 @@
let networkTimeSkew = 0;
let isOffline: boolean | undefined;

Onyx.connect({

Check warning on line 88 in src/libs/DateUtils.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function

Check warning on line 88 in src/libs/DateUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NETWORK,
callback: (val) => {
networkTimeSkew = val?.timeSkew ?? 0;
Expand Down Expand Up @@ -731,6 +731,78 @@
return '';
};

/**
* Pre-cached Intl.DateTimeFormat instances for common date formats
* This provides significant performance improvements over creating new formatters each time
*/
const CACHED_DATE_FORMATTERS = {
MONTH_DAY_YEAR_FULL: new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}),
MONTH_DAY_YEAR_SHORT: new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}),
MONTH_DAY: new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'short'
}),
} as const;

/**
* Maps date-fns format strings to pre-cached formatter keys
*/
const FORMAT_TO_FORMATTER_KEY = new Map<string, keyof typeof CACHED_DATE_FORMATTERS>([
['MMMM d, yyyy', 'MONTH_DAY_YEAR_FULL'],
['MMM d, yyyy', 'MONTH_DAY_YEAR_SHORT'],
['MMM d', 'MONTH_DAY'],
]);

/**
* Maps date-fns format strings to Intl.DateTimeFormat options
*/
function getIntlFormatterOptions(dateFormat: string): Intl.DateTimeFormatOptions | null {
const formatMap = new Map<string, Intl.DateTimeFormatOptions>([
['MMMM d, yyyy', { year: 'numeric', month: 'long', day: 'numeric' }],
['MMM d, yyyy', { year: 'numeric', month: 'short', day: 'numeric' }],
['MMM d', { day: 'numeric', month: 'short' }],
['MM/dd/yyyy', { year: 'numeric', month: '2-digit', day: '2-digit' }],
['MM-dd', { month: '2-digit', day: '2-digit' }],
['yyyy-MM-dd', { year: 'numeric', month: '2-digit', day: '2-digit' }],
]);
return formatMap.get(dateFormat) ?? null;
}

/**
* Fast path date formatting using cached Intl.DateTimeFormat instances
*/
function formatDateWithCachedFormatter(date: Date, dateFormat: string): string | null {
// Check if we have a pre-cached formatter
const formatterKey = FORMAT_TO_FORMATTER_KEY.get(dateFormat);
const cachedFormatter = formatterKey ? CACHED_DATE_FORMATTERS[formatterKey] : null;

if (cachedFormatter) {
return cachedFormatter.format(date);
}

// Try to create a formatter on-demand for supported formats
const intlOptions = getIntlFormatterOptions(dateFormat);
if (intlOptions) {
try {
const formatter = new Intl.DateTimeFormat('en-US', intlOptions);
return formatter.format(date);
} catch (error) {
// Fall back if Intl formatting fails
return null;
}
}

return null;
}

/**
*
* Get a date and format this date using the UTC timezone.
Expand All @@ -739,20 +811,29 @@
* returns If the date is valid, returns the formatted date with the UTC timezone, otherwise returns an empty string.
*/
function formatWithUTCTimeZone(datetime: string, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING) {
const date = toDate(datetime, {timeZone: 'UTC'});
// Try fast path: native Date parsing + cached formatters
const date = new Date(datetime.replace(' ', 'T')); // Simple normalization for "YYYY-MM-DD HH:mm:ss"

if (!Number.isNaN(date.getTime())) {
// Try cached Intl formatter (fastest for common formats)
const fastResult = formatDateWithCachedFormatter(date, dateFormat);
if (fastResult !== null) {
return fastResult;
}

if (isValid(date)) {
return tzFormat(toZonedTime(date, 'UTC'), dateFormat);
return format(date, dateFormat);
}

return '';
// Original implementation as fallback
const dateFromTz = toDate(datetime, {timeZone: 'UTC'});
return isValid(dateFromTz) ? tzFormat(toZonedTime(dateFromTz, 'UTC'), dateFormat) : '';
}

/**
*
* @param timezone
* Convert unsupported old timezone to app supported timezone
* @returns Timezone
* @param timezoneInput
*/
function formatToSupportedTimezone(timezoneInput: Timezone): Timezone {
if (!timezoneInput?.selected) {
Expand Down
Loading