@@ -46,7 +46,7 @@ const StateUserRow = ({
heading,
}: {
user: StateUser
- heading?: string
+ heading?: string | React.ReactElement
}) => {
return (
@@ -99,7 +99,7 @@ const LandingRow = ({ isLoading }: { isLoading: boolean }) => {
type PageHeadingProps = {
isLoading?: boolean
loggedInUser?: User
- heading?: string
+ heading?: string | React.ReactElement
route?: string
}
diff --git a/services/app-web/src/components/SectionHeader/SectionHeader.module.scss b/services/app-web/src/components/SectionHeader/SectionHeader.module.scss
index 5f2e119edb..d566e49b51 100644
--- a/services/app-web/src/components/SectionHeader/SectionHeader.module.scss
+++ b/services/app-web/src/components/SectionHeader/SectionHeader.module.scss
@@ -12,6 +12,16 @@
align-items: center;
padding-bottom: units(1);
min-height: 3rem;
+
+ &.alignTop {
+ align-items: start;
+ }
+
+ a {
+ display: block;
+ margin-top: 17.5px;
+ padding-bottom: units(1);
+ }
}
.summarySectionHeaderBorder {
diff --git a/services/app-web/src/components/SectionHeader/SectionHeader.tsx b/services/app-web/src/components/SectionHeader/SectionHeader.tsx
index d77533ac53..acd740209d 100644
--- a/services/app-web/src/components/SectionHeader/SectionHeader.tsx
+++ b/services/app-web/src/components/SectionHeader/SectionHeader.tsx
@@ -7,6 +7,7 @@ export type SectionHeaderProps = {
header: string
navigateTo?: string
children?: React.ReactNode
+ subHeaderComponent?: React.ReactNode
sectionId?: string
headerId?: string
hideBorder?: boolean
@@ -14,6 +15,7 @@ export type SectionHeaderProps = {
export const SectionHeader = ({
header,
+ subHeaderComponent,
navigateTo,
children,
sectionId,
@@ -23,10 +25,14 @@ export const SectionHeader = ({
const classes = classNames({
[styles.summarySectionHeader]: true,
[styles.summarySectionHeaderBorder]: !hideBorder,
+ [styles.alignTop]: subHeaderComponent,
})
return (
-
+
+
+ {subHeaderComponent}
+
{navigateTo && (
diff --git a/services/app-web/src/constants/routes.ts b/services/app-web/src/constants/routes.ts
index 78581264a9..b92105e250 100644
--- a/services/app-web/src/constants/routes.ts
+++ b/services/app-web/src/constants/routes.ts
@@ -24,6 +24,7 @@ const ROUTES = [
'SUBMISSIONS_REVIEW_SUBMIT',
'SUBMISSIONS_REVISION',
'SUBMISSIONS_SUMMARY',
+ 'SUBMISSIONS_MCCRSID',
'SUBMISSIONS_QUESTIONS_AND_ANSWERS',
'SUBMISSIONS_UPLOAD_QUESTION',
'SUBMISSIONS_UPLOAD_RESPONSE',
@@ -58,6 +59,7 @@ const RoutesRecord: Record = {
SUBMISSIONS_DOCUMENTS: '/submissions/:id/edit/documents',
SUBMISSIONS_REVIEW_SUBMIT: '/submissions/:id/edit/review-and-submit',
SUBMISSIONS_SUMMARY: '/submissions/:id',
+ SUBMISSIONS_MCCRSID: '/submissions/:id/mccrs-record-number',
SUBMISSIONS_REVISION: '/submissions/:id/revisions/:revisionVersion',
SUBMISSIONS_QUESTIONS_AND_ANSWERS: '/submissions/:id/question-and-answers',
SUBMISSIONS_UPLOAD_QUESTION:
@@ -131,6 +133,7 @@ const PageTitlesRecord: Record = {
SUBMISSIONS_RATE_DETAILS: 'Rate details',
SUBMISSIONS_CONTACTS: 'Contacts',
SUBMISSIONS_DOCUMENTS: 'Supporting documents',
+ SUBMISSIONS_MCCRSID: 'Add MC-CRS record number',
SUBMISSIONS_REVIEW_SUBMIT: 'Review and submit',
SUBMISSIONS_REVISION: 'Submission revision',
SUBMISSIONS_SUMMARY: 'Submission summary',
diff --git a/services/app-web/src/constants/tealium.ts b/services/app-web/src/constants/tealium.ts
index 8e60ba373a..ce0cb008a6 100644
--- a/services/app-web/src/constants/tealium.ts
+++ b/services/app-web/src/constants/tealium.ts
@@ -51,6 +51,7 @@ const CONTENT_TYPE_BY_ROUTE: Record = {
SUBMISSIONS_SUMMARY: 'summary',
SUBMISSIONS_REVISION: 'summary',
SUBMISSIONS_QUESTIONS_AND_ANSWERS: 'summary',
+ SUBMISSIONS_MCCRSID: 'form',
SUBMISSIONS_UPLOAD_QUESTION: 'form',
SUBMISSIONS_UPLOAD_RESPONSE: 'form',
UNKNOWN_ROUTE: '404',
@@ -83,7 +84,7 @@ const getTealiumPageName = ({
user,
}: {
route: RouteT | 'UNKNOWN_ROUTE'
- heading: string | undefined
+ heading: string | React.ReactElement | undefined
user: User | undefined
}) => {
const addSubmissionNameHeading =
@@ -95,7 +96,7 @@ const getTealiumPageName = ({
title,
}: {
title: string
- heading?: string
+ heading?: string | React.ReactElement
}) => {
const headingPrefix =
heading && addSubmissionNameHeading ? `${heading}: ` : ''
diff --git a/services/app-web/src/contexts/PageContext.tsx b/services/app-web/src/contexts/PageContext.tsx
index 470304e9b3..a64ab2daa4 100644
--- a/services/app-web/src/contexts/PageContext.tsx
+++ b/services/app-web/src/contexts/PageContext.tsx
@@ -7,8 +7,8 @@ import { useCurrentRoute } from '../hooks/useCurrentRoute'
Intended for page specific context that must be shared across the application, not for data that can be fetched from the api.
*/
type PageContextType = {
- heading?: string
- updateHeading: ({ customHeading }: { customHeading?: string }) => void
+ heading?: string | React.ReactElement
+ updateHeading: ({ customHeading }: { customHeading?: string | React.ReactElement }) => void
}
const PageContext = React.createContext(null as unknown as PageContextType)
@@ -21,14 +21,14 @@ const PageProvider: React.FC<
React.PropsWithChildren>
>
> = ({ children }) => {
- const [heading, setHeading] = React.useState(undefined)
+ const [heading, setHeading] = React.useState(undefined)
const { currentRoute: routeName } = useCurrentRoute()
/*
Set headings in priority order
1. If there a custom heading, use that (relevant for heading related to the api loaded resource, such as the submission name)
2. Otherwise, use default static headings for the current location when defined.
*/
- const updateHeading = ({ customHeading }: { customHeading?: string }) => {
+ const updateHeading = ({ customHeading }: { customHeading?: string | React.ReactElement }) => {
const defaultHeading = PageHeadingsRecord[routeName]
? PageHeadingsRecord[routeName]
: undefined
diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx
index 1bd9df2376..9d9186ddb0 100644
--- a/services/app-web/src/pages/App/AppRoutes.tsx
+++ b/services/app-web/src/pages/App/AppRoutes.tsx
@@ -22,6 +22,7 @@ import { AuthenticatedRouteWrapper } from '../Wrapper/AuthenticatedRouteWrapper'
import { Error404 } from '../Errors/Error404Page'
import { Help } from '../Help/Help'
import { Landing } from '../Landing/Landing'
+import { MccrsId } from '../MccrsId/MccrsId'
import { NewStateSubmissionForm, StateSubmissionForm } from '../StateSubmission'
import { SubmissionSummary } from '../SubmissionSummary'
import { SubmissionRevisionSummary } from '../SubmissionRevisionSummary'
@@ -158,6 +159,7 @@ const CMSUserRoutes = ({
featureFlags.RATE_REVIEWS_DASHBOARD.flag,
featureFlags.RATE_REVIEWS_DASHBOARD.defaultValue
)
+
return (
@@ -205,6 +207,12 @@ const CMSUserRoutes = ({
path={RoutesRecord.SUBMISSIONS_SUMMARY}
element={}
/>
+ {
+ }
+ />
+ }
[class^='usa-fieldset'] {
+ padding: units(4);
+ margin-bottom: units(2);
+ margin-top: units(2);
+ background: $cms-color-white;
+ border: 1px solid $theme-color-base-lighter;
+ @include u-radius('md');
+ }
+
+ h3 {
+ font-size: 22px;
+ }
+
+ > div[class^='usa-form-group']:not(:first-of-type) {
+ margin-top: 2.5rem;
+ }
+
+ &[class^='usa-form'] {
+
+ min-width: 100%;
+ max-width: 100%;
+
+ @include at-media(tablet){
+ min-width: 40rem;
+ max-width: 20rem;
+ margin: 0 auto;
+ }
+ }
+}
+
+[class^='usa-legend'] {
+ margin-top: 0;
+}
+
+div[class^='usa-form-group'] {
+ margin-top: 24px;
+ label[class^='usa-label'] {
+ font-size: 16px;
+ max-width: 100%;
+ }
+}
+
+.customHeading {
+ font-weight: 700;
+
+ span {
+ font-weight: normal;
+ margin-left: 16px;
+ }
+}
+
+div[class^='usa-hint'] span {
+ span {
+ display: inline;
+ font-weight: bold;
+ }
+}
+
+ul[class^='usa-button-group'] {
+ justify-content: end;
+ li[class^='usa-button-group__item'] {
+ margin-top: 0;
+ button {
+ margin-top: 16px;
+ margin-bottom: 44px;
+ margin-right: .25rem;
+ }
+ }
+}
+
diff --git a/services/app-web/src/pages/MccrsId/MccrsId.test.tsx b/services/app-web/src/pages/MccrsId/MccrsId.test.tsx
new file mode 100644
index 0000000000..85d999b869
--- /dev/null
+++ b/services/app-web/src/pages/MccrsId/MccrsId.test.tsx
@@ -0,0 +1,129 @@
+import { screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { Route, Routes } from 'react-router'
+import { SubmissionSideNav } from '../SubmissionSideNav'
+import { RoutesRecord } from '../../constants/routes'
+import {
+ fetchCurrentUserMock,
+ fetchStateHealthPlanPackageWithQuestionsMockSuccess,
+ mockValidCMSUser,
+} from '../../testHelpers/apolloMocks'
+import { renderWithProviders } from '../../testHelpers/jestHelpers'
+import { MccrsId } from './MccrsId'
+
+describe('MCCRSID', () => {
+ afterEach(() => {
+ jest.resetAllMocks()
+ })
+
+ it('renders without errors', async () => {
+ renderWithProviders(
+
+ }>
+ }
+ />
+
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ fetchStateHealthPlanPackageWithQuestionsMockSuccess({
+ id: '15',
+ }),
+ ],
+ },
+ routerProvider: {
+ route: '/submissions/15/mccrs-record-number',
+ },
+ }
+ )
+
+ expect(
+ await screen.findByRole('heading', { name: 'MC-CRS record number' })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('button', { name: 'Save MC-CRS number' })
+ ).not.toHaveAttribute('aria-disabled')
+ })
+
+ it('displays the text field for mccrs id', async () => {
+ renderWithProviders(
+
+ }>
+ }
+ />
+
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ fetchStateHealthPlanPackageWithQuestionsMockSuccess({
+ id: '15',
+ }),
+ ],
+ },
+ routerProvider: {
+ route: '/submissions/15/mccrs-record-number',
+ },
+ }
+ )
+
+ expect(await screen.findByTestId('textInput')).toBeInTheDocument()
+ })
+
+ it('cannot continue with MCCRS ID with non number input', async () => {
+ renderWithProviders(
+
+ }>
+ }
+ />
+
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ fetchStateHealthPlanPackageWithQuestionsMockSuccess({
+ id: '15',
+ }),
+ ],
+ },
+ routerProvider: {
+ route: '/submissions/15/mccrs-record-number',
+ },
+ }
+ )
+
+ const input = await screen.findByTestId('textInput')
+ input.focus()
+
+ await userEvent.paste('123a')
+ const continueButton = screen.getByRole('button', {
+ name: 'Save MC-CRS number',
+ })
+ continueButton.click()
+ await waitFor(() => {
+ expect(screen.getAllByText('You must enter a number')).toHaveLength(
+ 1
+ )
+ expect(continueButton).toHaveAttribute('aria-disabled', 'true')
+ })
+ })
+})
diff --git a/services/app-web/src/pages/MccrsId/MccrsId.tsx b/services/app-web/src/pages/MccrsId/MccrsId.tsx
new file mode 100644
index 0000000000..167ae92bfc
--- /dev/null
+++ b/services/app-web/src/pages/MccrsId/MccrsId.tsx
@@ -0,0 +1,171 @@
+import React, { useState, useEffect, useMemo } from 'react'
+import { Form as UswdsForm, ButtonGroup } from '@trussworks/react-uswds'
+import { Formik, FormikErrors } from 'formik'
+import { FieldTextInput } from '../../components/Form'
+import { MccrsIdFormSchema } from './MccrsIdSchema'
+import { recordJSException } from '../../otelHelpers/tracingHelper'
+import { useNavigate, useParams, useOutletContext } from 'react-router-dom'
+import { GenericApiErrorBanner } from '../../components/Banner/GenericApiErrorBanner/GenericApiErrorBanner'
+import { ActionButton } from '../../components/ActionButton'
+import { usePage } from '../../contexts/PageContext'
+import { SideNavOutletContextType } from '../SubmissionSideNav/SubmissionSideNav'
+
+import {
+ HealthPlanPackage,
+ useUpdateContractMutation,
+} from '../../gen/gqlClient'
+import styles from './MccrsId.module.scss'
+
+export interface MccrsIdFormValues {
+ mccrsId: number | undefined
+}
+type FormError =
+ FormikErrors[keyof FormikErrors]
+
+type RouteParams = {
+ id: string
+}
+
+export const MccrsId = (): React.ReactElement => {
+ const [shouldValidate, setShouldValidate] = React.useState(true)
+ const { id } = useParams()
+ if (!id) {
+ throw new Error(
+ 'PROGRAMMING ERROR: id param not set in state submission form.'
+ )
+ }
+ const navigate = useNavigate()
+ const { pkg, packageName } = useOutletContext()
+
+ // page context
+ const { updateHeading } = usePage()
+
+ const customHeading = useMemo(() => {
+ return (
+
+ {packageName}
+ MC-CRS record number
+
+ )
+ }, [packageName])
+ useEffect(() => {
+ updateHeading({ customHeading })
+ }, [customHeading, updateHeading])
+
+ const [showPageErrorMessage, setShowPageErrorMessage] = useState<
+ boolean | string
+ >(false) // string is a custom error message, defaults to generic of true
+
+ const [updateFormData] = useUpdateContractMutation()
+
+ const mccrsIDInitialValues: MccrsIdFormValues = {
+ mccrsId: pkg.mccrsID ? Number(pkg.mccrsID) : undefined,
+ }
+
+ const showFieldErrors = (error?: FormError) =>
+ shouldValidate && Boolean(error)
+
+ const handleFormSubmit = async (values: MccrsIdFormValues) => {
+ setShowPageErrorMessage(false)
+ try {
+ const updateResult = await updateFormData({
+ variables: {
+ input: {
+ mccrsID: values?.mccrsId?.toString(),
+ id: id,
+ },
+ },
+ })
+
+ const updatedSubmission: HealthPlanPackage | undefined =
+ updateResult?.data?.updateContract.pkg
+
+ if (!updatedSubmission) {
+ setShowPageErrorMessage(true)
+
+ console.info('Failed to update form data', updateResult)
+ recordJSException(
+ `MCCRSIDForm: Apollo error reported. Error message: Failed to update form data ${updateResult}`
+ )
+ return new Error('Failed to update form data')
+ }
+ navigate(`/submissions/${updatedSubmission.id}`)
+ } catch (serverError) {
+ setShowPageErrorMessage(true)
+ recordJSException(
+ `MCCRSIDForm: Apollo error reported. Error message: ${serverError.message}`
+ )
+ return new Error(serverError)
+ }
+ }
+
+ return (
+ <>
+ {showPageErrorMessage && }
+ {
+ return handleFormSubmit(values)
+ }}
+ validationSchema={() => MccrsIdFormSchema()}
+ >
+ {({ values, errors, handleSubmit, isSubmitting }) => (
+ <>
+ {
+ setShouldValidate(true)
+ handleSubmit(e)
+ }}
+ >
+
+
+ handleFormSubmit(values)}
+ animationTimeout={1000}
+ loading={
+ isSubmitting &&
+ shouldValidate &&
+ !errors.mccrsId
+ }
+ >
+ Save MC-CRS number
+
+
+
+ >
+ )}
+
+ >
+ )
+}
diff --git a/services/app-web/src/pages/MccrsId/MccrsIdSchema.ts b/services/app-web/src/pages/MccrsId/MccrsIdSchema.ts
new file mode 100644
index 0000000000..e1cff27b23
--- /dev/null
+++ b/services/app-web/src/pages/MccrsId/MccrsIdSchema.ts
@@ -0,0 +1,7 @@
+import * as Yup from 'yup'
+
+export const MccrsIdFormSchema = () => {
+ return Yup.object().shape({
+ mccrsId: Yup.number().typeError('You must enter a number'),
+ })
+}
diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss
index b428edeae3..023f272cea 100644
--- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss
+++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss
@@ -17,6 +17,21 @@
padding: 2rem 0;
}
+.subHeader {
+ margin-top: 17.5px;
+ span {
+ font-weight: bold;
+ a {
+ display: inline;
+ margin-left: 5px;
+ }
+
+ }
+ .editLink {
+ margin-top: 0;
+ }
+}
+
.banner {
margin: units(2) auto;
}
diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx
index 25d888500c..ffc0d84e57 100644
--- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx
@@ -22,7 +22,6 @@ import {
} from '../../testHelpers/jestHelpers'
import { SubmissionSummary } from './SubmissionSummary'
import { SubmissionSideNav } from '../SubmissionSideNav'
-import React from 'react'
import { testS3Client } from '../../testHelpers/s3Helpers'
describe('SubmissionSummary', () => {
@@ -161,6 +160,78 @@ describe('SubmissionSummary', () => {
})
})
+ it('renders add mccrs-id link for CMS user', async () => {
+ const submissionsWithRevisions = mockUnlockedHealthPlanPackage()
+ renderWithProviders(
+
+ }>
+ }
+ />
+
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ fetchStateHealthPlanPackageWithQuestionsMockSuccess({
+ stateSubmission: submissionsWithRevisions,
+ id: '15',
+ }),
+ ],
+ },
+ routerProvider: {
+ route: '/submissions/15',
+ },
+ }
+ )
+ await waitFor(() => {
+ expect(
+ screen.getByText('Add MC-CRS record number')
+ ).toBeInTheDocument()
+ })
+ })
+
+ it('does not render an add mccrs-id link for state user', async () => {
+ const submissionsWithRevisions = mockUnlockedHealthPlanPackage()
+ renderWithProviders(
+
+ }>
+ }
+ />
+
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidUser(),
+ statusCode: 200,
+ }),
+ fetchStateHealthPlanPackageWithQuestionsMockSuccess({
+ stateSubmission: submissionsWithRevisions,
+ id: '15',
+ }),
+ ],
+ },
+ routerProvider: {
+ route: '/submissions/15',
+ },
+ }
+ )
+ await waitFor(() => {
+ expect(
+ screen.queryByText('Add MC-CRS record number')
+ ).not.toBeInTheDocument()
+ })
+ })
+
it('renders submission unlocked banner for State user', async () => {
const submissionsWithRevisions = mockUnlockedHealthPlanPackage()
renderWithProviders(
diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx
index afdaf632d3..21fed8f71c 100644
--- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.tsx
@@ -161,6 +161,35 @@ export const SubmissionSummary = (): React.ReactElement => {
)}
+ {pkg.mccrsID && (
+
+ MC-CRS record number:
+
+ {pkg.mccrsID}
+
+
+ )}
+
+ {pkg.mccrsID
+ ? 'Edit MC-CRS number'
+ : 'Add MC-CRS record number'}
+
+
+ ) : undefined
+ }
submission={packageData}
submissionName={name}
headerChildComponent={