diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5b296a00bb193..e3deb23c7d9db 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13058,6 +13058,32 @@ function getReportFieldMaps(report: OnyxEntry, fieldList: Record, policy: OnyxEntry): boolean { + if (!report || !policy?.fieldList || !policy?.areReportFieldsEnabled) { + return false; + } + + if (!isPaidGroupPolicyExpenseReport(report) && !isInvoiceReport(report)) { + return false; + } + + const reportViolations = allReportsViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`]; + const {fieldsByName} = getReportFieldMaps(report, policy.fieldList); + + return Object.values(fieldsByName).some((field) => { + if (field.target !== report.type) { + return false; + } + if (shouldHideSingleReportField(field)) { + return false; + } + if (isReportFieldDisabledForUser(report, field, policy)) { + return false; + } + return !!getFieldViolation(reportViolations, field); + }); +} + export { areAllRequestsBeingSmartScanned, buildOptimisticAddCommentReportAction, @@ -13243,6 +13269,7 @@ export { hasSmartscanError, hasUpdatedTotal, hasViolations, + hasVisibleReportFieldViolations, hasWarningTypeViolations, hasNoticeTypeViolations, hasAnyViolations, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 7be982ceb0080..9a5f880cde1d8 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -1,6 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import {computeReportName} from '@libs/ReportNameUtils'; -import {generateIsEmptyReport, generateReportAttributes, isArchivedReport, isValidReport} from '@libs/ReportUtils'; +import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isValidReport} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils'; @@ -199,9 +199,23 @@ export default createOnyxDerivedValueConfig({ isReportArchived, }); + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const hasFieldViolations = hasVisibleReportFieldViolations(report, policy); + let brickRoadStatus; // if report has errors or violations, show red dot - if (SidebarUtils.shouldShowRedBrickRoad(report, chatReport, reportActionsList, hasAnyViolations, reportErrors, transactions, transactionViolations, !!isReportArchived)) { + if ( + SidebarUtils.shouldShowRedBrickRoad( + report, + chatReport, + reportActionsList, + hasAnyViolations || hasFieldViolations, + reportErrors, + transactions, + transactionViolations, + !!isReportArchived, + ) + ) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } // if report does not have error, check if it should show green dot diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 081c6df5e3b42..8ea6ebe2a7626 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -97,6 +97,7 @@ import { hasActionWithErrorsForTransaction, hasEmptyReportsForPolicy, hasReceiptError, + hasVisibleReportFieldViolations, isAllowedToApproveExpenseReport, isArchivedNonExpenseReport, isArchivedReport, @@ -13695,6 +13696,86 @@ describe('ReportUtils', () => { await Onyx.clear(); }); }); + + describe('hasVisibleReportFieldViolations', () => { + const policyID = 'policy-field-violations'; + + const baseField: PolicyReportField = { + name: 'project', + fieldID: 'project_field', + defaultValue: '', + orderWeight: 1, + type: 'text', + deletable: true, + target: CONST.REPORT.TYPE.EXPENSE, + values: [], + keys: [], + externalIDs: [], + disabledOptions: [], + isTax: false, + }; + + const basePolicy = { + ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + areReportFieldsEnabled: true, + fieldList: { + [`expensify_${baseField.fieldID}`]: baseField, + }, + }; + + const expenseReport: Report = { + reportID: 'report-field-violations', + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + ownerAccountID: currentUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + beforeEach(async () => { + await Onyx.clear(); + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + }); + + it('should return false when policy does not have areReportFieldsEnabled enabled', () => { + const policyWithFieldsDisabled = {...basePolicy, areReportFieldsEnabled: false}; + + expect(hasVisibleReportFieldViolations(expenseReport, policyWithFieldsDisabled)).toBe(false); + }); + + it('should return false when the report is not an expense report or invoice report', () => { + const chatReport: Report = { + reportID: 'chat-report-field-violations', + type: CONST.REPORT.TYPE.CHAT, + policyID, + }; + + expect(hasVisibleReportFieldViolations(chatReport, basePolicy)).toBe(false); + }); + + it('should return true when expense report has a required field with no value', async () => { + const fieldWithNoValue: PolicyReportField = { + ...baseField, + value: null, + defaultValue: '', + }; + + const policyWithEmptyField = { + ...basePolicy, + fieldList: {[`expensify_${fieldWithNoValue.fieldID}`]: fieldWithNoValue}, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policyWithEmptyField); + await waitForBatchedUpdates(); + + expect(hasVisibleReportFieldViolations(expenseReport, policyWithEmptyField)).toBe(true); + }); + }); + describe('getAddExpenseDropdownOptions', () => { const mockTranslate: LocaleContextProps['translate'] = (path, ...params) => translate(CONST.LOCALES.EN, path, ...params); const mockIcons = {Location: jest.fn(), ReceiptPlus: jest.fn()} as unknown as Record<'Location' | 'ReceiptPlus', IconAsset>;