diff --git a/src/CONST.ts b/src/CONST.ts index edceb11edd85..bb85ac16bd83 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -937,9 +937,12 @@ const CONST = { MERCHANT: 'merchant', FROM: 'from', TO: 'to', + CATEGORY: 'category', + TAG: 'tag', TOTAL: 'total', TYPE: 'type', ACTION: 'action', + TAX_AMOUNT: 'taxAmount', }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/components/Search.tsx b/src/components/Search.tsx index fbd352a798c6..0e39039f59a0 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -65,11 +65,10 @@ function Search({query}: SearchProps) { const ListItem = SearchUtils.getListItem(type); const data = SearchUtils.getSections(searchResults?.data ?? {}, type); - const shouldShowMerchant = SearchUtils.getShouldShowMerchant(searchResults?.data ?? {}); return ( } + customListHeader={} ListItem={ListItem} sections={[{data, isDisabled: false}]} onSelectRow={(item) => { diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 02986f22dccc..ec0267d20c04 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -1,24 +1,30 @@ import React from 'react'; import {View} from 'react-native'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as SearchUtils from '@libs/SearchUtils'; import CONST from '@src/CONST'; +import type * as OnyxTypes from '@src/types/onyx'; +import SearchTableHeaderColumn from './SearchTableHeaderColumn'; type SearchTableHeaderProps = { - /** Whether we should show the merchant or description column */ - shouldShowMerchant: boolean; + data: OnyxTypes.SearchResults['data']; }; -function SearchTableHeader({shouldShowMerchant}: SearchTableHeaderProps) { +function SearchTableHeader({data}: SearchTableHeaderProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; + const shouldShowCategoryColumn = SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY); + const shouldShowTagColumn = SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG); + const shouldShowTaxColumn = SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT); + const shouldShowMerchant = SearchUtils.getShouldShowMerchant(data); + if (displayNarrowVersion) { return; } @@ -26,27 +32,50 @@ function SearchTableHeader({shouldShowMerchant}: SearchTableHeaderProps) { return ( - - {translate('common.date')} - - - {translate(shouldShowMerchant ? 'common.merchant' : 'common.description')} - - - {translate('common.from')} - - - {translate('common.to')} - - - {translate('common.total')} - - - {translate('common.type')} - - - {translate('common.action')} - + + + + + + + + + + ); diff --git a/src/components/SelectionList/SearchTableHeaderColumn.tsx b/src/components/SelectionList/SearchTableHeaderColumn.tsx new file mode 100644 index 000000000000..92b59f702cb1 --- /dev/null +++ b/src/components/SelectionList/SearchTableHeaderColumn.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type SearchTableHeaderColumnProps = { + shouldShow?: boolean; + containerStyle?: StyleProp; + text: string; + textStyle?: StyleProp; +}; + +export default function SearchTableHeaderColumn({containerStyle, text, textStyle, shouldShow = true}: SearchTableHeaderColumnProps) { + const styles = useThemeStyles(); + + if (!shouldShow) { + return null; + } + + return ( + + + {text} + + + ); +} diff --git a/src/components/SelectionList/TextWithIconCell.tsx b/src/components/SelectionList/TextWithIconCell.tsx new file mode 100644 index 000000000000..bd51133a362f --- /dev/null +++ b/src/components/SelectionList/TextWithIconCell.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type TextWithIconCellProps = { + icon: IconAsset; + text?: string; + showTooltip: boolean; +}; + +export default function TextWithIconCell({icon, text, showTooltip}: TextWithIconCellProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + if (!text) { + return null; + } + + return ( + + + + + {text} + + + + ); +} diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 0965ce6dabce..04929bf883d1 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -17,8 +17,9 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {Transaction} from '@src/types/onyx'; -import type {SearchPersonalDetails, SearchPolicyDetails, SearchTransactionType} from '@src/types/onyx/SearchResults'; +import type {SearchAccountDetails, SearchTransactionType} from '@src/types/onyx/SearchResults'; import BaseListItem from './BaseListItem'; +import TextWithIconCell from './TextWithIconCell'; import type {ListItem, TransactionListItemProps, TransactionListItemType} from './types'; const getTypeIcon = (type?: SearchTransactionType) => { @@ -61,6 +62,7 @@ function TransactionListItem({ const isFromExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE; const date = TransactionUtils.getCreated(transactionItem as OnyxEntry, CONST.DATE.MONTH_DAY_ABBR_FORMAT); const amount = TransactionUtils.getAmount(transactionItem as OnyxEntry, isFromExpenseReport); + const taxAmount = TransactionUtils.getTaxAmount(transactionItem as OnyxEntry, isFromExpenseReport); const currency = TransactionUtils.getCurrency(transactionItem as OnyxEntry); const description = TransactionUtils.getDescription(transactionItem as OnyxEntry); const merchant = getMerchant(); @@ -82,7 +84,7 @@ function TransactionListItem({ /> ); - const userCell = (participant: SearchPersonalDetails & SearchPolicyDetails) => { + const userCell = (participant: SearchAccountDetails) => { const displayName = participant?.name ?? participant?.displayName ?? participant?.login; const avatarURL = participant?.avatarURL ?? participant?.avatar; const isWorkspace = participant?.avatarURL !== undefined; @@ -108,6 +110,42 @@ function TransactionListItem({ ); }; + const categoryCell = isLargeScreenWidth ? ( + + ) : ( + + ); + + const tagCell = isLargeScreenWidth ? ( + + ) : ( + + ); + + const taxCell = ( + + ); + const totalCell = ( ({ > {() => ( <> - + {userCell(transactionItem.from)} ({ height={variables.iconSizeXXSmall} fill={theme.icon} /> - {userCell(transactionItem.to)} + {userCell(transactionItem.to)} {actionCell} - {merchantCell} - + + {merchantCell} + + {categoryCell} + {tagCell} + + + {totalCell} {typeCell} @@ -216,7 +260,10 @@ function TransactionListItem({ {dateCell} {merchantCell} {userCell(transactionItem.from)} - {userCell(transactionItem.to)} + {userCell(transactionItem.to)} + {transactionItem.shouldShowCategory && {categoryCell}} + {transactionItem.shouldShowTag && {tagCell}} + {transactionItem.shouldShowTax && {taxCell}} {totalCell} {typeCell} {actionCell} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 8a2a1efdd030..79e47e4aa4d7 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,11 +1,10 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; -import type {ValueOf} from 'type-fest'; import type {MaybePhraseKey} from '@libs/Localize'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import type CONST from '@src/CONST'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {SearchPersonalDetails, SearchPolicyDetails} from '@src/types/onyx/SearchResults'; +import type {SearchAccountDetails, SearchTransaction} from '@src/types/onyx/SearchResults'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -128,64 +127,26 @@ type ListItem = { brickRoadIndicator?: BrickRoad | '' | null; }; -type TransactionListItemType = ListItem & { - /** The ID of the transaction */ - transactionID: string; +type TransactionListItemType = ListItem & + SearchTransaction & { + /** The personal details of the user requesting money */ + from: SearchAccountDetails; - /** The transaction created date */ - created: string; + /** The personal details of the user paying the request */ + to: SearchAccountDetails; - /** The edited transaction created date */ - modifiedCreated: string; + /** Whether we should show the merchant column */ + shouldShowMerchant: boolean; - /** The transaction amount */ - amount: number; + /** Whether we should show the category column */ + shouldShowCategory: boolean; - /** The edited transaction amount */ - modifiedAmount: number; + /** Whether we should show the tag column */ + shouldShowTag: boolean; - /** The transaction currency */ - currency: string; - - /** The edited transaction currency */ - modifiedCurrency: string; - - /** The transaction merchant */ - merchant: string; - - /** The edited transaction merchant */ - modifiedMerchant: string; - - /** The receipt object */ - receipt?: {source?: string}; - - /** The personal details of the user requesting money */ - from: SearchPersonalDetails & SearchPolicyDetails; - - /** The personal details of the user paying the request */ - to: SearchPersonalDetails & SearchPolicyDetails; - - /** The transaction tag */ - tag: string; - - /** The transaction description */ - comment: {comment: string}; - - /** The transaction category */ - category: string; - - /** The type of request */ - type: ValueOf; - - /** The type of report the transaction is associated with */ - reportType: string; - - /** The ID of the policy the transaction is associated with */ - policyID: string; - - /** Whether we should show the merchant column */ - shouldShowMerchant: boolean; -}; + /** Whether we should show the tax column */ + shouldShowTax: boolean; + }; type ListItemProps = CommonListItemProps & { /** The section list item */ @@ -448,7 +409,7 @@ export type { SelectionListHandle, TableListItemProps, TransactionListItemProps, + TransactionListItemType, UserListItemProps, ValidListItem, - TransactionListItemType, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index d21c4883ee21..927ffbbbbf66 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -331,6 +331,7 @@ export default { type: 'Type', action: 'Action', expenses: 'Expenses', + tax: 'Tax', }, location: { useCurrent: 'Use current location', diff --git a/src/languages/es.ts b/src/languages/es.ts index 707526056e04..4e36529c77a3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -321,6 +321,7 @@ export default { type: 'Tipo', action: 'Acción', expenses: 'Gastos', + tax: 'Impuesto', }, connectionComplete: { title: 'Conexión Completa', diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 55e81a717bac..cef9989d22d2 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,8 +1,10 @@ +import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; import TransactionListItem from '@components/SelectionList/TransactionListItem'; +import type {TransactionListItemType} from '@components/SelectionList/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {SearchDataTypes, SearchTransaction, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; import * as UserUtils from './UserUtils'; function getSearchType(search: OnyxTypes.SearchResults['search']): SearchDataTypes | undefined { @@ -21,8 +23,15 @@ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { }); } -function getTransactionsSections(data: OnyxTypes.SearchResults['data']): SearchTransaction[] { +function getShouldShowColumn(data: OnyxTypes.SearchResults['data'], columnName: ValueOf) { + return Object.values(data).some((item) => !!item[columnName]); +} + +function getTransactionsSections(data: OnyxTypes.SearchResults['data']): TransactionListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); + const shouldShowCategory = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY); + const shouldShowTag = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG); + const shouldShowTax = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT); return Object.entries(data) .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) .map(([, value]) => { @@ -32,6 +41,9 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): SearchT from: data.personalDetailsList?.[value.accountID], to: isExpenseReport ? data[`${ONYXKEYS.COLLECTION.POLICY}${value.policyID}`] : data.personalDetailsList?.[value.managerID], shouldShowMerchant, + shouldShowCategory, + shouldShowTag, + shouldShowTax, keyForList: value.transactionID, }; }) @@ -61,4 +73,4 @@ function getQueryHash(query: string): number { return UserUtils.hashText(query, 2 ** 32); } -export {getListItem, getQueryHash, getSections, getShouldShowMerchant, getSearchType}; +export {getListItem, getQueryHash, getSections, getShouldShowColumn, getShouldShowMerchant, getSearchType}; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 35a39902b973..4c2f9a180518 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1568,14 +1568,13 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ columnWidth = getWidthStyle(variables.w44); break; case CONST.SEARCH_TABLE_COLUMNS.MERCHANT: - columnWidth = styles.flex1; - break; case CONST.SEARCH_TABLE_COLUMNS.FROM: - columnWidth = styles.flex1; - break; case CONST.SEARCH_TABLE_COLUMNS.TO: + case CONST.SEARCH_TABLE_COLUMNS.CATEGORY: + case CONST.SEARCH_TABLE_COLUMNS.TAG: columnWidth = styles.flex1; break; + case CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT: case CONST.SEARCH_TABLE_COLUMNS.TOTAL: columnWidth = {...getWidthStyle(variables.w96), ...styles.alignItemsEnd}; break; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index ead7cc591fe5..a4cf9c2870d0 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,7 +1,6 @@ import type {ValueOf} from 'type-fest'; import type TransactionListItem from '@components/SelectionList/TransactionListItem'; import type CONST from '@src/CONST'; -import type {Receipt} from './Transaction'; type SearchDataTypes = ValueOf; @@ -36,35 +35,87 @@ type SearchPolicyDetails = { }; type SearchTransaction = { + /** The ID of the transaction */ transactionID: string; - parentTransactionID?: string; - receipt?: Receipt; - hasEReceipt?: boolean; + + /** The transaction created date */ created: string; + + /** The edited transaction created date */ + modifiedCreated: string; + + /** The transaction amount */ + amount: number; + + /** The edited transaction amount */ + modifiedAmount: number; + + /** The transaction currency */ + currency: string; + + /** The edited transaction currency */ + modifiedCurrency: string; + + /** The transaction merchant */ merchant: string; - modifiedCreated?: string; - modifiedMerchant?: string; + + /** The edited transaction merchant */ + modifiedMerchant: string; + + /** The receipt object */ + receipt?: {source?: string}; + + /** The transaction tag */ + tag: string; + + /** The transaction description */ + comment: {comment: string}; + + /** The transaction category */ + category: string; + + /** The type of request */ + type: ValueOf; + + /** The type of report the transaction is associated with */ + reportType: string; + + /** The ID of the policy the transaction is associated with */ + policyID: string; + + /** The ID of the parent of the transaction */ + parentTransactionID?: string; + + /** If the transaction has an Ereceipt */ + hasEReceipt?: boolean; + + /** The transaction description */ description: string; + + /** The transaction sender ID */ accountID: number; + + /** The transaction recipient ID */ managerID: number; - from: SearchPersonalDetails | SearchPolicyDetails; - to: SearchPersonalDetails | SearchPolicyDetails; - amount: number; - modifiedAmount?: number; - category?: string; - currency: string; - tag?: string; - type: SearchTransactionType; + + /** If the transaction has a Ereceipt */ hasViolation: boolean; + + /** The transaction tax amount */ taxAmount?: number; + + /** The ID of the report the transaction is associated with */ reportID: string; - reportType: string; - policyID: string; + + /** The report ID of the transaction thread associated with the transaction */ transactionThreadReportID: string; - shouldShowMerchant: boolean; + + /** The action that can be performed for the transaction */ action: string; }; +type SearchAccountDetails = Partial; + type SearchTransactionType = ValueOf; type SearchQuery = ValueOf; @@ -76,4 +127,4 @@ type SearchResults = { export default SearchResults; -export type {SearchQuery, SearchTransaction, SearchTransactionType, SearchPersonalDetails, SearchPolicyDetails, SearchDataTypes, SearchTypeToItemMap}; +export type {SearchQuery, SearchTransaction, SearchTransactionType, SearchPersonalDetails, SearchPolicyDetails, SearchAccountDetails, SearchDataTypes, SearchTypeToItemMap};