Skip to content

Commit 9eda44b

Browse files
authored
Merge pull request #86824 from mukhrr/fix/custom-report-columns-bugs
fixed custom report deploy blockers
2 parents 9aec7e9 + 017a242 commit 9eda44b

11 files changed

Lines changed: 308 additions & 53 deletions

File tree

src/CONST/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7663,12 +7663,12 @@ const CONST = {
76637663
CATEGORY: this.TABLE_COLUMNS.CATEGORY,
76647664
TAG: this.TABLE_COLUMNS.TAG,
76657665
EXCHANGE_RATE: this.TABLE_COLUMNS.EXCHANGE_RATE,
7666-
ORIGINAL_AMOUNT: this.TABLE_COLUMNS.ORIGINAL_AMOUNT,
76677666
REIMBURSABLE: this.TABLE_COLUMNS.REIMBURSABLE,
76687667
BILLABLE: this.TABLE_COLUMNS.BILLABLE,
76697668
TAX_RATE: this.TABLE_COLUMNS.TAX_RATE,
76707669
TAX_AMOUNT: this.TABLE_COLUMNS.TAX_AMOUNT,
76717670
AMOUNT: this.TABLE_COLUMNS.TOTAL_AMOUNT,
7671+
TOTAL: this.TABLE_COLUMNS.TOTAL,
76727672
};
76737673
},
76747674
get GROUP_CUSTOM_COLUMNS() {

src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {View} from 'react-native';
66
// ScrollView type is needed for the horizontal scroll ref; the project ScrollView component is used for rendering.
77
// eslint-disable-next-line no-restricted-imports
88
import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView} from 'react-native';
9+
import Button from '@components/Button';
910
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
1011
import Checkbox from '@components/Checkbox';
1112
import MenuItem from '@components/MenuItem';
@@ -32,6 +33,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived';
3233
import useResponsiveLayout from '@hooks/useResponsiveLayout';
3334
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
3435
import useStyleUtils from '@hooks/useStyleUtils';
36+
import useTheme from '@hooks/useTheme';
3537
import useThemeStyles from '@hooks/useThemeStyles';
3638
import useWindowDimensions from '@hooks/useWindowDimensions';
3739
import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
@@ -141,6 +143,7 @@ function MoneyRequestReportTransactionList({
141143
useCopySelectionHelper();
142144
const {convertToDisplayString} = useCurrencyListActions();
143145
const styles = useThemeStyles();
146+
const theme = useTheme();
144147
const StyleUtils = useStyleUtils();
145148
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'CheckSquare', 'ReceiptPlus', 'Columns', 'Plus']);
146149
const {translate, localeCompare} = useLocalize();
@@ -295,6 +298,7 @@ function MoneyRequestReportTransactionList({
295298
// Always use default columns for money request report view (don't use user-customized search columns)
296299
const isExpenseReportViewFromIOUReport = isIOUReport(report);
297300
const shouldShowBillableColumn = isBillableEnabledOnPolicy(policy);
301+
const shouldShowCommentsColumn = useMemo(() => Object.values(reportActions ?? {}).some((action) => (action?.childVisibleActionCount ?? 0) > 0), [reportActions]);
298302
const columnsToShow = useMemo(() => {
299303
return getColumnsToShow({
300304
currentAccountID: currentUserDetails?.accountID,
@@ -303,10 +307,11 @@ function MoneyRequestReportTransactionList({
303307
isExpenseReportView: true,
304308
isExpenseReportViewFromIOUReport,
305309
shouldShowBillableColumn,
310+
shouldShowCommentsColumn,
306311
shouldShowReimbursableColumn: hasNonReimbursableTransactions(transactions),
307312
reportCurrency: report?.currency,
308313
});
309-
}, [currentUserDetails?.accountID, transactions, isExpenseReportViewFromIOUReport, reportDetailsColumns, shouldShowBillableColumn, report?.currency]);
314+
}, [transactions, currentUserDetails?.accountID, isExpenseReportViewFromIOUReport, shouldShowBillableColumn, shouldShowCommentsColumn, reportDetailsColumns, report?.currency]);
310315

311316
const {windowWidth, windowHeight} = useWindowDimensions();
312317
const minTableWidth = getTableMinWidth(columnsToShow);
@@ -519,6 +524,10 @@ function MoneyRequestReportTransactionList({
519524
[translate],
520525
);
521526

527+
const openColumnsPage = useCallback(() => {
528+
Navigation.navigate(ROUTES.REPORT_SETTINGS_COLUMNS.getRoute(report.reportID));
529+
}, [report.reportID]);
530+
522531
const selectedGroupByItem = useMemo(() => groupByItems.find((item) => item.value === currentGroupBy) ?? groupByItems.at(0), [groupByItems, currentGroupBy]);
523532

524533
const groupByOptions = useMemo(
@@ -731,6 +740,19 @@ function MoneyRequestReportTransactionList({
731740
PopoverComponent={groupByPopoverComponent}
732741
/>
733742
)}
743+
{!shouldUseNarrowLayout && !isExpenseReportViewFromIOUReport && (
744+
<Button
745+
link
746+
small
747+
shouldUseDefaultHover={false}
748+
text={translate('search.columns')}
749+
iconFill={theme.link}
750+
iconHoverFill={theme.linkHover}
751+
icon={expensifyIcons.Columns}
752+
textStyles={[styles.textMicroBold]}
753+
onPress={openColumnsPage}
754+
/>
755+
)}
734756
</View>
735757
{!shouldUseNarrowLayout && !shouldScrollHorizontally && tableHeaderContent}
736758
{shouldScrollHorizontally ? (

src/components/Search/SearchTableHeader.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [
118118
columnName: CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT,
119119
translationKey: 'common.originalAmount',
120120
},
121+
{
122+
columnName: CONST.SEARCH.TABLE_COLUMNS.TOTAL,
123+
translationKey: 'common.total',
124+
},
121125
{
122126
columnName: CONST.SEARCH.TABLE_COLUMNS.WITHDRAWAL_ID,
123127
translationKey: 'common.withdrawalID',

src/components/TransactionItemRow/index.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import StringUtils from '@libs/StringUtils';
3333
import {
3434
getAmount,
3535
getAttendees,
36+
getConvertedAmount,
3637
getCurrency,
3738
getDescription,
3839
getExchangeRate,
@@ -258,7 +259,7 @@ function TransactionItemRow({
258259
}
259260
}, [transactionItem, translate, report]);
260261

261-
const exchangeRateMessage = getExchangeRate(transactionItem, report?.currency);
262+
const exchangeRateMessage = getExchangeRate(transactionItem, report?.currency ?? policy?.outputCurrency);
262263
const cardName = getCompanyCardDescription(translate, transactionItem?.cardName, transactionItem?.cardID, nonPersonalAndWorkspaceCards);
263264
const transactionAttendees = useMemo(() => getAttendees(transactionItem, currentUserPersonalDetails), [transactionItem, currentUserPersonalDetails]);
264265

@@ -583,6 +584,26 @@ function TransactionItemRow({
583584
/>
584585
</View>
585586
);
587+
case CONST.SEARCH.TABLE_COLUMNS.TOTAL: {
588+
const isFromExpenseReport = isExpenseReport(transactionItem.report ?? report);
589+
const hasConvertedAmount = transactionItem.convertedAmount != null;
590+
// Offline expenses don't have a BE-computed convertedAmount yet — fall back to the unconverted
591+
// amount in the transaction's own currency so users don't see a misleading $0.00 placeholder.
592+
const totalAmount = hasConvertedAmount ? getConvertedAmount(transactionItem, isFromExpenseReport, false, true) : getAmount(transactionItem, isFromExpenseReport, false, true);
593+
// When converted, display in the report's output currency; otherwise use the transaction's own currency.
594+
const totalCurrency = hasConvertedAmount ? (report?.currency ?? policy?.outputCurrency ?? getCurrency(transactionItem)) : getCurrency(transactionItem);
595+
return (
596+
<View
597+
key={column}
598+
style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, {isAmountColumnWide})]}
599+
>
600+
<AmountCell
601+
total={totalAmount}
602+
currency={totalCurrency}
603+
/>
604+
</View>
605+
);
606+
}
586607
case CONST.SEARCH.TABLE_COLUMNS.REPORT_ID:
587608
return (
588609
<View

src/libs/CardUtils.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
Policy,
2525
PolicyConnectionName,
2626
PrivatePersonalDetails,
27+
Transaction,
2728
WorkspaceCardsList,
2829
} from '@src/types/onyx';
2930
import type {UnassignedCard} from '@src/types/onyx/Card';
@@ -1753,11 +1754,7 @@ function getCardHintText(validFrom: string | undefined, validThru: string | unde
17531754
* The search API pre-resolves cardName, but local Onyx transactions have raw values.
17541755
* This ensures the report layout matches the search page.
17551756
*/
1756-
function resolveTransactionCardFields<T extends {cardID?: number; cardName?: string; bank?: string}>(
1757-
transactions: T[],
1758-
cardList: CardList | undefined,
1759-
translate: LocalizedTranslate,
1760-
): Array<T & {isCardFeedDeleted?: boolean}> {
1757+
function resolveTransactionCardFields<T extends Transaction>(transactions: T[], cardList: CardList | undefined, translate: LocalizedTranslate): Array<T & {isCardFeedDeleted?: boolean}> {
17611758
return transactions.map((transaction) => {
17621759
let updates: Partial<T & {isCardFeedDeleted?: boolean}> = {};
17631760

src/libs/ReportUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13248,6 +13248,7 @@ const sortableColumnNames = [
1324813248
CONST.SEARCH.TABLE_COLUMNS.CATEGORY,
1324913249
CONST.SEARCH.TABLE_COLUMNS.TAG,
1325013250
CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT,
13251+
CONST.SEARCH.TABLE_COLUMNS.TOTAL,
1325113252
CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE,
1325213253
CONST.SEARCH.TABLE_COLUMNS.BILLABLE,
1325313254
CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE,
@@ -13282,6 +13283,8 @@ function getTransactionSortValue(transaction: Transaction, key: SortableColumnNa
1328213283
return getTag(transaction);
1328313284
case CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT:
1328413285
return getTransactionAmount(transaction, isExpenseReport(report), transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID);
13286+
case CONST.SEARCH.TABLE_COLUMNS.TOTAL:
13287+
return Math.abs(transaction.convertedAmount ?? 0);
1328513288
case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION:
1328613289
return Parser.htmlToText(transaction.comment?.comment ?? '');
1328713290
case CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE:

src/libs/SearchUIUtils.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5041,6 +5041,7 @@ function getColumnsToShow({
50415041
isExpenseReportViewFromIOUReport = false,
50425042
shouldShowBillableColumn = false,
50435043
shouldShowReimbursableColumn = false,
5044+
shouldShowCommentsColumn = false,
50445045
reportCurrency,
50455046
shouldUseStrictDefaultExpenseColumns = false,
50465047
}: {
@@ -5053,8 +5054,10 @@ function getColumnsToShow({
50535054
isExpenseReportViewFromIOUReport?: boolean;
50545055
shouldShowBillableColumn?: boolean;
50555056
shouldShowReimbursableColumn?: boolean;
5056-
shouldUseStrictDefaultExpenseColumns?: boolean;
5057+
shouldShowCommentsColumn?: boolean;
50575058
reportCurrency?: string;
5059+
shouldUseStrictDefaultExpenseColumns?: boolean;
5060+
policy?: OnyxTypes.Policy;
50585061
}): SearchColumnType[] {
50595062
if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) {
50605063
const defaultReportColumns: SearchColumnType[] = [
@@ -5168,11 +5171,11 @@ function getColumnsToShow({
51685171
[CONST.SEARCH.TABLE_COLUMNS.TAX_RATE]: false,
51695172
[CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: false,
51705173
[CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE]: false,
5171-
[CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT]: false,
51725174
[CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE]: shouldShowReimbursableColumn,
51735175
[CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: shouldShowBillableColumn,
5174-
[CONST.SEARCH.TABLE_COLUMNS.COMMENTS]: true,
5175-
[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: true,
5176+
[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: false,
5177+
[CONST.SEARCH.TABLE_COLUMNS.TOTAL]: true,
5178+
[CONST.SEARCH.TABLE_COLUMNS.COMMENTS]: shouldShowCommentsColumn,
51765179
}
51775180
: {
51785181
[CONST.SEARCH.TABLE_COLUMNS.AVATAR]: true,
@@ -5233,7 +5236,7 @@ function getColumnsToShow({
52335236
}
52345237
}
52355238

5236-
if (!addedColumns.has(CONST.SEARCH.TABLE_COLUMNS.COMMENTS)) {
5239+
if (shouldShowCommentsColumn && !addedColumns.has(CONST.SEARCH.TABLE_COLUMNS.COMMENTS)) {
52375240
result.push(CONST.SEARCH.TABLE_COLUMNS.COMMENTS);
52385241
}
52395242

@@ -5296,19 +5299,28 @@ function getColumnsToShow({
52965299
columns[CONST.SEARCH.TABLE_COLUMNS.CARD] = true;
52975300
}
52985301

5299-
if (transaction.taxCode) {
5302+
// If the transaction has any tax info (code, value, or amount),
5303+
// show both TAX_RATE and TAX_AMOUNT columns. Zero is valid tax data.
5304+
const hasTaxInfo = !!transaction.taxCode || transaction.taxAmount != null || (transaction.taxValue !== undefined && transaction.taxValue !== '');
5305+
if (hasTaxInfo) {
53005306
columns[CONST.SEARCH.TABLE_COLUMNS.TAX_RATE] = true;
5301-
}
5302-
5303-
if (transaction.taxAmount) {
53045307
columns[CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT] = true;
53055308
}
53065309

53075310
const hasExchangeRate = getExchangeRate(transaction, reportCurrency) !== '';
5308-
if (hasExchangeRate || transaction.originalAmount) {
5309-
columns[CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT] = true;
5311+
if (hasExchangeRate) {
53105312
columns[CONST.SEARCH.TABLE_COLUMNS.EXCHANGE_RATE] = true;
53115313
}
5314+
// Expense report view: TOTAL (workspace currency) is always shown; add AMOUNT
5315+
// (transaction's own currency) when a conversion exists so both are visible.
5316+
// Search page: show ORIGINAL_AMOUNT column (transaction's original amount).
5317+
if (hasExchangeRate) {
5318+
if (isExpenseReportView) {
5319+
columns[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT] = true;
5320+
} else {
5321+
columns[CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT] = true;
5322+
}
5323+
}
53125324

53135325
if (!Array.isArray(data)) {
53145326
const report = data[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
@@ -5390,8 +5402,11 @@ function getColumnsToShow({
53905402
CONST.SEARCH.TABLE_COLUMNS.TYPE,
53915403
CONST.SEARCH.TABLE_COLUMNS.DATE,
53925404
CONST.SEARCH.TABLE_COLUMNS.STATUS,
5405+
// TOTAL_AMOUNT (Amount) is data-driven in expense report view: shown only when a
5406+
// conversion exists. In search view, TOTAL_AMOUNT is always-true via the default
5407+
// columns map, so we don't need to list it here as non-data for either surface.
5408+
CONST.SEARCH.TABLE_COLUMNS.TOTAL,
53935409
CONST.SEARCH.TABLE_COLUMNS.COMMENTS,
5394-
CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT,
53955410
CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE,
53965411
CONST.SEARCH.TABLE_COLUMNS.BILLABLE,
53975412
CONST.SEARCH.TABLE_COLUMNS.BASE_62_REPORT_ID,

src/libs/TransactionUtils/index.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,17 +1292,35 @@ function getTagArrayFromName(tagName: string): string[] {
12921292
*/
12931293
function getExchangeRate(transaction: TransactionWithOptionalSearchFields, reportCurrency?: string) {
12941294
const fromCurrency = getCurrency(transaction);
1295-
const toCurrency = transaction.groupCurrency ?? reportCurrency ?? fromCurrency;
12961295

1297-
if (transaction.groupExchangeRate && Number(transaction.groupExchangeRate) !== 1) {
1298-
return `${transaction.groupExchangeRate} ${fromCurrency}/${toCurrency}`;
1296+
// On the report view, "unconverted" means the transaction currency matches the report currency.
1297+
// groupCurrency reflects the user's default workspace and is unrelated to the report being viewed,
1298+
// so we must gate on reportCurrency here to match Expensify Classic behavior.
1299+
if (reportCurrency && fromCurrency === reportCurrency) {
1300+
return '';
12991301
}
13001302

1301-
if (!transaction.currencyConversionRate || Number(transaction.currencyConversionRate) === 1) {
1302-
return '';
1303+
// groupExchangeRate: search-page rate (fromCurrency → groupCurrency).
1304+
if (transaction.groupExchangeRate != null && transaction.groupCurrency && fromCurrency !== transaction.groupCurrency) {
1305+
const groupRate = Number(transaction.groupExchangeRate);
1306+
if (groupRate !== 1) {
1307+
return `${transaction.groupExchangeRate} ${fromCurrency}/${transaction.groupCurrency}`;
1308+
}
13031309
}
13041310

1305-
return `${transaction.currencyConversionRate} ${fromCurrency}/${toCurrency}`;
1311+
// currencyConversionRate: report-layout rate (fromCurrency → reportCurrency).
1312+
// When no reportCurrency is provided (e.g. search sort), fall back to groupCurrency so we can still
1313+
// surface a meaningful rate. We intentionally do not use transaction.currency here, since it reflects
1314+
// the pre-modification currency and is almost never the correct conversion target.
1315+
const conversionToCurrency = reportCurrency ?? transaction.groupCurrency;
1316+
if (conversionToCurrency && transaction.currencyConversionRate != null && fromCurrency !== conversionToCurrency) {
1317+
const conversionRate = Number(transaction.currencyConversionRate);
1318+
if (conversionRate !== 1) {
1319+
return `${transaction.currencyConversionRate} ${fromCurrency}/${conversionToCurrency}`;
1320+
}
1321+
}
1322+
1323+
return '';
13061324
}
13071325

13081326
/**

0 commit comments

Comments
 (0)