diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3fdcecffb04c..75bdd48c2894 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6687,6 +6687,7 @@ const CONST = { EXPENSE: { RECEIPT: this.TABLE_COLUMNS.RECEIPT, DATE: this.TABLE_COLUMNS.DATE, + SUBMITTED: this.TABLE_COLUMNS.SUBMITTED, MERCHANT: this.TABLE_COLUMNS.MERCHANT, FROM: this.TABLE_COLUMNS.FROM, TO: this.TABLE_COLUMNS.TO, @@ -6696,6 +6697,7 @@ const CONST = { }, EXPENSE_REPORT: { DATE: this.TABLE_COLUMNS.DATE, + SUBMITTED: this.TABLE_COLUMNS.SUBMITTED, STATUS: this.TABLE_COLUMNS.STATUS, TITLE: this.TABLE_COLUMNS.TITLE, FROM: this.TABLE_COLUMNS.FROM, @@ -6773,6 +6775,7 @@ const CONST = { TABLE_COLUMNS: { RECEIPT: 'receipt', DATE: 'date', + SUBMITTED: 'submitted', MERCHANT: 'merchant', DESCRIPTION: 'description', FROM: 'from', diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 586154caa388..fda71d655181 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1037,7 +1037,7 @@ function Search({ navigation.setParams({q: newQuery, rawQuery: undefined}); }; - const shouldShowYear = shouldShowYearUtil(searchResults?.data, isExpenseReportType ?? false); + const {shouldShowYearCreated, shouldShowYearSubmitted} = shouldShowYearUtil(searchResults?.data, isExpenseReportType ?? false); const {shouldShowAmountInWideColumn, shouldShowTaxAmountInWideColumn} = getWideAmountIndicators(searchResults?.data); const shouldShowSorting = !validGroupBy; const shouldShowTableHeader = isLargeScreenWidth && !isChat && !validGroupBy; @@ -1067,7 +1067,8 @@ function Search({ onSortPress={onSortPress} sortOrder={sortOrder} sortBy={sortBy} - shouldShowYear={shouldShowYear} + shouldShowYear={shouldShowYearCreated} + shouldShowYearSubmitted={shouldShowYearSubmitted} isAmountColumnWide={shouldShowAmountInWideColumn} isTaxAmountColumnWide={shouldShowTaxAmountInWideColumn} shouldShowSorting={shouldShowSorting} diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx index 53547f29416e..558671578da9 100644 --- a/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx +++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx @@ -88,6 +88,15 @@ function ExpenseReportListItemRow({ /> ), + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: ( + + + + ), [CONST.SEARCH.TABLE_COLUMNS.STATUS]: ( ({ backgroundColor: theme.highlightBG, }); - const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => { + const {amountColumnSize, dateColumnSize, taxAmountColumnSize, submittedColumnSize} = useMemo(() => { return { amountColumnSize: transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, taxAmountColumnSize: transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, dateColumnSize: transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, + submittedColumnSize: transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, }; - }, [transactionItem.isAmountColumnWide, transactionItem.isTaxAmountColumnWide, transactionItem.shouldShowYear]); + }, [transactionItem.isAmountColumnWide, transactionItem.isTaxAmountColumnWide, transactionItem.shouldShowYear, transactionItem.shouldShowYearSubmitted]); const transactionViolations = useMemo(() => { return (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( @@ -183,6 +184,7 @@ function TransactionListItem({ isActionLoading={isLoading ?? isActionLoading} isSelected={!!transactionItem.isSelected} dateColumnSize={dateColumnSize} + submittedColumnSize={submittedColumnSize} amountColumnSize={amountColumnSize} taxAmountColumnSize={taxAmountColumnSize} shouldShowCheckbox={!!canSelectMultiple} diff --git a/src/components/SelectionListWithSections/SearchTableHeader.tsx b/src/components/SelectionListWithSections/SearchTableHeader.tsx index 2672d1225dee..8ab5da2c19a3 100644 --- a/src/components/SelectionListWithSections/SearchTableHeader.tsx +++ b/src/components/SelectionListWithSections/SearchTableHeader.tsx @@ -34,6 +34,10 @@ const getExpenseHeaders = (groupBy?: SearchGroupBy): SearchColumnConfig[] => [ columnName: CONST.SEARCH.TABLE_COLUMNS.DATE, translationKey: 'common.date', }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.SUBMITTED, + translationKey: 'common.submitted', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.MERCHANT, translationKey: 'common.merchant', @@ -140,6 +144,10 @@ const getExpenseReportHeaders = (profileIcon?: IconAsset): SearchColumnConfig[] columnName: CONST.SEARCH.TABLE_COLUMNS.DATE, translationKey: 'common.date', }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.SUBMITTED, + translationKey: 'common.submitted', + }, { columnName: CONST.SEARCH.TABLE_COLUMNS.STATUS, translationKey: 'common.status', @@ -192,6 +200,7 @@ type SearchTableHeaderProps = { sortOrder?: SortOrder; onSortPress: (column: SearchColumnType, order: SortOrder) => void; shouldShowYear: boolean; + shouldShowYearSubmitted?: boolean; isAmountColumnWide: boolean; isTaxAmountColumnWide: boolean; shouldShowSorting: boolean; @@ -207,6 +216,7 @@ function SearchTableHeader({ sortOrder, onSortPress, shouldShowYear, + shouldShowYearSubmitted, shouldShowSorting, canSelectMultiple, isAmountColumnWide, @@ -245,6 +255,7 @@ function SearchTableHeader({ areAllOptionalColumnsHidden={areAllOptionalColumnsHidden} shouldShowColumn={shouldShowColumn} dateColumnSize={shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} + submittedColumnSize={shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} amountColumnSize={isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} taxAmountColumnSize={isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} shouldShowSorting={shouldShowSorting} diff --git a/src/components/SelectionListWithSections/SortableTableHeader.tsx b/src/components/SelectionListWithSections/SortableTableHeader.tsx index 1d1ff42243b7..6157f929ec04 100644 --- a/src/components/SelectionListWithSections/SortableTableHeader.tsx +++ b/src/components/SelectionListWithSections/SortableTableHeader.tsx @@ -25,6 +25,7 @@ type SearchTableHeaderProps = { sortOrder?: SortOrder; shouldShowSorting: boolean; dateColumnSize: TableColumnSize; + submittedColumnSize?: TableColumnSize; amountColumnSize: TableColumnSize; taxAmountColumnSize: TableColumnSize; containerStyles?: StyleProp; @@ -39,6 +40,7 @@ function SortableTableHeader({ sortOrder, shouldShowColumn, dateColumnSize, + submittedColumnSize, containerStyles, shouldShowSorting, onSortPress, @@ -77,6 +79,7 @@ function SortableTableHeader({ amountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, taxAmountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, !!areAllOptionalColumnsHidden, + submittedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE, ), ]} isSortable={isSortable} diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index 85a1c84d0168..61c3af8db33d 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -243,6 +243,9 @@ type TransactionListItemType = ListItem & /** Report to which the transaction belongs */ report: Report | undefined; + /** The date the report was submitted */ + submitted?: string; + /** Policy to which the transaction belongs */ policy: Policy | undefined; @@ -281,6 +284,11 @@ type TransactionListItemType = ListItem & */ shouldShowYear: boolean; + /** Whether we should show the year for the submitted date. + * This is true if at least one transaction in the dataset was submitted in past years + */ + shouldShowYearSubmitted: boolean; + isAmountColumnWide: boolean; isTaxAmountColumnWide: boolean; @@ -399,6 +407,12 @@ type TransactionReportGroupListItemType = TransactionGroupListItemType & {groupe */ shouldShowYear: boolean; + /** + * Whether we should show the year for the submitted date. + * This is true if at least one report in the dataset was submitted in past years + */ + shouldShowYearSubmitted: boolean; + /** The main action that can be performed for the report */ action: SearchTransactionAction | undefined; diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 729ccb358f5d..bd06a2d3f912 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -89,6 +89,7 @@ type TransactionItemRowProps = { isSelected: boolean; shouldShowTooltip: boolean; dateColumnSize: TableColumnSize; + submittedColumnSize?: TableColumnSize; amountColumnSize: TableColumnSize; taxAmountColumnSize: TableColumnSize; onCheckboxPress?: (transactionID: string) => void; @@ -131,6 +132,7 @@ function TransactionItemRow({ isSelected, shouldShowTooltip, dateColumnSize, + submittedColumnSize, amountColumnSize, taxAmountColumnSize, onCheckboxPress = () => {}, @@ -162,6 +164,7 @@ function TransactionItemRow({ const expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const isDateColumnWide = dateColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; + const isSubmittedColumnWide = submittedColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; const isAmountColumnWide = amountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; const isTaxAmountColumnWide = taxAmountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; @@ -253,6 +256,18 @@ function TransactionItemRow({ /> ), + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: ( + + + + ), [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: ( = Partial>; @@ -139,6 +138,7 @@ const transactionColumnNamesToSortingProperty: TransactionSorting = { [CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const, [CONST.SEARCH.TABLE_COLUMNS.FROM]: 'formattedFrom' as const, [CONST.SEARCH.TABLE_COLUMNS.DATE]: 'date' as const, + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: 'submitted' as const, [CONST.SEARCH.TABLE_COLUMNS.TAG]: 'tag' as const, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: 'formattedTotal' as const, @@ -162,6 +162,7 @@ const taskColumnNamesToSortingProperty: TaskSorting = { const expenseReportColumnNamesToSortingProperty: ExpenseReportSorting = { [CONST.SEARCH.TABLE_COLUMNS.AVATAR]: null, [CONST.SEARCH.TABLE_COLUMNS.DATE]: 'created' as const, + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: 'submitted' as const, [CONST.SEARCH.TABLE_COLUMNS.STATUS]: 'formattedStatus' as const, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: 'reportName' as const, [CONST.SEARCH.TABLE_COLUMNS.FROM]: 'formattedFrom' as const, @@ -646,7 +647,7 @@ function getTransactionItemCommonFormattedProperties( policy: OnyxTypes.Policy, formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], report: OnyxTypes.Report | undefined, -): Pick { +): Pick { const isExpenseReport = report?.type === CONST.REPORT.TYPE.EXPENSE; const fromName = getDisplayNameOrDefault(from); @@ -664,11 +665,13 @@ function getTransactionItemCommonFormattedProperties( const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created; const merchant = getTransactionMerchant(transactionItem, policy); const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? '' : merchant; + const submitted = report?.submitted; return { formattedFrom, formattedTo, date, + submitted, formattedTotal, formattedMerchant, }; @@ -847,59 +850,98 @@ function getWideAmountIndicators(data: TransactionListItemType[] | TransactionGr }; } +type ShouldShowYearResult = { + shouldShowYearCreated: boolean; + shouldShowYearSubmitted: boolean; +}; + /** * Checks if the date of transactions or reports indicate the need to display the year because they are from a past year. * @param data - The search results data (array or object) * @param checkOnlyReports - When true and data is an object, only check report dates (skip transactions and report actions) + * @returns An object indicating which date fields should display the full year */ -function shouldShowYear(data: TransactionListItemType[] | TransactionGroupListItemType[] | TaskListItemType[] | OnyxTypes.SearchResults['data'], checkOnlyReports = false) { +function shouldShowYear( + data: TransactionListItemType[] | TransactionGroupListItemType[] | TaskListItemType[] | OnyxTypes.SearchResults['data'], + checkOnlyReports = false, +): ShouldShowYearResult { + const result: ShouldShowYearResult = { + shouldShowYearCreated: false, + shouldShowYearSubmitted: false, + }; + const currentYear = new Date().getFullYear(); if (Array.isArray(data)) { - return data.some((item: TransactionListItemType | TransactionGroupListItemType | TaskListItemType) => { + for (const item of data) { if (isTaskListItemType(item)) { const taskYear = new Date(item.created).getFullYear(); - return taskYear !== currentYear; + if (taskYear !== currentYear) { + result.shouldShowYearCreated = true; + } } - - if (isTransactionGroupListItemType(item)) { - // If the item is a TransactionGroupListItemType, iterate over its transactions and check them - return item.transactions.some((transaction) => { - const transactionYear = new Date(getTransactionCreatedDate(transaction)).getFullYear(); - return transactionYear !== currentYear; - }); + if (isTransactionReportGroupListItemType(item)) { + if (item.created && DateUtils.doesDateBelongToAPastYear(item.created)) { + result.shouldShowYearCreated = true; + } + if (item.submitted && DateUtils.doesDateBelongToAPastYear(item.submitted)) { + result.shouldShowYearSubmitted = true; + } + } + if (isTransactionListItemType(item)) { + const transactionCreated = getTransactionCreatedDate(item); + if (transactionCreated && DateUtils.doesDateBelongToAPastYear(transactionCreated)) { + result.shouldShowYearCreated = true; + } + if (item.submitted && DateUtils.doesDateBelongToAPastYear(item.submitted)) { + result.shouldShowYearSubmitted = true; + } } - const createdYear = new Date(item?.modifiedCreated ? item.modifiedCreated : item?.created || '').getFullYear(); - return createdYear !== currentYear; - }); + // Early exit if all flags are true + if (result.shouldShowYearCreated && result.shouldShowYearSubmitted) { + return result; + } + } + return result; } - for (const key in data) { + for (const key of Object.keys(data)) { if (!checkOnlyReports && isTransactionEntry(key)) { const item = data[key]; - if (shouldShowTransactionYear(item)) { - return true; + if (item.created && DateUtils.doesDateBelongToAPastYear(item.created)) { + result.shouldShowYearCreated = true; + } + const report = data[`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`]; + if (report?.submitted && DateUtils.doesDateBelongToAPastYear(report.submitted)) { + result.shouldShowYearSubmitted = true; } } else if (!checkOnlyReports && isReportActionEntry(key)) { const item = data[key]; for (const action of Object.values(item)) { const date = action.created; - if (DateUtils.doesDateBelongToAPastYear(date)) { - return true; + result.shouldShowYearCreated = true; } } } else if (isReportEntry(key)) { const item = data[key]; - const date = item.created; - if (date && DateUtils.doesDateBelongToAPastYear(date)) { - return true; + if (item.created && DateUtils.doesDateBelongToAPastYear(item.created)) { + result.shouldShowYearCreated = true; + } + if (item.submitted && DateUtils.doesDateBelongToAPastYear(item.submitted)) { + result.shouldShowYearSubmitted = true; } } + + // Early exit if all flags are true + if (result.shouldShowYearCreated && result.shouldShowYearSubmitted) { + return result; + } } - return false; + + return result; } /** @@ -1051,7 +1093,7 @@ function getTransactionsSections( isActionLoadingSet: ReadonlySet | undefined, ): [TransactionListItemType[], number] { const shouldShowMerchant = getShouldShowMerchant(data); - const doesDataContainAPastYearTransaction = shouldShowYear(data); + const {shouldShowYearCreated, shouldShowYearSubmitted} = shouldShowYear(data); const {shouldShowAmountInWideColumn, shouldShowTaxAmountInWideColumn} = getWideAmountIndicators(data); // Pre-filter transaction keys to avoid repeated checks @@ -1100,7 +1142,7 @@ function getTransactionsSections( const from = reportAction?.actorAccountID ? (personalDetailsMap.get(reportAction.actorAccountID.toString()) ?? emptyPersonalDetails) : emptyPersonalDetails; const to = getToFieldValueForTransaction(transactionItem, report, data.personalDetailsList, reportAction); - const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties( + const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date, submitted} = getTransactionItemCommonFormattedProperties( transactionItem, from, to, @@ -1125,8 +1167,10 @@ function getTransactionsSections( formattedTotal, formattedMerchant, date, + submitted, shouldShowMerchant, - shouldShowYear: doesDataContainAPastYearTransaction, + shouldShowYear: shouldShowYearCreated, + shouldShowYearSubmitted, isAmountColumnWide: shouldShowAmountInWideColumn, isTaxAmountColumnWide: shouldShowTaxAmountInWideColumn, violations: transactionViolations, @@ -1338,7 +1382,7 @@ function getTaskSections( const report = getReportOrDraftReport(taskItem.reportID) ?? taskItem; const parentReport = getReportOrDraftReport(taskItem.parentReportID); - const doesDataContainAPastYearTransaction = shouldShowYear(data); + const {shouldShowYearCreated} = shouldShowYear(data); const reportName = StringUtils.lineBreaksToSpaces(Parser.htmlToText(taskItem.reportName)); const description = StringUtils.lineBreaksToSpaces(Parser.htmlToText(taskItem.description)); @@ -1351,7 +1395,7 @@ function getTaskSections( createdBy, formattedCreatedBy, keyForList: taskItem.reportID, - shouldShowYear: doesDataContainAPastYearTransaction, + shouldShowYear: shouldShowYearCreated, }; if (parentReport && personalDetails) { @@ -1498,7 +1542,7 @@ function getReportSections( ): [TransactionGroupListItemType[], number] { const shouldShowMerchant = getShouldShowMerchant(data); - const doesDataContainAPastYearTransaction = shouldShowYear(data); + const {shouldShowYearCreated: shouldShowYearCreatedTransaction, shouldShowYearSubmitted: shouldShowYearSubmittedTransaction} = shouldShowYear(data); const {shouldShowAmountInWideColumn, shouldShowTaxAmountInWideColumn} = getWideAmountIndicators(data); const {moneyRequestReportActionsByTransactionID, holdReportActionsByTransactionID} = createReportActionsLookupMaps(data); @@ -1521,7 +1565,7 @@ function getReportSections( ); const orderedKeys: string[] = [...reportKeys, ...transactionKeys]; - const doesDataContainAPastYearReport = shouldShowYear(data, true); + const {shouldShowYearCreated: shouldShowYearCreatedReport, shouldShowYearSubmitted: shouldShowYearSubmittedReport} = shouldShowYear(data, true); for (const key of orderedKeys) { if (isReportEntry(key) && (data[key].type === CONST.REPORT.TYPE.IOU || data[key].type === CONST.REPORT.TYPE.EXPENSE || data[key].type === CONST.REPORT.TYPE.INVOICE)) { @@ -1594,7 +1638,8 @@ function getReportSections( formattedStatus, transactions, ...(reportPendingAction ? {pendingAction: reportPendingAction} : {}), - shouldShowYear: doesDataContainAPastYearReport, + shouldShowYear: shouldShowYearCreatedReport, + shouldShowYearSubmitted: shouldShowYearSubmittedReport, hasVisibleViolations: hasVisibleViolationsForReport, }; @@ -1640,7 +1685,8 @@ function getReportSections( formattedMerchant, date, shouldShowMerchant, - shouldShowYear: doesDataContainAPastYearTransaction, + shouldShowYear: shouldShowYearCreatedTransaction, + shouldShowYearSubmitted: shouldShowYearSubmittedTransaction, keyForList: transactionItem.transactionID, violations: transactionViolations, isAmountColumnWide: shouldShowAmountInWideColumn, @@ -1877,11 +1923,21 @@ function getSortedSections( */ function compareValues(a: unknown, b: unknown, sortOrder: SortOrder, sortBy: string, localeCompare: LocaleContextProps['localeCompare'], shouldCompareOriginalValue = false): number { const isAsc = sortOrder === CONST.SEARCH.SORT_ORDER.ASC; + const aIsEmpty = a === undefined || a === null || a === ''; + const bIsEmpty = b === undefined || b === null || b === ''; - if (a === undefined || b === undefined) { + if (aIsEmpty && bIsEmpty) { return 0; } + if (aIsEmpty) { + return isAsc ? -1 : 1; + } + + if (bIsEmpty) { + return isAsc ? 1 : -1; + } + if (typeof a === 'string' && typeof b === 'string') { return isAsc ? localeCompare(a, b) : localeCompare(b, a); } @@ -2091,6 +2147,8 @@ function getSearchColumnTranslationKey(columnId: SearchCustomColumnIds): Transla switch (columnId) { case CONST.SEARCH.TABLE_COLUMNS.DATE: return 'common.date'; + case CONST.SEARCH.TABLE_COLUMNS.SUBMITTED: + return 'common.submitted'; case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: return 'common.merchant'; case CONST.SEARCH.TABLE_COLUMNS.FROM: @@ -2486,20 +2544,21 @@ function getColumnsToShow( type?: SearchDataTypes, ): ColumnVisibility { if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { - const reportColumns = [ - CONST.SEARCH.TABLE_COLUMNS.AVATAR, - CONST.SEARCH.TABLE_COLUMNS.DATE, - CONST.SEARCH.TABLE_COLUMNS.STATUS, - CONST.SEARCH.TABLE_COLUMNS.TITLE, - CONST.SEARCH.TABLE_COLUMNS.FROM, - CONST.SEARCH.TABLE_COLUMNS.TO, - CONST.SEARCH.TABLE_COLUMNS.TOTAL, - CONST.SEARCH.TABLE_COLUMNS.ACTION, - ]; + const reportColumns: ColumnVisibility = { + [CONST.SEARCH.TABLE_COLUMNS.AVATAR]: true, + [CONST.SEARCH.TABLE_COLUMNS.DATE]: true, + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: false, + [CONST.SEARCH.TABLE_COLUMNS.STATUS]: true, + [CONST.SEARCH.TABLE_COLUMNS.TITLE]: true, + [CONST.SEARCH.TABLE_COLUMNS.FROM]: true, + [CONST.SEARCH.TABLE_COLUMNS.TO]: true, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL]: true, + [CONST.SEARCH.TABLE_COLUMNS.ACTION]: true, + }; // If there are no visible columns, everything should be visible if (!visibleColumns.length) { - return Object.fromEntries(reportColumns.map((column) => [column, true])); + return reportColumns; } // If the user has set custom columns, toggle the visible columns on, with all other @@ -2507,7 +2566,7 @@ function getColumnsToShow( const columns: ColumnVisibility = {}; const requiredColumns = new Set([CONST.SEARCH.TABLE_COLUMNS.AVATAR, CONST.SEARCH.TABLE_COLUMNS.TOTAL]); - for (const columnId of reportColumns) { + for (const columnId of Object.keys(reportColumns) as SearchColumnType[]) { columns[columnId] = requiredColumns.has(columnId); } @@ -2557,6 +2616,7 @@ function getColumnsToShow( [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: true, [CONST.SEARCH.TABLE_COLUMNS.TYPE]: true, [CONST.SEARCH.TABLE_COLUMNS.DATE]: true, + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: false, [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: false, [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: false, [CONST.SEARCH.TABLE_COLUMNS.FROM]: false, @@ -2766,6 +2826,8 @@ function getTableMinWidth(columns: SearchColumnType[]) { minWidth += 80; } else if (column === CONST.SEARCH.TABLE_COLUMNS.DATE) { minWidth += 48; + } else if (column === CONST.SEARCH.TABLE_COLUMNS.SUBMITTED) { + minWidth += 72; } else if (column === CONST.SEARCH.TABLE_COLUMNS.TYPE) { minWidth += 20; } else { diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 9002658e60be..a82d65ac4835 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1718,7 +1718,14 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ return isDragging ? styles.cursorGrabbing : styles.cursorZoomOut; }, - getReportTableColumnStyles: (columnName: string, isDateColumnWide = false, isAmountColumnWide = false, isTaxAmountColumnWide = false, isDateColumnFullWidth = false): ViewStyle => { + getReportTableColumnStyles: ( + columnName: string, + isDateColumnWide = false, + isAmountColumnWide = false, + isTaxAmountColumnWide = false, + isDateColumnFullWidth = false, + isSubmittedColumnWide = false, + ): ViewStyle => { let columnWidth; switch (columnName) { case CONST.SEARCH.TABLE_COLUMNS.COMMENTS: @@ -1731,6 +1738,9 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ case CONST.SEARCH.TABLE_COLUMNS.STATUS: columnWidth = {...getWidthStyle(variables.w80), ...styles.alignItemsCenter}; break; + case CONST.SEARCH.TABLE_COLUMNS.SUBMITTED: + columnWidth = {...getWidthStyle(isSubmittedColumnWide ? variables.w92 : variables.w72)}; + break; case CONST.SEARCH.TABLE_COLUMNS.DATE: // We will remove this variable & param, but in a follow up PR. We are duplicating the logic here to "use" the variable // to prevent eslint errors. This will be removed diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 1e2235392ed9..5e2fca054ae2 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -89,6 +89,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** The date the report was created */ created?: string; + /** The date the report was submitted */ + submitted?: string; + /** The specific type of chat */ chatType?: ValueOf; diff --git a/src/types/utils/whitelistedReportKeys.ts b/src/types/utils/whitelistedReportKeys.ts index 0a6b4100ebbc..c3f10b4da0e2 100644 --- a/src/types/utils/whitelistedReportKeys.ts +++ b/src/types/utils/whitelistedReportKeys.ts @@ -32,6 +32,7 @@ type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback< writeCapability: unknown; type: unknown; created: unknown; + submitted: unknown; visibility: unknown; invoiceReceiver: unknown; parentReportID: unknown; diff --git a/tests/unit/MoneyRequestReportUtilsTest.ts b/tests/unit/MoneyRequestReportUtilsTest.ts index 9cae1e021d32..616603708fea 100644 --- a/tests/unit/MoneyRequestReportUtilsTest.ts +++ b/tests/unit/MoneyRequestReportUtilsTest.ts @@ -16,6 +16,7 @@ const policyBaseMock: Policy = { const reportBaseMock: Report = { chatReportID: '1706144653204915', created: '2024-12-21 13:05:20', + submitted: '2024-12-21 13:05:20', currency: 'USD', isWaitingOnBankAccount: false, managerID: 100, @@ -60,6 +61,7 @@ const transactionItemBaseMock: TransactionListItemType = { category: '', comment: {comment: ''}, created: '2024-12-21', + submitted: '2024-12-21', currency: 'USD', date: '2024-12-21', formattedFrom: 'Admin', @@ -84,6 +86,7 @@ const transactionItemBaseMock: TransactionListItemType = { reportID: reportBaseMock.reportID, shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index b5b16c076022..fd11d68d88f3 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -110,6 +110,7 @@ const report2 = { action: 'view', chatReportID: '1706144653204915', created: '2024-12-21 13:05:20', + submitted: '2024-12-21 13:05:20', currency: 'USD', isOneTransactionReport: true, isWaitingOnBankAccount: false, @@ -132,6 +133,7 @@ const report3 = { chatReportID: '6155022250251839', chatType: undefined, created: '2025-03-05 16:34:27', + submitted: '2025-03-05', currency: 'VND', isOneTransactionReport: false, isOwnPolicyExpenseChat: false, @@ -764,6 +766,7 @@ const transactionsListItems = [ category: '', comment: {comment: ''}, created: '2024-12-21', + submitted: undefined, currency: 'USD', date: '2024-12-21', formattedFrom: 'Admin', @@ -788,6 +791,7 @@ const transactionsListItems = [ reportID: '123456789', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -815,6 +819,7 @@ const transactionsListItems = [ category: '', comment: {comment: ''}, created: '2024-12-21', + submitted: '2024-12-21 13:05:20', currency: 'USD', date: '2024-12-21', formattedFrom: 'Admin', @@ -838,6 +843,7 @@ const transactionsListItems = [ reportID: '11111', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -876,6 +882,7 @@ const transactionsListItems = [ category: '', comment: {comment: ''}, created: '2025-03-05', + submitted: '2025-03-05', currency: 'VND', hasEReceipt: false, merchant: '(none)', @@ -906,6 +913,7 @@ const transactionsListItems = [ date: '2025-03-05', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, keyForList: '3', isAmountColumnWide: false, isTaxAmountColumnWide: false, @@ -932,6 +940,7 @@ const transactionsListItems = [ category: '', comment: {comment: ''}, created: '2025-03-05', + submitted: '2025-03-05', currency: 'VND', hasEReceipt: false, merchant: '(none)', @@ -962,6 +971,7 @@ const transactionsListItems = [ date: '2025-03-05', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, keyForList: '4', isAmountColumnWide: false, isTaxAmountColumnWide: false, @@ -1006,6 +1016,7 @@ const transactionReportGroupListItems = [ reportID: '123456789', reportName: 'Expense Report #123', shouldShowYear: true, + shouldShowYearSubmitted: true, stateNum: 0, statusNum: 0, to: emptyPersonalDetails, @@ -1049,6 +1060,7 @@ const transactionReportGroupListItems = [ reportID: '123456789', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -1074,6 +1086,7 @@ const transactionReportGroupListItems = [ allActions: ['approve'], chatReportID: '1706144653204915', created: '2024-12-21 13:05:20', + submitted: '2024-12-21 13:05:20', currency: 'USD', formattedFrom: 'Admin', formattedStatus: 'Outstanding', @@ -1095,6 +1108,7 @@ const transactionReportGroupListItems = [ reportID: '11111', reportName: 'Expense Report #123', shouldShowYear: true, + shouldShowYearSubmitted: true, stateNum: 1, statusNum: 1, to: { @@ -1148,6 +1162,7 @@ const transactionReportGroupListItems = [ reportID: '11111', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: true, isAmountColumnWide: false, isTaxAmountColumnWide: false, tag: '', @@ -1177,6 +1192,7 @@ const transactionReportGroupListItems = [ chatReportID: '6155022250251839', chatType: undefined, created: '2025-03-05 16:34:27', + submitted: '2025-03-05', currency: 'VND', formattedFrom: 'Admin', formattedStatus: 'Outstanding', @@ -1194,6 +1210,7 @@ const transactionReportGroupListItems = [ reportID: '99999', reportName: 'Approver owes ₫44.00', shouldShowYear: true, + shouldShowYearSubmitted: true, stateNum: 1, statusNum: 1, total: 4400, @@ -1215,7 +1232,122 @@ const transactionReportGroupListItems = [ displayName: 'Approver', login: 'approver@policy.com', }, - transactions: [transactionsListItems.at(2), transactionsListItems.at(3)], + transactions: [ + { + amount: 1200, + action: 'view', + allActions: ['view'], + report: report3, + policy, + reportAction: reportAction3, + holdReportAction: undefined, + cardID: undefined, + cardName: undefined, + category: '', + comment: {comment: ''}, + created: '2025-03-05', + currency: 'VND', + hasEReceipt: false, + merchant: '(none)', + modifiedAmount: 0, + modifiedCreated: '', + modifiedCurrency: '', + modifiedMerchant: '', + parentTransactionID: '', + reportID: '99999', + tag: '', + transactionID: '3', + from: { + accountID: 18439984, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + displayName: 'Admin', + login: 'admin@policy.com', + }, + to: { + accountID: 1111111, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + displayName: 'Approver', + login: 'approver@policy.com', + }, + formattedFrom: 'Admin', + formattedTo: 'Approver', + formattedTotal: 1200, + formattedMerchant: '', + date: '2025-03-05', + shouldShowMerchant: true, + shouldShowYear: true, + shouldShowYearSubmitted: true, + keyForList: '3', + isAmountColumnWide: false, + isTaxAmountColumnWide: false, + receipt: undefined, + taxAmount: undefined, + mccGroup: undefined, + modifiedMCCGroup: undefined, + pendingAction: undefined, + errors: undefined, + violations: [], + groupAmount: -5000, + groupCurrency: 'USD', + }, + { + amount: 3200, + action: 'view', + allActions: ['view'], + report: report3, + policy, + reportAction: reportAction4, + holdReportAction: undefined, + cardID: undefined, + cardName: undefined, + category: '', + comment: {comment: ''}, + created: '2025-03-05', + currency: 'VND', + hasEReceipt: false, + merchant: '(none)', + modifiedAmount: 0, + modifiedCreated: '', + modifiedCurrency: '', + modifiedMerchant: '', + parentTransactionID: '', + reportID: '99999', + tag: '', + transactionID: '4', + from: { + accountID: 18439984, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + displayName: 'Admin', + login: 'admin@policy.com', + }, + to: { + accountID: 1111111, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + displayName: 'Approver', + login: 'approver@policy.com', + }, + formattedFrom: 'Admin', + formattedTo: 'Approver', + formattedTotal: 3200, + formattedMerchant: '', + date: '2025-03-05', + shouldShowMerchant: true, + shouldShowYear: true, + shouldShowYearSubmitted: true, + keyForList: '4', + isAmountColumnWide: false, + isTaxAmountColumnWide: false, + receipt: undefined, + taxAmount: undefined, + mccGroup: undefined, + modifiedMCCGroup: undefined, + pendingAction: undefined, + errors: undefined, + violations: [], + groupAmount: -5000, + groupCurrency: 'USD', + }, + ], }, { groupedBy: 'expense-report', @@ -1245,6 +1377,7 @@ const transactionReportGroupListItems = [ reportID: reportID5, reportName: 'Expense Report #123', shouldShowYear: true, + shouldShowYearSubmitted: true, stateNum: 0, statusNum: 0, to: emptyPersonalDetails, @@ -1696,7 +1829,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 45; + const expectedPropertyCount = 47; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); @@ -1729,7 +1862,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 45; + const expectedPropertyCount = 46; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); @@ -2570,6 +2703,7 @@ describe('SearchUIUtils', () => { expect(SearchUIUtils.getColumnsToShow(1, [], [], false, CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT)).toEqual({ [CONST.SEARCH.TABLE_COLUMNS.AVATAR]: true, [CONST.SEARCH.TABLE_COLUMNS.DATE]: true, + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: false, [CONST.SEARCH.TABLE_COLUMNS.STATUS]: true, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: true, [CONST.SEARCH.TABLE_COLUMNS.FROM]: true, @@ -2586,6 +2720,7 @@ describe('SearchUIUtils', () => { // Avatar should always be visible [CONST.SEARCH.TABLE_COLUMNS.AVATAR]: true, [CONST.SEARCH.TABLE_COLUMNS.DATE]: true, + [CONST.SEARCH.TABLE_COLUMNS.SUBMITTED]: false, [CONST.SEARCH.TABLE_COLUMNS.STATUS]: true, [CONST.SEARCH.TABLE_COLUMNS.TITLE]: true, [CONST.SEARCH.TABLE_COLUMNS.FROM]: false, diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index 5bb137e4228e..6adf6279af0b 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -16,6 +16,7 @@ const mockReportItemWithHold = { allActions: ['approve'], chatReportID: '2108006919825366', created: '2024-12-04 23:18:33', + submitted: '2024-12-04', currency: 'USD', isOneTransactionReport: false, isPolicyExpenseChat: false, @@ -64,6 +65,7 @@ const mockReportItemWithHold = { validated: false, }, shouldShowYear: false, + shouldShowYearSubmitted: false, transactions: [ { report: { @@ -107,6 +109,7 @@ const mockReportItemWithHold = { modifiedCurrency: '', modifiedMerchant: '', parentTransactionID: '', + submitted: '2024-12-04', policyID: '48D7178DE42EE9F9', reportID: '1350959062018695', reportType: 'expense', @@ -150,6 +153,7 @@ const mockReportItemWithHold = { date: '2024-12-04', shouldShowMerchant: true, shouldShowYear: false, + shouldShowYearSubmitted: false, keyForList: '1049531721038862176', isAmountColumnWide: false, isTaxAmountColumnWide: false, @@ -185,6 +189,7 @@ const mockReportItemWithHold = { comment: '', }, created: '2024-12-04', + submitted: '2024-12-04', currency: 'USD', hasEReceipt: false, merchant: 'Forbes', @@ -215,6 +220,7 @@ const mockReportItemWithHold = { date: '2024-12-04', shouldShowMerchant: true, shouldShowYear: false, + shouldShowYearSubmitted: false, keyForList: '5345995386715609966', isAmountColumnWide: false, isTaxAmountColumnWide: false, diff --git a/tests/unit/TransactionGroupListItemTest.tsx b/tests/unit/TransactionGroupListItemTest.tsx index 05cc0ab57376..6e951836b48b 100644 --- a/tests/unit/TransactionGroupListItemTest.tsx +++ b/tests/unit/TransactionGroupListItemTest.tsx @@ -30,6 +30,7 @@ const mockTransaction: TransactionListItemType = { groupAmount: 1284, groupCurrency: 'USD', created: '2025-09-19', + submitted: '2025-09-19', currency: 'USD', policy: { id: '06F34677820A4D07', @@ -64,6 +65,7 @@ const mockTransaction: TransactionListItemType = { date: '2025-09-07', shouldShowMerchant: true, shouldShowYear: true, + shouldShowYearSubmitted: false, keyForList: '1', isAmountColumnWide: false, isTaxAmountColumnWide: false, @@ -88,6 +90,7 @@ const mockReport: TransactionReportGroupListItemType = { chatReportID: '4735435600700077', chatType: undefined, created: '2025-09-19 20:00:47', + submitted: '2025-09-19', currency: 'USD', isOneTransactionReport: true, isOwnPolicyExpenseChat: false, @@ -117,6 +120,7 @@ const mockReport: TransactionReportGroupListItemType = { displayName: 'Main Applause QA', }, shouldShowYear: false, + shouldShowYearSubmitted: false, action: 'view', transactions: [], groupedBy: 'expense-report',