Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7417,6 +7417,11 @@ const CONST = {
search: true,
reportView: true,
},
VENDOR: {
column: this.TABLE_COLUMNS.VENDOR,
search: true,
reportView: true,
},
DESCRIPTION: {
column: this.TABLE_COLUMNS.DESCRIPTION,
search: true,
Expand Down Expand Up @@ -7778,6 +7783,7 @@ const CONST = {
POSTED: 'posted',
EXPORTED: 'exported',
MERCHANT: 'merchant',
VENDOR: 'vendor',
DESCRIPTION: 'description',
FROM: 'from',
TO: 'to',
Expand Down Expand Up @@ -8020,6 +8026,7 @@ const CONST = {
[this.TABLE_COLUMNS.POSTED]: 'posted',
[this.TABLE_COLUMNS.EXPORTED]: 'exported',
[this.TABLE_COLUMNS.MERCHANT]: 'merchant',
[this.TABLE_COLUMNS.VENDOR]: 'vendor',
[this.TABLE_COLUMNS.DESCRIPTION]: 'description',
[this.TABLE_COLUMNS.FROM]: 'from',
[this.TABLE_COLUMNS.TO]: 'to',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNavigateToTransactionThread from '@hooks/useNavigateToTransactionThread';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useTheme from '@hooks/useTheme';
Expand All @@ -23,7 +24,7 @@ import {clearActiveTransactionIDs, getActiveTransactionIDs, setActiveTransaction
import {resolveTransactionCardFields} from '@libs/CardUtils';
import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
import {navigationRef} from '@libs/Navigation/Navigation';
import {isPolicyTaxEnabled} from '@libs/PolicyUtils';
import {hasVendorFeature, isPolicyTaxEnabled} from '@libs/PolicyUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils';
import {
Expand Down Expand Up @@ -269,6 +270,7 @@ function MoneyRequestReportTransactionList({
const longPressModalRef = useRef<MoneyRequestReportTransactionLongPressModalHandle>(null);
const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report);
const {isOffline} = useNetwork();
const {isBetaEnabled} = usePermissions();

const isTaxEnabled = isPolicyTaxEnabled(policy);
const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(report);
Expand Down Expand Up @@ -446,12 +448,15 @@ function MoneyRequestReportTransactionList({
const isExpenseReportViewFromIOUReport = isIOUReport(report);
const shouldShowBillableColumn = isBillableEnabledOnPolicy(policy);
const shouldShowCommentsColumn = useMemo(() => Object.values(reportActions ?? {}).some((action) => (action?.childVisibleActionCount ?? 0) > 0), [reportActions]);
// The saved column list is account-wide, so drop the vendor column on reports whose workspace lacks the vendor feature.
const isVendorColumnAvailable = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING));
const columnsToShow = useMemo(() => {
const savedColumns = (reportDetailsColumns ?? []).filter((column) => isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR);
return getColumnsToShow({
currentAccountID: currentUserDetails?.accountID,
data: transactions,
report,
visibleColumns: (isExpenseReportViewFromIOUReport ? [] : (reportDetailsColumns ?? [])) as SearchCustomColumnIds[],
visibleColumns: (isExpenseReportViewFromIOUReport ? [] : savedColumns) as SearchCustomColumnIds[],
isExpenseReportView: true,
isExpenseReportViewFromIOUReport,
shouldShowBillableColumn,
Expand All @@ -467,6 +472,7 @@ function MoneyRequestReportTransactionList({
shouldShowBillableColumn,
shouldShowCommentsColumn,
reportDetailsColumns,
isVendorColumnAvailable,
report,
isTaxEnabled,
shouldShowExpenseReportBreakDown,
Expand Down
5 changes: 5 additions & 0 deletions src/components/Search/SearchTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [
translationKey: 'common.merchant',
canEdit: true,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.VENDOR,
translationKey: 'common.vendor',
isColumnSortable: false,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION,
translationKey: 'common.description',
Expand Down
11 changes: 10 additions & 1 deletion src/components/TransactionItemRow/TransactionItemRowWide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import useThemeStyles from '@hooks/useThemeStyles';

import {getCategoryGLCode} from '@libs/CategoryUtils';
import getBase62ReportID from '@libs/getBase62ReportID';
import {isTaxCodeCustomized, getTagGLCode} from '@libs/PolicyUtils';
import {getTagGLCode, getVendorDisplayName, isTaxCodeCustomized} from '@libs/PolicyUtils';
import {getReportName} from '@libs/ReportNameUtils';
import {getReimbursableTotal, isExpenseReport} from '@libs/ReportUtils';
import {getViolationsForTransaction} from '@libs/SearchUIUtils';
Expand Down Expand Up @@ -391,6 +391,15 @@ function TransactionItemRowWide({
/>
</View>
);
case CONST.SEARCH.TABLE_COLUMNS.VENDOR:
return (
<View
key={column}
style={[getSearchColumnStyles(CONST.SEARCH.TABLE_COLUMNS.VENDOR)]}
>
<TextCell text={getVendorDisplayName(policy, transactionItem.comment?.vendor)} />
</View>
);
case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION:
return (
<View
Expand Down
22 changes: 22 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
} from '@src/types/onyx/Policy';
import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
import type Rule from '@src/types/onyx/Rule';
import type {TransactionCommentVendor} from '@src/types/onyx/Transaction';
import type {WorkspaceTravelSettings} from '@src/types/onyx/TravelSettings';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

Expand Down Expand Up @@ -2732,6 +2733,13 @@ function hasVendorFeature(policy: OnyxEntry<Policy>, isVendorMatchingBetaEnabled
return isVendorMatchingBetaEnabled && (isXeroVendorMatchingActive(policy) || isRilletVendorMatchingActive(policy) || isDualEntryVendorMatchingActive(policy));
}

/**
* Search spans every workspace at once, so the vendor column is offered when any workspace has the vendor feature.
*/
function hasVendorFeatureOnAnyPolicy(policies: OnyxCollection<Policy>, isVendorMatchingBetaEnabled: boolean): boolean {
return Object.values(policies ?? {}).some((policy) => hasVendorFeature(policy, isVendorMatchingBetaEnabled));
}

/**
* Single source of truth for which connected integration scopes the vendor field for this workspace
* (QBO, Sage Intacct, Xero, Rillet, or DualEntry) and what its vendor list looks like. Returns `undefined` when no
Expand Down Expand Up @@ -2917,6 +2925,18 @@ function findVendorByID(policy: OnyxEntry<Policy>, vendorID: string | undefined)
return getDualEntryVendors(policy).find((vendor) => vendor.id === vendorID);
}

/**
* Display name of a transaction's vendor, or an empty string when none is assigned. The workspace's synced vendor list
* wins so renames in the accounting system show through. The name stored on the transaction covers vendors since
* removed from that list.
*/
function getVendorDisplayName(policy: OnyxEntry<Policy>, vendor: TransactionCommentVendor | undefined): string {
if (!vendor?.externalID) {
return '';
}
return findVendorByID(policy, vendor.externalID)?.name ?? vendor.name ?? '';
}

/**
* Resolves the text shown for a stored merchant-rule vendor ID. Prefer the active vendor-matching
* source, use the unavailable label when its loaded list no longer contains the vendor, and retain
Expand Down Expand Up @@ -3482,6 +3502,7 @@ export {
getConnectedIntegration,
getConnectionExporters,
findVendorByID,
getVendorDisplayName,
getActiveVendorMatchingIntegration,
getMatchingVendorByID,
getMatchingVendors,
Expand All @@ -3496,6 +3517,7 @@ export {
isXeroActiveMatchingSource,
isXeroVendorMatchingActive,
hasVendorFeature,
hasVendorFeatureOnAnyPolicy,
isMatchingVendorListLoaded,
getValidConnectedIntegration,
getCountOfEnabledTagsOfList,
Expand Down
10 changes: 10 additions & 0 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ const transactionColumnNamesToSortingProperty: TransactionSorting = {
[CONST.SEARCH.TABLE_COLUMNS.EXPORTED]: 'exported' as const,
[CONST.SEARCH.TABLE_COLUMNS.TAG]: 'tag' as const,
[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const,
[CONST.SEARCH.TABLE_COLUMNS.VENDOR]: null,
[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: 'formattedTotal' as const,
[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'category' as const,
[CONST.SEARCH.TABLE_COLUMNS.ORIGINAL_AMOUNT]: 'originalAmount' as const,
Expand Down Expand Up @@ -4328,6 +4329,8 @@ function getSearchColumnTranslationKey(column: SearchSortBy): TranslationPaths {
return 'search.filters.exported';
case CONST.SEARCH.TABLE_COLUMNS.MERCHANT:
return 'common.merchant';
case CONST.SEARCH.TABLE_COLUMNS.VENDOR:
return 'common.vendor';
case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION:
return 'common.description';
case CONST.SEARCH.TABLE_COLUMNS.FROM:
Expand Down Expand Up @@ -6357,6 +6360,7 @@ function getColumnsToShow({
[CONST.SEARCH.TABLE_COLUMNS.DATE]: true,
[CONST.SEARCH.TABLE_COLUMNS.POSTED]: false,
[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false,
[CONST.SEARCH.TABLE_COLUMNS.VENDOR]: false,
[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false,
[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: false,
[CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE]: false,
Expand Down Expand Up @@ -6387,6 +6391,7 @@ function getColumnsToShow({
[CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: false,
[CONST.SEARCH.TABLE_COLUMNS.APPROVED]: false,
[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false,
[CONST.SEARCH.TABLE_COLUMNS.VENDOR]: false,
[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false,
[CONST.SEARCH.TABLE_COLUMNS.FROM]: false,
[CONST.SEARCH.TABLE_COLUMNS.TO]: false,
Expand Down Expand Up @@ -6532,6 +6537,10 @@ function getColumnsToShow({
columns[CONST.SEARCH.TABLE_COLUMNS.CARD] = true;
}

if (transaction.comment?.vendor?.externalID) {
columns[CONST.SEARCH.TABLE_COLUMNS.VENDOR] = true;
}

// Only show tax columns when the user explicitly chooses to display them.
if (customResult) {
// Show both TAX_RATE and TAX_AMOUNT when the transaction has a meaningful tax signal.
Expand Down Expand Up @@ -7001,6 +7010,7 @@ function shouldShowDeleteOption(

const FLEX_COLUMNS = new Set<string>([
CONST.SEARCH.TABLE_COLUMNS.MERCHANT,
CONST.SEARCH.TABLE_COLUMNS.VENDOR,
CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION,
CONST.SEARCH.TABLE_COLUMNS.CATEGORY,
CONST.SEARCH.TABLE_COLUMNS.CATEGORY_GL_CODE,
Expand Down
6 changes: 5 additions & 1 deletion src/libs/getSearchColumnContentToMeasure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {MeasurableFont} from './measureTextWidth/types';
import {getCompanyCardDescription} from './CardUtils';
import {getCategoryGLCode, getDecodedLeafCategoryName, isCategoryMissing} from './CategoryUtils';
import getBase62ReportID from './getBase62ReportID';
import {getTagGLCode} from './PolicyUtils';
import {getTagGLCode, getVendorDisplayName} from './PolicyUtils';
import {getReportName} from './ReportNameUtils';
import {getReportStatusTranslation} from './ReportUtils';
import {
Expand Down Expand Up @@ -90,6 +90,7 @@ const EDITABLE_SEARCH_COLUMNS = new Set<SearchColumnType>([
const DYNAMICALLY_SIZED_SEARCH_COLUMNS = new Set<SearchColumnType>([
CONST.SEARCH.TABLE_COLUMNS.STATUS,
CONST.SEARCH.TABLE_COLUMNS.MERCHANT,
CONST.SEARCH.TABLE_COLUMNS.VENDOR,
CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION,
CONST.SEARCH.TABLE_COLUMNS.CATEGORY,
CONST.SEARCH.TABLE_COLUMNS.TAG,
Expand Down Expand Up @@ -131,6 +132,7 @@ const HUGGED_SEARCH_COLUMNS = new Set<SearchColumnType>([CONST.SEARCH.TABLE_COLU
const SEARCH_COLUMN_HEADER_TRANSLATION_KEYS: Partial<Record<SearchColumnType, TranslationPaths>> = {
[CONST.SEARCH.TABLE_COLUMNS.STATUS]: 'common.status',
[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'common.merchant',
[CONST.SEARCH.TABLE_COLUMNS.VENDOR]: 'common.vendor',
[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: 'common.description',
[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'common.category',
[CONST.SEARCH.TABLE_COLUMNS.TAG]: 'common.tag',
Expand Down Expand Up @@ -216,6 +218,8 @@ function getTransactionColumnContentToMeasure(
];
case CONST.SEARCH.TABLE_COLUMNS.MERCHANT:
return [{text: getMerchantName(item, translate)}];
case CONST.SEARCH.TABLE_COLUMNS.VENDOR:
return [{text: getVendorDisplayName(item.policy, item.comment?.vendor)}];
case CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION:
return [{text: getDescription(item)}];
case CONST.SEARCH.TABLE_COLUMNS.CATEGORY:
Expand Down
22 changes: 19 additions & 3 deletions src/pages/Search/SearchColumnsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,49 @@ import {useSearchQueryContext} from '@components/Search/SearchContext';
import type {SearchCustomColumnIds} from '@components/Search/types';

import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';

import Navigation from '@libs/Navigation/Navigation';
import {hasVendorFeatureOnAnyPolicy} from '@libs/PolicyUtils';
import {buildQueryStringFromFilterFormValues, getCurrentSearchQueryJSON, hasValuesIncludeViolationFilter} from '@libs/SearchQueryUtils';
import {getCustomColumnDefault, getCustomColumns, insertColumnBeforeTotalAmount} from '@libs/SearchUIUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
import type {Policy} from '@src/types/onyx';

import React from 'react';
import type {OnyxCollection} from 'react-native-onyx';

import React, {useCallback} from 'react';

function SearchColumnsPage() {
const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
const {currentSearchKey} = useSearchQueryContext();
const {isBetaEnabled} = usePermissions();
const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING);
const isVendorColumnAvailableSelector = useCallback(
(allPolicies: OnyxCollection<Policy>) => hasVendorFeatureOnAnyPolicy(allPolicies, isVendorMatchingBetaEnabled),
[isVendorMatchingBetaEnabled],
);
const [isVendorColumnAvailable = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: isVendorColumnAvailableSelector});

const groupBy = searchAdvancedFiltersForm?.groupBy;
const queryType = searchAdvancedFiltersForm?.type ?? CONST.SEARCH.DATA_TYPES.EXPENSE;

// Violations data is only returned when these filters are set, so hide the column otherwise.
const shouldRequireViolationsColumn = hasValuesIncludeViolationFilter(searchAdvancedFiltersForm?.has);

const allTypeCustomColumns = getCustomColumns(queryType).filter((column) => shouldRequireViolationsColumn || column !== CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS);
// The vendor column only exists for workspaces with the vendor feature, so hide it when none of the user's workspaces has it.
const isColumnAvailable = (column: SearchCustomColumnIds) =>
(shouldRequireViolationsColumn || column !== CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS) && (isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR);

const allTypeCustomColumns = getCustomColumns(queryType).filter(isColumnAvailable);
const allGroupCustomColumns = getCustomColumns(groupBy);
const defaultGroupCustomColumns = getCustomColumnDefault(groupBy);
const defaultTypeCustomColumns = [...getCustomColumnDefault(queryType)];
const currentColumns = [...(searchAdvancedFiltersForm?.columns ?? [])].filter((column) => shouldRequireViolationsColumn || column !== CONST.SEARCH.TABLE_COLUMNS.VIOLATIONS);
const currentColumns = [...(searchAdvancedFiltersForm?.columns ?? [])].filter(isColumnAvailable);

// We need at least one element with flex1 in the table to ensure the table looks good in the UI, so we don't allow removing the total columns
// since it makes sense for them to show up in an expense management App and it fixes the layout issues.
Expand Down
16 changes: 12 additions & 4 deletions src/pages/settings/Report/ReportDetailsColumnsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import type {SearchCustomColumnIds} from '@components/Search/types';

import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';

import {setReportDetailsColumns} from '@libs/actions/ReportLayout';
import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import {isPolicyTaxEnabled} from '@libs/PolicyUtils';
import {hasVendorFeature, isPolicyTaxEnabled} from '@libs/PolicyUtils';
import {isIOUReport} from '@libs/ReportUtils';
import {getColumnsToShow} from '@libs/SearchUIUtils';
import {hasNonReimbursableTransactions} from '@libs/TransactionUtils';
Expand Down Expand Up @@ -65,6 +66,11 @@ function ReportDetailsColumnsPage() {
},
});
const currentUserDetails = useCurrentUserPersonalDetails();
const {isBetaEnabled} = usePermissions();

// The vendor column only exists for workspaces with the vendor feature, so hide it when this report's workspace lacks it.
const isVendorColumnAvailable = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING));
const isColumnAvailable = (column: SearchCustomColumnIds) => isVendorColumnAvailable || column !== CONST.SEARCH.TABLE_COLUMNS.VENDOR;

// Wait for transactions to load before rendering. ColumnsSettingsList snapshots
// currentColumns in useState on mount and does not sync prop updates, so we must
Expand Down Expand Up @@ -100,12 +106,14 @@ function ReportDetailsColumnsPage() {
return visibleColumns.filter(isReportDetailsCustomColumn);
}, [reportDetailsColumns, reportTransactions, currentUserDetails?.accountID, report, policy]);

const allColumns = ALL_REPORT_DETAILS_CUSTOM_COLUMNS.filter(isColumnAvailable);
const currentColumns = effectiveColumns.filter(isColumnAvailable);
const requiredColumns = new Set<SearchCustomColumnIds>([CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]);

const handleSave = (selectedColumnIds: SearchCustomColumnIds[]) => {
// Skip saving if columns haven't changed from the effective state, to avoid
// switching from the default path to the custom path in getColumnsToShow unnecessarily.
if (!arraysEqual(selectedColumnIds, effectiveColumns)) {
if (!arraysEqual(selectedColumnIds, currentColumns)) {
setReportDetailsColumns(selectedColumnIds, reportDetailsColumns);
}
Navigation.goBack();
Expand All @@ -117,9 +125,9 @@ function ReportDetailsColumnsPage() {

return (
<ColumnsSettingsList
allColumns={ALL_REPORT_DETAILS_CUSTOM_COLUMNS}
allColumns={allColumns}
defaultSelectedColumns={REPORT_DETAILS_DEFAULT_COLUMNS}
currentColumns={effectiveColumns}
currentColumns={currentColumns}
requiredColumns={requiredColumns}
onSave={handleSave}
/>
Expand Down
1 change: 1 addition & 0 deletions src/styles/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2148,6 +2148,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
case CONST.SEARCH.TABLE_COLUMNS.REPORT_ID:
case CONST.SEARCH.TABLE_COLUMNS.BASE_62_REPORT_ID:
case CONST.SEARCH.TABLE_COLUMNS.MERCHANT:
case CONST.SEARCH.TABLE_COLUMNS.VENDOR:
case CONST.SEARCH.TABLE_COLUMNS.FROM:
case CONST.SEARCH.TABLE_COLUMNS.TO:
case CONST.SEARCH.TABLE_COLUMNS.FIRST_APPROVER:
Expand Down
Loading
Loading