diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index 8223449df6b5..ada56a885554 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -1,4 +1,5 @@ import {isTrackIntentUserSelector} from '@selectors/Onboarding'; +import {transactionViolationsByIDsSelector} from '@selectors/TransactionViolations'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; // We need direct access to useOnyx to fetch live policy data at render time @@ -31,6 +32,7 @@ import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isAttendeeTrackingEnabled} from '@libs/PolicyUtils'; import {getNonHeldAndFullAmount, isInvoiceReport, isOpenExpenseReport, isProcessingReport, isReportPendingDelete, shouldShowMarkAsDone} from '@libs/ReportUtils'; +import {hasVisibleViolations} from '@libs/SearchUIUtils'; import {isOnHold, isViolationDismissed, shouldShowViolation, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -179,8 +181,27 @@ function ExpenseReportListItem({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const {showConfirmModal} = useConfirmModal(); const {showHoldMenu} = useHoldMenuModal(); - const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(reportItem.reportID); + const {transactions: reportTransactions, violations: reportViolations} = useTransactionsAndViolationsForReport(reportItem.reportID); const liveReportTransactions = useMemo(() => Object.values(reportTransactions), [reportTransactions]); + + // Recompute the violations badge from live data at the row, replacing the screen-level + // violations merge that getSections previously did. Policy comes from the live `policyForViolations` + // (parentPolicy ?? snapshot); violations + transactions come from the report's live Onyx data. + const liveHasVisibleViolations = hasVisibleViolations( + reportForViolations, + reportViolations, + currentUserDetails.email ?? '', + currentUserDetails.accountID, + liveReportTransactions, + policyForViolations, + ); + + // Scoped live violations for the report's snapshot transactions: before the report's transactions + // hydrate into the live collection, rule/category changes still push violation updates that must + // reflect on the badge (per-row selector, not the screen-level collection merge this slice removed). + const snapshotTransactionIDs = (reportItem.transactions ?? []).map((transaction) => transaction.transactionID); + const liveViolationsSelector = transactionViolationsByIDsSelector(snapshotTransactionIDs); + const [liveViolationsForSnapshotTransactions] = originalUseOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {selector: liveViolationsSelector}, [liveViolationsSelector]); const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({ reportID: reportItem.reportID, chatReportPolicyID: chatReport?.policyID, @@ -308,11 +329,25 @@ function ExpenseReportListItem({ const shouldShowViolationDescription = isOpenExpenseReport(reportItem) || isProcessingReport(reportItem); // Show violation description if either: - // 1. Pre-computed hasVisibleViolations from search data, OR + // 1. Visible violations recomputed from the report's live transactions, or, before those hydrate, + // from the snapshot transactions joined with the scoped live violations (so a rule change updates + // the badge even for reports never opened this session); the snapshot's pre-computed value only + // covers the window before the violations subscription loads, OR // 2. Synced missingAttendees violation computed at render time (for stale data) // We're using || instead of ?? because the variables are boolean - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const hasAnyVisibleViolations = reportItem?.hasVisibleViolations || hasSyncedMissingAttendeesViolation; + const hasLiveTransactions = liveReportTransactions.length > 0; + const fallbackHasVisibleViolations = liveViolationsForSnapshotTransactions + ? hasVisibleViolations( + reportForViolations, + liveViolationsForSnapshotTransactions, + currentUserDetails.email ?? '', + currentUserDetails.accountID, + reportItem.transactions ?? [], + policyForViolations, + ) + : !!reportItem.hasVisibleViolations; + const hasVisibleReportViolations = hasLiveTransactions ? liveHasVisibleViolations : fallbackHasVisibleViolations; + const hasAnyVisibleViolations = hasVisibleReportViolations || hasSyncedMissingAttendeesViolation; const getDescription = useMemo(() => { if (reportItem?.isRejectedReport) { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index b974e2ffb743..89af401aac84 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -341,8 +341,6 @@ function Search({ const previousTransactions = usePrevious(transactions); const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); - const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const {accountID, email, login} = useCurrentUserPersonalDetails(); const selfDMReport = useSelfDMReport(); @@ -591,7 +589,6 @@ function Search({ const [filteredData1, allLength, hasDeletedTransactionFromSections] = getSections({ type, data: searchDataWithOptimisticTransaction, - policies, currentAccountID: accountID, currentUserEmail: email ?? '', translate, @@ -607,7 +604,6 @@ function Search({ cardList: personalAndWorkspaceCards, nonPersonalAndWorkspaceCardList: nonPersonalAndWorkspaceCards, isOffline, - allTransactionViolations: violations, customCardNames, conciergeReportID, onyxPersonalDetailsList, @@ -642,9 +638,7 @@ function Search({ cardFeeds, personalAndWorkspaceCards, nonPersonalAndWorkspaceCards, - policies, bankAccountList, - violations, customCardNames, conciergeReportID, onyxPersonalDetailsList, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index dd6f8082280e..a3d7e9469693 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -236,7 +236,6 @@ type TransactionQuarterGroupSorting = ColumnSortMapping; currentSearch: SearchKey; currentAccountID: number; currentUserEmail: string; @@ -245,7 +244,6 @@ type GetReportSectionsParams = { convertToDisplayString: CurrencyListActionsContextType['convertToDisplayString']; isActionLoadingSet: ReadonlySet | undefined; isOffline: boolean | undefined; - allTransactionViolations: OnyxCollection; bankAccountList: OnyxEntry; reportActions?: Record; queryJSON?: SearchQueryJSON; @@ -587,7 +585,6 @@ type GetSectionsResult = [ type GetSectionsParams = { type: SearchDataTypes; data: OnyxTypes.SearchResults['data']; - policies?: OnyxCollection; currentAccountID: number; currentUserEmail: string; translate: LocalizedTranslate; @@ -605,7 +602,6 @@ type GetSectionsParams = { cardList?: OnyxEntry; nonPersonalAndWorkspaceCardList?: OnyxEntry; customCardNames?: Record; - allTransactionViolations?: OnyxCollection; visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue; conciergeReportID: string | undefined; onyxPersonalDetailsList?: OnyxTypes.PersonalDetailsList; @@ -2736,7 +2732,6 @@ function getReportActionsSections( */ function getReportSections({ data, - policies, currentSearch, currentAccountID, currentUserEmail, @@ -2744,7 +2739,6 @@ function getReportSections({ isOffline, formatPhoneNumber, isActionLoadingSet, - allTransactionViolations, bankAccountList, reportActions = {}, queryJSON, @@ -2827,14 +2821,13 @@ function getReportSections({ const formattedTo = !shouldShowBlankTo ? formatPhoneNumber(getDisplayNameOrDefault(toDetails)) : ''; const formattedStatus = getReportStatusTranslation({stateNum: reportItem.stateNum, statusNum: reportItem.statusNum, translate}); - const policyFromKey = getPolicyFromKey(data, reportItem); - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem?.policyID ?? String(CONST.DEFAULT_NUMBER_ID)}`] ?? policyFromKey; + const policy = getPolicyFromKey(data, reportItem); const shouldShowStatusAsPending = !!isOffline && reportItem?.pendingFields?.nextStep === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; const hasVisibleViolationsForReport = hasVisibleViolations( reportItem, - allTransactionViolations ?? allViolations, + allViolations, currentUserEmail, currentAccountID ?? CONST.DEFAULT_NUMBER_ID, allReportTransactions, @@ -3582,7 +3575,6 @@ function getListItem(type: SearchDataTypes, status: SearchStatus, groupBy?: Sear function getSections({ type, data, - policies, currentAccountID, currentUserEmail, translate, @@ -3599,7 +3591,6 @@ function getSections({ cardList, nonPersonalAndWorkspaceCardList, customCardNames, - allTransactionViolations, visibleReportActionsData, conciergeReportID, onyxPersonalDetailsList, @@ -3618,7 +3609,6 @@ function getSections({ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) { return getReportSections({ data, - policies, currentSearch, currentAccountID, currentUserEmail, @@ -3626,7 +3616,6 @@ function getSections({ isOffline, formatPhoneNumber, isActionLoadingSet, - allTransactionViolations, bankAccountList, reportActions, queryJSON, @@ -6284,6 +6273,7 @@ export { shouldShowDeleteOption, getToFieldValueForTransaction, getSearchReportAvatarProps, + hasVisibleViolations, isTodoSearch, getActiveGroupSearchHashes, getSelectedGroupFilterEntry,