diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index 150d3393da45..d328b2b08a9e 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -16,6 +16,7 @@ type SearchTableHeaderProps = { sortOrder?: SortOrder; onSortPress: (column: SearchSortBy, order: SortOrder) => void; dateColumnSize: TableColumnSize; + postedColumnSize: TableColumnSize; amountColumnSize: TableColumnSize; taxAmountColumnSize: TableColumnSize; shouldShowSorting: boolean; @@ -27,6 +28,7 @@ function MoneyRequestReportTableHeader({ sortOrder, onSortPress, dateColumnSize, + postedColumnSize, shouldShowSorting, columns, amountColumnSize, @@ -86,6 +88,7 @@ function MoneyRequestReportTableHeader({ columns={orderedColumnConfig} shouldShowColumn={shouldShowColumn} dateColumnSize={dateColumnSize} + postedColumnSize={postedColumnSize} amountColumnSize={amountColumnSize} taxAmountColumnSize={taxAmountColumnSize} shouldShowSorting={shouldShowSorting} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx index 293b00073ffd..9b1830cad118 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx @@ -68,6 +68,9 @@ type MoneyRequestReportTransactionItemProps = { /** The size of the date column */ dateColumnSize: TableColumnSize; + /** The size of the posted column */ + postedColumnSize: TableColumnSize; + /** The size of the amount column */ amountColumnSize: TableColumnSize; @@ -122,6 +125,7 @@ function MoneyRequestReportTransactionItemBody({ handleLongPress, columns, dateColumnSize, + postedColumnSize, amountColumnSize, taxAmountColumnSize, scrollToNewTransaction, @@ -236,6 +240,7 @@ function MoneyRequestReportTransactionItemBody({ transactionThreadReportID={transactionThreadReportID} isSelected={isSelected} dateColumnSize={dateColumnSize} + postedColumnSize={postedColumnSize} amountColumnSize={amountColumnSize} taxAmountColumnSize={taxAmountColumnSize} shouldShowTooltip diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index db7c07293d98..1409953908fc 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -58,6 +58,7 @@ import {compareValues, getColumnsToShow, getTableMinWidth, hasFlexColumn, isTran import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction'; import {transactionHasRBR} from '@libs/TransactionPreviewUtils'; import {getTransactionPendingAction, getVisibleTransactionViolations, isTransactionPendingDelete, shouldShowExpenseBreakdown} from '@libs/TransactionUtils'; +import shouldShowTransactionPostedYear from '@libs/TransactionUtils/shouldShowTransactionPostedYear'; import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear'; import isReportOpenInSuperWideRHP from '@navigation/helpers/isReportOpenInSuperWideRHP'; @@ -571,14 +572,16 @@ function MoneyRequestReportTransactionList({ [navigateToTransactionThread, reportActions, sortedTransactions, report, visualOrderTransactionIDs], ); - const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => { + const {amountColumnSize, dateColumnSize, postedColumnSize, taxAmountColumnSize} = useMemo(() => { const isAmountColumnWide = transactions.some((transaction) => isTransactionAmountTooLong(transaction)); const isTaxAmountColumnWide = transactions.some((transaction) => isTransactionTaxAmountTooLong(transaction)); const shouldShowYearForSomeTransaction = transactions.some((transaction) => shouldShowTransactionYear(transaction)); + const shouldShowPostedYearForSomeTransaction = transactions.some((transaction) => shouldShowTransactionPostedYear(transaction)); return { 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, dateColumnSize: shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, + postedColumnSize: shouldShowPostedYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL, }; }, [transactions]); @@ -722,6 +725,7 @@ function MoneyRequestReportTransactionList({ handleOnPress={handleOnPress} handleLongPress={handleLongPress} dateColumnSize={dateColumnSize} + postedColumnSize={postedColumnSize} amountColumnSize={amountColumnSize} taxAmountColumnSize={taxAmountColumnSize} scrollToNewTransaction={transaction.transactionID === newTransactions?.at(0)?.transactionID ? scrollToNewTransaction : undefined} @@ -831,6 +835,7 @@ function MoneyRequestReportTransactionList({ shouldRemoveTotalColumnFlex={hasFlexColumn(columnsToShow)} columns={columnsToShow} dateColumnSize={dateColumnSize} + postedColumnSize={postedColumnSize} amountColumnSize={amountColumnSize} taxAmountColumnSize={taxAmountColumnSize} onSortPress={(selectedSortBy, selectedSortOrder) => { diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index c4b8ab123a44..0773606c85d3 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -40,6 +40,7 @@ import { isScanning, isTimeRequest, } from '@libs/TransactionUtils'; +import getFormattedPostedDate from '@libs/TransactionUtils/getFormattedPostedDate'; import variables from '@styles/variables'; @@ -248,7 +249,7 @@ function TransactionItemRowWide({ style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.POSTED, {isPostedColumnWide})]} > diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 4662b5aae58f..67c2077dfef0 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -218,6 +218,8 @@ import { shouldShowAttendees as shouldShowAttendeesUtils, shouldShowViolation, } from './TransactionUtils'; +import getFormattedPostedDate from './TransactionUtils/getFormattedPostedDate'; +import shouldShowTransactionPostedYear from './TransactionUtils/shouldShowTransactionPostedYear'; import {isInvalidMerchantValue} from './ValidationUtils'; type ColumnSortMapping = Partial>; @@ -1216,8 +1218,7 @@ function getTransactionItemCommonFormattedProperties( const submitted = report?.submitted; const approved = report?.approved; - // Posted date is in the YYYYMMDD format, so we format it to YYYY-MM-DD here since JS's Date constructor interprets it as an invalid date. - const posted = !transactionItem?.posted ? '' : `${transactionItem?.posted.slice(0, 4)}-${transactionItem?.posted.slice(4, 6)}-${transactionItem?.posted.slice(6, 8)}`; + const posted = getFormattedPostedDate(transactionItem?.posted); return { formattedFrom, @@ -1526,11 +1527,9 @@ function shouldShowYear( result.shouldShowYearApproved = true; } - // Posted date is in the YYYYMMDD format, so we extract the year manually here since JS's Date constructor interprets it as an invalid date. // Latch once true so a later current-year date can't reset it (the row renderer latches the same way). - if (!result.shouldShowYearPosted && item?.posted) { - const postedYear = parseInt(item.posted.slice(0, 4), 10); - result.shouldShowYearPosted = postedYear !== currentYear; + if (!result.shouldShowYearPosted && shouldShowTransactionPostedYear(item)) { + result.shouldShowYearPosted = true; } } @@ -1559,11 +1558,9 @@ function shouldShowYear( result.shouldShowYearApproved = true; } - // Posted date is in the YYYYMMDD format, so we extract the year manually here since JS's Date constructor interprets it as an invalid date. // Latch once true so a later current-year date can't reset it (the row renderer latches the same way). - if (!result.shouldShowYearPosted && item?.posted) { - const postedYear = parseInt(item.posted.slice(0, 4), 10); - result.shouldShowYearPosted = postedYear !== currentYear; + if (!result.shouldShowYearPosted && shouldShowTransactionPostedYear(item)) { + result.shouldShowYearPosted = true; } const exportedAction = item.reportID ? lastExportedActionByReportID.get(item.reportID) : undefined; @@ -1760,7 +1757,6 @@ type PreprocessingContext = { shouldShowAmountInWideColumn: boolean; shouldShowTaxAmountInWideColumn: boolean; hasDeletedTransaction: boolean; - currentYear: number; }; function createPreprocessingContext(): PreprocessingContext { @@ -1787,7 +1783,6 @@ function createPreprocessingContext(): PreprocessingContext { shouldShowAmountInWideColumn: false, shouldShowTaxAmountInWideColumn: false, hasDeletedTransaction: false, - currentYear: new Date().getFullYear(), }; } @@ -1905,11 +1900,8 @@ function processTransactionEntry(ctx: PreprocessingContext, transaction: OnyxTyp ctx.shouldShowYearApproved = true; } - if (!ctx.shouldShowYearPosted && transaction?.posted) { - const postedYear = parseInt(transaction.posted.slice(0, 4), 10); - if (postedYear !== ctx.currentYear) { - ctx.shouldShowYearPosted = true; - } + if (!ctx.shouldShowYearPosted && shouldShowTransactionPostedYear(transaction)) { + ctx.shouldShowYearPosted = true; } // Optimistic: works when report-action keys precede transaction keys (common case). @@ -1980,7 +1972,7 @@ function resolveExportedYearFlags(ctx: PreprocessingContext, data: OnyxTypes.Sea * (key classification, violations, export/action maps, year flags, column indicators) * needed by getTransactionsSections and getReportSections. */ -function classifyAndPreprocess(data: OnyxTypes.SearchResults['data']): Omit { +function classifyAndPreprocess(data: OnyxTypes.SearchResults['data']): Omit { const ctx = createPreprocessingContext(); for (const key of Object.keys(data)) { diff --git a/src/libs/TransactionUtils/getFormattedPostedDate.ts b/src/libs/TransactionUtils/getFormattedPostedDate.ts new file mode 100644 index 000000000000..39622d896d75 --- /dev/null +++ b/src/libs/TransactionUtils/getFormattedPostedDate.ts @@ -0,0 +1,11 @@ +function getFormattedPostedDate(posted?: string): string { + if (!posted) { + return ''; + } + if (!/^\d{8}$/.test(posted)) { + return posted; + } + return `${posted.slice(0, 4)}-${posted.slice(4, 6)}-${posted.slice(6, 8)}`; +} + +export default getFormattedPostedDate; diff --git a/src/libs/TransactionUtils/shouldShowTransactionPostedYear.ts b/src/libs/TransactionUtils/shouldShowTransactionPostedYear.ts new file mode 100644 index 000000000000..a9b2ac7f15b8 --- /dev/null +++ b/src/libs/TransactionUtils/shouldShowTransactionPostedYear.ts @@ -0,0 +1,15 @@ +import DateUtils from '@libs/DateUtils'; + +import type {OnyxInputOrEntry, Transaction} from '@src/types/onyx'; + +import getFormattedPostedDate from './getFormattedPostedDate'; + +function shouldShowTransactionPostedYear(transaction: OnyxInputOrEntry) { + const posted = transaction?.posted; + if (!posted) { + return false; + } + return DateUtils.doesDateBelongToAPastYear(getFormattedPostedDate(posted)); +} + +export default shouldShowTransactionPostedYear; diff --git a/tests/unit/getFormattedPostedDateTest.ts b/tests/unit/getFormattedPostedDateTest.ts new file mode 100644 index 000000000000..1323a35bef21 --- /dev/null +++ b/tests/unit/getFormattedPostedDateTest.ts @@ -0,0 +1,22 @@ +import getFormattedPostedDate from '@libs/TransactionUtils/getFormattedPostedDate'; + +describe('getFormattedPostedDate', () => { + it('converts a raw YYYYMMDD card posted date to YYYY-MM-DD', () => { + expect(getFormattedPostedDate('20260710')).toBe('2026-07-10'); + expect(getFormattedPostedDate('20251231')).toBe('2025-12-31'); + }); + + it('passes an already-ISO date through unchanged (idempotent)', () => { + expect(getFormattedPostedDate('2026-07-10')).toBe('2026-07-10'); + }); + + it('returns an empty string for empty/undefined input', () => { + expect(getFormattedPostedDate(undefined)).toBe(''); + expect(getFormattedPostedDate('')).toBe(''); + }); + + it('passes any non-YYYYMMDD string through unchanged', () => { + expect(getFormattedPostedDate('2026-7-1')).toBe('2026-7-1'); + expect(getFormattedPostedDate('not-a-date')).toBe('not-a-date'); + }); +}); diff --git a/tests/unit/shouldShowTransactionPostedYearTest.ts b/tests/unit/shouldShowTransactionPostedYearTest.ts new file mode 100644 index 000000000000..67c4349717d4 --- /dev/null +++ b/tests/unit/shouldShowTransactionPostedYearTest.ts @@ -0,0 +1,26 @@ +import shouldShowTransactionPostedYear from '@libs/TransactionUtils/shouldShowTransactionPostedYear'; + +import createRandomTransaction from '../utils/collections/transaction'; + +const currentYear = new Date().getFullYear(); +const transactionWithPosted = (posted?: string) => ({...createRandomTransaction(0), posted}); + +describe('shouldShowTransactionPostedYear', () => { + it('returns true when the raw YYYYMMDD posted date is in a past year', () => { + expect(shouldShowTransactionPostedYear(transactionWithPosted(`${currentYear - 1}0701`))).toBe(true); + }); + + it('returns false when the raw YYYYMMDD posted date is in the current year', () => { + expect(shouldShowTransactionPostedYear(transactionWithPosted(`${currentYear}0701`))).toBe(false); + }); + + it('returns false when there is no posted date', () => { + expect(shouldShowTransactionPostedYear(transactionWithPosted(undefined))).toBe(false); + expect(shouldShowTransactionPostedYear(transactionWithPosted(''))).toBe(false); + }); + + it('returns false for a nullish transaction', () => { + expect(shouldShowTransactionPostedYear(undefined)).toBe(false); + expect(shouldShowTransactionPostedYear(null)).toBe(false); + }); +});