diff --git a/services/app-graphql/src/queries/fetchContract.graphql b/services/app-graphql/src/queries/fetchContract.graphql index 788ee46f82..0a7ee0f023 100644 --- a/services/app-graphql/src/queries/fetchContract.graphql +++ b/services/app-graphql/src/queries/fetchContract.graphql @@ -1,7 +1,7 @@ query fetchContract($input: FetchContractInput!) { fetchContract(input: $input) { contract { - ...contractFields + ...contractFieldsFetchContract draftRevision { ...contractRevisionFragment @@ -26,13 +26,14 @@ query fetchContract($input: FetchContractInput!) { } } -fragment contractFields on Contract { +fragment contractFieldsFetchContract on Contract { id status createdAt updatedAt initiallySubmittedAt stateCode + mccrsID state { code name diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index fb3e2d29ab..19fc776b0c 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1415,6 +1415,7 @@ type Contract { This value is used to generate the contractName """ stateNumber: Int! + mccrsID: String "draftRevision is the currently modifiable revision if the rate is DRAFT or UNLOCKED" draftRevision: ContractRevision diff --git a/services/app-web/src/components/ChangeHistory/ChangeHistoryV2.tsx b/services/app-web/src/components/ChangeHistory/ChangeHistoryV2.tsx new file mode 100644 index 0000000000..c76edd1b40 --- /dev/null +++ b/services/app-web/src/components/ChangeHistory/ChangeHistoryV2.tsx @@ -0,0 +1,137 @@ +import React from 'react' +import { dayjs } from '../../common-code/dateHelpers/dayjs' +import { SectionHeader } from '../SectionHeader' +import { Accordion, Link } from '@trussworks/react-uswds' +import type { AccordionItemProps } from '@trussworks/react-uswds/lib/components/Accordion/Accordion' +import { UpdateInformation, Contract } from '../../gen/gqlClient' +import styles from './ChangeHistory.module.scss' +type ChangeHistoryProps = { + contract: Contract +} + +type flatRevisions = UpdateInformation & { + kind: 'submit' | 'unlock' + revisionVersion: string | undefined +} + +export const ChangeHistoryV2 = ({ + contract, +}: ChangeHistoryProps): React.ReactElement => { + const flattenedRevisions = (): flatRevisions[] => { + const result: flatRevisions[] = [] + const contractSubmissions = contract.packageSubmissions.filter( + (submission) => { + return submission.cause === 'CONTRACT_SUBMISSION' + } + ) + //Reverse revisions to order from earliest to latest revision. This is to correctly set version for each + // contract & recontract. + const reversedRevisions = [...contractSubmissions].reverse() + reversedRevisions.forEach((r, index) => { + if (r.contractRevision.unlockInfo) { + const newUnlock: flatRevisions = {} as flatRevisions + newUnlock.updatedAt = r.contractRevision.unlockInfo.updatedAt + newUnlock.updatedBy = r.contractRevision.unlockInfo.updatedBy + newUnlock.updatedReason = + r.contractRevision.unlockInfo.updatedReason + newUnlock.kind = 'unlock' + //Use unshift to push the latest revision unlock info to the beginning of the array + result.unshift(newUnlock) + } + if (r.submitInfo) { + const newSubmit: flatRevisions = {} as flatRevisions + + const revisionVersion = + index !== contract.packageSubmissions.length - 1 + ? String(index + 1) //Offset version, we want to start at 1 + : undefined + + newSubmit.updatedAt = r.submitInfo.updatedAt + newSubmit.updatedBy = r.submitInfo.updatedBy + newSubmit.updatedReason = r.submitInfo.updatedReason + newSubmit.kind = 'submit' + newSubmit.revisionVersion = revisionVersion + //Use unshift to push the latest revision submit info to the beginning of the array + result.unshift(newSubmit) + } + }) + return result + } + + const revisionHistory = flattenedRevisions() + + const revisedItems: AccordionItemProps[] = revisionHistory.map( + (r, index) => { + const isInitialSubmission = r.updatedReason === 'Initial contract' + const isSubsequentSubmission = r.kind === 'submit' + // We want to know if this contract has multiple submissions. To have multiple submissions, there must be minimum + // more than the initial contract revision. + const hasSubsequentSubmissions = revisionHistory.length > 1 + return { + title: ( +
+ {dayjs + .utc(r.updatedAt) + .tz('America/New_York') + .format('MM/DD/YY h:mma')}{' '} + ET - {isSubsequentSubmission ? 'Submission' : 'Unlock'} +
+ ), + // Display this code if this is the initial contract. We only want to display the link of the initial contract + // only if there has been subsequent contracts. We do not want to display a link if the package initial + // contract was unlocked, but has not been resubmitted yet. + headingLevel: 'h4', + content: isInitialSubmission ? ( +
+ Submitted by: + {r.updatedBy} +
+ {r.revisionVersion && hasSubsequentSubmissions && ( + + View past contract version + + )} +
+ ) : ( +
+
+ + {isSubsequentSubmission + ? 'Submitted by: ' + : 'Unlocked by: '}{' '} + + {r.updatedBy} +
+
+ + {isSubsequentSubmission + ? 'Changes made: ' + : 'Reason for unlock: '} + + {r.updatedReason} +
+ {isSubsequentSubmission && r.revisionVersion && ( + + View past contract version + + )} +
+ ), + expanded: false, + id: r.updatedAt.toString(), + } + } + ) + return ( +
+ + +
+ ) +} diff --git a/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx b/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx index 794f8d56fc..67a9a9afd4 100644 --- a/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx +++ b/services/app-web/src/components/Modal/V2/UnlockSubmitModalV2.tsx @@ -1,39 +1,35 @@ -import { UnlockedHealthPlanFormDataType } from '../../../common-code/healthPlanFormDataType' import React, { useEffect, useState } from 'react' import { FormGroup, ModalRef, Textarea } from '@trussworks/react-uswds' import { useNavigate } from 'react-router-dom' import { - HealthPlanPackage, - useSubmitHealthPlanPackageMutation, - useUnlockHealthPlanPackageMutation, Rate, + Contract, + useSubmitContractMutation, } from '../../../gen/gqlClient' -import { - submitMutationWrapper, - unlockMutationWrapper, -} from '../../../gqlHelpers' import { useFormik } from 'formik' import { usePrevious } from '../../../hooks/usePrevious' import { Modal } from '../Modal' import { PoliteErrorMessage } from '../../PoliteErrorMessage' import * as Yup from 'yup' -import styles from './UnlockSubmitModal.module.scss' +import styles from '../UnlockSubmitModal.module.scss' import { GenericApiErrorProps } from '../../Banner/GenericApiErrorBanner/GenericApiErrorBanner' import { ERROR_MESSAGES } from '../../../constants/errors' +import { submitMutationWrapperV2 } from '../../../gqlHelpers/mutationWrappersForUserFriendlyErrors' -const PACKAGE_UNLOCK_SUBMIT_TYPES = [ - 'SUBMIT_PACKAGE', - 'RESUBMIT_PACKAGE', - 'UNLOCK_PACKAGE', -] as const const RATE_UNLOCK_SUBMIT_TYPES = [ 'SUBMIT_RATE', 'RESUBMIT_RATE', 'UNLOCK_RATE', ] as const -type PackageModalType = (typeof PACKAGE_UNLOCK_SUBMIT_TYPES)[number] +const CONTRACT_UNLOCK_SUBMIT_TYPES = [ + 'SUBMIT_CONTRACT', + 'RESUBMIT_CONTRACT', + 'UNLOCK_CONTRACT', +] as const + type RateModalType = (typeof RATE_UNLOCK_SUBMIT_TYPES)[number] -type SharedModalType = PackageModalType & RateModalType +type ContractModalType = (typeof CONTRACT_UNLOCK_SUBMIT_TYPES)[number] +type SharedModalType = ContractModalType & RateModalType type SharedAdditionalProps = { submissionName?: string modalRef: React.RefObject @@ -45,12 +41,12 @@ type RateModalProps = { modalType: RateModalType[number] } & SharedAdditionalProps -type PackageModalProps = { - submissionData: UnlockedHealthPlanFormDataType | HealthPlanPackage - modalType: PackageModalType +type ContractModalProps = { + submissionData: Contract + modalType: ContractModalType[number] } & SharedAdditionalProps -type UnlockSubmitModalProps = PackageModalProps | RateModalProps +type UnlockSubmitModalProps = RateModalProps | ContractModalProps type ModalValueType = { modalHeading?: string @@ -64,38 +60,22 @@ type ModalValueType = { const modalValueDictionary: { [Property in SharedModalType]: ModalValueType } = { - RESUBMIT_PACKAGE: { + RESUBMIT_RATE: { modalHeading: 'Summarize changes', onSubmitText: 'Resubmit', modalDescription: - 'Once you submit, this package will be sent to CMS for review and you will no longer be able to make changes.', - inputHint: 'Provide summary of all changes made to this submission', + 'Once you submit, this rate will be sent to CMS for review and you will no longer be able to make changes.', + inputHint: 'Provide summary of all changes made to this rate', unlockSubmitModalInputValidation: 'You must provide a summary of changes', errorHeading: ERROR_MESSAGES.resubmit_error_heading, }, - UNLOCK_PACKAGE: { - modalHeading: 'Reason for unlocking submission', - onSubmitText: 'Unlock', - inputHint: 'Provide reason for unlocking', - unlockSubmitModalInputValidation: - 'You must provide a reason for unlocking this submission', - errorHeading: ERROR_MESSAGES.unlock_error_heading, - }, - SUBMIT_PACKAGE: { - modalHeading: 'Ready to submit?', - onSubmitText: 'Submit', - modalDescription: - 'Submitting this package will send it to CMS to begin their review.', - errorHeading: ERROR_MESSAGES.submit_error_heading, - errorSuggestion: ERROR_MESSAGES.submit_error_suggestion, - }, - RESUBMIT_RATE: { + RESUBMIT_CONTRACT: { modalHeading: 'Summarize changes', onSubmitText: 'Resubmit', modalDescription: - 'Once you submit, this rate will be sent to CMS for review and you will no longer be able to make changes.', - inputHint: 'Provide summary of all changes made to this rate', + 'Once you submit, this contract will be sent to CMS for review and you will no longer be able to make changes.', + inputHint: 'Provide summary of all changes made to this contract', unlockSubmitModalInputValidation: 'You must provide a summary of changes', errorHeading: ERROR_MESSAGES.resubmit_error_heading, @@ -108,6 +88,14 @@ const modalValueDictionary: { [Property in SharedModalType]: ModalValueType } = 'You must provide a reason for unlocking this rate', errorHeading: ERROR_MESSAGES.unlock_error_heading, }, + UNLOCK_CONTRACT: { + modalHeading: 'Reason for unlocking rate', + onSubmitText: 'Unlock', + inputHint: 'Provide reason for unlocking', + unlockSubmitModalInputValidation: + 'You must provide a reason for unlocking this contract', + errorHeading: ERROR_MESSAGES.unlock_error_heading, + }, SUBMIT_RATE: { modalHeading: 'Ready to submit?', onSubmitText: 'Submit', @@ -116,6 +104,14 @@ const modalValueDictionary: { [Property in SharedModalType]: ModalValueType } = errorHeading: ERROR_MESSAGES.submit_error_heading, errorSuggestion: ERROR_MESSAGES.submit_error_suggestion, }, + SUBMIT_CONTRACT: { + modalHeading: 'Ready to submit?', + onSubmitText: 'Submit', + modalDescription: + 'Submitting this contract will send it to CMS to begin their review.', + errorHeading: ERROR_MESSAGES.submit_error_heading, + errorSuggestion: ERROR_MESSAGES.submit_error_suggestion, + }, } export const UnlockSubmitModalV2 = ({ @@ -138,12 +134,11 @@ export const UnlockSubmitModalV2 = ({ unlockSubmitModalInput: '', } - const [submitHealthPlanPackage, { loading: submitMutationLoading }] = - useSubmitHealthPlanPackageMutation() // TODO this should be submitContract - linked rates epic - const [unlockHealthPlanPackage, { loading: unlockMutationLoading }] = - useUnlockHealthPlanPackageMutation() // TODO this should be unlockContract - linked rates epic + const [submitContract, { loading: submitContractLoading }] = + useSubmitContractMutation() // TODO this should be unlockContract - linked rates epic // TODO submitRate and unlockRate should also be set up here - nunlock and edit rate epic + // TODO unlockContract should also be set up here - nunlock and edit rate epic const formik = useFormik({ initialValues: modalFormInitialValues, validationSchema: Yup.object().shape({ @@ -155,13 +150,11 @@ export const UnlockSubmitModalV2 = ({ }) const mutationLoading = - modalType === 'UNLOCK_PACKAGE' - ? unlockMutationLoading - : submitMutationLoading + modalType === 'SUBMIT_CONTRACT' && submitContractLoading const isSubmitting = mutationLoading || formik.isSubmitting const includesFormInput = - modalType === 'UNLOCK_PACKAGE' || - modalType === 'RESUBMIT_PACKAGE' || + modalType === 'UNLOCK_CONTRACT' || + modalType === 'RESUBMIT_CONTRACT' || modalType === 'UNLOCK_RATE' || modalType === 'RESUBMIT_RATE' @@ -180,24 +173,6 @@ export const UnlockSubmitModalV2 = ({ let result switch (modalType) { - case 'UNLOCK_PACKAGE': - if (unlockSubmitModalInput) { - await unlockMutationWrapper( - unlockHealthPlanPackage, - submissionData.id, - unlockSubmitModalInput - ) - } - break - - case 'SUBMIT_PACKAGE' || 'RESUBMIT_PACKAGE': - result = await submitMutationWrapper( - submitHealthPlanPackage, - submissionData.id, - unlockSubmitModalInput - ) - break - case 'UNLOCK_RATE': console.info('unlock rate not implemented yet') break @@ -205,25 +180,23 @@ export const UnlockSubmitModalV2 = ({ case 'SUBMIT_RATE' || 'RESUBMIT_RATE': console.info('submit/resubmit rate not implemented yet') break + case 'SUBMIT_CONTRACT': + result = await submitMutationWrapperV2( + submitContract, + submissionData.id, + unlockSubmitModalInput + ) + break + case 'RESUBMIT_CONTRACT': + result = await submitMutationWrapperV2( + submitContract, + submissionData.id, + unlockSubmitModalInput + ) + break } - //Allow submitting/unlocking to continue on EMAIL_ERROR. - if (result instanceof Error && result.cause === 'EMAIL_ERROR') { - modalRef.current?.toggleModal(undefined, false) - - if ( - (modalType === 'SUBMIT_PACKAGE' || - modalType === 'RESUBMIT_PACKAGE') && - submissionName - ) { - // TODO make sure dashboard data is up to date - navigate( - `/dashboard/submissions?justSubmitted=${submissionName}` - ) - } else if (modalType === 'UNLOCK_PACKAGE') { - // TODO make sure on unlock the submission banners are up to date - } - } else if (result instanceof Error) { + if (result instanceof Error) { setModalAlert({ heading: modalValues.errorHeading, message: result.message, @@ -237,8 +210,8 @@ export const UnlockSubmitModalV2 = ({ } else { modalRef.current?.toggleModal(undefined, false) if ( - (modalType === 'SUBMIT_PACKAGE' || - modalType === 'RESUBMIT_PACKAGE') && + (modalType === 'RESUBMIT_CONTRACT' || + modalType === 'SUBMIT_CONTRACT') && submissionName ) { navigate( diff --git a/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts b/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts index f47dc12db7..13319047dd 100644 --- a/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts +++ b/services/app-web/src/gqlHelpers/mutationWrappersForUserFriendlyErrors.ts @@ -13,6 +13,8 @@ import { Division, CreateQuestionInput, CreateQuestionResponseInput, + SubmitContractMutationFn, + Contract, } from '../gen/gqlClient' import { ApolloError, GraphQLErrors } from '@apollo/client/errors' @@ -154,6 +156,42 @@ export const submitMutationWrapper = async ( } } +export const submitMutationWrapperV2 = async ( + submitContract: SubmitContractMutationFn, + id: string, + submittedReason?: string +): Promise | GraphQLErrors | Error> => { + const input = { contractID: id } + + if (submittedReason) { + Object.assign(input, { + submittedReason, + }) + } + + try { + const { data } = await submitContract({ + variables: { + input, + }, + }) + + if (data?.submitContract.contract) { + return data.submitContract.contract + } else { + recordJSException( + `[UNEXPECTED]: Error attempting to submit, no data present but returning 200.` + ) + return new Error(ERROR_MESSAGES.submit_error_generic) + } + } catch (error) { + return handleApolloErrorsAndAddUserFacingMessages( + error, + 'SUBMIT_HEALTH_PLAN_PACKAGE' + ) + } +} + /** * Manually updating the cache for Q&A mutations because the Q&A page is in a layout route that is not unmounted during the Q&A * workflow. So, when calling Q&A mutations the Q&A page will not refetch the data. The alternative would be to use diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index 4c88255134..98d894be85 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -25,6 +25,7 @@ import { Landing } from '../Landing/Landing' import { MccrsId } from '../MccrsId/MccrsId' import { NewStateSubmissionForm, StateSubmissionForm } from '../StateSubmission' import { SubmissionSummary } from '../SubmissionSummary' +import { SubmissionSummaryV2 } from '../SubmissionSummary/V2/SubmissionSummaryV2' import { SubmissionRevisionSummary } from '../SubmissionRevisionSummary' import { useScrollToPageTop } from '../../hooks/useScrollToPageTop' import { featureFlags } from '../../common-code/featureFlags' @@ -81,6 +82,10 @@ const StateUserRoutes = ({ featureFlags.RATE_EDIT_UNLOCK.flag, featureFlags.RATE_EDIT_UNLOCK.defaultValue ) + const useLinkedRates = ldClient?.variation( + featureFlags.LINK_RATES.flag, + featureFlags.LINK_RATES.defaultValue + ) return ( @@ -137,7 +142,13 @@ const StateUserRoutes = ({ )} } + element={ + useLinkedRates ? ( + + ) : ( + + ) + } /> { + const ldClient = useLDClient() + const useLinkedRates = ldClient?.variation( + featureFlags.LINK_RATES.flag, + featureFlags.LINK_RATES.defaultValue + ) return ( @@ -216,7 +232,13 @@ const CMSUserRoutes = ({ )} } + element={ + useLinkedRates ? ( + + ) : ( + + ) + } /> diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx index 430e1a6eb3..29f85dc0fe 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2.tsx @@ -68,7 +68,8 @@ export const ContactsSummarySection = ({
- {contractFormData && contractFormData.stateContacts.length > 0 ? ( + {contractFormData && + contractFormData.stateContacts.length > 0 ? ( contractFormData?.stateContacts.map( (stateContact, index) => ( { - return rateFormData?.packagesWithSharedRateCerts?.map( + return rateFormData.packagesWithSharedRateCerts?.map( ({ packageId, packageName }) => { const refreshedName = packageId && @@ -115,8 +115,9 @@ export const RateDetailsSummarySectionV2 = ({ const rateCapitationType = (rate: Rate | RateRevision) => { const rateFormData = getRateFormData(rate) - return rateFormData?.rateCapitationType - ? rateFormData?.rateCapitationType === 'RATE_CELL' + if (!rateFormData) return + return rateFormData.rateCapitationType + ? rateFormData.rateCapitationType === 'RATE_CELL' ? 'Certification of capitation rates specific to each rate cell' : 'Certification of rate ranges of capitation rates per rate cell' : '' @@ -126,12 +127,12 @@ export const RateDetailsSummarySectionV2 = ({ /* if we have rateProgramIDs, use them, otherwise use programIDs */ let programIDs = [] as string[] const rateFormData = getRateFormData(rate) - + if (!rateFormData) return if ( - rateFormData?.rateProgramIDs && - rateFormData?.rateProgramIDs.length > 0 + rateFormData.rateProgramIDs && + rateFormData.rateProgramIDs.length > 0 ) { - programIDs = rateFormData?.rateProgramIDs + programIDs = rateFormData.rateProgramIDs } else if ( contractFormData?.programIDs && contractFormData?.programIDs.length > 0 @@ -147,10 +148,11 @@ export const RateDetailsSummarySectionV2 = ({ const rateCertificationType = (rate: Rate | RateRevision) => { const rateFormData = getRateFormData(rate) - if (rateFormData?.rateType === 'AMENDMENT') { + if (!rateFormData) return + if (rateFormData.rateType === 'AMENDMENT') { return 'Amendment to prior rate certification' } - if (rateFormData?.rateType === 'NEW') { + if (rateFormData.rateType === 'NEW') { return 'New rate certification' } } @@ -232,19 +234,22 @@ export const RateDetailsSummarySectionV2 = ({ !isPreviousSubmission && renderDownloadButton(zippedFilesURL)} - {rates.length > 0 ? ( + {rates && rates.length > 0 ? ( rates.map((rate) => { const rateFormData = getRateFormData(rate) + if (!rateFormData) { + return + } return (

- {rateFormData?.rateCertificationName} + {rateFormData.rateCertificationName}

@@ -265,19 +270,19 @@ export const RateDetailsSummarySectionV2 = ({ @@ -287,29 +292,29 @@ export const RateDetailsSummarySectionV2 = ({ - {rateFormData?.amendmentEffectiveDateStart ? ( + {rateFormData.amendmentEffectiveDateStart ? ( ) : null} {rateFormData - ?.certifyingActuaryContacts[0] && ( + .certifyingActuaryContacts[0] && (
- {rateFormData?.rateDocuments && ( + {rateFormData.rateDocuments && ( )} - {rateFormData?.supportingDocuments && ( + {rateFormData.supportingDocuments && ( { }) expect(screen.getByTestId('form-submit')).toBeDefined() - expect(screen.getByText('Submit')).toBeInTheDocument() - await userClickByRole(screen, 'button', { name: 'Submit' }) + expect(screen.getAllByText('Submit')).toHaveLength(2) + await screen.getAllByText('Submit')[0].click() }) }) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx index 9993eb470b..06b8de606c 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/ReviewSubmitV2.tsx @@ -14,6 +14,7 @@ import { RoutesRecord, STATE_SUBMISSION_FORM_ROUTES, } from '../../../../../constants' +import { UnlockSubmitModalV2 } from '../../../../../components/Modal/V2/UnlockSubmitModalV2' import { getLatestContractFormData } from '../../../../../gqlHelpers/contractsAndRates' import { useAuth } from '../../../../../contexts/AuthContext' import { RateDetailsSummarySectionV2 } from './RateDetailsSummarySectionV2' @@ -32,11 +33,11 @@ import { usePage } from '../../../../../contexts/PageContext' export const ReviewSubmitV2 = (): React.ReactElement => { const navigate = useNavigate() const modalRef = useRef(null) - const [isSubmitting] = useState(false) const statePrograms = useStatePrograms() const { loggedInUser } = useAuth() const { updateHeading } = usePage() const { id } = useRouteParams() + const [isSubmitting, setIsSubmitting] = useState(false) const { data, loading, error } = useFetchContractQuery({ variables: { @@ -91,6 +92,7 @@ export const ReviewSubmitV2 = (): React.ReactElement => { contractFormData.programIDs, programs ) || '' + return ( <>
@@ -164,14 +166,18 @@ export const ReviewSubmitV2 = (): React.ReactElement => { - {/* // if the session is expiring, close this modal so the countdown modal can appear - */} + /> ) diff --git a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx index 18c70e37ea..bf31fc0259 100644 --- a/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx +++ b/services/app-web/src/pages/StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2.tsx @@ -8,9 +8,8 @@ import { ContractTypeRecord, PopulationCoveredRecord, } from '../../../../../constants/healthPlanPackages' -import { - getLastContractSubmission, -} from '../../../../../gqlHelpers/contractsAndRates' +import { GenericErrorPage } from '../../../../Errors/GenericErrorPage' +import { getLatestContractFormData } from '../../../../../gqlHelpers/contractsAndRates' import { Program, Contract } from '../../../../../gen/gqlClient' import { usePreviousSubmission } from '../../../../../hooks/usePreviousSubmission' import { booleanAsYesNoUserValue } from '../../../../../components/Form/FieldYesNo/FieldYesNo' @@ -37,15 +36,14 @@ export const SubmissionTypeSummarySectionV2 = ({ submissionName, }: SubmissionTypeSummarySectionV2Props): React.ReactElement => { const isPreviousSubmission = usePreviousSubmission() - const contractFormData = - contract.draftRevision?.formData || - getLastContractSubmission(contract)?.contractRevision.formData + const contractFormData = getLatestContractFormData(contract) + if (!contractFormData) return const programNames = statePrograms - .filter((p) => contractFormData?.programIDs.includes(p.id)) + .filter((p) => contractFormData.programIDs.includes(p.id)) .map((p) => p.name) const isSubmitted = contract.status === 'SUBMITTED' - // const isRiskBasedContract = contractFormData.riskBasedContract === + return ( {contractFormData && ( - )} - {( - + id="submissionType" + label="Submission type" + explainMissingData={!isSubmitted} + children={ + SubmissionTypeRecord[ + contractFormData.submissionType + ] + } + /> )} - {contractFormData && contractFormData?.riskBasedContract !== null && ( + { - )} + } + {contractFormData && + contractFormData.riskBasedContract !== null && ( + + )} {contractFormData && ( + id="populationCoverage" + label="Which populations does this contract action cover?" + explainMissingData={!isSubmitted} + children={ + contractFormData.populationCovered && + PopulationCoveredRecord[ + contractFormData.populationCovered + ] + } + /> )} @@ -137,11 +136,13 @@ export const SubmissionTypeSummarySectionV2 = ({ {contractFormData && ( + id="submissionDescription" + label="Submission description" + explainMissingData={!isSubmitted} + children={ + contractFormData.submissionDescription + } + /> )} diff --git a/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx b/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx new file mode 100644 index 0000000000..0bbcd1d6c5 --- /dev/null +++ b/services/app-web/src/pages/SubmissionSummary/V2/SubmissionSummaryV2.tsx @@ -0,0 +1,299 @@ +import { + GridContainer, + Icon, + Link, + ModalRef, + ModalToggleButton, +} from '@trussworks/react-uswds' +import React, { useEffect, useRef, useState } from 'react' +import { NavLink } from 'react-router-dom' +import { useAuth } from '../../../contexts/AuthContext' +import { packageName } from '../../../common-code/healthPlanFormDataType' +import { ContractDetailsSummarySectionV2 } from '../../StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContractDetailsSummarySectionV2' +import { ContactsSummarySection } from '../../StateSubmission/ReviewSubmit/V2/ReviewSubmit/ContactsSummarySectionV2' +import { RateDetailsSummarySectionV2 } from '../../StateSubmission/ReviewSubmit/V2/ReviewSubmit/RateDetailsSummarySectionV2' +import { SubmissionTypeSummarySectionV2 } from '../../StateSubmission/ReviewSubmit/V2/ReviewSubmit/SubmissionTypeSummarySectionV2' +import { + SubmissionUnlockedBanner, + SubmissionUpdatedBanner, + DocumentWarningBanner, +} from '../../../components' +import { Loading } from '../../../components' +import { usePage } from '../../../contexts/PageContext' +import { + useFetchContractQuery, + UpdateInformation, +} from '../../../gen/gqlClient' +import { ErrorForbiddenPage } from '../../Errors/ErrorForbiddenPage' +import { Error404 } from '../../Errors/Error404Page' +import { GenericErrorPage } from '../../Errors/GenericErrorPage' +import styles from '../SubmissionSummary.module.scss' +import { ChangeHistoryV2 } from '../../../components/ChangeHistory/ChangeHistoryV2' +import { UnlockSubmitModalV2 } from '../../../components/Modal/V2/UnlockSubmitModalV2' +import { useLDClient } from 'launchdarkly-react-client-sdk' +import { featureFlags } from '../../../common-code/featureFlags' +import { RoutesRecord } from '../../../constants' +import { useRouteParams } from '../../../hooks' + +function UnlockModalButton({ + disabled, + modalRef, +}: { + disabled: boolean + modalRef: React.RefObject +}) { + return ( + + Unlock submission + + ) +} + +export const SubmissionSummaryV2 = (): React.ReactElement => { + // Page level state + const { updateHeading } = usePage() + const modalRef = useRef(null) + const [pkgName, setPkgName] = useState(undefined) + const [documentError, setDocumentError] = useState(false) + const { loggedInUser } = useAuth() + + useEffect(() => { + updateHeading({ customHeading: pkgName }) + }, [pkgName, updateHeading]) + const { id } = useRouteParams() + + const ldClient = useLDClient() + const showQuestionResponse = ldClient?.variation( + featureFlags.CMS_QUESTIONS.flag, + featureFlags.CMS_QUESTIONS.defaultValue + ) + + // API requests + const { + data: fetchContractData, + loading: fetchContractLoading, + error: fetchContractError, + } = useFetchContractQuery({ + variables: { + input: { + contractID: id ?? 'unknown-contract', + }, + }, + }) + const contract = fetchContractData?.fetchContract.contract + if (fetchContractLoading) { + return ( + + + + ) + } else if (fetchContractError || !contract) { + //error handling for a state user that tries to access rates for a different state + if ( + fetchContractError?.graphQLErrors[0]?.extensions?.code === + 'FORBIDDEN' + ) { + return ( + + ) + } else if ( + fetchContractError?.graphQLErrors[0]?.extensions?.code === + 'NOT_FOUND' + ) { + return + } else { + return + } + } + const isCMSUser = loggedInUser?.role === 'CMS_USER' + const submissionStatus = contract.status + const statePrograms = contract.state.programs + const contractFormData = + contract.draftRevision?.formData || + contract.packageSubmissions[0].contractRevision.formData + const programIDs = contractFormData.programIDs + const programs = statePrograms.filter((program) => + programIDs.includes(program.id) + ) + // set the page heading + const name = packageName( + contract.stateCode, + contract.stateNumber, + contractFormData.programIDs, + programs + ) + if (pkgName !== name) { + setPkgName(name) + } + + // Get the correct update info depending on the submission status + let updateInfo: UpdateInformation | undefined = undefined + if (submissionStatus === 'UNLOCKED' || submissionStatus === 'RESUBMITTED') { + updateInfo = + (submissionStatus === 'UNLOCKED' + ? contract.packageSubmissions[0].submittedRevisions.find( + (rev) => rev.unlockInfo + )?.unlockInfo + : contract.packageSubmissions[0].contractRevision.submitInfo) || + undefined + } + + const isContractActionAndRateCertification = + contractFormData?.submissionType === 'CONTRACT_AND_RATES' + + const handleDocumentDownloadError = (error: boolean) => + setDocumentError(error) + + const editOrAddMCCRSID = contract.mccrsID + ? 'Edit MC-CRS number' + : 'Add MC-CRS record number' + + return ( +
+
+ This is the V2 page of the SubmissionSummary +
+ + {submissionStatus === 'UNLOCKED' && updateInfo && ( + + )} + + {submissionStatus === 'RESUBMITTED' && updateInfo && ( + + )} + + {documentError && ( + + )} + + {!showQuestionResponse && ( + + + {loggedInUser?.__typename === 'StateUser' ? ( +  Back to state dashboard + ) : ( +  Back to dashboard + )} + + )} + + {contract && statePrograms && ( + + {contract.mccrsID && ( + + MC-CRS record number: + + {contract.mccrsID} + + + )} + + {editOrAddMCCRSID} + +
+ ) : undefined + } + contract={contract} + submissionName={name} + headerChildComponent={ + isCMSUser ? ( + + ) : undefined + } + statePrograms={statePrograms} + initiallySubmittedAt={contract.initiallySubmittedAt} + /> + )} + + {contract && ( + + )} + + {contract && + statePrograms && + isContractActionAndRateCertification && ( + + )} + + {contract && } + + {contract && } + {contract && ( + + )} + +
+ ) +} + +export type SectionHeaderProps = { + header: string + submissionName?: boolean + href: string +} diff --git a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts index 83b77da14f..bc7166e547 100644 --- a/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/contractPackageDataMock.ts @@ -216,8 +216,10 @@ function mockContractWithLinkedRateDraft( revisions: [], state: mockMNState(), stateNumber: 5, + parentContractID: 'foo-baz', draftRevision: { id: '123', + rateID: '456', contractRevisions: [], createdAt: new Date(), updatedAt: new Date(), @@ -384,4 +386,4 @@ function mockContractPackageSubmitted( } } -export { mockContractPackageDraft, mockContractPackageSubmitted, mockContractWithLinkedRateDraft } +export { mockContractPackageDraft, mockContractPackageSubmitted, mockContractWithLinkedRateDraft } \ No newline at end of file