Skip to content

Commit 6a38bfc

Browse files
authored
Merge pull request #86477 from abelhailefen/fix-85553-rbr-carousel-sorting
Fix 85553 rbr carousel sorting
2 parents 927ad1c + 5196596 commit 6a38bfc

4 files changed

Lines changed: 356 additions & 6 deletions

File tree

src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
import type {SortableColumnName} from '@libs/ReportUtils';
6363
import {compareValues, getColumnsToShow, getTableMinWidth, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils';
6464
import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
65+
import {compareByRBR} from '@libs/TransactionPreviewUtils';
6566
import {getTransactionPendingAction, isTransactionPendingDelete, shouldShowExpenseBreakdown} from '@libs/TransactionUtils';
6667
import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear';
6768
import isReportOpenInSuperWideRHP from '@navigation/helpers/isReportOpenInSuperWideRHP';
@@ -181,6 +182,7 @@ function MoneyRequestReportTransactionList({
181182
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST);
182183
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
183184
const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});
185+
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
184186

185187
const shouldShowGroupedTransactions = isExpenseReport(report) && !isIOUReport(report);
186188

@@ -286,12 +288,32 @@ function MoneyRequestReportTransactionList({
286288
});
287289

288290
const {sortBy, sortOrder} = sortConfig;
291+
const isDefaultSort = sortBy === CONST.SEARCH.TABLE_COLUMNS.DATE && sortOrder === CONST.SEARCH.SORT_ORDER.ASC;
292+
293+
// Convert reportActions array to a record keyed by reportActionID for compareByRBR
294+
const reportActionsMap = useMemo(() => Object.fromEntries(reportActions.map((ra) => [ra.reportActionID, ra])), [reportActions]);
289295

290296
const sortedTransactions: TransactionWithOptionalHighlight[] = useMemo(() => {
291-
return [...transactions].sort((a, b) =>
292-
compareValues(getTransactionSortValue(a, sortBy, report, policy), getTransactionSortValue(b, sortBy, report, policy), sortOrder, sortBy, localeCompare, true),
293-
);
294-
}, [sortBy, sortOrder, transactions, localeCompare, report, policy]);
297+
return [...transactions].sort((a, b) => {
298+
// When on default sort (Date/ASC), prioritize RBR-flagged transactions
299+
if (isDefaultSort && allTransactionViolations) {
300+
const rbrComparison = compareByRBR(
301+
a,
302+
b,
303+
allTransactionViolations,
304+
currentUserDetails?.login ?? '',
305+
currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID,
306+
report,
307+
policy,
308+
reportActionsMap,
309+
);
310+
if (rbrComparison !== 0) {
311+
return rbrComparison;
312+
}
313+
}
314+
return compareValues(getTransactionSortValue(a, sortBy, report, policy), getTransactionSortValue(b, sortBy, report, policy), sortOrder, sortBy, localeCompare, true);
315+
});
316+
}, [sortBy, sortOrder, transactions, localeCompare, report, policy, isDefaultSort, allTransactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, reportActionsMap]);
295317

296318
const resolvedTransactions = useMemo(() => resolveTransactionCardFields(sortedTransactions, cardList, translate), [sortedTransactions, cardList, translate]);
297319

src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {showContextMenuForReport} from '@components/ShowContextMenuContext';
1919
import StatusBadge from '@components/StatusBadge';
2020
import Text from '@components/Text';
2121
import {useCurrencyListActions} from '@hooks/useCurrencyList';
22+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
2223
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
2324
import useLocalize from '@hooks/useLocalize';
2425
import useOnyx from '@hooks/useOnyx';
@@ -55,6 +56,7 @@ import shouldAdjustScroll from '@libs/shouldAdjustScroll';
5556
import {startSpan} from '@libs/telemetry/activeSpans';
5657
import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
5758
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
59+
import {compareByRBR} from '@libs/TransactionPreviewUtils';
5860
import {hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';
5961
import colors from '@styles/theme/colors';
6062
import variables from '@styles/variables';
@@ -132,6 +134,7 @@ function MoneyRequestReportPreviewContent({
132134
const shouldShowLoading =
133135
chatReportLoadingState != null && chatReportLoadingState.hasOnceLoadedReportActions !== true && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
134136
// `hasOnceLoadedReportActions` becomes true before transactions populate fully,
137+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
135138
// so we defer the loading state update to ensure transactions are loaded
136139
const shouldShowLoadingDeferred = useDeferredValue(shouldShowLoading);
137140
const lastTransaction = transactions?.at(0);
@@ -158,6 +161,7 @@ function MoneyRequestReportPreviewContent({
158161
const theme = useTheme();
159162
const styles = useThemeStyles();
160163
const StyleUtils = useStyleUtils();
164+
const currentUserDetails = useCurrentUserPersonalDetails();
161165
const {translate, formatPhoneNumber} = useLocalize();
162166
const {convertToDisplayString} = useCurrencyListActions();
163167
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -351,7 +355,15 @@ function MoneyRequestReportPreviewContent({
351355
thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBS_UP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBS_UP_DURATION})) : 1);
352356
}, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
353357

354-
const carouselTransactions = useMemo(() => (shouldShowAccessPlaceHolder ? [] : transactions.slice(0, 11)), [shouldShowAccessPlaceHolder, transactions]);
358+
const carouselTransactions = useMemo(() => {
359+
if (shouldShowAccessPlaceHolder) {
360+
return [];
361+
}
362+
const sorted = [...transactions].sort((a, b) =>
363+
compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy),
364+
);
365+
return sorted.slice(0, 11);
366+
}, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy]);
355367
const prevCarouselTransactionLength = useRef(0);
356368

