diff --git a/src/components/ShareExport/index.test.tsx b/src/components/ShareExport/index.test.tsx index 91050c5eeb..98d8d42436 100644 --- a/src/components/ShareExport/index.test.tsx +++ b/src/components/ShareExport/index.test.tsx @@ -1,37 +1,50 @@ import React from 'react'; +import { Provider } from 'react-redux'; import { MemoryRouter, Route } from 'react-router-dom'; import { render, waitFor } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; import Sinon from 'sinon'; -import allMocks, { modelID, summaryMock } from 'data/mock/readonly'; +import allMocks, { + modelID, + operationalNeedsMock, + summaryMock +} from 'data/mock/readonly'; import VerboseMockedProvider from 'utils/testing/MockedProvider'; import setup from 'utils/testing/setup'; import ShareExportModal from './index'; +const mockStore = configureMockStore(); +const store = mockStore({ auth: { euaId: 'MINT' } }); + describe('ShareExportModal', () => { // Stubing Math.random that occurs in Truss Tooltip component for deterministic output Sinon.stub(Math, 'random').returns(0.5); it('renders modal with prepopulated filter', async () => { const { user, getByText, getByTestId } = setup( - - + - - null} - filteredView="ccw" - setStatusMessage={() => null} - /> - - - + + + null} + filteredView="ccw" + setStatusMessage={() => null} + /> + + + + ); await waitFor(async () => { @@ -40,7 +53,9 @@ describe('ShareExportModal', () => { await user.click(exportButton); // Renders default Fitler group option if supplied - expect(getByText('Testing Model Summary')).toBeInTheDocument(); + expect( + getByText('My excellent plan that I just initiated') + ).toBeInTheDocument(); const combobox = getByTestId('combo-box-select'); expect(combobox).toHaveValue('ccw'); diff --git a/src/components/shared/Alert/index.tsx b/src/components/shared/Alert/index.tsx index f912a52724..4b44c0addc 100644 --- a/src/components/shared/Alert/index.tsx +++ b/src/components/shared/Alert/index.tsx @@ -24,6 +24,7 @@ type AlertProps = { isClosable?: boolean; headingLevel?: HeadingLevel; closeAlert?: (closed: any) => void; + validation?: boolean; // Adds usa-alert--validation class, convert p to span to allow list nesting } & JSX.IntrinsicElements['div']; export const Alert = ({ @@ -39,6 +40,7 @@ export const Alert = ({ // Default to closable button if type = success or error isClosable = type === 'success' || type === 'error', closeAlert, + validation, ...props }: AlertProps & React.HTMLAttributes): React.ReactElement => { const classes = classnames( @@ -69,6 +71,7 @@ export const Alert = ({ className={classes} {...props} headingLevel={headingLevel} + validation={validation} > {children} {isClosable && ( diff --git a/src/data/mock/readonly.ts b/src/data/mock/readonly.ts index a7490a18d4..aa0f3892ca 100644 --- a/src/data/mock/readonly.ts +++ b/src/data/mock/readonly.ts @@ -42,6 +42,9 @@ import { ModelType, MonitoringFileType, NonClaimsBasedPayType, + OperationalNeedKey, + OperationalSolutionKey, + OpSolutionStatus, OverlapType, ParticipantCommunicationType, ParticipantRiskType, @@ -66,6 +69,8 @@ import { import GetModelPlanCollaborators from 'queries/Collaborators/GetModelCollaborators'; import { GetModelCollaborators_modelPlan_collaborators as GetModelCollaboratorsType } from 'queries/Collaborators/types/GetModelCollaborators'; +import GetOperationalNeeds from 'queries/ITSolutions/GetOperationalNeeds'; +import { GetOperationalNeeds as GetOperationalNeedsType } from 'queries/ITSolutions/types/GetOperationalNeeds'; import GetModelSummary from 'queries/ReadOnly/GetModelSummary'; import { GetModelSummary_modelPlan as GetModelSummaryTypes } from 'queries/ReadOnly/types/GetModelSummary'; @@ -764,6 +769,58 @@ export const collaboratorsMocks = [ } ]; +const opNeedsData: GetOperationalNeedsType = { + modelPlan: { + __typename: 'ModelPlan', + id: modelID, + isCollaborator: true, + modelName: 'My excellent plan that I just initiated', + operationalNeeds: [ + { + __typename: 'OperationalNeed', + id: '123', + modelPlanID: modelID, + name: 'Recruit participants', + key: OperationalNeedKey.RECRUIT_PARTICIPANTS, + nameOther: null, + needed: true, + modifiedDts: '2022-05-12T15:01:39.190679Z', + solutions: [ + { + __typename: 'OperationalSolution', + id: '123', + status: OpSolutionStatus.IN_PROGRESS, + name: 'Shared Systems', + key: OperationalSolutionKey.SHARED_SYSTEMS, + otherHeader: '', + mustStartDts: null, + mustFinishDts: null, + operationalSolutionSubtasks: [], + needed: true, + nameOther: null, + pocEmail: null, + pocName: null, + createdBy: '', + createdDts: '' + } + ] + } + ] + } +}; + +export const operationalNeedsMock = [ + { + request: { + query: GetOperationalNeeds, + variables: { id: modelID } + }, + result: { + data: opNeedsData + } + } +]; + const allMocks = [ ...benficiaryMocks, ...generalCharacteristicMocks, diff --git a/src/hooks/useFetchCSVData.test.tsx b/src/hooks/useFetchCSVData.test.tsx index a940fd0bfb..d36fe78419 100644 --- a/src/hooks/useFetchCSVData.test.tsx +++ b/src/hooks/useFetchCSVData.test.tsx @@ -148,6 +148,7 @@ describe('fetch csv utils', () => { 'beneficiaries.beneficiariesNote', 'beneficiaries.numberPeopleImpacted', 'beneficiaries.estimateConfidence', + 'beneficiaries.confidenceNote', 'beneficiaries.beneficiaryOverlap', 'beneficiaries.beneficiaryOverlapNote', 'beneficiaries.precedenceRules', diff --git a/src/hooks/usePlanTranslation.tsx b/src/hooks/usePlanTranslation.tsx index 118de71421..857271a0a4 100644 --- a/src/hooks/usePlanTranslation.tsx +++ b/src/hooks/usePlanTranslation.tsx @@ -8,7 +8,7 @@ Fetches i18n store and returns a model plan translation section or all translati import { useTranslation } from 'react-i18next'; import { ResourceKey } from 'i18next'; -import { TranslationPlan } from 'types/translation'; +import { getKeys, PlanSection, TranslationPlan } from 'types/translation'; // Function overload // Conditionally return type based parameter @@ -30,8 +30,19 @@ function usePlanTranslation( return planTranslationMap[type] as TranslationPlan[T]; } + const allSections: Record = {} as Record< + PlanSection, + TranslationPlan[T] + >; + + getKeys(PlanSection).forEach(section => { + allSections[PlanSection[section]] = planTranslationMap[ + PlanSection[section] + ] as TranslationPlan[T]; + }); + // Return all translations for all sections - return planTranslationMap as TranslationPlan; + return allSections as TranslationPlan; } export default usePlanTranslation; diff --git a/src/i18n/en-US/draftModelPlan/itSolutions.ts b/src/i18n/en-US/draftModelPlan/itSolutions.ts index 0cefc3a4f3..0476a77a07 100644 --- a/src/i18n/en-US/draftModelPlan/itSolutions.ts +++ b/src/i18n/en-US/draftModelPlan/itSolutions.ts @@ -77,7 +77,9 @@ const itSolutions = { 'If you have additional operational needs you want to track, add an operational need or solution below.', noNeedsReadonlyInfo: 'Check back again later for updates.', noNeedsReadonlyEditInfo: - 'To determine the operational needs, fill out more of the Model Plan.' + 'To determine the operational needs, fill out more of the Model Plan.', + unusedSolutionsAlert: + 'This model has not specified they will use the following solution(s):' }, status: { notAnswered: 'Not answered', diff --git a/src/i18n/en-US/modelPlan/beneficiaries.ts b/src/i18n/en-US/modelPlan/beneficiaries.ts index 64ed631fe0..51f80070bf 100644 --- a/src/i18n/en-US/modelPlan/beneficiaries.ts +++ b/src/i18n/en-US/modelPlan/beneficiaries.ts @@ -174,13 +174,7 @@ export const beneficiaries: TranslationBeneficiaries = { position: 'right', adjacentField: 'numberPeopleImpacted' }, - filterGroups: [ - ModelViewFilter.CBOSC, - ModelViewFilter.CCW, - ModelViewFilter.DFSDM, - ModelViewFilter.IPC, - ModelViewFilter.MDM - ] + filterGroups: [ModelViewFilter.MDM] }, confidenceNote: { gqlField: 'confidenceNote', @@ -189,12 +183,7 @@ export const beneficiaries: TranslationBeneficiaries = { label: 'Notes', dataType: 'string', formType: 'textarea', - filterGroups: [ - ModelViewFilter.CBOSC, - ModelViewFilter.CCW, - ModelViewFilter.DFSDM, - ModelViewFilter.IPC - ] + filterGroups: [ModelViewFilter.MDM] }, beneficiarySelectionMethod: { gqlField: 'beneficiarySelectionMethod', diff --git a/src/i18n/en-US/modelPlan/opsEvalAndLearning.ts b/src/i18n/en-US/modelPlan/opsEvalAndLearning.ts index 32dbd621e0..cd2bc0fb60 100644 --- a/src/i18n/en-US/modelPlan/opsEvalAndLearning.ts +++ b/src/i18n/en-US/modelPlan/opsEvalAndLearning.ts @@ -397,7 +397,8 @@ export const opsEvalAndLearning: TranslationOpsEvalAndLearning = { true: 'Yes', false: 'No' }, - parentRelation: () => opsEvalAndLearning.iddocSupport + parentRelation: () => opsEvalAndLearning.iddocSupport, + filterGroups: [ModelViewFilter.IDDOC] }, unsolicitedAdjustmentsIncluded: { gqlField: 'unsolicitedAdjustmentsIncluded', @@ -424,7 +425,8 @@ export const opsEvalAndLearning: TranslationOpsEvalAndLearning = { true: 'Yes', false: 'No' }, - parentRelation: () => opsEvalAndLearning.iddocSupport + parentRelation: () => opsEvalAndLearning.iddocSupport, + filterGroups: [ModelViewFilter.IDDOC] }, produceBenefitEnhancementFiles: { gqlField: 'produceBenefitEnhancementFiles', diff --git a/src/types/translation.ts b/src/types/translation.ts index 9504e74439..4ed1ed62e8 100644 --- a/src/types/translation.ts +++ b/src/types/translation.ts @@ -845,3 +845,14 @@ export type TranslationPlanSection = | TranslationPlan['beneficiaries'] | TranslationPlan['opsEvalAndLearning'] | TranslationPlan['payments']; + +export enum PlanSection { + MODEL_PLAN = 'modelPlan', + BASICS = 'basics', + GENERAL_CHARACTERISTICS = 'generalCharacteristics', + PARTICPANTS_AND_PROVIDERS = 'participantsAndProviders', + BENEFICIARIES = 'beneficiaries', + OPS_EVAL_AND_LEARNING = 'opsEvalAndLearning', + PAYMENTS = 'payments', + COLLABORATORS = 'collaborators' +} diff --git a/src/views/ModelPlan/ReadOnly/OpsEvalAndLearning/index.tsx b/src/views/ModelPlan/ReadOnly/OpsEvalAndLearning/index.tsx index f2d5fc3e73..1772c716ba 100644 --- a/src/views/ModelPlan/ReadOnly/OpsEvalAndLearning/index.tsx +++ b/src/views/ModelPlan/ReadOnly/OpsEvalAndLearning/index.tsx @@ -240,7 +240,6 @@ const ReadOnlyOpsEvalAndLearning = ({ childrenToCheck={ filteredView ? claimsFilterGroupFields : undefined } - hideAlert={!!filteredView} /> = { pbg: ModelViewFilter.PBG }; -const FilterGroupMap: Record = { - cmmi: { - 'general-characteristics': [ - 'alternativePaymentModelTypes', - 'alternativePaymentModelTypesNote', - 'keyCharacteristics', - 'keyCharacteristicsNote', - 'keyCharacteristicsOther', - 'participationOptions', - 'participationOptionsNote', - 'agreementTypes', - 'agreementTypesOther', - 'multiplePatricipationAgreementsNeeded', - 'multiplePatricipationAgreementsNeededNote', - 'authorityAllowances', - 'authorityAllowancesOther', - 'authorityAllowancesNote', - 'waiversRequired', - 'waiversRequiredTypes', - 'waiversRequiredNote' - ], - 'participants-and-providers': [ - 'participants', - 'medicareProviderType', - 'statesEngagement', - 'selectionMethod', - 'selectionOther', - 'selectionNote' - ], - beneficiaries: [ - 'beneficiarySelectionMethod', - 'beneficiarySelectionOther', - 'beneficiarySelectionNote', - 'beneficiarySelectionFrequency', - 'beneficiarySelectionFrequencyOther', - 'beneficiarySelectionFrequencyNote' - ], - 'ops-eval-and-learning': [ - 'benchmarkForPerformance', - 'benchmarkForPerformanceNote', - 'computePerformanceScores', - 'computePerformanceScoresNote', - 'riskAdjustPerformance', - 'riskAdjustNote', - 'riskAdjustPayments', - 'dataNeededForMonitoring', - 'dataNeededForMonitoringOther', - 'dataNeededForMonitoringNote', - 'dataToSendParticicipants', - 'dataToSendParticicipantsOther', - 'dataToSendParticicipantsNote', - 'shareCclfData', - 'developNewQualityMeasures', - 'developNewQualityMeasuresNote', - 'qualityPerformanceImpactsPayment', - 'qualityPerformanceImpactsPaymentNote', - 'dataSharingFrequency', - 'dataSharingFrequencyOther', - 'dataSharingFrequencyNote', - 'dataCollectionFrequency', - 'dataCollectionFrequencyOther', - 'dataCollectionFrequencyNote' - ], - payments: [ - 'payType', - 'payTypeNote', - 'payClaims', - 'payClaimsOther', - 'payClaimsNote', - 'nonClaimsPayments', - 'nonClaimsPaymentOther', - 'nonClaimsPaymentsNote', - 'canParticipantsSelectBetweenPaymentMechanisms', - 'canParticipantsSelectBetweenPaymentMechanismsHow', - 'canParticipantsSelectBetweenPaymentMechanismsNote', - 'anticipatedPaymentFrequency', - 'anticipatedPaymentFrequencyOther', - 'anticipatedPaymentFrequencyNote', - 'willRecoverPayments', - 'willRecoverPaymentsNote' - ] - }, - oact: { - basics: ['nameHistory'], - 'general-characteristics': [ - 'alternativePaymentModelTypes', - 'alternativePaymentModelTypesNote' - ], - 'participants-and-providers': [ - 'providerAdditionFrequency', - 'providerAdditionFrequencyOther', - 'providerAdditionFrequencyNote', - 'providerAddMethod', - 'providerAddMethodOther', - 'providerAddMethodNote', - 'providerLeaveMethod', - 'providerLeaveMethodOther', - 'providerLeaveMethodNote' - ], - beneficiaries: [ - 'precedenceRules', - 'precedenceRulesYes', - 'precedenceRulesNo', - 'precedenceRulesNote' - ], - payments: [ - 'fundingSource', - 'fundingSourceMedicareAInfo', - 'fundingSourceMedicareBInfo', - 'fundingSourceOther', - 'fundingSourceNote', - 'fundingSourceR', - 'fundingSourceRMedicareAInfo', - 'fundingSourceRMedicareBInfo', - 'fundingSourceROther', - 'fundingSourceRNote', - 'payClaims', - 'payClaimsOther', - 'payClaimsNote', - 'isContractorAwareTestDataRequirements', - 'beneficiaryCostSharingLevelAndHandling', - 'waiveBeneficiaryCostSharingForAnyServices', - 'waiveBeneficiaryCostSharingServiceSpecification', - 'waiveBeneficiaryCostSharingNote' - ] - }, - dfsdm: { - basics: [ - 'nameHistory', - 'modelType', - 'modelTypeOther', - 'goal', - 'performancePeriodStarts' - ], - 'participants-and-providers': [ - 'expectedNumberOfParticipants', - 'estimateConfidence', - 'confidenceNote' - ], - payments: [ - 'fundingSource', - 'fundingSourceMedicareAInfo', - 'fundingSourceMedicareBInfo', - 'fundingSourceOther', - 'fundingSourceNote', - 'fundingSourceR', - 'fundingSourceRMedicareAInfo', - 'fundingSourceRMedicareBInfo', - 'fundingSourceROther', - 'fundingSourceRNote', - 'numberPaymentsPerPayCycle', - 'numberPaymentsPerPayCycleNote', - 'planningToUseInnovationPaymentContractor', - 'planningToUseInnovationPaymentContractorNote', - 'anticipatedPaymentFrequency', - 'anticipatedPaymentFrequencyOther', - 'anticipatedPaymentFrequencyNote', - 'paymentStartDate', - 'paymentStartDateNote' - ] - }, - ccw: { - basics: ['nameHistory', 'performancePeriodStarts'], - 'participants-and-providers': [ - 'expectedNumberOfParticipants', - 'estimateConfidence', - 'confidenceNote' - ], - 'ops-eval-and-learning': [ - 'ccmInvolvment', - 'ccmInvolvmentOther', - 'ccmInvolvmentNote', - 'sendFilesBetweenCcw', - 'sendFilesBetweenCcwNote', - 'appToSendFilesToKnown', - 'appToSendFilesToWhich', - 'appToSendFilesToNote', - 'useCcwForFileDistribiutionToParticipants', - 'useCcwForFileDistribiutionToParticipantsNote' - ], - payments: [ - 'sharedSystemsInvolvedAdditionalClaimPayment', - 'sharedSystemsInvolvedAdditionalClaimPaymentNote' - ] - }, - ipc: { - basics: [ - 'nameHistory', - 'modelCategory', - 'cmsCenters', - 'cmmiGroups', - 'modelType', - 'modelTypeOther', - 'goal', - 'completeICIP', - 'clearanceStarts', - 'clearanceEnds', - 'announced', - 'applicationsStart', - 'applicationsEnd', - 'performancePeriodStarts', - 'performancePeriodEnds', - 'wrapUpEnds' - ], - 'general-characteristics': [ - 'rulemakingRequired', - 'rulemakingRequiredDescription', - 'rulemakingRequiredNote' - ], - 'participants-and-providers': [ - 'participants', - 'medicareProviderType', - 'statesEngagement', - 'expectedNumberOfParticipants', - 'estimateConfidence', - 'confidenceNote', - 'providerAdditionFrequency', - 'providerAdditionFrequencyOther', - 'providerAdditionFrequencyNote', - 'providerAddMethod', - 'providerAddMethodOther', - 'providerAddMethodNote', - 'providerLeaveMethod', - 'providerLeaveMethodOther', - 'providerLeaveMethodNote' - ], - payments: [ - 'fundingSource', - 'fundingSourceMedicareAInfo', - 'fundingSourceMedicareBInfo', - 'fundingSourceOther', - 'fundingSourceNote', - 'fundingSourceR', - 'fundingSourceRMedicareAInfo', - 'fundingSourceRMedicareBInfo', - 'fundingSourceROther', - 'fundingSourceRNote', - 'payType', - 'payTypeNote', - 'nonClaimsPayments', - 'nonClaimsPaymentOther', - 'nonClaimsPaymentsNote', - 'numberPaymentsPerPayCycle', - 'numberPaymentsPerPayCycleNote', - 'planningToUseInnovationPaymentContractor', - 'planningToUseInnovationPaymentContractorNote', - 'anticipatedPaymentFrequency', - 'anticipatedPaymentFrequencyOther', - 'anticipatedPaymentFrequencyNote', - 'willRecoverPayments', - 'willRecoverPaymentsNote', - 'anticipateReconcilingPaymentsRetrospectively', - 'anticipateReconcilingPaymentsRetrospectivelyNote', - 'paymentDemandRecoupmentFrequency', - 'paymentDemandRecoupmentFrequencyNote', - 'paymentStartDate', - 'paymentStartDateNote' - ] - }, - iddoc: { - basics: [ - 'nameHistory', - 'modelType', - 'modelTypeOther', - 'goal', - 'announced', - 'performancePeriodStarts', - 'phasedIn', - 'phasedInNote' - ], - 'general-characteristics': [ - 'keyCharacteristics', - 'keyCharacteristicsNote', - 'keyCharacteristicsOther', - 'geographiesTargeted', - 'geographiesTargetedNote', - 'geographiesTargetedTypes', - 'geographiesTargetedTypesOther', - 'geographiesTargetedAppliedTo', - 'geographiesTargetedAppliedToOther', - 'rulemakingRequired', - 'rulemakingRequiredDescription', - 'rulemakingRequiredNote' - ], - 'participants-and-providers': [ - 'participants', - 'medicareProviderType', - 'statesEngagement', - 'modelApplicationLevel', - 'selectionMethod', - 'selectionOther', - 'selectionNote', - 'participantsIds', - 'participantsIdsOther', - 'participantsIdsNote', - 'providerOverlap', - 'providerOverlapHierarchy', - 'providerOverlapNote' - ], - beneficiaries: [ - 'treatDualElligibleDifferent', - 'treatDualElligibleDifferentHow', - 'treatDualElligibleDifferentNote', - 'excludeCertainCharacteristics', - 'excludeCertainCharacteristicsCriteria', - 'excludeCertainCharacteristicsNote' - ], - 'ops-eval-and-learning': [ - 'contractorSupport', - 'contractorSupportOther', - 'contractorSupportHow', - 'contractorSupportNote', - 'iddocSupport', - 'iddocSupportNote', - 'technicalContactsIdentified', - 'technicalContactsIdentifiedDetail', - 'technicalContactsIdentifiedNote', - 'captureParticipantInfo', - 'captureParticipantInfoNote', - 'icdOwner', - 'draftIcdDueDate', - 'icdNote', - 'uatNeeds', - 'stcNeeds', - 'testingTimelines', - 'dataMonitoringFileTypes', - 'dataResponseType', - 'dataResponseFileFrequency', - 'dataFullTimeOrIncremental', - 'unsolicitedAdjustmentsIncluded', - 'produceBenefitEnhancementFiles', - 'fileNamingConventions', - 'dataNeededForMonitoring', - 'dataNeededForMonitoringOther', - 'dataNeededForMonitoringNote', - 'dataSharingStarts', - 'dataSharingStartsOther', - 'dataSharingFrequency', - 'dataSharingFrequencyOther', - 'dataSharingFrequencyNote', - 'dataCollectionStarts', - 'dataCollectionStartsOther', - 'anticipatedChallenges' - ], - payments: [ - 'shouldAnyProvidersExcludedFFSSystems', - 'shouldAnyProviderExcludedFFSSystemsNote', - 'changesMedicarePhysicianFeeSchedule', - 'changesMedicarePhysicianFeeScheduleNote', - 'affectsMedicareSecondaryPayerClaims', - 'affectsMedicareSecondaryPayerClaimsHow', - 'affectsMedicareSecondaryPayerClaimsNote', - 'payModelDifferentiation', - 'creatingDependenciesBetweenServices', - 'creatingDependenciesBetweenServicesNote', - 'needsClaimsDataCollection', - 'needsClaimsDataCollectionNote', - 'providingThirdPartyFile', - 'isContractorAwareTestDataRequirements', - 'beneficiaryCostSharingLevelAndHandling', - 'waiveBeneficiaryCostSharingForAnyServices', - 'waiveBeneficiaryCostSharingServiceSpecification', - 'waiveBeneficiaryCostSharingNote', - 'nonClaimsPayments', - 'nonClaimsPaymentOther', - 'nonClaimsPaymentsNote', - 'waiverOnlyAppliesPartOfPayment', - 'waiveBeneficiaryCostSharingNote', - 'planningToUseInnovationPaymentContractor', - 'planningToUseInnovationPaymentContractorNote', - 'anticipateReconcilingPaymentsRetrospectively', - 'anticipateReconcilingPaymentsRetrospectivelyNote' - ] - }, - pbg: { - basics: [ - 'nameHistory', - 'modelType', - 'modelTypeOther', - 'goal', - 'announced', - 'performancePeriodStarts', - 'phasedIn', - 'phasedInNote' - ], - 'general-characteristics': [ - 'keyCharacteristics', - 'keyCharacteristicsNote', - 'keyCharacteristicsOther', - 'geographiesTargeted', - 'geographiesTargetedNote', - 'geographiesTargetedTypes', - 'geographiesTargetedTypesOther', - 'geographiesTargetedAppliedTo', - 'geographiesTargetedAppliedToOther', - 'rulemakingRequired', - 'rulemakingRequiredDescription', - 'rulemakingRequiredNote' - ], - 'participants-and-providers': [ - 'participants', - 'medicareProviderType', - 'statesEngagement', - 'modelApplicationLevel', - 'selectionMethod', - 'selectionOther', - 'selectionNote', - 'providerOverlap', - 'providerOverlapHierarchy', - 'providerOverlapNote' - ], - beneficiaries: [ - 'treatDualElligibleDifferent', - 'treatDualElligibleDifferentHow', - 'treatDualElligibleDifferentNote', - 'excludeCertainCharacteristics', - 'excludeCertainCharacteristicsCriteria', - 'excludeCertainCharacteristicsNote' - ], - 'ops-eval-and-learning': [ - 'contractorSupport', - 'contractorSupportHow', - 'anticipatedChallenges' - ], - payments: [ - 'shouldAnyProvidersExcludedFFSSystems', - 'shouldAnyProviderExcludedFFSSystemsNote', - 'changesMedicarePhysicianFeeSchedule', - 'changesMedicarePhysicianFeeScheduleNote', - 'affectsMedicareSecondaryPayerClaims', - 'affectsMedicareSecondaryPayerClaimsHow', - 'affectsMedicareSecondaryPayerClaimsNote', - 'payModelDifferentiation', - 'creatingDependenciesBetweenServices', - 'creatingDependenciesBetweenServicesNote', - 'needsClaimsDataCollection', - 'needsClaimsDataCollectionNote', - 'providingThirdPartyFile', - 'isContractorAwareTestDataRequirements', - 'waiverOnlyAppliesPartOfPayment', - 'beneficiaryCostSharingLevelAndHandling', - 'waiveBeneficiaryCostSharingForAnyServices', - 'waiveBeneficiaryCostSharingServiceSpecification', - 'waiveBeneficiaryCostSharingNote', - 'nonClaimsPayments', - 'nonClaimsPaymentOther', - 'nonClaimsPaymentsNote', - 'planningToUseInnovationPaymentContractor', - 'planningToUseInnovationPaymentContractorNote', - 'anticipateReconcilingPaymentsRetrospectively', - 'anticipateReconcilingPaymentsRetrospectivelyNote' - ] - }, - mdm: { - basics: ['nameHistory'], - beneficiaries: [ - 'beneficiaries', - 'beneficiariesOther', - 'beneficiariesNote', - 'numberPeopleImpacted', - 'estimateConfidence', - 'beneficiaryOverlap', - 'beneficiaryOverlapNote', - 'precedenceRules', - 'precedenceRulesYes', - 'precedenceRulesNo', - 'precedenceRulesNote' - ] - }, - cbosc: { - basics: [ - 'nameHistory', - 'announced', - 'applicationsStart', - 'performancePeriodStarts' - ], - 'participants-and-providers': [ - 'participants', - 'medicareProviderType', - 'statesEngagement', - 'expectedNumberOfParticipants', - 'estimateConfidence', - 'confidenceNote', - 'communicationMethod', - 'communicationMethodOther', - 'communicationMethodNote' - ], - 'ops-eval-and-learning': [ - 'stakeholders', - 'stakeholdersOther', - 'stakeholdersNote', - 'helpdeskUse', - 'helpdeskUseNote', - 'contractorSupport', - 'contractorSupportOther', - 'contractorSupportHow', - 'contractorSupportNote' - ] - } +// Map of operational solutions that should be rendered on each filter view +export const filteredGroupSolutions: Record< + FilterGroup, + OperationalSolutionKey[] +> = { + cbosc: [OperationalSolutionKey.CBOSC], + ccw: [OperationalSolutionKey.CCW, OperationalSolutionKey.SHARED_SYSTEMS], + dfsdm: [OperationalSolutionKey.IPC], + ipc: [ + OperationalSolutionKey.INNOVATION, + OperationalSolutionKey.ACO_OS, + OperationalSolutionKey.CDX, + OperationalSolutionKey.IPC + ], + iddoc: [OperationalSolutionKey.INNOVATION, OperationalSolutionKey.ACO_OS], + mdm: [OperationalSolutionKey.MDM], + pbg: [OperationalSolutionKey.IPC] }; - -export default FilterGroupMap; diff --git a/src/views/ModelPlan/ReadOnly/_components/FilterView/BodyContent/index.test.tsx b/src/views/ModelPlan/ReadOnly/_components/FilterView/BodyContent/index.test.tsx index cafb6132af..66cef6f414 100644 --- a/src/views/ModelPlan/ReadOnly/_components/FilterView/BodyContent/index.test.tsx +++ b/src/views/ModelPlan/ReadOnly/_components/FilterView/BodyContent/index.test.tsx @@ -11,6 +11,14 @@ import configureMockStore from 'redux-mock-store'; import { ASSESSMENT } from 'constants/jobCodes'; import allMocks from 'data/mock/readonly'; +import basics from 'i18n/en-US/modelPlan/basics'; +import beneficiaries from 'i18n/en-US/modelPlan/beneficiaries'; +import collaborators from 'i18n/en-US/modelPlan/collaborators'; +import generalCharacteristics from 'i18n/en-US/modelPlan/generalCharacteristics'; +import modelPlan from 'i18n/en-US/modelPlan/modelPlan'; +import opsEvalAndLearning from 'i18n/en-US/modelPlan/opsEvalAndLearning'; +import participantsAndProviders from 'i18n/en-US/modelPlan/participantsAndProviders'; +import payments from 'i18n/en-US/modelPlan/payments'; import GetModelSummary from 'queries/ReadOnly/GetModelSummary'; import { GetModelSummary_modelPlan as GetModelSummaryTypes } from 'queries/ReadOnly/types/GetModelSummary'; import { @@ -18,8 +26,11 @@ import { ModelStatus, TeamRole } from 'types/graphql-global-types'; +import { TranslationPlan } from 'types/translation'; import ReadOnly from 'views/ModelPlan/ReadOnly'; +import { getAllFilterViewQuestions } from '.'; + const mockData: GetModelSummaryTypes = { __typename: 'ModelPlan', id: 'f11eb129-2c80-4080-9440-439cbe1a286f', @@ -96,6 +107,42 @@ const mockStore = configureMockStore(); const store = mockStore({ auth: mockAuthReducer }); describe('Read Only Filtered View Body Content', () => { + it('formats filter view translation mappings', async () => { + expect( + getAllFilterViewQuestions( + { + basics, + collaborators, + generalCharacteristics, + participantsAndProviders, + beneficiaries, + modelPlan, + opsEvalAndLearning, + payments + } as TranslationPlan, + 'mdm' + ) + ).toEqual({ + basics: ['nameHistory'], + modelPlan: ['nameHistory'], + beneficiaries: [ + 'beneficiaries', + 'diseaseSpecificGroup', + 'beneficiariesOther', + 'beneficiariesNote', + 'numberPeopleImpacted', + 'estimateConfidence', + 'confidenceNote', + 'beneficiaryOverlap', + 'beneficiaryOverlapNote', + 'precedenceRules', + 'precedenceRulesYes', + 'precedenceRulesNo', + 'precedenceRulesNote' + ] + }); + }); + it('renders without crashing', async () => { const { getByTestId } = render( = { + basics: ReadOnlyModelBasics, + generalCharacteristics: ReadOnlyGeneralCharacteristics, + participantsAndProviders: ReadOnlyParticipantsAndProviders, + beneficiaries: ReadOnlyBeneficiaries, + opsEvalAndLearning: ReadOnlyOpsEvalAndLearning, + payments: ReadOnlyPayments +}; const FitleredViewSection = ({ children, @@ -42,18 +63,14 @@ const BodyContent = ({ filteredView: typeof filterGroups[number]; }) => { const { t } = useTranslation('filterView'); + const { t: opSolutionsT } = useTranslation('itSolutions'); - const individualFilterView = - allPossibleFilterViews[filteredView as keyof typeof allPossibleFilterViews]; + const filterMappings = usePlanTranslation(); - const components: Record = { - basics: ReadOnlyModelBasics, - 'general-characteristics': ReadOnlyGeneralCharacteristics, - 'participants-and-providers': ReadOnlyParticipantsAndProviders, - beneficiaries: ReadOnlyBeneficiaries, - 'ops-eval-and-learning': ReadOnlyOpsEvalAndLearning, - payments: ReadOnlyPayments - }; + const mappedQuestions = getAllFilterViewQuestions( + filterMappings, + filteredView + ); return ( @@ -66,14 +83,16 @@ const BodyContent = ({ /> - {Object.keys(individualFilterView).map(task => { + {Object.keys(mappedQuestions).map(task => { const FitleredViewSectionContent = components[task]; + if (!FitleredViewSectionContent) return null; + return ( + +

{opSolutionsT('headingReadOnly')}

+ + + )} +
@@ -99,4 +137,49 @@ const BodyContent = ({ ); }; +/* + Traversed all translation mappings to return an refined mapping pertaining to the relative filter view +*/ +export const getAllFilterViewQuestions = ( + filterMappings: TranslationPlan, + filteredView: typeof filterGroups[number] +) => { + let mappedQuestions: Record = {}; + + // Diving into each model plan section, as well as each question to identify if it contains the appropriate filter view mapping + getKeys(filterMappings).forEach(section => { + getKeys(filterMappings[section]).forEach(question => { + const filterGroupConfig = (filterMappings[section][ + question + ] as TranslationFieldProperties)?.filterGroups; + + if ( + Array.isArray(filterGroupConfig) && + filterGroupConfig.includes(filterGroupKey[filteredView]) + ) { + if (mappedQuestions[section as string]) { + mappedQuestions[section as string].push( + (filterMappings[section][question] as TranslationFieldProperties) + .gqlField + ); + } else { + mappedQuestions[section as string] = [ + (filterMappings[section][question] as TranslationFieldProperties) + .gqlField + ]; + } + } + }); + }); + + // Transferring mapped questions from modelPlan to basics. Readonly treats modelPlan as if were in basics schema + if (mappedQuestions.modelPlan) { + mappedQuestions = { + basics: (mappedQuestions.basics || []).concat(mappedQuestions.modelPlan), + ...mappedQuestions + }; + } + return mappedQuestions; +}; + export default BodyContent; diff --git a/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/index.tsx b/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/index.tsx index a91a2cfac3..485ce72d94 100644 --- a/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/index.tsx +++ b/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/index.tsx @@ -396,7 +396,7 @@ export const RelatedUnneededQuestions = < ? // Render a disconnected translations text readOnlyT(disconnectedLabel, { count: relatedConditions.length, - question: config.label + question: config.readonlyLabel || config.label }) : // Render default alert text readOnlyT('questionNotApplicable', { diff --git a/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/util.tsx b/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/util.tsx index c40bdc7169..8c12cc2d4f 100644 --- a/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/util.tsx +++ b/src/views/ModelPlan/ReadOnly/_components/ReadOnlySection/util.tsx @@ -171,12 +171,14 @@ export const getRelatedUneededQuestions = < (!Array.isArray(value) && value !== undefined && String(value) !== option) ) { childRelations?.forEach(childField => { - neededRelations.push(childField().label); + neededRelations.push(childField().readonlyLabel || childField().label); }); } else { unneededRelations = [ ...unneededRelations, - ...(childRelations?.map(childField => childField().label) as []) + ...(childRelations?.map( + childField => childField().readonlyLabel || childField().label + ) as []) ]; } }); diff --git a/src/views/ModelPlan/TaskList/ITSolutions/Home/__snapshots__/operationalNeedsTable.test.tsx.snap b/src/views/ModelPlan/TaskList/ITSolutions/Home/__snapshots__/operationalNeedsTable.test.tsx.snap index 6e91d976ed..2c699d40d1 100644 --- a/src/views/ModelPlan/TaskList/ITSolutions/Home/__snapshots__/operationalNeedsTable.test.tsx.snap +++ b/src/views/ModelPlan/TaskList/ITSolutions/Home/__snapshots__/operationalNeedsTable.test.tsx.snap @@ -259,3 +259,27 @@ exports[`Operational Solutions Home > matches snapshot 1`] = `
`; + +exports[`Operational Solutions Home > renders filter view solutions alert 1`] = ` + +
+
+ + This model has not specified they will use the following solution(s): +
    +
  • + Chronic Conditions Warehouse (CCW) +
  • +
+
+
+
+
+`; diff --git a/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.test.tsx b/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.test.tsx index 649340aeb6..ce78279271 100644 --- a/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.test.tsx +++ b/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.test.tsx @@ -12,7 +12,9 @@ import { OpSolutionStatus } from 'types/graphql-global-types'; -import OperationalNeedsTable from './operationalNeedsTable'; +import OperationalNeedsTable, { + FilterViewSolutionsAlert +} from './operationalNeedsTable'; const modelID: string = 'ec2d1105-b722-4c99-94aa-5b76838d7a54'; @@ -139,4 +141,17 @@ describe('Operational Solutions Home', () => { expect(asFragment()).toMatchSnapshot(); }); + + it('renders filter view solutions alert', async () => { + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); }); diff --git a/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.tsx b/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.tsx index 56a3211964..21a31a0ec8 100644 --- a/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.tsx +++ b/src/views/ModelPlan/TaskList/ITSolutions/Home/operationalNeedsTable.tsx @@ -5,7 +5,6 @@ Table component for rendering both Other Operational Needs and Operational Need Queries operationalNeeds which contains possible needs and needs Can render table of type GetOperationalNeeds_modelPlan_operationalNeeds or GetOperationalNeeds_modelPlan_operationalNeeds_solutions_solutions */ - import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { RootStateOrAny, useSelector } from 'react-redux'; @@ -21,6 +20,7 @@ import { import { useQuery } from '@apollo/client'; import { Icon, Table as UswdsTable } from '@trussworks/react-uswds'; import classNames from 'classnames'; +import { OperationalSolutionKey } from 'gql/gen/graphql'; import i18next from 'i18next'; import { useFlags } from 'launchdarkly-react-client-sdk'; @@ -47,6 +47,7 @@ import { sortColumnValues } from 'utils/tableSort'; import { isAssessment } from 'utils/user'; +import { helpSolutions } from 'views/HelpAndKnowledge/SolutionsHelp/solutionsMap'; import OperationalNeedsStatusTag, { OperationalNeedsSolutionsStatus @@ -69,13 +70,17 @@ type OperationalNeedsTableProps = { modelID: string; type: 'needs' | 'possibleNeeds'; readOnly?: boolean; + hideGlobalFilter?: boolean; + filterSolutions?: OperationalSolutionKey[]; }; const OperationalNeedsTable = ({ hiddenColumns, modelID, type, - readOnly + readOnly, + hideGlobalFilter, + filterSolutions }: OperationalNeedsTableProps) => { const { t } = useTranslation('itSolutions'); @@ -96,10 +101,19 @@ const OperationalNeedsTable = ({ ? data?.modelPlan?.operationalNeeds : ([] as GetOperationalNeedsOperationalNeedsType[]); - return type === 'possibleNeeds' - ? filterPossibleNeeds(needData) - : filterNeedsFormatSolutions(needData); - }, [data?.modelPlan?.operationalNeeds, type]); + let formattedData = + type === 'possibleNeeds' + ? filterPossibleNeeds(needData) + : filterNeedsFormatSolutions(needData); + + if (filterSolutions && Array.isArray(formattedData)) { + formattedData = (formattedData as [])?.filter((solution: any) => { + return filterSolutions.includes(solution.key); + }); + } + + return formattedData; + }, [data?.modelPlan?.operationalNeeds, type, filterSolutions]); const isCollaborator = data?.modelPlan?.isCollaborator; @@ -267,6 +281,11 @@ const OperationalNeedsTable = ({ ]; }, [t, modelID]); + // Swap the solution and need positions if readonly filter view + if (filterSolutions) { + [needsColumns[0], needsColumns[1]] = [needsColumns[1], needsColumns[0]]; + } + const { getTableProps, getTableBodyProps, @@ -337,6 +356,15 @@ const OperationalNeedsTable = ({ ); } + if (readOnly && filterSolutions && operationalNeeds.length === 0) { + return ( + + ); + } + if (readOnly && operationalNeeds.length === 0) { return ( @@ -349,23 +377,27 @@ const OperationalNeedsTable = ({ return (
-
- -
+ {!hideGlobalFilter && ( +
+ +
+ )} - + {!hideGlobalFilter && ( + + )} @@ -487,8 +519,47 @@ const OperationalNeedsTable = ({ {t('itSolutionsTable.noNeedsInfo')} )} + + {filterSolutions && ( + + )}
); }; +export const FilterViewSolutionsAlert = ({ + filterSolutions, + operationalNeeds +}: { + filterSolutions: OperationalSolutionKey[]; + operationalNeeds: any[]; +}) => { + const { t } = useTranslation('itSolutions'); + + const unusedSolutions = filterSolutions.filter( + solution => !operationalNeeds.find((need: any) => need.key === solution) + ); + + if (unusedSolutions.length === 0) return null; + + return ( + + {t('itSolutionsTable.unusedSolutionsAlert')} +
    + {helpSolutions + .filter(solution => unusedSolutions.includes(solution.enum)) + .map(solution => ( +
  • + {solution.name} + {solution.acronym ? ` (${solution.acronym})` : ''} +
  • + ))} +
+
+ ); +}; + export default OperationalNeedsTable;