357369
useEffect(() => {

src/libs/TransactionPreviewUtils.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx';
33
import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList';
44
import CONST from '@src/CONST';
55
import type {TranslationPaths} from '@src/languages/types';
6+
import ONYXKEYS from '@src/ONYXKEYS';
67
import ROUTES from '@src/ROUTES';
78
import type * as OnyxTypes from '@src/types/onyx';
89
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -457,6 +458,108 @@ function createTransactionPreviewConditionals({
457458
};
458459
}
459460

461+
/**
462+
* Lightweight check for whether a transaction has any RBR (Red Brick Road) indicator.
463+
* Evaluates transaction-level signals (violations, hold, missing fields, receipt errors,
464+
* report action errors, and DEW submit failures) with proper context for dismissed
465+
* violations and report settlement/approval status.
466+
*
467+
* This logic mirrors the `shouldShowRBR` computation in `createTransactionPreviewConditionals`.
468+
*/
469+
function transactionHasRBR(
470+
transaction: OnyxEntry<OnyxTypes.Transaction>,
471+
violations: OnyxTypes.TransactionViolations,
472+
currentUserEmail: string,
473+
currentUserAccountID: number,
474+
iouReport: OnyxEntry<OnyxTypes.Report>,
475+
policy: OnyxEntry<OnyxTypes.Policy>,
476+
reportActions?: OnyxTypes.ReportActions,
477+
): boolean {
478+
if (!transaction) {
479+
return false;
480+
}
481+
482+
// Check for non-dismissed violation-type or warning-type violations
483+
if (
484+
hasViolation(transaction, violations, currentUserEmail, currentUserAccountID, iouReport, policy, true) ||
485+
hasWarningTypeViolation(transaction, violations, currentUserEmail, currentUserAccountID, iouReport, policy)
486+
) {
487+
return true;
488+
}
489+
490+
// Check for notice-type violations (only on paid group policies)
491+
if (hasNoticeTypeViolation(transaction, violations, currentUserEmail, currentUserAccountID, iouReport, policy, true) && isPaidGroupPolicyUtil(iouReport)) {
492+
return true;
493+
}
494+
495+
// Check for distance-request modified-amount violations (type VIOLATION or NOTICE)
496+
if (
497+
isDistanceRequest(transaction) &&
498+
violations?.some(
499+
(violation) => violation.name === CONST.VIOLATIONS.MODIFIED_AMOUNT && (violation.type === CONST.VIOLATION_TYPES.VIOLATION || violation.type === CONST.VIOLATION_TYPES.NOTICE),
500+
)
501+
) {
502+
return true;
503+
}
504+
505+
// Check if transaction is on hold — only counts as RBR when the report
506+
// is not fully settled and not fully approved (matching createTransactionPreviewConditionals)
507+
const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial;
508+
const isFullySettled = isSettled(iouReport?.reportID) && !isSettlementOrApprovalPartial;
509+
const isFullyApproved = isReportApproved({report: iouReport}) && !isSettlementOrApprovalPartial;
510+
if (!isFullySettled && !isFullyApproved && isOnHold(transaction)) {
511+
return true;
512+
}
513+
514+
// Check if transaction has missing required fields (uses hasMissingSmartscanFields
515+
// which guards against distance requests and receipts being scanned)
516+
if (hasMissingSmartscanFields(transaction, iouReport)) {
517+
return true;
518+
}
519+
520+
// Check if transaction has receipt error
521+
if (hasReceiptError(transaction)) {
522+
return true;
523+
}
524+
525+
// Check for report action errors associated with this transaction
526+
if (hasActionWithErrorsForTransaction(iouReport?.reportID, transaction, reportActions)) {
527+
return true;
528+
}
529+
530+
// Check for DEW submit failures
531+
if (hasDynamicExternalWorkflow(policy) && !!getMostRecentActiveDEWSubmitFailedAction(reportActions)) {
532+
return true;
533+
}
534+
535+
return false;
536+
}
537+
538+
/**
539+
* Compare two transactions by their RBR (Red Brick Road) status.
540+
* Transactions with RBR indicators are sorted before those without.
541+
* Returns 0 when both transactions have the same RBR status.
542+
*/
543+
function compareByRBR(
544+
a: OnyxTypes.Transaction,
545+
b: OnyxTypes.Transaction,
546+
violations: Record<string, OnyxTypes.TransactionViolations | undefined> | undefined,
547+
currentUserEmail: string,
548+
currentUserAccountID: number,
549+
iouReport: OnyxEntry<OnyxTypes.Report>,
550+
policy: OnyxEntry<OnyxTypes.Policy>,
551+
reportActions?: OnyxTypes.ReportActions,
552+
): number {
553+
const aViolations = violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${a.transactionID}`] ?? [];
554+
const bViolations = violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${b.transactionID}`] ?? [];
555+
const aHasRBR = transactionHasRBR(a, aViolations, currentUserEmail, currentUserAccountID, iouReport, policy, reportActions);
556+
const bHasRBR = transactionHasRBR(b, bViolations, currentUserEmail, currentUserAccountID, iouReport, policy, reportActions);
557+
if (aHasRBR === bHasRBR) {
558+
return 0;
559+
}
560+
return aHasRBR ? -1 : 1;
561+
}
562+
460563
export {
461564
getReviewNavigationRoute,
462565
getIOUPayerAndReceiver,
@@ -465,5 +568,7 @@ export {
465568
getViolationTranslatePath,
466569
getUniqueActionErrorsForTransaction,
467570
formatLastFourPAN,
571+
transactionHasRBR,
572+
compareByRBR,
468573
};
469574
export type {TranslationPathOrText};

0 commit comments

Comments
 (0)