From a4e24450f39cd693dc70756e3ad42f2841bd2537 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Sun, 28 Jan 2024 21:49:26 -0600 Subject: [PATCH 01/26] Start to add RateDetailsV2 --- .../app-web/src/formHelpers/formatters.ts | 5 +- .../Contacts/ActuaryContactFields.tsx | 3 +- .../RateDetails/RateDetailsV2.tsx | 409 ++++++++++++++ .../SingleRateCert/SingleRateCertV2.tsx | 531 ++++++++++++++++++ 4 files changed, 945 insertions(+), 3 deletions(-) create mode 100644 services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx create mode 100644 services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index a3a42e4395..9f32d2ae61 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -4,6 +4,7 @@ import { ActuaryContact, } from '../common-code/healthPlanFormDataType' import { FileItemT } from '../components' +import { GenericDocument } from '../gen/gqlClient' import { S3ClientT } from '../s3' import { v4 as uuidv4 } from 'uuid' @@ -49,7 +50,7 @@ const formatForForm = ( } } -const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[]) => { +const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[] | GQLActuaryContact) => { return actuaryContacts && actuaryContacts.length > 0 ? actuaryContacts : [ @@ -109,7 +110,7 @@ const formatDocumentsForForm = ({ documents, getKey, }: { - documents?: SubmissionDocument[] + documents?: SubmissionDocument[] | GenericDocument[] getKey: S3ClientT['getKey'] // S3 function to call when formatting to double check we have valid documents, probably the backend should be doing this to reduce client async errors handling with bad data }): FileItemT[] => { if (!documents) return [] diff --git a/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx b/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx index c8058bb67c..f0c50a2a17 100644 --- a/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx +++ b/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx @@ -6,12 +6,13 @@ import { FieldRadio, FieldTextInput } from '../../../components/Form' import { PoliteErrorMessage } from '../../../components/PoliteErrorMessage' import { RateCertFormType } from '../RateDetails/SingleRateCert/SingleRateCert' import styles from '../StateSubmissionForm.module.scss' +import { ActuaryContact as ActuaryContactGQL } from '../../../gen/gqlClient' type FormError = FormikErrors[keyof FormikErrors] type ActuaryFormPropType = { - actuaryContact: ActuaryContact + actuaryContact: ActuaryContact | ActuaryContactGQL // GQl type for v2 API errors: FormikErrors shouldValidate: boolean fieldNamePrefix: string diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx new file mode 100644 index 0000000000..e87b67d5d9 --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -0,0 +1,409 @@ +import React, { useEffect } from 'react' +import { Form as UswdsForm } from '@trussworks/react-uswds' +import { FieldArray, FieldArrayRenderProps, Formik, FormikErrors } from 'formik' +import { useNavigate } from 'react-router-dom' +import { v4 as uuidv4 } from 'uuid' + +import styles from '../StateSubmissionForm.module.scss' + +import { RateInfoType } from '../../../common-code/healthPlanFormDataType' + +import { ErrorSummary } from '../../../components' +import { formatFormDateForDomain } from '../../../formHelpers' +import { RateDetailsFormSchema } from './RateDetailsSchema' +import { PageActions } from '../PageActions' +import type { HealthPlanFormPageProps } from '../StateSubmissionForm' +import { useFocus } from '../../../hooks' + +import { + formatActuaryContactsForForm, + formatDocumentsForDomain, + formatDocumentsForForm, + formatForForm, +} from '../../../formHelpers/formatters' +import { + RateCertFormType, + SingleRateCert, +} from './SingleRateCert/SingleRateCert' +import { useS3 } from '../../../contexts/S3Context' +import { S3ClientT } from '../../../s3' +import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' +import { RoutesRecord } from '../../../constants' +import { SectionCard } from '../../../components/SectionCard' +import { Rate, RateRevision } from '../../../gen/gqlClient' + +// This function is used to get initial form values as well return empty form values when we add a new rate or delete a rate +// We need to include the getKey function in params because there are no guarantees currently file is in s3 even if when we load data from API +const generateRateCertFormValues = ( + rateRev: RateRevision, + getKey: S3ClientT['getKey']): RateCertFormType => { + const rateInfo = rateRev.formData + + return { + id: rateRev.id, + key: uuidv4(), + rateType: rateInfo.rateType, + rateCapitationType: rateInfo?.rateCapitationType, + rateDateStart: formatForForm(rateInfo?.rateDateStart), + rateDateEnd: formatForForm(rateInfo?.rateDateEnd), + rateDateCertified: formatForForm(rateInfo?.rateDateCertified), + effectiveDateStart: formatForForm( + rateInfo?.amendmentEffectiveDateStart + ), + effectiveDateEnd: formatForForm( + rateInfo?.amendmentEffectiveDateEnd + ), + rateProgramIDs: rateInfo?.rateProgramIDs ?? [], + rateDocuments: formatDocumentsForForm({ + documents: rateInfo?.rateDocuments, + getKey: getKey, + }), + supportingDocuments:formatDocumentsForForm({ + documents: rateInfo?.supportingDocuments, + getKey: getKey, + }) + actuaryContacts: formatActuaryContactsForForm( + rateInfo.certifyingActuaryContacts + ), + actuaryCommunicationPreference: + rateInfo?.actuaryCommunicationPreference, + packagesWithSharedRateCerts: + rateInfo?.packagesWithSharedRateCerts ?? [], + hasSharedRateCert: + rateInfo?.packagesWithSharedRateCerts === undefined + ? undefined + : (rateInfo?.packagesWithSharedRateCerts && + rateInfo?.packagesWithSharedRateCerts.length) >= 1 + ? 'YES' + : 'NO', + } +} + +interface RateInfoArrayType { + rateInfos: RateCertFormType[] +} + +export const rateErrorHandling = ( + error: string | FormikErrors | undefined +): FormikErrors | undefined => { + if (typeof error === 'string') { + return undefined + } + return error +} + +type RateDetailsV2Props = { + draftRate: Rate + showValidations?: boolean + previousDocuments: string[] + // updateRate: UpdateRateMutation +} +export const RateDetailsV2 = ({ + draftRate, + showValidations = false, + previousDocuments, + // updateRate, +}: RateDetailsV2Props): React.ReactElement => { + const navigate = useNavigate() + const { getKey } = useS3() + + // form validation state management + const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = + React.useState(false) + const errorSummaryHeadingRef = React.useRef(null) + const [shouldValidate, setShouldValidate] = React.useState(showValidations) + + useEffect(() => { + // Focus the error summary heading only if we are displaying + // validation errors and the heading element exists + if (focusErrorSummaryHeading && errorSummaryHeadingRef.current) { + errorSummaryHeadingRef.current.focus() + } + setFocusErrorSummaryHeading(false) + }, [focusErrorSummaryHeading]) + + // multi-rates state management + const [focusNewRate, setFocusNewRate] = React.useState(false) + const newRateNameRef = React.useRef(null) + const [newRateButtonRef, setNewRateButtonFocus] = useFocus() // This ref.current is always the same element + + const rateDetailsFormSchema = RateDetailsFormSchema() + + React.useEffect(() => { + if (focusNewRate) { + newRateNameRef?.current?.focus() + setFocusNewRate(false) + newRateNameRef.current = null + } + }, [focusNewRate]) + + const rateInfosInitialValues: RateInfoArrayType = { + rateInfos: [generateRateCertFormValues()], + } + + const handleFormSubmit = async ( + form: RateInfoArrayType, + setSubmitting: (isSubmitting: boolean) => void, // formik setSubmitting + options: { + shouldValidateDocuments: boolean + redirectPath: string + } + ) => { + const { rateInfos } = form + if (options.shouldValidateDocuments) { + const fileErrorsNeedAttention = rateInfos.some((rateInfo) => + isLoadingOrHasFileErrors( + rateInfo.supportingDocuments.concat(rateInfo.rateDocuments) + ) + ) + if (fileErrorsNeedAttention) { + // make inline field errors visible so user can correct documents, direct user focus to errors, and manually exit formik submit + setShouldValidate(true) + setFocusErrorSummaryHeading(true) + setSubmitting(false) + return + } + } + + const cleanedRateInfos = rateInfos.map((rateInfo) => { + return { + id: rateInfo.id, + rateType: rateInfo.rateType, + rateCapitationType: rateInfo.rateCapitationType, + rateDocuments: formatDocumentsForDomain(rateInfo.rateDocuments), + supportingDocuments: formatDocumentsForDomain( + rateInfo.supportingDocuments + ), + rateDateStart: formatFormDateForDomain(rateInfo.rateDateStart), + rateDateEnd: formatFormDateForDomain(rateInfo.rateDateEnd), + rateDateCertified: formatFormDateForDomain( + rateInfo.rateDateCertified + ), + rateAmendmentInfo: + rateInfo.rateType === 'AMENDMENT' + ? { + effectiveDateStart: formatFormDateForDomain( + rateInfo.effectiveDateStart + ), + effectiveDateEnd: formatFormDateForDomain( + rateInfo.effectiveDateEnd + ), + } + : undefined, + rateProgramIDs: rateInfo.rateProgramIDs, + actuaryContacts: rateInfo.actuaryContacts, + actuaryCommunicationPreference: + rateInfo.actuaryCommunicationPreference, + packagesWithSharedRateCerts: + rateInfo.hasSharedRateCert === 'YES' + ? rateInfo.packagesWithSharedRateCerts + : [], + } + }) + + draftRate.rateInfos = cleanedRateInfos + + try { + const updatedSubmission = await updateRate(draftRate) + if (updatedSubmission instanceof Error) { + setSubmitting(false) + console.info( + 'Error updating draft submission: ', + updatedSubmission + ) + } else if (updatedSubmission) { + navigate(options.redirectPath) + } + } catch (serverError) { + setSubmitting(false) + } + } + + // Due to multi-rates we have extra handling around how error summary apperas + // Error summary object keys will be used as DOM focus point from error-summary. Must be valid html selector + // Error summary object values will be used as messages displays in error summary + const generateErrorSummaryErrors = ( + errors: FormikErrors + ) => { + const rateErrors = errors.rateInfos + const errorObject: { [field: string]: string } = {} + + if (rateErrors && Array.isArray(rateErrors)) { + rateErrors.forEach((rateError, index) => { + if (!rateError) return + + Object.entries(rateError).forEach(([field, value]) => { + if (typeof value === 'string') { + //rateProgramIDs error message needs a # proceeding the key name because this is the only way to be able to link to the Select component element see comments in ErrorSummaryMessage component. + const errorKey = + field === 'rateProgramIDs' || + field === 'packagesWithSharedRateCerts' + ? `#rateInfos.${index}.${field}` + : `rateInfos.${index}.${field}` + errorObject[errorKey] = value + } + // If the field is actuaryContacts then the value should be an array with at least one object of errors + if ( + field === 'actuaryContacts' && + Array.isArray(value) && + Array.length > 0 + ) { + //Currently, rate certifications only have 1 actuary contact + const actuaryContact = value[0] + Object.entries(actuaryContact).forEach( + ([contactField, contactValue]) => { + if (typeof contactValue === 'string') { + const errorKey = `rateInfos.${index}.actuaryContacts.0.${contactField}` + errorObject[errorKey] = contactValue + } + } + ) + } + }) + }) + } + + return errorObject + } + + return ( + { + return handleFormSubmit({ rateInfos }, setSubmitting, { + shouldValidateDocuments: true, + redirectPath: `../contacts`, + }) + }} + validationSchema={rateDetailsFormSchema} + > + {({ + values: { rateInfos }, + errors, + dirty, + handleSubmit, + isSubmitting, + setSubmitting, + }) => { + return ( + <> + { + setShouldValidate(true) + setFocusErrorSummaryHeading(true) + handleSubmit(e) + }} + > +
+ Rate Details + + {shouldValidate && ( + + )} + + {({ + remove, + push, + }: FieldArrayRenderProps) => ( + <> + {rateInfos.map( + (rateInfo, index) => ( + { + remove(index) + setNewRateButtonFocus() + }, + reassignNewRateRef: + (el) => + (newRateNameRef.current = + el), + }} + /> + ) + )} + +

+ Additional rate + certification +

+ +
+ + )} +
+
+ { + const redirectPath = `../contract-details` + if (dirty) { + await handleFormSubmit( + { rateInfos }, + setSubmitting, + { + shouldValidateDocuments: false, + redirectPath, + } + ) + } else { + navigate(redirectPath) + } + }} + saveAsDraftOnClick={async () => { + await handleFormSubmit( + { rateInfos }, + setSubmitting, + { + shouldValidateDocuments: true, + redirectPath: + RoutesRecord.DASHBOARD_SUBMISSIONS, + } + ) + }} + disableContinue={ + shouldValidate && + !!Object.keys(errors).length + } + actionInProgress={isSubmitting} + /> +
+ + ) + }} +
+ ) +} diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx new file mode 100644 index 0000000000..ef02c4a75b --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx @@ -0,0 +1,531 @@ +import React from 'react' +import { + Button, + DatePicker, + DateRangePicker, + Fieldset, + FormGroup, + Label, + Link, +} from '@trussworks/react-uswds' +import classnames from 'classnames' +import { + FieldRadio, + FileUpload, + PoliteErrorMessage, + ProgramSelect, + SectionCard, +} from '../../../../components' + +import styles from '../../StateSubmissionForm.module.scss' +import { formatUserInputDate, isDateRangeEmpty } from '../../../../formHelpers' +import { + ACCEPTED_RATE_SUPPORTING_DOCS_FILE_TYPES, + ACCEPTED_RATE_CERTIFICATION_FILE_TYPES, +} from '../../../../components/FileUpload' +import { useS3 } from '../../../../contexts/S3Context' + +import { FormikErrors, getIn, useFormikContext } from 'formik' +import { ActuaryContactFields } from '../../Contacts' +import { RateRevision } from '../../../../gen/gqlClient' +import { formatDocumentsForForm } from '../../../../formHelpers/formatters' + +const isRateTypeEmpty = (rate: RateRevision): boolean => + rate.formData.rateType === undefined +const isRateTypeAmendment = (rate: RateRevision): boolean => + rate.formData.rateType === 'AMENDMENT' + +export type RateRevisionArray = { + rates: RateRevision[] +} + +export type SingleRateFormError = + FormikErrors[keyof FormikErrors] + +type MultiRatesConfig = { + reassignNewRateRef: ((el: HTMLInputElement) => void) | undefined + removeSelf: () => void // callback to Formik FieldArray to imperatively remove this rate from overall list and refocus on add new rate button +} + +type SingleRateCertV2Props = { + rate: RateRevision + shouldValidate: boolean + index: number // defaults to 0 + previousDocuments: string[] // this only passed in to ensure S3 deleteFile doesn't remove valid files for previous revisions + multiRatesConfig?: MultiRatesConfig // this is only passed in to enable displaying this rate within the multi-rates UI +} + +const RateDatesErrorMessage = ({ + startDate, + endDate, + startDateError, + endDateError, + shouldValidate, +}: { + shouldValidate: boolean + startDate?: string + endDate?: string + startDateError?: string // yup validation message + endDateError?: string // yup validation message +}): React.ReactElement => { + const hasError = shouldValidate && (startDateError || endDateError) + + // Error messages have hierarchy + // preference to show message for totally empty date, then errors for start date, then for end date + const validationErrorMessage = hasError + ? isDateRangeEmpty(startDate, endDate) + ? 'You must provide a start and an end date' + : startDateError ?? endDateError + : null + + return {validationErrorMessage} +} + +export const SingleRateCertV2 = ({ + rate, + shouldValidate, + multiRatesConfig, + index = 0, + previousDocuments +}: SingleRateCertV2Props): React.ReactElement => { + // page level setup + const { handleDeleteFile, handleUploadFile, handleScanFile, getKey } = useS3() + const key = rate.id + const displayAsStandaloneRate = multiRatesConfig === undefined + const fieldNamePrefix = `rates.${index}` + const rateCertNumber = index + 1 + const { errors, setFieldValue } = useFormikContext() + + const showFieldErrors = ( + fieldName: keyof RateRevision['formData'] + ): string | undefined => { + if (!shouldValidate) return undefined + return getIn(errors, `${fieldNamePrefix}.${fieldName}`) + } + + return ( + +

+ {displayAsStandaloneRate + ? `Rate certification` + : `Rate certification ${rateCertNumber}`} +

+
+ + + + Document definitions and requirements + + + {`Upload only one rate certification document. Additional rates can be added later.`} + + + + This input only accepts one file in PDF, + DOC, or DOCX format. + + + } + accept={ACCEPTED_RATE_CERTIFICATION_FILE_TYPES} + initialItems={formatDocumentsForForm({documents:rate.formData.rateDocuments, getKey}) } + uploadFile={(file) => + handleUploadFile(file, 'HEALTH_PLAN_DOCS') + } + scanFile={(key) => + handleScanFile(key, 'HEALTH_PLAN_DOCS') + } + deleteFile={(key) => + handleDeleteFile( + key, + 'HEALTH_PLAN_DOCS', + previousDocuments + ) + } + innerInputRef={multiRatesConfig?.reassignNewRateRef} + onFileItemsUpdate={({ fileItems }) => + setFieldValue( + `${fieldNamePrefix}.rateDocuments`, + fileItems + ) + } + /> + + + + + + Document definitions and requirements + + + {`Upload any supporting documents for Rate certification ${rateCertNumber}`} + + + Additional rates can be added later. + + + + This input only accepts PDF, CSV, DOC, DOCX, + XLS, XLSX files. + + + } + accept={ACCEPTED_RATE_SUPPORTING_DOCS_FILE_TYPES} + initialItems={formatDocumentsForForm({documents:rate.formData.supportingDocuments, getKey}) } + uploadFile={(file) => + handleUploadFile(file, 'HEALTH_PLAN_DOCS') + } + scanFile={(key) => + handleScanFile(key, 'HEALTH_PLAN_DOCS') + } + deleteFile={(key) => + handleDeleteFile( + key, + 'HEALTH_PLAN_DOCS', + previousDocuments + ) + } + onFileItemsUpdate={({ fileItems }) => + setFieldValue( + `${fieldNamePrefix}.supportingDocuments`, + fileItems + ) + } + /> + + + + + Required + + + {showFieldErrors('rateProgramIDs')} + + + + + +
+ + Required + + + {showFieldErrors('rateType')} + + + + Rate certification type definitions + + + +
+
+ + +
+

+ Does the actuary certify capitation rates + specific to each rate cell or a rate range? +

+ + Required + +

+ See 42 CFR ยงยง 438.4(b) and 438.4(c) +

+ + } + role="radiogroup" + aria-required + > + + {showFieldErrors('rateCapitationType')} + + + +
+
+ + {!isRateTypeEmpty(rate) && ( + <> + +
+ + Required + + + + + setFieldValue( + `${fieldNamePrefix}.rateDateStart`, + formatUserInputDate(val) + ), + }} + endDateHint="mm/dd/yyyy" + endDateLabel="End date" + endDatePickerProps={{ + disabled: false, + id: `${fieldNamePrefix}.rateDateEnd`, + name: `${fieldNamePrefix}.rateDateEnd`, + 'aria-required': true, + defaultValue: rate.formData.rateDateEnd, + onChange: (val) => + setFieldValue( + `${fieldNamePrefix}.rateDateEnd`, + formatUserInputDate(val) + ), + }} + /> +
+
+ + {isRateTypeAmendment(rate) && ( + <> + +
+ + Required + + + + + setFieldValue( + `${fieldNamePrefix}.effectiveDateStart`, + formatUserInputDate(val) + ), + }} + endDateHint="mm/dd/yyyy" + endDateLabel="End date" + endDatePickerProps={{ + disabled: false, + id: `${fieldNamePrefix}.effectiveDateEnd`, + name: `${fieldNamePrefix}.effectiveDateEnd`, + 'aria-required': true, + defaultValue: + rate.formData.amendmentEffectiveDateEnd, + onChange: (val) => + setFieldValue( + `${fieldNamePrefix}.effectiveDateEnd`, + formatUserInputDate(val) + ), + }} + /> +
+
+ + )} + + + + Required + +
+ mm/dd/yyyy +
+ + {showFieldErrors('rateDateCertified')} + + + + setFieldValue( + `${fieldNamePrefix}.rateDateCertified`, + formatUserInputDate(val) + ) + } + /> +
+ + )} + + + + + {index >= 1 && multiRatesConfig && ( + + )} +
+
+ ) +} From 0b73291a8d7716706d86e559f5d77000caeb0f72 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 30 Jan 2024 09:47:55 -0600 Subject: [PATCH 02/26] wip --- services/app-graphql/src/schema.graphql | 4 +- .../RateDetails/RateDetailsV2.tsx | 45 ++++++++++--------- .../SingleRateCert/SingleRateCert.tsx | 2 +- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index deeee2f15a..d68ce6bc08 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1345,8 +1345,8 @@ input RateFormDataInput { input SubmitRateInput { rateID: ID! - "User given submission description" - submitReason: String! + "User given submission description - defaults to Initial submission if left blank" + submitReason: String "Rate related form data to be updated with submission" formData: RateFormDataInput } diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index e87b67d5d9..bedf697711 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -30,20 +30,21 @@ import { S3ClientT } from '../../../s3' import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' import { RoutesRecord } from '../../../constants' import { SectionCard } from '../../../components/SectionCard' -import { Rate, RateRevision } from '../../../gen/gqlClient' +import { Rate, RateRevision, useSubmitRateMutation } from '../../../gen/gqlClient' // This function is used to get initial form values as well return empty form values when we add a new rate or delete a rate // We need to include the getKey function in params because there are no guarantees currently file is in s3 even if when we load data from API const generateRateCertFormValues = ( - rateRev: RateRevision, - getKey: S3ClientT['getKey']): RateCertFormType => { - const rateInfo = rateRev.formData + getKey: S3ClientT['getKey'], + rateRev?: RateRevision): RateCertFormType => { + const rateInfo = rateRev?.formData + const newRateID = uuidv4(); return { - id: rateRev.id, - key: uuidv4(), - rateType: rateInfo.rateType, - rateCapitationType: rateInfo?.rateCapitationType, + id: rateRev?.id ?? newRateID, + key: rateRev?.id ?? newRateID, + rateType: rateInfo?.rateType ?? undefined, + rateCapitationType: rateInfo?.rateCapitationType ?? undefined, rateDateStart: formatForForm(rateInfo?.rateDateStart), rateDateEnd: formatForForm(rateInfo?.rateDateEnd), rateDateCertified: formatForForm(rateInfo?.rateDateCertified), @@ -61,21 +62,14 @@ const generateRateCertFormValues = ( supportingDocuments:formatDocumentsForForm({ documents: rateInfo?.supportingDocuments, getKey: getKey, - }) + }), actuaryContacts: formatActuaryContactsForForm( - rateInfo.certifyingActuaryContacts + rateInfo?.certifyingActuaryContacts ), actuaryCommunicationPreference: - rateInfo?.actuaryCommunicationPreference, + rateInfo?.actuaryCommunicationPreference ?? undefined, packagesWithSharedRateCerts: rateInfo?.packagesWithSharedRateCerts ?? [], - hasSharedRateCert: - rateInfo?.packagesWithSharedRateCerts === undefined - ? undefined - : (rateInfo?.packagesWithSharedRateCerts && - rateInfo?.packagesWithSharedRateCerts.length) >= 1 - ? 'YES' - : 'NO', } } @@ -112,6 +106,8 @@ export const RateDetailsV2 = ({ React.useState(false) const errorSummaryHeadingRef = React.useRef(null) const [shouldValidate, setShouldValidate] = React.useState(showValidations) + const [submitRate, { loading: submitRateLoading }] = + useSubmitRateMutation() useEffect(() => { // Focus the error summary heading only if we are displaying @@ -138,7 +134,7 @@ export const RateDetailsV2 = ({ }, [focusNewRate]) const rateInfosInitialValues: RateInfoArrayType = { - rateInfos: [generateRateCertFormValues()], + rateInfos: [generateRateCertFormValues(getKey, draftRate?.draftRevision ?? undefined)], } const handleFormSubmit = async ( @@ -201,10 +197,17 @@ export const RateDetailsV2 = ({ } }) - draftRate.rateInfos = cleanedRateInfos + const initialRateRevision = cleanedRateInfos[0] // only submit the first rate revision right now from RateDetailsV2, adapt later for mulit-rate workflow try { - const updatedSubmission = await updateRate(draftRate) + const updatedSubmission = await submitRate({ + variables: { + input: { + rateID: initialRateRevision.id, + formData: initialRateRevision + } + }, + }) if (updatedSubmission instanceof Error) { setSubmitting(false) console.info( diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx index 5ac5d903fc..d210e2cec0 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx @@ -45,7 +45,7 @@ const isRateTypeAmendment = (values: RateCertFormType): boolean => values.rateType === 'AMENDMENT' export type RateCertFormType = { - id?: string + id: string key: string rateType: RateType | undefined rateCapitationType: RateCapitationType | undefined From 8b74cd0f9369f85133eb9ff84e0a93db95a26652 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 30 Jan 2024 15:28:11 -0600 Subject: [PATCH 03/26] wip --- services/app-web/src/pages/App/AppRoutes.tsx | 6 -- .../RateDetails/RateDetailsV2.tsx | 66 +++++++++++-------- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index 58b1a9053f..2bf67b4544 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -107,12 +107,6 @@ const StateUserRoutes = ({ path={RoutesRecord.SUBMISSIONS_NEW} element={} /> - {showRatePages && ( - } - /> - )} {showRatePages && ( { const navigate = useNavigate() + const { id } = useParams() + if (!id) { + throw new Error( + 'PROGRAMMING ERROR: id param not set in rate edit form.' + ) + } const { getKey } = useS3() - // form validation state management - const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = - React.useState(false) - const errorSummaryHeadingRef = React.useRef(null) - const [shouldValidate, setShouldValidate] = React.useState(showValidations) - const [submitRate, { loading: submitRateLoading }] = - useSubmitRateMutation() + // API handling + const [submitRate, { loading: submitRateLoading }] = useSubmitRateMutation() + const [fetchRate, { loading: fetchRateLoading }] = useFetchRateQuery() - useEffect(() => { - // Focus the error summary heading only if we are displaying - // validation errors and the heading element exists - if (focusErrorSummaryHeading && errorSummaryHeadingRef.current) { - errorSummaryHeadingRef.current.focus() - } - setFocusErrorSummaryHeading(false) - }, [focusErrorSummaryHeading]) + if () { + return + } + + let draftRate = {} + const previousDocuments: string[] = [] + + // Form validation + const [shouldValidate, setShouldValidate] = React.useState(showValidations) + const rateDetailsFormSchema = RateDetailsFormSchema() + const rateInfosInitialValues: RateInfoArrayType = { + rateInfos: [generateRateCertFormValues(getKey, draftRate?.draftRevision ?? undefined)], + } - // multi-rates state management + // UI focus state management const [focusNewRate, setFocusNewRate] = React.useState(false) const newRateNameRef = React.useRef(null) const [newRateButtonRef, setNewRateButtonFocus] = useFocus() // This ref.current is always the same element - - const rateDetailsFormSchema = RateDetailsFormSchema() + const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = React.useState(false) + const errorSummaryHeadingRef = React.useRef(null) React.useEffect(() => { if (focusNewRate) { @@ -133,9 +135,15 @@ export const RateDetailsV2 = ({ } }, [focusNewRate]) - const rateInfosInitialValues: RateInfoArrayType = { - rateInfos: [generateRateCertFormValues(getKey, draftRate?.draftRevision ?? undefined)], - } + useEffect(() => { + // Focus the error summary heading only if we are displaying + // validation errors and the heading element exists + if (focusErrorSummaryHeading && errorSummaryHeadingRef.current) { + errorSummaryHeadingRef.current.focus() + } + setFocusErrorSummaryHeading(false) + }, [focusErrorSummaryHeading]) + const handleFormSubmit = async ( form: RateInfoArrayType, From 492de9fc746a1b14be486da08887f64cb7e55590 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 30 Jan 2024 16:10:18 -0600 Subject: [PATCH 04/26] add flag to side nav --- .../src/pages/SubmissionSideNav/SubmissionSideNav.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx index 604454012b..28d92a5a0d 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx @@ -64,6 +64,11 @@ export const SubmissionSideNav = () => { featureFlags.CMS_QUESTIONS.flag, featureFlags.CMS_QUESTIONS.defaultValue ) + + // const showRatePages = ldClient?.variation( + // featureFlags.CMS_QUESTIONS.flag, + // featureFlags.CMS_QUESTIONS.defaultValue + // ) const showSidebar = showQuestionResponse && QUESTION_RESPONSE_SHOW_SIDEBAR_ROUTES.includes(routeName) From a1120f8210e2761ef21a9e5a47d111f8ad058843 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 30 Jan 2024 17:43:20 -0600 Subject: [PATCH 05/26] wip --- package.json | 2 +- .../app-web/src/pages/RateEdit/RateEdit.tsx | 5 ++- .../RateDetails/RateDetailsV2.tsx | 32 +++++++++++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 2e2a4c1249..6b1f7fbefc 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "services/cypress" }, "scripts": { - "clean": "npx lerna clean", + "clean": "rm -rfnpx lerna clean", "format": "yarn prettier --write", "prettier": "prettier --ignore-path .gitignore \"**/*.+(ts|tsx|json)\"", "test:coverage": "npx lerna run test:coverage --scope=app-web --scope=app-api --scope=cypress", diff --git a/services/app-web/src/pages/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateEdit/RateEdit.tsx index af04cbfb00..552c094d96 100644 --- a/services/app-web/src/pages/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateEdit/RateEdit.tsx @@ -4,6 +4,7 @@ import { useFetchRateQuery } from "../../gen/gqlClient"; import { GridContainer } from "@trussworks/react-uswds"; import { Loading } from "../../components"; import { GenericErrorPage } from "../Errors/GenericErrorPage"; +import { RateDetailsV2 } from "../StateSubmission/RateDetails/RateDetailsV2"; type RouteParams = { id: string @@ -44,9 +45,7 @@ export const RateEdit = (): React.ReactElement => { return (

- You've reached the '/rates/:id/edit' url placeholder for the incoming standalone edit rate form -
- Ticket: MCR-3771 +

) } \ No newline at end of file diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index 51138cc76f..f5ee893fbc 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -8,7 +8,7 @@ import styles from '../StateSubmissionForm.module.scss' import { RateInfoType } from '../../../common-code/healthPlanFormDataType' -import { ErrorSummary, GenericApiErrorBanner } from '../../../components' +import { ErrorSummary, GenericApiErrorBanner, Loading } from '../../../components' import { formatFormDateForDomain } from '../../../formHelpers' import { RateDetailsFormSchema } from './RateDetailsSchema' import { PageActions } from '../PageActions' @@ -30,6 +30,8 @@ import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' import { RoutesRecord } from '../../../constants' import { SectionCard } from '../../../components/SectionCard' import { Rate, RateRevision, useFetchRateQuery, useSubmitRateMutation } from '../../../gen/gqlClient' +import { Error404 } from '../../Errors/Error404Page' +import { GenericErrorPage } from '../../Errors/GenericErrorPage' // This function is used to get initial form values as well return empty form values when we add a new rate or delete a rate // We need to include the getKey function in params because there are no guarantees currently file is in s3 even if when we load data from API @@ -104,20 +106,31 @@ export const RateDetailsV2 = ({ // API handling const [submitRate, { loading: submitRateLoading }] = useSubmitRateMutation() - const [fetchRate, { loading: fetchRateLoading }] = useFetchRateQuery() + const { data: fetchRateQuery, loading: fetchRateLoading, error: fetchRateError } = useFetchRateQuery({ + variables: { + input: { + rateID: id + } + } + }) - if () { - return + if (fetchRateLoading){ + return } - let draftRate = {} + if( fetchRateError) { + return + } else if (!fetchRateQuery) { + return + } + const rate = fetchRateQuery?.fetchRate.rate const previousDocuments: string[] = [] // Form validation const [shouldValidate, setShouldValidate] = React.useState(showValidations) const rateDetailsFormSchema = RateDetailsFormSchema() const rateInfosInitialValues: RateInfoArrayType = { - rateInfos: [generateRateCertFormValues(getKey, draftRate?.draftRevision ?? undefined)], + rateInfos: [generateRateCertFormValues(getKey, rate.draftRevision ?? undefined)], } // UI focus state management @@ -212,7 +225,7 @@ export const RateDetailsV2 = ({ variables: { input: { rateID: initialRateRevision.id, - formData: initialRateRevision + // formData: initialRateRevision } }, }) @@ -336,7 +349,7 @@ export const RateDetailsV2 = ({ shouldValidate } parentSubmissionID={ - draftRate.id + rate.id } previousDocuments={ previousDocuments @@ -363,8 +376,7 @@ export const RateDetailsV2 = ({ type="button" className={`usa-button usa-button--outline ${styles.addRateBtn}`} onClick={() => { - const newRate = - generateRateCertFormValues() + const newRate = generateRateCertFormValues(getKey) push(newRate) setFocusNewRate(true) }} From d8388bfd7936505e3d72d5e18b04eabc7be6ecc2 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Wed, 31 Jan 2024 10:48:12 -0600 Subject: [PATCH 06/26] Add nx to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c47371488e..b118b25871 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ tsconfig.tsbuildinfo .serverless .eslintcache /.env +.nx tests_output *.log coverage/ From d661aa97f55f426bd51c67425f63f94887c3cd13 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Wed, 31 Jan 2024 10:48:33 -0600 Subject: [PATCH 07/26] Fix up types --- services/app-api/src/resolvers/rate/submitRate.ts | 2 +- services/app-web/src/formHelpers/formatters.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/app-api/src/resolvers/rate/submitRate.ts b/services/app-api/src/resolvers/rate/submitRate.ts index e677d26ec4..da70fc8ede 100644 --- a/services/app-api/src/resolvers/rate/submitRate.ts +++ b/services/app-api/src/resolvers/rate/submitRate.ts @@ -90,7 +90,7 @@ export function submitRate( const submittedRate = await store.submitRate({ rateID, submittedByUserID: user.id, - submitReason, + submitReason: submitReason ?? 'Initial submission', formData: formData ? { rateType: (formData.rateType ?? diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index 9f32d2ae61..3a0b738898 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -4,7 +4,7 @@ import { ActuaryContact, } from '../common-code/healthPlanFormDataType' import { FileItemT } from '../components' -import { GenericDocument } from '../gen/gqlClient' +import { GenericDocument, ActuaryContact as GQLActuaryContact } from '../gen/gqlClient' import { S3ClientT } from '../s3' import { v4 as uuidv4 } from 'uuid' @@ -50,7 +50,8 @@ const formatForForm = ( } } -const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[] | GQLActuaryContact) => { + +const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[] | GQLActuaryContact[]) => { return actuaryContacts && actuaryContacts.length > 0 ? actuaryContacts : [ From 0a01796a5db237251eab868f022f953d7f3677c8 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Thu, 1 Feb 2024 09:27:27 -0600 Subject: [PATCH 08/26] Workaround actuary contacts types --- .../src/mutations/unlockRate.graphql | 2 ++ .../app-graphql/src/queries/fetchRate.graphql | 2 ++ services/app-graphql/src/schema.graphql | 1 + services/app-web/src/formHelpers/formatters.ts | 17 +++++++++++++---- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/services/app-graphql/src/mutations/unlockRate.graphql b/services/app-graphql/src/mutations/unlockRate.graphql index ec47fd0003..e03faeb404 100644 --- a/services/app-graphql/src/mutations/unlockRate.graphql +++ b/services/app-graphql/src/mutations/unlockRate.graphql @@ -33,6 +33,7 @@ fragment rateRevisionFragment on RateRevision { rateProgramIDs, rateCertificationName, certifyingActuaryContacts { + id name titleRole email @@ -40,6 +41,7 @@ fragment rateRevisionFragment on RateRevision { actuarialFirmOther }, addtlActuaryContacts { + id name titleRole email diff --git a/services/app-graphql/src/queries/fetchRate.graphql b/services/app-graphql/src/queries/fetchRate.graphql index 7da1b9b208..f895425717 100644 --- a/services/app-graphql/src/queries/fetchRate.graphql +++ b/services/app-graphql/src/queries/fetchRate.graphql @@ -33,6 +33,7 @@ fragment rateRevisionFragment on RateRevision { rateProgramIDs, rateCertificationName, certifyingActuaryContacts { + id name titleRole email @@ -40,6 +41,7 @@ fragment rateRevisionFragment on RateRevision { actuarialFirmOther }, addtlActuaryContacts { + id name titleRole email diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index d68ce6bc08..ab9fd990c1 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1013,6 +1013,7 @@ enum ActuarialFirm { "Contact information for the certifying or additional state actuary" type ActuaryContact { + id: ID name: String titleRole: String email: String diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index 3a0b738898..10c1a49a7c 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -50,10 +50,19 @@ const formatForForm = ( } } - -const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[] | GQLActuaryContact[]) => { - return actuaryContacts && actuaryContacts.length > 0 - ? actuaryContacts +// This function can be cleaned up when we move off domain types and only use graphql +const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[] | GQLActuaryContact[]) : ActuaryContact[] => { + return actuaryContacts && actuaryContacts.length > 0 + ? actuaryContacts.map( (contact) => { + const {name, titleRole,email,actuarialFirm, actuarialFirmOther} = contact + return { + name: name ?? '', + titleRole: titleRole ?? '', + email: email ?? '', + actuarialFirmOther: actuarialFirmOther ?? undefined, + actuarialFirm: actuarialFirm ?? undefined, + } + }) : [ { name: '', From 9423fc2f7d3c9ca4854a1318edd6d967f4a753f9 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Thu, 1 Feb 2024 09:28:08 -0600 Subject: [PATCH 09/26] Fix React console error about bad setState call in RateSummary --- .../pages/SubmissionSummary/RateSummary/RateSummary.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx index fc0855602f..bffa5e076f 100644 --- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx +++ b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx @@ -53,8 +53,12 @@ export const RateSummary = (): React.ReactElement => { return } - //Redirecting a state user to the edit page if rate is unlocked - if (loggedInUser?.role === 'STATE_USER' && rate.status === 'UNLOCKED') { + // Redirecting a state user to the edit page if rate is unlocked + if ( + data && + loggedInUser?.role === 'STATE_USER' && + rate.status === 'UNLOCKED' + ) { navigate(`/rates/${id}/edit`) } From 9ff21748921212f322bca04ca405c80456e4d8c1 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Thu, 1 Feb 2024 12:26:47 -0600 Subject: [PATCH 10/26] Wip to show what I'm doing --- .../app-web/src/formHelpers/formatters.ts | 2 +- services/app-web/src/pages/App/AppRoutes.tsx | 11 +- .../RateEdit/RateEdit.test.tsx | 10 +- .../RateEdit/RateEdit.tsx | 8 +- .../RateSummary/RateSummary.test.tsx | 26 +- .../RateSummary/RateSummary.tsx | 14 +- .../RateSummary/index.ts | 0 .../RateDetails/RateDetailsSchema.ts | 4 +- .../RateDetails/RateDetailsV2.tsx | 230 +++++++----------- .../src/pages/SubmissionSummary/index.ts | 2 +- 10 files changed, 125 insertions(+), 182 deletions(-) rename services/app-web/src/pages/{ => RateSubmission}/RateEdit/RateEdit.test.tsx (84%) rename services/app-web/src/pages/{ => RateSubmission}/RateEdit/RateEdit.tsx (79%) rename services/app-web/src/pages/{SubmissionSummary => }/RateSummary/RateSummary.test.tsx (92%) rename services/app-web/src/pages/{SubmissionSummary => }/RateSummary/RateSummary.tsx (86%) rename services/app-web/src/pages/{SubmissionSummary => }/RateSummary/index.ts (100%) diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index 10c1a49a7c..cdf6c2274e 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -145,7 +145,7 @@ const formatDocumentsForForm = ({ name: doc.name, key: key, s3URL: doc.s3URL, - sha256: doc.sha256, + sha256: doc.sha256 ?? undefined, status: 'UPLOAD_COMPLETE', } }) || [] diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index 2bf67b4544..d7d70df89b 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -37,8 +37,9 @@ import { UploadQuestions, } from '../QuestionResponse' import { GraphQLExplorer } from '../GraphQLExplorer/GraphQLExplorer' -import { RateSummary } from '../SubmissionSummary/RateSummary' -import { RateEdit } from '../RateEdit/RateEdit' +import { RateSummary } from '../RateSummary' +import { RateDetailsV2 } from '../StateSubmission/RateDetails/RateDetailsV2' +import { RateEdit } from '../RateSubmission/RateEdit/RateEdit' function componentForAuthMode( authMode: AuthModeType @@ -108,10 +109,16 @@ const StateUserRoutes = ({ element={} /> {showRatePages && ( + <> } /> + } + /> + )} }> {showQuestionResponse && ( diff --git a/services/app-web/src/pages/RateEdit/RateEdit.test.tsx b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.test.tsx similarity index 84% rename from services/app-web/src/pages/RateEdit/RateEdit.test.tsx rename to services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.test.tsx index 420d69c305..e62c45f946 100644 --- a/services/app-web/src/pages/RateEdit/RateEdit.test.tsx +++ b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.test.tsx @@ -1,8 +1,8 @@ import { screen, waitFor } from '@testing-library/react' -import { renderWithProviders } from "../../testHelpers" +import { renderWithProviders } from "../../../testHelpers" import { RateEdit } from "./RateEdit" -import { fetchCurrentUserMock, fetchRateMockSuccess, mockValidStateUser } from "../../testHelpers/apolloMocks" -import { RoutesRecord } from '../../constants' +import { fetchCurrentUserMock, fetchRateMockSuccess, mockValidStateUser } from "../../../testHelpers/apolloMocks" +import { RoutesRecord } from '../../../constants' import { Route, Routes } from 'react-router-dom' // Wrap test component in some top level routes to allow getParams to be tested @@ -33,8 +33,8 @@ describe('RateEdit', () => { routerProvider: { route: '/rates/1337/edit' }, - featureFlags: { - 'rate-edit-unlock': true + featureFlags: { + 'rate-edit-unlock': true } }) diff --git a/services/app-web/src/pages/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx similarity index 79% rename from services/app-web/src/pages/RateEdit/RateEdit.tsx rename to services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx index 552c094d96..2866d1baa6 100644 --- a/services/app-web/src/pages/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx @@ -1,10 +1,10 @@ import React from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { useFetchRateQuery } from "../../gen/gqlClient"; +import { useFetchRateQuery } from "../../../gen/gqlClient"; import { GridContainer } from "@trussworks/react-uswds"; -import { Loading } from "../../components"; -import { GenericErrorPage } from "../Errors/GenericErrorPage"; -import { RateDetailsV2 } from "../StateSubmission/RateDetails/RateDetailsV2"; +import { Loading } from "../../../components"; +import { GenericErrorPage } from "../../Errors/GenericErrorPage"; +import { RateDetailsV2 } from "../../StateSubmission/RateDetails/RateDetailsV2"; type RouteParams = { id: string diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx b/services/app-web/src/pages/RateSummary/RateSummary.test.tsx similarity index 92% rename from services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx rename to services/app-web/src/pages/RateSummary/RateSummary.test.tsx index 490f69e143..931131939c 100644 --- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx +++ b/services/app-web/src/pages/RateSummary/RateSummary.test.tsx @@ -1,15 +1,15 @@ import { screen, waitFor } from '@testing-library/react' -import { renderWithProviders, testS3Client } from '../../../testHelpers' +import { renderWithProviders, testS3Client } from '../../testHelpers' import { fetchCurrentUserMock, fetchRateMockSuccess, mockValidCMSUser, mockValidStateUser, -} from '../../../testHelpers/apolloMocks' +} from '../../testHelpers/apolloMocks' import { RateSummary } from './RateSummary' -import { RoutesRecord } from '../../../constants' +import { RoutesRecord } from '../../constants' import { Route, Routes } from 'react-router-dom' -import { RateEdit } from '../../RateEdit/RateEdit' +import { RateEdit } from '../RateSubmission/RateEdit/RateEdit' // Wrap test component in some top level routes to allow getParams to be tested const wrapInRoutes = (children: React.ReactNode) => { @@ -146,15 +146,15 @@ describe('RateSummary', () => { it('redirects to RateEdit component from RateSummary without errors for unlocked rate', async () => { renderWithProviders( - } + } /> - } + } /> - , + , { apolloProvider: { mocks: [ @@ -168,8 +168,8 @@ describe('RateSummary', () => { routerProvider: { route: '/rates/1337' }, - featureFlags: { - 'rate-edit-unlock': true + featureFlags: { + 'rate-edit-unlock': true } }) diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx b/services/app-web/src/pages/RateSummary/RateSummary.tsx similarity index 86% rename from services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx rename to services/app-web/src/pages/RateSummary/RateSummary.tsx index bffa5e076f..d4aa0cc2a0 100644 --- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx +++ b/services/app-web/src/pages/RateSummary/RateSummary.tsx @@ -2,14 +2,14 @@ import { GridContainer, Icon, Link } from '@trussworks/react-uswds' import React, { useEffect, useState } from 'react' import { NavLink, useNavigate, useParams } from 'react-router-dom' -import { Loading } from '../../../components' -import { usePage } from '../../../contexts/PageContext' -import { useFetchRateQuery } from '../../../gen/gqlClient' +import { Loading } from '../../components' +import { usePage } from '../../contexts/PageContext' +import { useFetchRateQuery } from '../../gen/gqlClient' import styles from '../SubmissionSummary.module.scss' -import { GenericErrorPage } from '../../Errors/GenericErrorPage' -import { RoutesRecord } from '../../../constants' -import { SingleRateSummarySection } from '../../../components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection' -import { useAuth } from '../../../contexts/AuthContext' +import { GenericErrorPage } from '../Errors/GenericErrorPage' +import { RoutesRecord } from '../../constants' +import { SingleRateSummarySection } from '../../components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection' +import { useAuth } from '../../contexts/AuthContext' type RouteParams = { id: string diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/index.ts b/services/app-web/src/pages/RateSummary/index.ts similarity index 100% rename from services/app-web/src/pages/SubmissionSummary/RateSummary/index.ts rename to services/app-web/src/pages/RateSummary/index.ts diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts index 3ec15222c7..63dafdc998 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts @@ -14,7 +14,7 @@ const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => rateDocuments: validateFileItemsListSingleUpload({ required: true }), supportingDocuments: validateFileItemsList({ required: false }), hasSharedRateCert: Yup.string().defined('You must select yes or no'), - packagesWithSharedRateCerts: Yup.array() + packagesWithSharedRateCerts: _activeFeatureFlags['rate-edit-unlock']? Yup.array() .when('hasSharedRateCert', { is: 'YES', then: Yup.array().min( @@ -22,7 +22,7 @@ const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => 'You must select at least one submission' ), }) - .required(), + .required(): Yup.array().optional(), rateProgramIDs: Yup.array().min(1, 'You must select a program'), rateType: Yup.string().defined( 'You must choose a rate certification type' diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index f5ee893fbc..b5eaf1af5b 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -1,13 +1,11 @@ import React, { useEffect } from 'react' import { Form as UswdsForm } from '@trussworks/react-uswds' -import { FieldArray, FieldArrayRenderProps, Formik, FormikErrors } from 'formik' +import { Formik, FormikErrors } from 'formik' import { useNavigate, useParams } from 'react-router-dom' import { v4 as uuidv4 } from 'uuid' import styles from '../StateSubmissionForm.module.scss' -import { RateInfoType } from '../../../common-code/healthPlanFormDataType' - import { ErrorSummary, GenericApiErrorBanner, Loading } from '../../../components' import { formatFormDateForDomain } from '../../../formHelpers' import { RateDetailsFormSchema } from './RateDetailsSchema' @@ -20,24 +18,23 @@ import { formatDocumentsForForm, formatForForm, } from '../../../formHelpers/formatters' -import { - RateCertFormType, - SingleRateCert, -} from './SingleRateCert/SingleRateCert' import { useS3 } from '../../../contexts/S3Context' import { S3ClientT } from '../../../s3' import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' import { RoutesRecord } from '../../../constants' -import { SectionCard } from '../../../components/SectionCard' import { Rate, RateRevision, useFetchRateQuery, useSubmitRateMutation } from '../../../gen/gqlClient' import { Error404 } from '../../Errors/Error404Page' import { GenericErrorPage } from '../../Errors/GenericErrorPage' +import { SingleRateCertV2 } from './SingleRateCert/SingleRateCertV2' + + +export type RateCertForm = {key: string} & RateRevision['formData'] // This function is used to get initial form values as well return empty form values when we add a new rate or delete a rate // We need to include the getKey function in params because there are no guarantees currently file is in s3 even if when we load data from API const generateRateCertFormValues = ( getKey: S3ClientT['getKey'], - rateRev?: RateRevision): RateCertFormType => { + rateRev?: RateRevision): RateCertForm => { const rateInfo = rateRev?.formData const newRateID = uuidv4(); @@ -49,12 +46,12 @@ const generateRateCertFormValues = ( rateDateStart: formatForForm(rateInfo?.rateDateStart), rateDateEnd: formatForForm(rateInfo?.rateDateEnd), rateDateCertified: formatForForm(rateInfo?.rateDateCertified), - effectiveDateStart: formatForForm( - rateInfo?.amendmentEffectiveDateStart - ), - effectiveDateEnd: formatForForm( - rateInfo?.amendmentEffectiveDateEnd - ), + effectiveDateStart: formatForForm( + rateInfo?.amendmentEffectiveDateStart + ), + effectiveDateEnd: formatForForm( + rateInfo?.amendmentEffectiveDateEnd + ), rateProgramIDs: rateInfo?.rateProgramIDs ?? [], rateDocuments: formatDocumentsForForm({ documents: rateInfo?.rateDocuments, @@ -69,15 +66,9 @@ const generateRateCertFormValues = ( ), actuaryCommunicationPreference: rateInfo?.actuaryCommunicationPreference ?? undefined, - packagesWithSharedRateCerts: - rateInfo?.packagesWithSharedRateCerts ?? [], } } -interface RateInfoArrayType { - rateInfos: RateCertFormType[] -} - export const rateErrorHandling = ( error: string | FormikErrors | undefined ): FormikErrors | undefined => { @@ -104,34 +95,10 @@ export const RateDetailsV2 = ({ } const { getKey } = useS3() - // API handling - const [submitRate, { loading: submitRateLoading }] = useSubmitRateMutation() - const { data: fetchRateQuery, loading: fetchRateLoading, error: fetchRateError } = useFetchRateQuery({ - variables: { - input: { - rateID: id - } - } - }) - - if (fetchRateLoading){ - return - } - - if( fetchRateError) { - return - } else if (!fetchRateQuery) { - return - } - const rate = fetchRateQuery?.fetchRate.rate - const previousDocuments: string[] = [] // Form validation const [shouldValidate, setShouldValidate] = React.useState(showValidations) - const rateDetailsFormSchema = RateDetailsFormSchema() - const rateInfosInitialValues: RateInfoArrayType = { - rateInfos: [generateRateCertFormValues(getKey, rate.draftRevision ?? undefined)], - } + const rateDetailsFormSchema = RateDetailsFormSchema({'rate-edit-unlock': true}) // UI focus state management const [focusNewRate, setFocusNewRate] = React.useState(false) @@ -158,21 +125,43 @@ export const RateDetailsV2 = ({ }, [focusErrorSummaryHeading]) + // API handling + const [submitRate, { loading: submitRateLoading }] = useSubmitRateMutation() + const { data: fetchRateQuery, loading: fetchRateLoading, error: fetchRateError } = useFetchRateQuery({ + variables: { + input: { + rateID: id + } + } + }) + + if (fetchRateLoading){ + return + } + + if( fetchRateError) { + return + } else if (!fetchRateQuery) { + return + } + const rate = fetchRateQuery?.fetchRate.rate + const previousDocuments: string[] = [] + + // Formik setup + const initialValues: RateCertForm = generateRateCertFormValues(getKey, rate.draftRevision ?? undefined) + const handleFormSubmit = async ( - form: RateInfoArrayType, + form: RateCertForm , setSubmitting: (isSubmitting: boolean) => void, // formik setSubmitting options: { shouldValidateDocuments: boolean redirectPath: string } ) => { - const { rateInfos } = form if (options.shouldValidateDocuments) { - const fileErrorsNeedAttention = rateInfos.some((rateInfo) => - isLoadingOrHasFileErrors( - rateInfo.supportingDocuments.concat(rateInfo.rateDocuments) + const fileErrorsNeedAttention = isLoadingOrHasFileErrors( + initialValues.supportingDocuments.concat(initialValues.rateDocuments) ) - ) if (fileErrorsNeedAttention) { // make inline field errors visible so user can correct documents, direct user focus to errors, and manually exit formik submit setShouldValidate(true) @@ -182,49 +171,41 @@ export const RateDetailsV2 = ({ } } - const cleanedRateInfos = rateInfos.map((rateInfo) => { - return { - id: rateInfo.id, - rateType: rateInfo.rateType, - rateCapitationType: rateInfo.rateCapitationType, - rateDocuments: formatDocumentsForDomain(rateInfo.rateDocuments), + const cleanedrateCertForms = { + id: form.id, + rateType: form.rateType, + rateCapitationType: form.rateCapitationType, + rateDocuments: formatDocumentsForDomain(form.rateDocuments), supportingDocuments: formatDocumentsForDomain( - rateInfo.supportingDocuments + form.supportingDocuments ), - rateDateStart: formatFormDateForDomain(rateInfo.rateDateStart), - rateDateEnd: formatFormDateForDomain(rateInfo.rateDateEnd), + rateDateStart: formatFormDateForDomain(form.rateDateStart), + rateDateEnd: formatFormDateForDomain(form.rateDateEnd), rateDateCertified: formatFormDateForDomain( - rateInfo.rateDateCertified + form.rateDateCertified ), rateAmendmentInfo: - rateInfo.rateType === 'AMENDMENT' + form.rateType === 'AMENDMENT' ? { effectiveDateStart: formatFormDateForDomain( - rateInfo.effectiveDateStart + form.effectiveDateStart ), effectiveDateEnd: formatFormDateForDomain( - rateInfo.effectiveDateEnd + form.effectiveDateEnd ), } : undefined, - rateProgramIDs: rateInfo.rateProgramIDs, - actuaryContacts: rateInfo.actuaryContacts, + rateProgramIDs: form.rateProgramIDs, + actuaryContacts: form.actuaryContacts, actuaryCommunicationPreference: - rateInfo.actuaryCommunicationPreference, - packagesWithSharedRateCerts: - rateInfo.hasSharedRateCert === 'YES' - ? rateInfo.packagesWithSharedRateCerts - : [], - } - }) - - const initialRateRevision = cleanedRateInfos[0] // only submit the first rate revision right now from RateDetailsV2, adapt later for mulit-rate workflow + form.actuaryCommunicationPreference, + } try { const updatedSubmission = await submitRate({ variables: { input: { - rateID: initialRateRevision.id, + rateID: rate.id, // formData: initialRateRevision } }, @@ -247,9 +228,9 @@ export const RateDetailsV2 = ({ // Error summary object keys will be used as DOM focus point from error-summary. Must be valid html selector // Error summary object values will be used as messages displays in error summary const generateErrorSummaryErrors = ( - errors: FormikErrors + errors: FormikErrors ) => { - const rateErrors = errors.rateInfos + const rateErrors = errors const errorObject: { [field: string]: string } = {} if (rateErrors && Array.isArray(rateErrors)) { @@ -258,12 +239,11 @@ export const RateDetailsV2 = ({ Object.entries(rateError).forEach(([field, value]) => { if (typeof value === 'string') { + //rateProgramIDs error message needs a # proceeding the key name because this is the only way to be able to link to the Select component element see comments in ErrorSummaryMessage component. const errorKey = - field === 'rateProgramIDs' || - field === 'packagesWithSharedRateCerts' - ? `#rateInfos.${index}.${field}` - : `rateInfos.${index}.${field}` + field === 'rateProgramIDs' ? `#rateCertForms.${index}.${field}` + : `rateCertForms.${index}.${field}` errorObject[errorKey] = value } // If the field is actuaryContacts then the value should be an array with at least one object of errors @@ -277,7 +257,7 @@ export const RateDetailsV2 = ({ Object.entries(actuaryContact).forEach( ([contactField, contactValue]) => { if (typeof contactValue === 'string') { - const errorKey = `rateInfos.${index}.actuaryContacts.0.${contactField}` + const errorKey = `rateCertForms.${index}.actuaryContacts.0.${contactField}` errorObject[errorKey] = contactValue } } @@ -292,9 +272,9 @@ export const RateDetailsV2 = ({ return ( { - return handleFormSubmit({ rateInfos }, setSubmitting, { + initialValues={initialValues} + onSubmit={({ rateCertForm }, { setSubmitting }) => { + return handleFormSubmit({ rateCertForm }, setSubmitting, { shouldValidateDocuments: true, redirectPath: `../contacts`, }) @@ -302,7 +282,7 @@ export const RateDetailsV2 = ({ validationSchema={rateDetailsFormSchema} > {({ - values: { rateInfos }, + values: { rateCertForm }, errors, dirty, handleSubmit, @@ -313,7 +293,7 @@ export const RateDetailsV2 = ({ <> { @@ -333,69 +313,25 @@ export const RateDetailsV2 = ({ headingRef={errorSummaryHeadingRef} /> )} - - {({ - remove, - push, - }: FieldArrayRenderProps) => ( - <> - {rateInfos.map( - (rateInfo, index) => ( - { - remove(index) - setNewRateButtonFocus() - }, - reassignNewRateRef: - (el) => - (newRateNameRef.current = - el), - }} - /> - ) - )} - -

- Additional rate - certification -

- -
- - )} -
+ { const redirectPath = `../contract-details` if (dirty) { await handleFormSubmit( - { rateInfos }, + { rateCertForms }, setSubmitting, { shouldValidateDocuments: false, @@ -408,7 +344,7 @@ export const RateDetailsV2 = ({ }} saveAsDraftOnClick={async () => { await handleFormSubmit( - { rateInfos }, + { rateCertForms }, setSubmitting, { shouldValidateDocuments: true, diff --git a/services/app-web/src/pages/SubmissionSummary/index.ts b/services/app-web/src/pages/SubmissionSummary/index.ts index 9694162a2b..055197426d 100644 --- a/services/app-web/src/pages/SubmissionSummary/index.ts +++ b/services/app-web/src/pages/SubmissionSummary/index.ts @@ -1,2 +1,2 @@ export { SubmissionSummary } from './SubmissionSummary' -export { RateSummary } from './RateSummary' +export { RateSummary } from '../RateSummary' From d711b76cbdecbbd85b5624ad5dfb13de74dc086e Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Thu, 1 Feb 2024 18:23:16 -0600 Subject: [PATCH 11/26] Page finally loading - can see a few broken fields to address next --- services/app-graphql/src/schema.graphql | 2 +- .../app-web/src/formHelpers/formatters.ts | 46 ++++++++++- .../RateSubmission/RateEdit/RateEdit.tsx | 6 +- .../src/pages/RateSummary/RateSummary.tsx | 2 +- .../RateDetails/RateDetailsV2.tsx | 81 +++++++++++-------- .../SingleRateCert/SingleRateCertV2.tsx | 57 ++++++------- 6 files changed, 123 insertions(+), 71 deletions(-) diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index ab9fd990c1..76601d9286 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1328,7 +1328,7 @@ input RateFormDataInput { An array of additional ActuaryContacts Each element includes the the name, title/role and email """ - addtlActuaryContacts: [ActuaryContactInput!]! + addtlActuaryContacts: [ActuaryContactInput]! """ Is either OACT_TO_ACTUARY or OACT_TO_STATE It specifies whether the state wants CMS to reach out to their actuaries diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index cdf6c2274e..d7dbb6c196 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -81,9 +81,9 @@ const formatFormDateForDomain = (attribute: string): Date | undefined => { return dayjs.utc(attribute).toDate() } -const formatDocumentsForDomain = ( +const formatDocumentsForGQL = ( fileItems: FileItemT[] -): SubmissionDocument[] => { +): GenericDocument[] => { return fileItems.reduce((cleanedFileItems, fileItem) => { if (fileItem.status === 'UPLOAD_ERROR') { console.info( @@ -113,7 +113,7 @@ const formatDocumentsForDomain = ( }) } return cleanedFileItems - }, [] as SubmissionDocument[]) + }, [] as GenericDocument[]) } const formatDocumentsForForm = ({ @@ -152,6 +152,45 @@ const formatDocumentsForForm = ({ ) } +// DEPREACTED +// These helpers are for HPP code which we are slowly moving off of +// deprecated - HPP related +const formatDocumentsForDomain = ( + fileItems: FileItemT[] +): SubmissionDocument[] => { + return fileItems.reduce((cleanedFileItems, fileItem) => { + if (fileItem.status === 'UPLOAD_ERROR') { + console.info( + 'Attempting to save files that failed upload, discarding invalid files' + ) + } else if (fileItem.status === 'SCANNING_ERROR') { + console.info( + 'Attempting to save files that failed scanning, discarding invalid files' + ) + } else if (fileItem.status === 'DUPLICATE_NAME_ERROR') { + console.info( + 'Attempting to save files that are duplicate names, discarding duplicate' + ) + } else if (!fileItem.s3URL) { + console.info( + 'Attempting to save a seemingly valid file item is not yet uploaded to S3, this should not happen on form submit. Discarding file.' + ) + } else if (!fileItem.sha256) { + console.info( + 'Attempting to save a seemingly valid file item does not have a sha256 yet. this should not happen on form submit. Discarding file.' + ) + } else { + cleanedFileItems.push({ + name: fileItem.name, + s3URL: fileItem.s3URL, + sha256: fileItem.sha256, + }) + } + return cleanedFileItems + }, [] as SubmissionDocument[]) +} + + export { formatForApi, formatForForm, @@ -161,4 +200,5 @@ export { formatDocumentsForDomain, formatDocumentsForForm, formatActuaryContactsForForm, + formatDocumentsForGQL } diff --git a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx index 2866d1baa6..95c33842a0 100644 --- a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx @@ -44,8 +44,8 @@ export const RateEdit = (): React.ReactElement => { } return ( -

- -

+
+ +
) } \ No newline at end of file diff --git a/services/app-web/src/pages/RateSummary/RateSummary.tsx b/services/app-web/src/pages/RateSummary/RateSummary.tsx index d4aa0cc2a0..3d02150aa7 100644 --- a/services/app-web/src/pages/RateSummary/RateSummary.tsx +++ b/services/app-web/src/pages/RateSummary/RateSummary.tsx @@ -5,7 +5,7 @@ import { NavLink, useNavigate, useParams } from 'react-router-dom' import { Loading } from '../../components' import { usePage } from '../../contexts/PageContext' import { useFetchRateQuery } from '../../gen/gqlClient' -import styles from '../SubmissionSummary.module.scss' +import styles from '../SubmissionSummary/SubmissionSummary.module.scss' import { GenericErrorPage } from '../Errors/GenericErrorPage' import { RoutesRecord } from '../../constants' import { SingleRateSummarySection } from '../../components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection' diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index b5eaf1af5b..dde3ede1af 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -16,31 +16,43 @@ import { formatActuaryContactsForForm, formatDocumentsForDomain, formatDocumentsForForm, + formatDocumentsForGQL, formatForForm, } from '../../../formHelpers/formatters' import { useS3 } from '../../../contexts/S3Context' import { S3ClientT } from '../../../s3' -import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' +import { FileItemT, isLoadingOrHasFileErrors } from '../../../components/FileUpload' import { RoutesRecord } from '../../../constants' -import { Rate, RateRevision, useFetchRateQuery, useSubmitRateMutation } from '../../../gen/gqlClient' +import { Rate, RateFormData, RateFormDataInput, RateRevision, useFetchRateQuery, useSubmitRateMutation } from '../../../gen/gqlClient' import { Error404 } from '../../Errors/Error404Page' import { GenericErrorPage } from '../../Errors/GenericErrorPage' import { SingleRateCertV2 } from './SingleRateCert/SingleRateCertV2' +export type RateDetailFormValues ={ + id: RateRevision['id'], + rateType: RateRevision['formData']['rateType'], + rateCapitationType: RateRevision['formData']['rateCapitationType'], + rateDateStart: RateRevision['formData']['rateDateStart'], + rateDateEnd:RateRevision['formData']['rateDateEnd'], + rateDateCertified: RateRevision['formData']['rateDateCertified'], + effectiveDateStart: RateRevision['formData']['amendmentEffectiveDateStart'], + effectiveDateEnd: RateRevision['formData']['amendmentEffectiveDateEnd'], + rateProgramIDs: RateRevision['formData']['rateProgramIDs'], + rateDocuments: FileItemT[], + supportingDocuments: FileItemT[], + actuaryContacts: RateRevision['formData']['certifyingActuaryContacts'] + actuaryCommunicationPreference: RateRevision['formData']['actuaryCommunicationPreference'] +} -export type RateCertForm = {key: string} & RateRevision['formData'] -// This function is used to get initial form values as well return empty form values when we add a new rate or delete a rate -// We need to include the getKey function in params because there are no guarantees currently file is in s3 even if when we load data from API -const generateRateCertFormValues = ( +const generateFormValues = ( getKey: S3ClientT['getKey'], - rateRev?: RateRevision): RateCertForm => { + rateRev?: RateRevision): RateDetailFormValues => { const rateInfo = rateRev?.formData const newRateID = uuidv4(); return { id: rateRev?.id ?? newRateID, - key: rateRev?.id ?? newRateID, rateType: rateInfo?.rateType ?? undefined, rateCapitationType: rateInfo?.rateCapitationType ?? undefined, rateDateStart: formatForForm(rateInfo?.rateDateStart), @@ -70,8 +82,8 @@ const generateRateCertFormValues = ( } export const rateErrorHandling = ( - error: string | FormikErrors | undefined -): FormikErrors | undefined => { + error: string | FormikErrors | undefined +): FormikErrors | undefined => { if (typeof error === 'string') { return undefined } @@ -145,13 +157,14 @@ export const RateDetailsV2 = ({ return } const rate = fetchRateQuery?.fetchRate.rate + const draftRevision = rate.draftRevision const previousDocuments: string[] = [] // Formik setup - const initialValues: RateCertForm = generateRateCertFormValues(getKey, rate.draftRevision ?? undefined) + const initialValues: RateDetailFormValues = generateFormValues(getKey, draftRevision ?? undefined) const handleFormSubmit = async ( - form: RateCertForm , + form: RateDetailFormValues, setSubmitting: (isSubmitting: boolean) => void, // formik setSubmitting options: { shouldValidateDocuments: boolean @@ -160,7 +173,7 @@ export const RateDetailsV2 = ({ ) => { if (options.shouldValidateDocuments) { const fileErrorsNeedAttention = isLoadingOrHasFileErrors( - initialValues.supportingDocuments.concat(initialValues.rateDocuments) + form.supportingDocuments.concat(form.rateDocuments) ) if (fileErrorsNeedAttention) { // make inline field errors visible so user can correct documents, direct user focus to errors, and manually exit formik submit @@ -171,12 +184,11 @@ export const RateDetailsV2 = ({ } } - const cleanedrateCertForms = { - id: form.id, + const gqlFormData: RateFormDataInput = { rateType: form.rateType, rateCapitationType: form.rateCapitationType, - rateDocuments: formatDocumentsForDomain(form.rateDocuments), - supportingDocuments: formatDocumentsForDomain( + rateDocuments: formatDocumentsForGQL(form.rateDocuments), + supportingDocuments: formatDocumentsForGQL( form.supportingDocuments ), rateDateStart: formatFormDateForDomain(form.rateDateStart), @@ -184,21 +196,20 @@ export const RateDetailsV2 = ({ rateDateCertified: formatFormDateForDomain( form.rateDateCertified ), - rateAmendmentInfo: - form.rateType === 'AMENDMENT' - ? { - effectiveDateStart: formatFormDateForDomain( + + amendmentEffectiveDateStart: formatFormDateForDomain( form.effectiveDateStart ), - effectiveDateEnd: formatFormDateForDomain( + amendmentEffectiveDateEnd: formatFormDateForDomain( form.effectiveDateEnd ), - } - : undefined, + rateProgramIDs: form.rateProgramIDs, - actuaryContacts: form.actuaryContacts, + certifyingActuaryContacts: form.actuaryContacts, + addtlActuaryContacts: draftRevision?.formData.addtlActuaryContacts ?? [], // since this is on a different page just pass through the existing need to figure out actuaryCommunicationPreference: form.actuaryCommunicationPreference, + packagesWithSharedRateCerts: draftRevision?.formData.packagesWithSharedRateCerts ?? [] } try { @@ -206,7 +217,7 @@ export const RateDetailsV2 = ({ variables: { input: { rateID: rate.id, - // formData: initialRateRevision + formData: gqlFormData } }, }) @@ -228,7 +239,7 @@ export const RateDetailsV2 = ({ // Error summary object keys will be used as DOM focus point from error-summary. Must be valid html selector // Error summary object values will be used as messages displays in error summary const generateErrorSummaryErrors = ( - errors: FormikErrors + errors: FormikErrors ) => { const rateErrors = errors const errorObject: { [field: string]: string } = {} @@ -273,8 +284,8 @@ export const RateDetailsV2 = ({ return ( { - return handleFormSubmit({ rateCertForm }, setSubmitting, { + onSubmit={( rateFormValues, { setSubmitting }) => { + return handleFormSubmit(rateFormValues, setSubmitting, { shouldValidateDocuments: true, redirectPath: `../contacts`, }) @@ -282,7 +293,7 @@ export const RateDetailsV2 = ({ validationSchema={rateDetailsFormSchema} > {({ - values: { rateCertForm }, + values: rateFormValues, errors, dirty, handleSubmit, @@ -314,9 +325,9 @@ export const RateDetailsV2 = ({ /> )} { await handleFormSubmit( - { rateCertForms }, + rateFormValues, setSubmitting, { shouldValidateDocuments: true, diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx index ef02c4a75b..968f5328c0 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx @@ -29,18 +29,19 @@ import { FormikErrors, getIn, useFormikContext } from 'formik' import { ActuaryContactFields } from '../../Contacts' import { RateRevision } from '../../../../gen/gqlClient' import { formatDocumentsForForm } from '../../../../formHelpers/formatters' +import { RateDetailFormValues } from '../RateDetailsV2' -const isRateTypeEmpty = (rate: RateRevision): boolean => - rate.formData.rateType === undefined -const isRateTypeAmendment = (rate: RateRevision): boolean => - rate.formData.rateType === 'AMENDMENT' +const isRateTypeEmpty = (rateForm: RateDetailFormValues): boolean => + rateForm.rateType === undefined +const isRateTypeAmendment = (rateForm: RateDetailFormValues): boolean => + rateForm.rateType === 'AMENDMENT' -export type RateRevisionArray = { - rates: RateRevision[] +export type RateDetailFormValuesArray = { + rates: RateDetailFormValues[] } export type SingleRateFormError = - FormikErrors[keyof FormikErrors] + FormikErrors< RateDetailFormValuesArray>[keyof FormikErrors< RateDetailFormValuesArray>] type MultiRatesConfig = { reassignNewRateRef: ((el: HTMLInputElement) => void) | undefined @@ -48,7 +49,7 @@ type MultiRatesConfig = { } type SingleRateCertV2Props = { - rate: RateRevision + rateForm: RateDetailFormValues shouldValidate: boolean index: number // defaults to 0 previousDocuments: string[] // this only passed in to ensure S3 deleteFile doesn't remove valid files for previous revisions @@ -82,7 +83,7 @@ const RateDatesErrorMessage = ({ } export const SingleRateCertV2 = ({ - rate, + rateForm, shouldValidate, multiRatesConfig, index = 0, @@ -90,7 +91,7 @@ export const SingleRateCertV2 = ({ }: SingleRateCertV2Props): React.ReactElement => { // page level setup const { handleDeleteFile, handleUploadFile, handleScanFile, getKey } = useS3() - const key = rate.id + const key = rateForm.id const displayAsStandaloneRate = multiRatesConfig === undefined const fieldNamePrefix = `rates.${index}` const rateCertNumber = index + 1 @@ -113,7 +114,7 @@ export const SingleRateCertV2 = ({
} accept={ACCEPTED_RATE_CERTIFICATION_FILE_TYPES} - initialItems={formatDocumentsForForm({documents:rate.formData.rateDocuments, getKey}) } + initialItems={rateForm.rateDocuments} uploadFile={(file) => handleUploadFile(file, 'HEALTH_PLAN_DOCS') } @@ -201,7 +202,7 @@ export const SingleRateCertV2 = ({ } accept={ACCEPTED_RATE_SUPPORTING_DOCS_FILE_TYPES} - initialItems={formatDocumentsForForm({documents:rate.formData.supportingDocuments, getKey}) } + initialItems={rateForm.supportingDocuments} uploadFile={(file) => handleUploadFile(file, 'HEALTH_PLAN_DOCS') } @@ -236,7 +237,7 @@ export const SingleRateCertV2 = ({ @@ -328,7 +329,7 @@ export const SingleRateCertV2 = ({
- {!isRateTypeEmpty(rate) && ( + {!isRateTypeEmpty(rateForm) && ( <> setFieldValue( `${fieldNamePrefix}.rateDateStart`, @@ -382,7 +383,7 @@ export const SingleRateCertV2 = ({ id: `${fieldNamePrefix}.rateDateEnd`, name: `${fieldNamePrefix}.rateDateEnd`, 'aria-required': true, - defaultValue: rate.formData.rateDateEnd, + defaultValue: rateForm.rateDateEnd, onChange: (val) => setFieldValue( `${fieldNamePrefix}.rateDateEnd`, @@ -393,7 +394,7 @@ export const SingleRateCertV2 = ({ - {isRateTypeAmendment(rate) && ( + {isRateTypeAmendment(rateForm) && ( <> setFieldValue( `${fieldNamePrefix}.effectiveDateStart`, @@ -451,7 +452,7 @@ export const SingleRateCertV2 = ({ name: `${fieldNamePrefix}.effectiveDateEnd`, 'aria-required': true, defaultValue: - rate.formData.amendmentEffectiveDateEnd, + rateForm.effectiveDateEnd, onChange: (val) => setFieldValue( `${fieldNamePrefix}.effectiveDateEnd`, @@ -472,7 +473,7 @@ export const SingleRateCertV2 = ({ htmlFor={`${fieldNamePrefix}.rateDateCertified`} id={`rateDateCertifiedLabel.${index}`} > - {isRateTypeAmendment(rate) + {isRateTypeAmendment(rateForm) ? 'Date certified for rate amendment' : 'Date certified'} @@ -494,7 +495,7 @@ export const SingleRateCertV2 = ({ aria-describedby={`rateDateCertifiedLabel.${index} rateDateCertifiedHint.${index}`} id={`${fieldNamePrefix}.rateDateCertified`} name={`${fieldNamePrefix}.rateDateCertified`} - defaultValue={rate.formData.rateDateCertified} + defaultValue={rateForm.rateDateCertified} onChange={(val) => setFieldValue( `${fieldNamePrefix}.rateDateCertified`, @@ -508,7 +509,7 @@ export const SingleRateCertV2 = ({ Date: Thu, 1 Feb 2024 18:24:48 -0600 Subject: [PATCH 12/26] cleanup --- package.json | 2 +- services/app-graphql/src/schema.graphql | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6b1f7fbefc..4a84027c69 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "services/cypress" }, "scripts": { - "clean": "rm -rfnpx lerna clean", + "clean": "lerna clean", "format": "yarn prettier --write", "prettier": "prettier --ignore-path .gitignore \"**/*.+(ts|tsx|json)\"", "test:coverage": "npx lerna run test:coverage --scope=app-web --scope=app-api --scope=cypress", diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index 76601d9286..868d8caf97 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1038,13 +1038,13 @@ It's used as a part of RateFormData type PackageWithSameRate { packageName: String! packageId: String! - packageStatus: String + packageStatus: HealthPlanPackageStatus } input PackageWithSameRateInput { packageName: String! packageId: String! - packageStatus: HealthPlanPackageStatus! + packageStatus: HealthPlanPackageStatus } """ From bb20c3d68e2f6716ad376715f5693cc5969bdeb2 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Mon, 5 Feb 2024 19:57:36 -0600 Subject: [PATCH 13/26] Set up page action buttons properly, updateRate not available yet --- .../app-api/src/resolvers/rate/submitRate.ts | 6 +- services/app-graphql/src/schema.graphql | 2 +- .../app-web/src/formHelpers/formatters.ts | 22 +- services/app-web/src/pages/App/AppRoutes.tsx | 19 +- .../RateSubmission/RateEdit/RateEdit.tsx | 137 +++++--- ...missionContainer.tsx => FormContainer.tsx} | 12 +- .../New/NewStateSubmissionForm.tsx | 6 +- .../RateDetails/RateDetailsSchema.ts | 10 +- .../RateDetails/RateDetailsV2.tsx | 320 +++++++++--------- .../SingleRateCert/SingleRateCertV2.tsx | 34 +- .../StateSubmission/StateSubmissionForm.tsx | 8 +- 11 files changed, 319 insertions(+), 257 deletions(-) rename services/app-web/src/pages/StateSubmission/{StateSubmissionContainer.tsx => FormContainer.tsx} (50%) diff --git a/services/app-api/src/resolvers/rate/submitRate.ts b/services/app-api/src/resolvers/rate/submitRate.ts index da70fc8ede..550bc137a5 100644 --- a/services/app-api/src/resolvers/rate/submitRate.ts +++ b/services/app-api/src/resolvers/rate/submitRate.ts @@ -140,7 +140,11 @@ export function submitRate( actuaryCommunicationPreference: formData.actuaryCommunicationPreference ?? undefined, packagesWithSharedRateCerts: - formData.packagesWithSharedRateCerts ?? [], + formData.packagesWithSharedRateCerts.map((pkg) => ({ + packageName: pkg.packageName ?? undefined, + packageId: pkg.packageId ?? undefined, + packageStatus: pkg.packageStatus ?? undefined, + })), } : undefined, }) diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index 868d8caf97..16f4b2b6b4 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1328,7 +1328,7 @@ input RateFormDataInput { An array of additional ActuaryContacts Each element includes the the name, title/role and email """ - addtlActuaryContacts: [ActuaryContactInput]! + addtlActuaryContacts: [ActuaryContactInput!] """ Is either OACT_TO_ACTUARY or OACT_TO_STATE It specifies whether the state wants CMS to reach out to their actuaries diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index d7dbb6c196..9fde5da88f 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -74,11 +74,10 @@ const formatActuaryContactsForForm = (actuaryContacts?: ActuaryContact[] | GQLAc ] } -const formatFormDateForDomain = (attribute: string): Date | undefined => { - if (attribute === '') { - return undefined - } - return dayjs.utc(attribute).toDate() + + +const formatFormDateForGQL = (attribute: string): string | undefined => { + return (attribute === '') ? undefined : attribute } const formatDocumentsForGQL = ( @@ -155,6 +154,16 @@ const formatDocumentsForForm = ({ // DEPREACTED // These helpers are for HPP code which we are slowly moving off of // deprecated - HPP related + +const formatFormDateForDomain = (attribute: string): Date | undefined => { + if (attribute === '') { + return undefined + } + return dayjs.utc(attribute).toDate() +} + + + const formatDocumentsForDomain = ( fileItems: FileItemT[] ): SubmissionDocument[] => { @@ -200,5 +209,6 @@ export { formatDocumentsForDomain, formatDocumentsForForm, formatActuaryContactsForForm, - formatDocumentsForGQL + formatDocumentsForGQL, + formatFormDateForGQL } diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx index d7d70df89b..d22a80a3c1 100644 --- a/services/app-web/src/pages/App/AppRoutes.tsx +++ b/services/app-web/src/pages/App/AppRoutes.tsx @@ -38,7 +38,6 @@ import { } from '../QuestionResponse' import { GraphQLExplorer } from '../GraphQLExplorer/GraphQLExplorer' import { RateSummary } from '../RateSummary' -import { RateDetailsV2 } from '../StateSubmission/RateDetails/RateDetailsV2' import { RateEdit } from '../RateSubmission/RateEdit/RateEdit' function componentForAuthMode( @@ -110,15 +109,15 @@ const StateUserRoutes = ({ /> {showRatePages && ( <> - } - /> - } - /> - + } + /> + } + /> + )} }> {showQuestionResponse && ( diff --git a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx index 95c33842a0..fd610e5052 100644 --- a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx @@ -1,51 +1,108 @@ -import React from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useFetchRateQuery } from "../../../gen/gqlClient"; -import { GridContainer } from "@trussworks/react-uswds"; -import { Loading } from "../../../components"; -import { GenericErrorPage } from "../../Errors/GenericErrorPage"; -import { RateDetailsV2 } from "../../StateSubmission/RateDetails/RateDetailsV2"; +import React from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { + RateFormDataInput, + UpdateInformation, + useFetchRateQuery, + useSubmitRateMutation, +} from '../../../gen/gqlClient' +import { GridContainer } from '@trussworks/react-uswds' +import { Loading } from '../../../components' +import { GenericErrorPage } from '../../Errors/GenericErrorPage' +import { RateDetailsV2 } from '../../StateSubmission/RateDetails/RateDetailsV2' +import { RouteT, RoutesRecord } from '../../../constants' +import { PageBannerAlerts } from '../../StateSubmission/StateSubmissionForm' +import { useAuth } from '../../../contexts/AuthContext' +import { FormContainer } from '../../StateSubmission/FormContainer' + +export type SubmitOrUpdateRate = ( + rateID: string, + formInput: RateFormDataInput, + setIsSubmitting: (isSubmitting: boolean) => void, + redirect: RouteT +) => void type RouteParams = { - id: string + id: string } export const RateEdit = (): React.ReactElement => { - const navigate = useNavigate() - const { id } = useParams() - if (!id) { - throw new Error( - 'PROGRAMMING ERROR: id param not set in state submission form.' - ) - } + const navigate = useNavigate() + const { id } = useParams() + const { loggedInUser } = useAuth() + if (!id) { + throw new Error( + 'PROGRAMMING ERROR: id param not set in state submission form.' + ) + } - const { data, loading, error } = useFetchRateQuery({ - variables: { - input: { - rateID: id, + // API handling + const { + data: fetchData, + loading: fetchLoading, + error: fetchError, + } = useFetchRateQuery({ + variables: { + input: { + rateID: id, + }, }, - }, - }) + }) + const rate = fetchData?.fetchRate.rate + + const [submitRate, { error: submitError }] = useSubmitRateMutation() + const submitRateHandler: SubmitOrUpdateRate = async ( + rateID, + formInput, + setIsSubmitting, + redirect + ) => { + setIsSubmitting(true) + try { + const updatedSubmission = await submitRate({ + variables: { + input: { + rateID: rateID, + formData: formInput, + }, + }, + }) + if (updatedSubmission instanceof Error) { + setIsSubmitting(false) + } else if (updatedSubmission) { + navigate(RoutesRecord[redirect]) + } + } catch (serverError) { + setIsSubmitting(false) + } + } - const rate = data?.fetchRate.rate + if (fetchLoading) { + return ( + + + + ) + } else if (fetchError || !rate) { + return + } + + if (rate.status !== 'UNLOCKED') { + navigate(`/rates/${id}`) + } + + // An unlocked revision is defined by having unlockInfo on it, pull it out here if it exists + const unlockedInfo: UpdateInformation | undefined = + rate.revisions[0].unlockInfo || undefined - if (loading) { return ( - - - + + + + ) - } else if (error || !rate ) { - return - } - - if (rate.status !== 'UNLOCKED') { - navigate(`/rates/${id}`) - } - - return ( -
- -
- ) -} \ No newline at end of file +} diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionContainer.tsx b/services/app-web/src/pages/StateSubmission/FormContainer.tsx similarity index 50% rename from services/app-web/src/pages/StateSubmission/StateSubmissionContainer.tsx rename to services/app-web/src/pages/StateSubmission/FormContainer.tsx index 4958871806..65480bf269 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionContainer.tsx +++ b/services/app-web/src/pages/StateSubmission/FormContainer.tsx @@ -1,16 +1,18 @@ import { GridContainer } from '@trussworks/react-uswds' import styles from './StateSubmissionForm.module.scss' -type StateSubmissionContainerProps = { +type FormContainerProps = { + id: string children: React.ReactNode & JSX.IntrinsicElements['div'] } -export const StateSubmissionContainer = ( - props: StateSubmissionContainerProps +export const FormContainer = ( + props: FormContainerProps ): React.ReactElement => { + const { id, children } = props return ( - - {props.children} + + {children} ) } diff --git a/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.tsx b/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.tsx index 92c0dd5f74..a4d47c793e 100644 --- a/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.tsx +++ b/services/app-web/src/pages/StateSubmission/New/NewStateSubmissionForm.tsx @@ -3,7 +3,7 @@ import React from 'react' import { DynamicStepIndicator } from '../../../components/DynamicStepIndicator' import { STATE_SUBMISSION_FORM_ROUTES } from '../../../constants/routes' import styles from '../StateSubmissionForm.module.scss' -import { StateSubmissionContainer } from '../StateSubmissionContainer' +import { FormContainer } from '../FormContainer' import { SubmissionType } from '../SubmissionType/SubmissionType' export const NewStateSubmissionForm = (): React.ReactElement => { @@ -16,9 +16,9 @@ export const NewStateSubmissionForm = (): React.ReactElement => { /> - + - + ) } diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts index 63dafdc998..aff200a92f 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts @@ -13,7 +13,7 @@ const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => Yup.object().shape({ rateDocuments: validateFileItemsListSingleUpload({ required: true }), supportingDocuments: validateFileItemsList({ required: false }), - hasSharedRateCert: Yup.string().defined('You must select yes or no'), + hasSharedRateCert: _activeFeatureFlags['rate-edit-unlock']? Yup.string(): Yup.string().defined('You must select yes or no'), packagesWithSharedRateCerts: _activeFeatureFlags['rate-edit-unlock']? Yup.array() .when('hasSharedRateCert', { is: 'YES', @@ -135,7 +135,13 @@ const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => }) const RateDetailsFormSchema = (activeFeatureFlags?: FeatureFlagSettings) => { - return Yup.object().shape({ + return activeFeatureFlags?.['rate-edit-unlock']? + Yup.object().shape({ + rates: Yup.array().of( + SingleRateCertSchema(activeFeatureFlags || {}) + ), + }): + Yup.object().shape({ rateInfos: Yup.array().of( SingleRateCertSchema(activeFeatureFlags || {}) ), diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index dde3ede1af..f3af1a8481 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -6,50 +6,57 @@ import { v4 as uuidv4 } from 'uuid' import styles from '../StateSubmissionForm.module.scss' -import { ErrorSummary, GenericApiErrorBanner, Loading } from '../../../components' -import { formatFormDateForDomain } from '../../../formHelpers' +import { ErrorSummary } from '../../../components' import { RateDetailsFormSchema } from './RateDetailsSchema' import { PageActions } from '../PageActions' -import { useFocus } from '../../../hooks' import { formatActuaryContactsForForm, - formatDocumentsForDomain, formatDocumentsForForm, formatDocumentsForGQL, formatForForm, + formatFormDateForGQL, } from '../../../formHelpers/formatters' import { useS3 } from '../../../contexts/S3Context' import { S3ClientT } from '../../../s3' -import { FileItemT, isLoadingOrHasFileErrors } from '../../../components/FileUpload' -import { RoutesRecord } from '../../../constants' -import { Rate, RateFormData, RateFormDataInput, RateRevision, useFetchRateQuery, useSubmitRateMutation } from '../../../gen/gqlClient' -import { Error404 } from '../../Errors/Error404Page' -import { GenericErrorPage } from '../../Errors/GenericErrorPage' +import { + FileItemT, + isLoadingOrHasFileErrors, +} from '../../../components/FileUpload' +import { RouteT, RoutesRecord } from '../../../constants' +import { Rate, RateFormDataInput, RateRevision } from '../../../gen/gqlClient' import { SingleRateCertV2 } from './SingleRateCert/SingleRateCertV2' +import type { SubmitOrUpdateRate } from '../../RateSubmission/RateEdit/RateEdit' + +export type RateDetailFormValues = { + id: RateRevision['id'] + rateType: RateRevision['formData']['rateType'] + rateCapitationType: RateRevision['formData']['rateCapitationType'] + rateDateStart: RateRevision['formData']['rateDateStart'] + rateDateEnd: RateRevision['formData']['rateDateEnd'] + rateDateCertified: RateRevision['formData']['rateDateCertified'] + effectiveDateStart: RateRevision['formData']['amendmentEffectiveDateStart'] + effectiveDateEnd: RateRevision['formData']['amendmentEffectiveDateEnd'] + rateProgramIDs: RateRevision['formData']['rateProgramIDs'] + rateDocuments: FileItemT[] + supportingDocuments: FileItemT[] + actuaryContacts: RateRevision['formData']['certifyingActuaryContacts'] + addtlActuaryContacts: RateRevision['formData']['addtlActuaryContacts'] + actuaryCommunicationPreference: RateRevision['formData']['actuaryCommunicationPreference'] + packagesWithSharedRateCerts: RateRevision['formData']['packagesWithSharedRateCerts'] +} - -export type RateDetailFormValues ={ - id: RateRevision['id'], - rateType: RateRevision['formData']['rateType'], - rateCapitationType: RateRevision['formData']['rateCapitationType'], - rateDateStart: RateRevision['formData']['rateDateStart'], - rateDateEnd:RateRevision['formData']['rateDateEnd'], - rateDateCertified: RateRevision['formData']['rateDateCertified'], - effectiveDateStart: RateRevision['formData']['amendmentEffectiveDateStart'], - effectiveDateEnd: RateRevision['formData']['amendmentEffectiveDateEnd'], - rateProgramIDs: RateRevision['formData']['rateProgramIDs'], - rateDocuments: FileItemT[], - supportingDocuments: FileItemT[], - actuaryContacts: RateRevision['formData']['certifyingActuaryContacts'] - actuaryCommunicationPreference: RateRevision['formData']['actuaryCommunicationPreference'] +// We have a list of rates to enable multi-rate behavior +export type RateDetailFormConfig = { + rates: RateDetailFormValues[] } const generateFormValues = ( getKey: S3ClientT['getKey'], - rateRev?: RateRevision): RateDetailFormValues => { + rateRev?: RateRevision +): RateDetailFormValues => { const rateInfo = rateRev?.formData - const newRateID = uuidv4(); + const newRateID = uuidv4() return { id: rateRev?.id ?? newRateID, @@ -58,26 +65,29 @@ const generateFormValues = ( rateDateStart: formatForForm(rateInfo?.rateDateStart), rateDateEnd: formatForForm(rateInfo?.rateDateEnd), rateDateCertified: formatForForm(rateInfo?.rateDateCertified), - effectiveDateStart: formatForForm( - rateInfo?.amendmentEffectiveDateStart - ), - effectiveDateEnd: formatForForm( - rateInfo?.amendmentEffectiveDateEnd - ), + effectiveDateStart: formatForForm( + rateInfo?.amendmentEffectiveDateStart + ), + effectiveDateEnd: formatForForm(rateInfo?.amendmentEffectiveDateEnd), rateProgramIDs: rateInfo?.rateProgramIDs ?? [], rateDocuments: formatDocumentsForForm({ - documents: rateInfo?.rateDocuments, - getKey: getKey, + documents: rateInfo?.rateDocuments, + getKey: getKey, + }), + supportingDocuments: formatDocumentsForForm({ + documents: rateInfo?.supportingDocuments, + getKey: getKey, }), - supportingDocuments:formatDocumentsForForm({ - documents: rateInfo?.supportingDocuments, - getKey: getKey, - }), actuaryContacts: formatActuaryContactsForForm( rateInfo?.certifyingActuaryContacts ), + addtlActuaryContacts: formatActuaryContactsForForm( + rateInfo?.certifyingActuaryContacts + ), actuaryCommunicationPreference: rateInfo?.actuaryCommunicationPreference ?? undefined, + packagesWithSharedRateCerts: + rateInfo?.packagesWithSharedRateCerts ?? [], } } @@ -92,11 +102,14 @@ export const rateErrorHandling = ( type RateDetailsV2Props = { showValidations?: boolean - // updateRate: UpdateRateMutation + rates: Rate[] + submitRate: SubmitOrUpdateRate + // updateRate: SubmitOrUpdateRate - will be implemented in Link Rates Epic } export const RateDetailsV2 = ({ showValidations = false, - // updateRate, + rates, + submitRate, }: RateDetailsV2Props): React.ReactElement => { const navigate = useNavigate() const { id } = useParams() @@ -107,26 +120,17 @@ export const RateDetailsV2 = ({ } const { getKey } = useS3() - // Form validation const [shouldValidate, setShouldValidate] = React.useState(showValidations) - const rateDetailsFormSchema = RateDetailsFormSchema({'rate-edit-unlock': true}) + const rateDetailsFormSchema = RateDetailsFormSchema({ + 'rate-edit-unlock': true, + }) // UI focus state management - const [focusNewRate, setFocusNewRate] = React.useState(false) - const newRateNameRef = React.useRef(null) - const [newRateButtonRef, setNewRateButtonFocus] = useFocus() // This ref.current is always the same element - const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = React.useState(false) + const [focusErrorSummaryHeading, setFocusErrorSummaryHeading] = + React.useState(false) const errorSummaryHeadingRef = React.useRef(null) - React.useEffect(() => { - if (focusNewRate) { - newRateNameRef?.current?.focus() - setFocusNewRate(false) - newRateNameRef.current = null - } - }, [focusNewRate]) - useEffect(() => { // Focus the error summary heading only if we are displaying // validation errors and the heading element exists @@ -136,45 +140,42 @@ export const RateDetailsV2 = ({ setFocusErrorSummaryHeading(false) }, [focusErrorSummaryHeading]) - - // API handling - const [submitRate, { loading: submitRateLoading }] = useSubmitRateMutation() - const { data: fetchRateQuery, loading: fetchRateLoading, error: fetchRateError } = useFetchRateQuery({ - variables: { - input: { - rateID: id - } - } - }) - - if (fetchRateLoading){ - return - } - - if( fetchRateError) { - return - } else if (!fetchRateQuery) { - return - } - const rate = fetchRateQuery?.fetchRate.rate - const draftRevision = rate.draftRevision const previousDocuments: string[] = [] // Formik setup - const initialValues: RateDetailFormValues = generateFormValues(getKey, draftRevision ?? undefined) - const handleFormSubmit = async ( - form: RateDetailFormValues, + const initialValues: RateDetailFormConfig = { + rates: + rates.length > 0 + ? rates.map((rate) => + generateFormValues( + getKey, + rate.draftRevision ?? undefined + ) + ) + : [ + generateFormValues( + getKey, + rates[0].draftRevision ?? undefined + ), + ], + } + + const handlePageAction = async ( + rates: RateDetailFormValues[], setSubmitting: (isSubmitting: boolean) => void, // formik setSubmitting options: { - shouldValidateDocuments: boolean - redirectPath: string + type: 'SAVE_AS_DRAFT' | 'CANCEL' | 'CONTINUE' + redirectPath: RouteT } ) => { - if (options.shouldValidateDocuments) { - const fileErrorsNeedAttention = isLoadingOrHasFileErrors( - form.supportingDocuments.concat(form.rateDocuments) + if (options.type === 'CONTINUE') { + const fileErrorsNeedAttention = rates.some((rateForm) => + isLoadingOrHasFileErrors( + rateForm.supportingDocuments.concat(rateForm.rateDocuments) ) + ) + if (fileErrorsNeedAttention) { // make inline field errors visible so user can correct documents, direct user focus to errors, and manually exit formik submit setShouldValidate(true) @@ -184,64 +185,57 @@ export const RateDetailsV2 = ({ } } - const gqlFormData: RateFormDataInput = { - rateType: form.rateType, - rateCapitationType: form.rateCapitationType, - rateDocuments: formatDocumentsForGQL(form.rateDocuments), - supportingDocuments: formatDocumentsForGQL( - form.supportingDocuments - ), - rateDateStart: formatFormDateForDomain(form.rateDateStart), - rateDateEnd: formatFormDateForDomain(form.rateDateEnd), - rateDateCertified: formatFormDateForDomain( - form.rateDateCertified - ), - - amendmentEffectiveDateStart: formatFormDateForDomain( - form.effectiveDateStart - ), - amendmentEffectiveDateEnd: formatFormDateForDomain( - form.effectiveDateEnd - ), + const gqlFormDatas: Array<{ id: string } & RateFormDataInput> = + rates.map((form) => { + return { + id: form.id, + rateType: form.rateType, + rateCapitationType: form.rateCapitationType, + rateDocuments: formatDocumentsForGQL(form.rateDocuments), + supportingDocuments: formatDocumentsForGQL( + form.supportingDocuments + ), + rateDateStart: formatFormDateForGQL(form.rateDateStart), + rateDateEnd: formatFormDateForGQL(form.rateDateEnd), + rateDateCertified: formatFormDateForGQL( + form.rateDateCertified + ), + amendmentEffectiveDateStart: formatFormDateForGQL( + form.effectiveDateStart + ), + amendmentEffectiveDateEnd: formatFormDateForGQL( + form.effectiveDateEnd + ), + rateProgramIDs: form.rateProgramIDs, + certifyingActuaryContacts: form.actuaryContacts, + addtlActuaryContacts: form.addtlActuaryContacts, + actuaryCommunicationPreference: + form.actuaryCommunicationPreference, + packagesWithSharedRateCerts: + form.packagesWithSharedRateCerts, + } + }) - rateProgramIDs: form.rateProgramIDs, - certifyingActuaryContacts: form.actuaryContacts, - addtlActuaryContacts: draftRevision?.formData.addtlActuaryContacts ?? [], // since this is on a different page just pass through the existing need to figure out - actuaryCommunicationPreference: - form.actuaryCommunicationPreference, - packagesWithSharedRateCerts: draftRevision?.formData.packagesWithSharedRateCerts ?? [] - } + const { id, ...formData } = gqlFormDatas[0] // only grab the first rate in the array because multi-rates functionality not added yet. This will be part of Link Rates epic - try { - const updatedSubmission = await submitRate({ - variables: { - input: { - rateID: rate.id, - formData: gqlFormData - } - }, - }) - if (updatedSubmission instanceof Error) { - setSubmitting(false) - console.info( - 'Error updating draft submission: ', - updatedSubmission - ) - } else if (updatedSubmission) { - navigate(options.redirectPath) - } - } catch (serverError) { - setSubmitting(false) + if (options.type === 'CONTINUE') { + await submitRate(id, formData, setSubmitting, 'DASHBOARD') + } else if (options.type === 'SAVE_AS_DRAFT') { + throw new Error( + 'Rate update is not yet implemented so save as draft is not possible. This will be part of Link Rates epic.' + ) + } else { + navigate(RoutesRecord[options.redirectPath]) } } - // Due to multi-rates we have extra handling around how error summary apperas + // Due to multi-rates we have extra handling around how error summary appears // Error summary object keys will be used as DOM focus point from error-summary. Must be valid html selector // Error summary object values will be used as messages displays in error summary const generateErrorSummaryErrors = ( - errors: FormikErrors + errors: FormikErrors ) => { - const rateErrors = errors + const rateErrors = errors.rates const errorObject: { [field: string]: string } = {} if (rateErrors && Array.isArray(rateErrors)) { @@ -250,11 +244,11 @@ export const RateDetailsV2 = ({ Object.entries(rateError).forEach(([field, value]) => { if (typeof value === 'string') { - //rateProgramIDs error message needs a # proceeding the key name because this is the only way to be able to link to the Select component element see comments in ErrorSummaryMessage component. const errorKey = - field === 'rateProgramIDs' ? `#rateCertForms.${index}.${field}` - : `rateCertForms.${index}.${field}` + field === 'rateProgramIDs' + ? `#rates.${index}.${field}` + : `rates.${index}.${field}` errorObject[errorKey] = value } // If the field is actuaryContacts then the value should be an array with at least one object of errors @@ -263,12 +257,12 @@ export const RateDetailsV2 = ({ Array.isArray(value) && Array.length > 0 ) { - //Currently, rate certifications only have 1 actuary contact + // Rate certifications only have 1 certifying actuary contact const actuaryContact = value[0] Object.entries(actuaryContact).forEach( ([contactField, contactValue]) => { if (typeof contactValue === 'string') { - const errorKey = `rateCertForms.${index}.actuaryContacts.0.${contactField}` + const errorKey = `rates.${index}.actuaryContacts.0.${contactField}` errorObject[errorKey] = contactValue } } @@ -284,10 +278,10 @@ export const RateDetailsV2 = ({ return ( { - return handleFormSubmit(rateFormValues, setSubmitting, { - shouldValidateDocuments: true, - redirectPath: `../contacts`, + onSubmit={(rateFormValues, { setSubmitting }) => { + return handlePageAction(rateFormValues.rates, setSubmitting, { + type: 'CONTINUE', + redirectPath: 'DASHBOARD_SUBMISSIONS', }) }} validationSchema={rateDetailsFormSchema} @@ -315,7 +309,6 @@ export const RateDetailsV2 = ({ >
Rate Details - {shouldValidate && ( )} - +
{ - const redirectPath = `../contract-details` if (dirty) { - await handleFormSubmit( - rateFormValues, + await handlePageAction( + rateFormValues.rates, setSubmitting, { - shouldValidateDocuments: false, - redirectPath, + type: 'CANCEL', + redirectPath: + 'DASHBOARD_SUBMISSIONS', } ) } else { - navigate(redirectPath) + navigate( + RoutesRecord.DASHBOARD_SUBMISSIONS + ) } }} saveAsDraftOnClick={async () => { - await handleFormSubmit( - rateFormValues, + await handlePageAction( + rateFormValues.rates, setSubmitting, { - shouldValidateDocuments: true, + type: 'SAVE_AS_DRAFT', redirectPath: - RoutesRecord.DASHBOARD_SUBMISSIONS, + 'DASHBOARD_SUBMISSIONS', } ) }} diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx index 968f5328c0..481b9d4b72 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx @@ -27,21 +27,15 @@ import { useS3 } from '../../../../contexts/S3Context' import { FormikErrors, getIn, useFormikContext } from 'formik' import { ActuaryContactFields } from '../../Contacts' -import { RateRevision } from '../../../../gen/gqlClient' -import { formatDocumentsForForm } from '../../../../formHelpers/formatters' -import { RateDetailFormValues } from '../RateDetailsV2' +import { RateDetailFormValues, RateDetailFormConfig } from '../RateDetailsV2' -const isRateTypeEmpty = (rateForm: RateDetailFormValues): boolean => +const isRateTypeEmpty = (rateForm: RateDetailFormValues): boolean => rateForm.rateType === undefined -const isRateTypeAmendment = (rateForm: RateDetailFormValues): boolean => +const isRateTypeAmendment = (rateForm: RateDetailFormValues): boolean => rateForm.rateType === 'AMENDMENT' -export type RateDetailFormValuesArray = { - rates: RateDetailFormValues[] -} - export type SingleRateFormError = - FormikErrors< RateDetailFormValuesArray>[keyof FormikErrors< RateDetailFormValuesArray>] + FormikErrors[keyof FormikErrors] type MultiRatesConfig = { reassignNewRateRef: ((el: HTMLInputElement) => void) | undefined @@ -53,7 +47,7 @@ type SingleRateCertV2Props = { shouldValidate: boolean index: number // defaults to 0 previousDocuments: string[] // this only passed in to ensure S3 deleteFile doesn't remove valid files for previous revisions - multiRatesConfig?: MultiRatesConfig // this is only passed in to enable displaying this rate within the multi-rates UI + multiRatesConfig?: MultiRatesConfig //t his is for use with Linked Rates and multi-rates UI } const RateDatesErrorMessage = ({ @@ -87,18 +81,18 @@ export const SingleRateCertV2 = ({ shouldValidate, multiRatesConfig, index = 0, - previousDocuments + previousDocuments, }: SingleRateCertV2Props): React.ReactElement => { // page level setup - const { handleDeleteFile, handleUploadFile, handleScanFile, getKey } = useS3() + const { handleDeleteFile, handleUploadFile, handleScanFile } = useS3() const key = rateForm.id const displayAsStandaloneRate = multiRatesConfig === undefined const fieldNamePrefix = `rates.${index}` const rateCertNumber = index + 1 - const { errors, setFieldValue } = useFormikContext() + const { errors, setFieldValue } = useFormikContext() const showFieldErrors = ( - fieldName: keyof RateRevision['formData'] + fieldName: keyof RateDetailFormValues ): string | undefined => { if (!shouldValidate) return undefined return getIn(errors, `${fieldNamePrefix}.${fieldName}`) @@ -398,8 +392,8 @@ export const SingleRateCertV2 = ({ <>
@@ -509,7 +503,7 @@ export const SingleRateCertV2 = ({ targetPath: RoutesRecord[formRouteType], }) -const PageBannerAlerts = ({ +export const PageBannerAlerts = ({ showPageErrorMessage, loggedInUser, unlockedInfo, @@ -251,7 +251,7 @@ export const StateSubmissionForm = (): React.ReactElement => { showPageErrorMessage={showPageErrorMessage} /> - + { /> } /> - + ) } From e8f23fd99e96dbc6407d0775d5825f6d9baec283 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Mon, 5 Feb 2024 19:58:00 -0600 Subject: [PATCH 14/26] cleanup --- .../src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index f3af1a8481..398ac93e58 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -143,7 +143,6 @@ export const RateDetailsV2 = ({ const previousDocuments: string[] = [] // Formik setup - const initialValues: RateDetailFormConfig = { rates: rates.length > 0 From 3c3f4179cdf3a94c41a18c7c53a8c2cf79b4dce4 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 08:19:06 -0600 Subject: [PATCH 15/26] cleanup --- package.json | 2 +- .../app-web/src/formHelpers/formatters.ts | 28 +++++++++---------- .../SubmissionSideNav/SubmissionSideNav.tsx | 4 --- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 4a84027c69..2e2a4c1249 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "services/cypress" }, "scripts": { - "clean": "lerna clean", + "clean": "npx lerna clean", "format": "yarn prettier --write", "prettier": "prettier --ignore-path .gitignore \"**/*.+(ts|tsx|json)\"", "test:coverage": "npx lerna run test:coverage --scope=app-web --scope=app-api --scope=cypress", diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index 9fde5da88f..788e57d94b 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -22,18 +22,6 @@ const formatForApi = (attribute: string): string | null => { return attribute } -const formatYesNoForProto = ( - attribute: string | undefined -): boolean | undefined => { - if (attribute === 'YES') { - return true - } - if (attribute === 'NO') { - return false - } - return undefined -} - // Convert api data for use in form. Form fields must be a string. // Empty values as an empty string, dates in date picker as YYYY-MM-DD, boolean as "Yes" "No" values const formatForForm = ( @@ -151,9 +139,8 @@ const formatDocumentsForForm = ({ ) } -// DEPREACTED -// These helpers are for HPP code which we are slowly moving off of -// deprecated - HPP related +// DEPRECATED +// Domain helpers are for HPP code. We are migrating off this in favor of directly using GQL utilities const formatFormDateForDomain = (attribute: string): Date | undefined => { if (attribute === '') { @@ -199,6 +186,17 @@ const formatDocumentsForDomain = ( }, [] as SubmissionDocument[]) } +const formatYesNoForProto = ( + attribute: string | undefined +): boolean | undefined => { + if (attribute === 'YES') { + return true + } + if (attribute === 'NO') { + return false + } + return undefined +} export { formatForApi, diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx index 28d92a5a0d..300a55a4cd 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx @@ -65,10 +65,6 @@ export const SubmissionSideNav = () => { featureFlags.CMS_QUESTIONS.defaultValue ) - // const showRatePages = ldClient?.variation( - // featureFlags.CMS_QUESTIONS.flag, - // featureFlags.CMS_QUESTIONS.defaultValue - // ) const showSidebar = showQuestionResponse && QUESTION_RESPONSE_SHOW_SIDEBAR_ROUTES.includes(routeName) From 860ea5a9ce7fcbcac470b49ef7a8881b64a51ae4 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 08:19:39 -0600 Subject: [PATCH 16/26] Pass rateID not revision id to submit. Fix cacheing --- .../src/pages/StateDashboard/StateDashboard.tsx | 15 ++++++++++++++- .../StateSubmission/RateDetails/RateDetailsV2.tsx | 13 ++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/services/app-web/src/pages/StateDashboard/StateDashboard.tsx b/services/app-web/src/pages/StateDashboard/StateDashboard.tsx index 9c3844051f..6babe917fe 100644 --- a/services/app-web/src/pages/StateDashboard/StateDashboard.tsx +++ b/services/app-web/src/pages/StateDashboard/StateDashboard.tsx @@ -18,6 +18,8 @@ import { Loading, } from '../../components' import { getCurrentRevisionFromHealthPlanPackage } from '../../gqlHelpers' +import { useLDClient } from 'launchdarkly-react-client-sdk' +import { featureFlags } from '../../common-code/featureFlags' /** * We only pull a subset of data out of the submission and revisions for display in Dashboard @@ -28,7 +30,18 @@ export const StateDashboard = (): React.ReactElement => { const { loginStatus, loggedInUser } = useAuth() const location = useLocation() - const { loading, data, error } = useIndexHealthPlanPackagesQuery() + // Force use network until we have all the APIs in use and we can reimplement cacheing + const ldClient = useLDClient() + const useContractAndRatesAPI: boolean = ldClient?.variation( + featureFlags.RATE_EDIT_UNLOCK.flag, + featureFlags.RATE_EDIT_UNLOCK.defaultValue + ) + + const { loading, data, error } = useIndexHealthPlanPackagesQuery({ + fetchPolicy: useContractAndRatesAPI + ? 'network-only' + : 'cache-and-network', + }) if (error) { handleApolloError(error, true) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx index 398ac93e58..073c9c6a2e 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx @@ -29,7 +29,7 @@ import { SingleRateCertV2 } from './SingleRateCert/SingleRateCertV2' import type { SubmitOrUpdateRate } from '../../RateSubmission/RateEdit/RateEdit' export type RateDetailFormValues = { - id: RateRevision['id'] + id: Rate['id'] rateType: RateRevision['formData']['rateType'] rateCapitationType: RateRevision['formData']['rateCapitationType'] rateDateStart: RateRevision['formData']['rateDateStart'] @@ -53,13 +53,14 @@ export type RateDetailFormConfig = { const generateFormValues = ( getKey: S3ClientT['getKey'], - rateRev?: RateRevision + rateRev?: RateRevision, + rateID?: string ): RateDetailFormValues => { const rateInfo = rateRev?.formData const newRateID = uuidv4() return { - id: rateRev?.id ?? newRateID, + id: rateID ?? newRateID, rateType: rateInfo?.rateType ?? undefined, rateCapitationType: rateInfo?.rateCapitationType ?? undefined, rateDateStart: formatForForm(rateInfo?.rateDateStart), @@ -149,13 +150,15 @@ export const RateDetailsV2 = ({ ? rates.map((rate) => generateFormValues( getKey, - rate.draftRevision ?? undefined + rate.draftRevision ?? undefined, + rate.id ) ) : [ generateFormValues( getKey, - rates[0].draftRevision ?? undefined + rates[0].draftRevision ?? undefined, + rates[0].id ), ], } From 6dad58bf0030af9d8cb3ab95e4cd67ba79d0c98e Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 10:06:55 -0600 Subject: [PATCH 17/26] Move file nesting to combine all V2 in one folder --- .../RateSubmission/RateEdit/RateEdit.tsx | 2 +- .../Contacts/ActuaryContactFields.tsx | 2 +- .../PackagesWithSharedRates.tsx | 27 ++++++++---------- .../RateDetails/RateDetails.tsx | 5 +--- .../{SingleRateCert => }/SingleRateCert.tsx | 18 ++++++------ .../RateDetails/{ => V2}/RateDetailsV2.tsx | 28 +++++++++++-------- .../SingleRateCertV2.tsx | 2 +- 7 files changed, 41 insertions(+), 43 deletions(-) rename services/app-web/src/pages/StateSubmission/RateDetails/{PackagesWithSharedRates => }/PackagesWithSharedRates.tsx (91%) rename services/app-web/src/pages/StateSubmission/RateDetails/{SingleRateCert => }/SingleRateCert.tsx (98%) rename services/app-web/src/pages/StateSubmission/RateDetails/{ => V2}/RateDetailsV2.tsx (95%) rename services/app-web/src/pages/StateSubmission/RateDetails/{SingleRateCert => V2}/SingleRateCertV2.tsx (99%) diff --git a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx index fd610e5052..059c8aad3c 100644 --- a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx @@ -9,7 +9,7 @@ import { import { GridContainer } from '@trussworks/react-uswds' import { Loading } from '../../../components' import { GenericErrorPage } from '../../Errors/GenericErrorPage' -import { RateDetailsV2 } from '../../StateSubmission/RateDetails/RateDetailsV2' +import { RateDetailsV2 } from '../../StateSubmission/RateDetails/V2/RateDetailsV2' import { RouteT, RoutesRecord } from '../../../constants' import { PageBannerAlerts } from '../../StateSubmission/StateSubmissionForm' import { useAuth } from '../../../contexts/AuthContext' diff --git a/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx b/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx index f0c50a2a17..0d60fa0831 100644 --- a/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx +++ b/services/app-web/src/pages/StateSubmission/Contacts/ActuaryContactFields.tsx @@ -4,7 +4,7 @@ import { Field, FormikErrors, FormikValues, getIn } from 'formik' import { Fieldset, FormGroup } from '@trussworks/react-uswds' import { FieldRadio, FieldTextInput } from '../../../components/Form' import { PoliteErrorMessage } from '../../../components/PoliteErrorMessage' -import { RateCertFormType } from '../RateDetails/SingleRateCert/SingleRateCert' +import { RateCertFormType } from '../RateDetails/SingleRateCert' import styles from '../StateSubmissionForm.module.scss' import { ActuaryContact as ActuaryContactGQL } from '../../../gen/gqlClient' diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates.tsx similarity index 91% rename from services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx rename to services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates.tsx index 7abfee8a0f..3e3a4bde82 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates/PackagesWithSharedRates.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/PackagesWithSharedRates.tsx @@ -1,23 +1,20 @@ import React from 'react' -import { useIndexHealthPlanPackagesQuery } from '../../../../gen/gqlClient' -import { recordJSException } from '../../../../otelHelpers' -import { dayjs } from '../../../../common-code/dateHelpers' -import { getCurrentRevisionFromHealthPlanPackage } from '../../../../gqlHelpers' -import type { PackageOptionType } from '../../../../components/Select' -import { useStatePrograms } from '../../../../hooks' -import { packageName } from '../../../../common-code/healthPlanFormDataType' -import { FieldYesNo, PoliteErrorMessage } from '../../../../components' +import { useIndexHealthPlanPackagesQuery } from '../../../gen/gqlClient' +import { recordJSException } from '../../../otelHelpers' +import { dayjs } from '../../../common-code/dateHelpers' +import { getCurrentRevisionFromHealthPlanPackage } from '../../../gqlHelpers' +import type { PackageOptionType } from '../../../components/Select' +import { useStatePrograms } from '../../../hooks' +import { packageName } from '../../../common-code/healthPlanFormDataType' +import { FieldYesNo, PoliteErrorMessage } from '../../../components' import { FormGroup, Label, Link } from '@trussworks/react-uswds' -import { PackageSelect } from '../../../../components/Select' +import { PackageSelect } from '../../../components/Select' import { getIn, useFormikContext } from 'formik' -import { - RateCertFormType, - RateInfoArrayType, -} from '../SingleRateCert/SingleRateCert' +import { RateCertFormType, RateInfoArrayType } from './SingleRateCert' -import styles from '../../StateSubmissionForm.module.scss' -import { RoutesRecord } from '../../../../constants' +import styles from '../StateSubmissionForm.module.scss' +import { RoutesRecord } from '../../../constants' export type PackagesWithSharedRatesProps = { index: number diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx index 41a177595f..40fa7eabe1 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetails.tsx @@ -21,10 +21,7 @@ import { formatDocumentsForForm, formatForForm, } from '../../../formHelpers/formatters' -import { - RateCertFormType, - SingleRateCert, -} from './SingleRateCert/SingleRateCert' +import { RateCertFormType, SingleRateCert } from './SingleRateCert' import { useS3 } from '../../../contexts/S3Context' import { S3ClientT } from '../../../s3' import { isLoadingOrHasFileErrors } from '../../../components/FileUpload' diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx similarity index 98% rename from services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx rename to services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx index d210e2cec0..2c20d49f24 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCert.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert.tsx @@ -13,7 +13,7 @@ import { ActuaryContact, RateCapitationType, RateType, -} from '../../../../common-code/healthPlanFormDataType' +} from '../../../common-code/healthPlanFormDataType' import { FieldRadio, FileItemT, @@ -21,23 +21,23 @@ import { PoliteErrorMessage, ProgramSelect, SectionCard, -} from '../../../../components' +} from '../../../components' -import styles from '../../StateSubmissionForm.module.scss' -import { formatUserInputDate, isDateRangeEmpty } from '../../../../formHelpers' +import styles from '../StateSubmissionForm.module.scss' +import { formatUserInputDate, isDateRangeEmpty } from '../../../formHelpers' import { ACCEPTED_RATE_SUPPORTING_DOCS_FILE_TYPES, ACCEPTED_RATE_CERTIFICATION_FILE_TYPES, -} from '../../../../components/FileUpload' -import { useS3 } from '../../../../contexts/S3Context' +} from '../../../components/FileUpload' +import { useS3 } from '../../../contexts/S3Context' import { FormikErrors, getIn, useFormikContext } from 'formik' import { ActuaryCommunicationType, SharedRateCertDisplay, -} from '../../../../common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType' -import { ActuaryContactFields } from '../../Contacts' -import { PackagesWithSharedRates } from '../PackagesWithSharedRates/PackagesWithSharedRates' +} from '../../../common-code/healthPlanFormDataType/UnlockedHealthPlanFormDataType' +import { ActuaryContactFields } from '../Contacts' +import { PackagesWithSharedRates } from './PackagesWithSharedRates' const isRateTypeEmpty = (values: RateCertFormType): boolean => values.rateType === undefined diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx similarity index 95% rename from services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx rename to services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx index 073c9c6a2e..16f9f7e3fc 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx @@ -4,11 +4,11 @@ import { Formik, FormikErrors } from 'formik' import { useNavigate, useParams } from 'react-router-dom' import { v4 as uuidv4 } from 'uuid' -import styles from '../StateSubmissionForm.module.scss' +import styles from '../../StateSubmissionForm.module.scss' -import { ErrorSummary } from '../../../components' -import { RateDetailsFormSchema } from './RateDetailsSchema' -import { PageActions } from '../PageActions' +import { ErrorSummary } from '../../../../components' +import { RateDetailsFormSchema } from '../RateDetailsSchema' +import { PageActions } from '../../PageActions' import { formatActuaryContactsForForm, @@ -16,17 +16,21 @@ import { formatDocumentsForGQL, formatForForm, formatFormDateForGQL, -} from '../../../formHelpers/formatters' -import { useS3 } from '../../../contexts/S3Context' -import { S3ClientT } from '../../../s3' +} from '../../../../formHelpers/formatters' +import { useS3 } from '../../../../contexts/S3Context' +import { S3ClientT } from '../../../../s3' import { FileItemT, isLoadingOrHasFileErrors, -} from '../../../components/FileUpload' -import { RouteT, RoutesRecord } from '../../../constants' -import { Rate, RateFormDataInput, RateRevision } from '../../../gen/gqlClient' -import { SingleRateCertV2 } from './SingleRateCert/SingleRateCertV2' -import type { SubmitOrUpdateRate } from '../../RateSubmission/RateEdit/RateEdit' +} from '../../../../components/FileUpload' +import { RouteT, RoutesRecord } from '../../../../constants' +import { + Rate, + RateFormDataInput, + RateRevision, +} from '../../../../gen/gqlClient' +import { SingleRateCertV2 } from './SingleRateCertV2' +import type { SubmitOrUpdateRate } from '../../../RateSubmission/RateEdit/RateEdit' export type RateDetailFormValues = { id: Rate['id'] diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/SingleRateCertV2.tsx similarity index 99% rename from services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx rename to services/app-web/src/pages/StateSubmission/RateDetails/V2/SingleRateCertV2.tsx index 481b9d4b72..beaf32fc52 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/SingleRateCert/SingleRateCertV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/SingleRateCertV2.tsx @@ -27,7 +27,7 @@ import { useS3 } from '../../../../contexts/S3Context' import { FormikErrors, getIn, useFormikContext } from 'formik' import { ActuaryContactFields } from '../../Contacts' -import { RateDetailFormValues, RateDetailFormConfig } from '../RateDetailsV2' +import { RateDetailFormValues, RateDetailFormConfig } from './RateDetailsV2' const isRateTypeEmpty = (rateForm: RateDetailFormValues): boolean => rateForm.rateType === undefined From 9a6e98690b25bebb7ec4ab8c6c710b62f427be02 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 10:08:06 -0600 Subject: [PATCH 18/26] Add basic test and todos for all the untested paths --- .../RateDetails/V2/RateDetailsV2.test.tsx | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx new file mode 100644 index 0000000000..34f5a08b2c --- /dev/null +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.test.tsx @@ -0,0 +1,154 @@ +import { screen, waitFor } from '@testing-library/react' +import { RateDetailsV2 } from './RateDetailsV2' +import { renderWithProviders } from '../../../../testHelpers' +import { + fetchCurrentUserMock, + rateDataMock, +} from '../../../../testHelpers/apolloMocks' +import { Route, Routes } from 'react-router-dom' +import { RoutesRecord } from '../../../../constants' + +describe('RateDetails', () => { + describe('handles a single rate', () => { + it('renders without errors', async () => { + const mockSubmit = jest.fn() + const rate = rateDataMock() + renderWithProviders( + + + } + /> + , + { + apolloProvider: { + mocks: [fetchCurrentUserMock({ statusCode: 200 })], + }, + routerProvider: { + route: `/rates/${rate.id}/edit`, + }, + } + ) + await waitFor(() => { + expect( + screen.getByText('Rate certification type') + ).toBeInTheDocument() + }) + expect( + screen.getByText('Upload one rate certification document') + ).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'Continue' }) + ).not.toHaveAttribute('aria-disabled') + }) + + it.todo('displays correct form guidance') + it.todo('loads with empty rate type and document upload fields visible') + it.todo('cannot continue without selecting rate type') + + it.todo('cannot continue without selecting rate capitation type') + + it.todo('cannot continue if no documents are added') + it.todo('progressively disclose new rate form fields as expected') + it.todo('displays program options based on current user state') + + describe('handles documents and file upload', () => { + it.todo('renders file upload') + it.todo('accepts documents on new rate') + it.todo('accepts a single file for rate cert') + + it.todo( + 'accepts multiple pdf, word, excel documents for supporting documents' + ) + it.todo('handles multiple rates') + it.todo( + 'renders add another rate button, which adds another set of rate certification fields to the form' + ) + it.todo( + 'renders remove rate certification button, which removes set of rate certification fields from the form' + ) + it.todo('accepts documents on second rate') + it.todo( + 'cannot continue without selecting rate type for a second rate' + ) + it.todo( + 'cannot continue if no documents are added to the second rate' + ) + }) + + describe('handles rates across submissions', () => { + it.todo( + 'correctly checks shared rate certification radios and selects shared package' + ) + it.todo('cannot continue when shared rate radio is unchecked') + it.todo( + 'cannot continue when shared rate radio is checked and no package is selected' + ) + }) + + describe('Continue button', () => { + it.todo('enabled when valid files are present') + + it.todo( + 'enabled when invalid files have been dropped but valid files are present' + ) + + it.todo( + 'disabled with alert after first attempt to continue with zero files' + ) + it.todo( + 'disabled with alert if previously submitted with more than one rate cert file' + ) + + it.todo( + 'disabled with alert after first attempt to continue with invalid duplicate files' + ) + + it.todo( + 'disabled with alert after first attempt to continue with invalid files' + ) + // eslint-disable-next-line jest/no-disabled-tests + it.todo( + 'disabled with alert when trying to continue while a file is still uploading' + ) + }) + + describe('Save as draft button', () => { + it.todo('enabled when valid files are present') + + it.todo( + 'enabled when invalid files have been dropped but valid files are present' + ) + it.todo( + 'when zero files present, does not trigger missing documents alert on click but still saves the in progress draft' + ) + it.todo( + 'when existing file is removed, does not trigger missing documents alert on click but still saves the in progress draft' + ) + it.todo( + 'when duplicate files present, triggers error alert on click' + ) + }) + + describe('Back button', () => { + it.todo('enabled when valid files are present') + + it.todo( + 'enabled when invalid files have been dropped but valid files are present' + ) + + it.todo( + 'when zero files present, does not trigger missing documents alert on click' + ) + + it.todo( + 'when duplicate files present, does not trigger duplicate documents alert on click and silently updates rate and supporting documents lists without duplicates' + ) + }) + }) +}) From 6b20930029386caf8d944b260334ad9681e46b73 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 13:52:23 -0600 Subject: [PATCH 19/26] Cleanup app-web tests --- .../RateEdit/RateEdit.test.tsx | 10 ++++++---- .../{RateSubmission => }/RateEdit/RateEdit.tsx | 16 ++++++++-------- .../src/pages/RateSummary/RateSummary.test.tsx | 12 +++++++++--- .../RateDetails/RateDetailsSchema.ts | 18 +++++++++--------- .../RateDetails/V2/RateDetailsV2.tsx | 2 +- .../testHelpers/apolloMocks/rateDataMock.ts | 2 ++ 6 files changed, 35 insertions(+), 25 deletions(-) rename services/app-web/src/pages/{RateSubmission => }/RateEdit/RateEdit.test.tsx (84%) rename services/app-web/src/pages/{RateSubmission => }/RateEdit/RateEdit.tsx (84%) diff --git a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.test.tsx b/services/app-web/src/pages/RateEdit/RateEdit.test.tsx similarity index 84% rename from services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.test.tsx rename to services/app-web/src/pages/RateEdit/RateEdit.test.tsx index b0a1bad390..b9134bee8f 100644 --- a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.test.tsx +++ b/services/app-web/src/pages/RateEdit/RateEdit.test.tsx @@ -1,13 +1,13 @@ import { screen, waitFor } from '@testing-library/react' -import { renderWithProviders } from '../../../testHelpers' +import { renderWithProviders } from '../../testHelpers' import { RateEdit } from './RateEdit' import { fetchCurrentUserMock, fetchRateMockSuccess, mockValidStateUser, -} from '../../../testHelpers/apolloMocks' -import { RoutesRecord } from '../../../constants' +} from '../../testHelpers/apolloMocks' +import { RoutesRecord } from '../../constants' import { Route, Routes } from 'react-router-dom' // Wrap test component in some top level routes to allow getParams to be tested @@ -45,7 +45,9 @@ describe('RateEdit', () => { }) await waitFor(() => { - expect(screen.queryByTestId('rate-edit')).toBeInTheDocument() + expect( + screen.queryByTestId('single-rate-edit') + ).toBeInTheDocument() }) }) }) diff --git a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateEdit/RateEdit.tsx similarity index 84% rename from services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx rename to services/app-web/src/pages/RateEdit/RateEdit.tsx index 059c8aad3c..f4d6b89b07 100644 --- a/services/app-web/src/pages/RateSubmission/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateEdit/RateEdit.tsx @@ -5,15 +5,15 @@ import { UpdateInformation, useFetchRateQuery, useSubmitRateMutation, -} from '../../../gen/gqlClient' +} from '../../gen/gqlClient' import { GridContainer } from '@trussworks/react-uswds' -import { Loading } from '../../../components' -import { GenericErrorPage } from '../../Errors/GenericErrorPage' -import { RateDetailsV2 } from '../../StateSubmission/RateDetails/V2/RateDetailsV2' -import { RouteT, RoutesRecord } from '../../../constants' -import { PageBannerAlerts } from '../../StateSubmission/StateSubmissionForm' -import { useAuth } from '../../../contexts/AuthContext' -import { FormContainer } from '../../StateSubmission/FormContainer' +import { Loading } from '../../components' +import { GenericErrorPage } from '../Errors/GenericErrorPage' +import { RateDetailsV2 } from '../StateSubmission/RateDetails/V2/RateDetailsV2' +import { RouteT, RoutesRecord } from '../../constants' +import { PageBannerAlerts } from '../StateSubmission/StateSubmissionForm' +import { useAuth } from '../../contexts/AuthContext' +import { FormContainer } from '../StateSubmission/FormContainer' export type SubmitOrUpdateRate = ( rateID: string, diff --git a/services/app-web/src/pages/RateSummary/RateSummary.test.tsx b/services/app-web/src/pages/RateSummary/RateSummary.test.tsx index 4d78090fb1..e2e6700027 100644 --- a/services/app-web/src/pages/RateSummary/RateSummary.test.tsx +++ b/services/app-web/src/pages/RateSummary/RateSummary.test.tsx @@ -9,7 +9,7 @@ import { import { RateSummary } from './RateSummary' import { RoutesRecord } from '../../constants' import { Route, Routes } from 'react-router-dom' -import { RateEdit } from '../RateSubmission/RateEdit/RateEdit' +import { RateEdit } from '../RateEdit/RateEdit' // Wrap test component in some top level routes to allow getParams to be tested const wrapInRoutes = (children: React.ReactNode) => { @@ -177,11 +177,17 @@ describe('RateSummary', () => { ) await waitFor(() => { - expect(screen.queryByTestId('rate-edit')).toBeInTheDocument() + expect( + screen.queryByTestId('single-rate-edit') + ).toBeInTheDocument() }) }) it('renders expected error page when rate ID is invalid', async () => { + const consoleWarnMock = jest + .spyOn(console, 'warn') + .mockImplementation() + renderWithProviders(wrapInRoutes(), { apolloProvider: { mocks: [ @@ -198,7 +204,7 @@ describe('RateSummary', () => { }, featureFlags: { 'rate-edit-unlock': true }, }) - + expect(consoleWarnMock).toHaveBeenCalled() // apollo testing mocks will console warn that your query is invalid - this is intentional expect(await screen.findByText('System error')).toBeInTheDocument() }) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts index aff200a92f..4ca791d230 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts +++ b/services/app-web/src/pages/StateSubmission/RateDetails/RateDetailsSchema.ts @@ -14,15 +14,15 @@ const SingleRateCertSchema = (_activeFeatureFlags: FeatureFlagSettings) => rateDocuments: validateFileItemsListSingleUpload({ required: true }), supportingDocuments: validateFileItemsList({ required: false }), hasSharedRateCert: _activeFeatureFlags['rate-edit-unlock']? Yup.string(): Yup.string().defined('You must select yes or no'), - packagesWithSharedRateCerts: _activeFeatureFlags['rate-edit-unlock']? Yup.array() - .when('hasSharedRateCert', { - is: 'YES', - then: Yup.array().min( - 1, - 'You must select at least one submission' - ), - }) - .required(): Yup.array().optional(), + packagesWithSharedRateCerts: _activeFeatureFlags['rate-edit-unlock']? Yup.array().optional(): Yup.array() + .when('hasSharedRateCert', { + is: 'YES', + then: Yup.array().min( + 1, + 'You must select at least one submission' + ), + }) + .required(), rateProgramIDs: Yup.array().min(1, 'You must select a program'), rateType: Yup.string().defined( 'You must choose a rate certification type' diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx index 16f9f7e3fc..ff0a3f07d3 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx @@ -30,7 +30,7 @@ import { RateRevision, } from '../../../../gen/gqlClient' import { SingleRateCertV2 } from './SingleRateCertV2' -import type { SubmitOrUpdateRate } from '../../../RateSubmission/RateEdit/RateEdit' +import type { SubmitOrUpdateRate } from '../../../RateEdit/RateEdit' export type RateDetailFormValues = { id: Rate['id'] diff --git a/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts b/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts index 6864845ee6..b5f0946002 100644 --- a/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/rateDataMock.ts @@ -118,6 +118,7 @@ const rateRevisionDataMock = (data?: Partial): RateRevision => { certifyingActuaryContacts: [ { __typename: 'ActuaryContact', + id: '123-cert-actuary', name: 'Actuary Contact Person', titleRole: 'Actuary Contact Title', email: 'actuarycontact@example.com', @@ -128,6 +129,7 @@ const rateRevisionDataMock = (data?: Partial): RateRevision => { addtlActuaryContacts: [ { __typename: 'ActuaryContact', + id: '123-additional-actuary', name: 'Additional actuary name', titleRole: 'Additional actuary title', email: 'additonalactuary@example.com', From fa8f49882f7ae3f627c87c4f0529acbdbe7ea3e0 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 14:30:55 -0600 Subject: [PATCH 20/26] app-api tests passing --- services/app-api/src/testHelpers/gqlHelpers.ts | 2 ++ services/app-graphql/src/mutations/submitRate.graphql | 2 ++ services/app-graphql/src/schema.graphql | 1 + services/app-proto/src/health_plan_form_data.proto | 1 + .../src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts | 1 + 5 files changed, 7 insertions(+) diff --git a/services/app-api/src/testHelpers/gqlHelpers.ts b/services/app-api/src/testHelpers/gqlHelpers.ts index e2f7cc706f..a775bfe2c0 100644 --- a/services/app-api/src/testHelpers/gqlHelpers.ts +++ b/services/app-api/src/testHelpers/gqlHelpers.ts @@ -262,6 +262,7 @@ const createAndUpdateTestHealthPlanPackage = async ( rateProgramIDs: [ratePrograms.reverse()[0].id], actuaryContacts: [ { + id: '123-abc', name: 'test name', titleRole: 'test title', email: 'email@example.com', @@ -275,6 +276,7 @@ const createAndUpdateTestHealthPlanPackage = async ( ] draft.addtlActuaryContacts = [ { + id: '123-addtl-abv', name: 'test name', titleRole: 'test title', email: 'email@example.com', diff --git a/services/app-graphql/src/mutations/submitRate.graphql b/services/app-graphql/src/mutations/submitRate.graphql index 87a1be9562..96c3de8365 100644 --- a/services/app-graphql/src/mutations/submitRate.graphql +++ b/services/app-graphql/src/mutations/submitRate.graphql @@ -52,6 +52,7 @@ mutation submitRate($input: SubmitRateInput!) { rateProgramIDs, rateCertificationName, certifyingActuaryContacts { + id name titleRole email @@ -59,6 +60,7 @@ mutation submitRate($input: SubmitRateInput!) { actuarialFirmOther }, addtlActuaryContacts { + id name titleRole email diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index 16f4b2b6b4..f39f27b7c4 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -1023,6 +1023,7 @@ type ActuaryContact { "Contact information input for the certifying or additional state actuary" input ActuaryContactInput { + id: ID name: String titleRole: String email: String diff --git a/services/app-proto/src/health_plan_form_data.proto b/services/app-proto/src/health_plan_form_data.proto index adc552ccb2..33185706af 100644 --- a/services/app-proto/src/health_plan_form_data.proto +++ b/services/app-proto/src/health_plan_form_data.proto @@ -70,6 +70,7 @@ message Contact { optional string name = 1; optional string title_role = 2; optional string email = 3; + optional string id = 4; } // ContractInfo subtypes diff --git a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts index 537498e3cb..466fe5adc1 100644 --- a/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts +++ b/services/app-web/src/common-code/proto/healthPlanFormDataProto/zodSchemas.ts @@ -96,6 +96,7 @@ const stateContactSchema = z.object({ }) const actuaryContactSchema = z.object({ + id: z.string().optional(), name: z.string().optional(), titleRole: z.string().optional(), email: z.string().optional(), From 13c4d03c043b90cf4e46efc122c829fbe0004075 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 16:46:03 -0600 Subject: [PATCH 21/26] cypress re-run From 0dfda94b009a74e8b61443c4e955b9317e4e63a3 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Tue, 6 Feb 2024 17:16:15 -0600 Subject: [PATCH 22/26] Fix test id and help cypress out --- .../app-web/src/pages/StateSubmission/StateSubmissionForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx index fdf4721eae..12e1283682 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.tsx @@ -251,7 +251,7 @@ export const StateSubmissionForm = (): React.ReactElement => { showPageErrorMessage={showPageErrorMessage} /> - + Date: Fri, 9 Feb 2024 15:36:06 -0600 Subject: [PATCH 23/26] Address @macrael code review --- services/app-web/src/formHelpers/formatters.ts | 2 +- .../app-web/src/pages/RateEdit/RateEdit.tsx | 9 +++------ .../RateDetails/V2/RateDetailsV2.tsx | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/services/app-web/src/formHelpers/formatters.ts b/services/app-web/src/formHelpers/formatters.ts index 788e57d94b..e1f534a688 100644 --- a/services/app-web/src/formHelpers/formatters.ts +++ b/services/app-web/src/formHelpers/formatters.ts @@ -132,7 +132,7 @@ const formatDocumentsForForm = ({ name: doc.name, key: key, s3URL: doc.s3URL, - sha256: doc.sha256 ?? undefined, + sha256: doc.sha256, status: 'UPLOAD_COMPLETE', } }) || [] diff --git a/services/app-web/src/pages/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateEdit/RateEdit.tsx index 57e379166a..f193cfacb0 100644 --- a/services/app-web/src/pages/RateEdit/RateEdit.tsx +++ b/services/app-web/src/pages/RateEdit/RateEdit.tsx @@ -61,7 +61,7 @@ export const RateEdit = (): React.ReactElement => { ) => { setIsSubmitting(true) try { - const updatedSubmission = await submitRate({ + await submitRate({ variables: { input: { rateID: rateID, @@ -69,11 +69,8 @@ export const RateEdit = (): React.ReactElement => { }, }, }) - if (updatedSubmission instanceof Error) { - setIsSubmitting(false) - } else if (updatedSubmission) { - navigate(RoutesRecord[redirect]) - } + + navigate(RoutesRecord[redirect]) } catch (serverError) { setIsSubmitting(false) } diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx index ff0a3f07d3..1c3766d97f 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx @@ -2,7 +2,6 @@ import React, { useEffect } from 'react' import { Form as UswdsForm } from '@trussworks/react-uswds' import { Formik, FormikErrors } from 'formik' import { useNavigate, useParams } from 'react-router-dom' -import { v4 as uuidv4 } from 'uuid' import styles from '../../StateSubmissionForm.module.scss' @@ -33,7 +32,7 @@ import { SingleRateCertV2 } from './SingleRateCertV2' import type { SubmitOrUpdateRate } from '../../../RateEdit/RateEdit' export type RateDetailFormValues = { - id: Rate['id'] + id?: string // no id if its a new rate rateType: RateRevision['formData']['rateType'] rateCapitationType: RateRevision['formData']['rateCapitationType'] rateDateStart: RateRevision['formData']['rateDateStart'] @@ -61,11 +60,10 @@ const generateFormValues = ( rateID?: string ): RateDetailFormValues => { const rateInfo = rateRev?.formData - const newRateID = uuidv4() return { - id: rateID ?? newRateID, - rateType: rateInfo?.rateType ?? undefined, + id: rateID, + rateType: rateInfo?.rateType, rateCapitationType: rateInfo?.rateCapitationType ?? undefined, rateDateStart: formatForForm(rateInfo?.rateDateStart), rateDateEnd: formatForForm(rateInfo?.rateDateEnd), @@ -191,7 +189,7 @@ export const RateDetailsV2 = ({ } } - const gqlFormDatas: Array<{ id: string } & RateFormDataInput> = + const gqlFormDatas: Array<{ id?: string } & RateFormDataInput> = rates.map((form) => { return { id: form.id, @@ -224,11 +222,15 @@ export const RateDetailsV2 = ({ const { id, ...formData } = gqlFormDatas[0] // only grab the first rate in the array because multi-rates functionality not added yet. This will be part of Link Rates epic - if (options.type === 'CONTINUE') { + if (options.type === 'CONTINUE' && id) { await submitRate(id, formData, setSubmitting, 'DASHBOARD') + } else if (options.type === 'CONTINUE' && !id) { + throw new Error( + 'Rate create and update for a new rate is not yet implemented. This will be part of Link Rates epic.' + ) } else if (options.type === 'SAVE_AS_DRAFT') { throw new Error( - 'Rate update is not yet implemented so save as draft is not possible. This will be part of Link Rates epic.' + 'Rate save as draft is not possible. This will be part of Link Rates epic.' ) } else { navigate(RoutesRecord[options.redirectPath]) From 7a8a24d5b6180c75be3718ac01fb99629d498cf8 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Fri, 9 Feb 2024 15:39:45 -0600 Subject: [PATCH 24/26] Think I can remove null-coalescing work now as well --- .../pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx index 1c3766d97f..c2d3400343 100644 --- a/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx +++ b/services/app-web/src/pages/StateSubmission/RateDetails/V2/RateDetailsV2.tsx @@ -64,7 +64,7 @@ const generateFormValues = ( return { id: rateID, rateType: rateInfo?.rateType, - rateCapitationType: rateInfo?.rateCapitationType ?? undefined, + rateCapitationType: rateInfo?.rateCapitationType, rateDateStart: formatForForm(rateInfo?.rateDateStart), rateDateEnd: formatForForm(rateInfo?.rateDateEnd), rateDateCertified: formatForForm(rateInfo?.rateDateCertified), @@ -88,7 +88,7 @@ const generateFormValues = ( rateInfo?.certifyingActuaryContacts ), actuaryCommunicationPreference: - rateInfo?.actuaryCommunicationPreference ?? undefined, + rateInfo?.actuaryCommunicationPreference, packagesWithSharedRateCerts: rateInfo?.packagesWithSharedRateCerts ?? [], } From 24c9f5ace34da1a9f2d4ebbce233d467d61cd85e Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Mon, 12 Feb 2024 11:27:38 -0600 Subject: [PATCH 25/26] Start generating rate names for standalone rate submits --- .../src/domain-models/nullstoUndefined.ts | 37 ++++++++++++ .../rate/generateRateCertificationName.ts | 56 +++++++++++++++++++ .../app-api/src/resolvers/rate/submitRate.ts | 41 ++++++++++++-- services/app-graphql/src/schema.graphql | 6 +- 4 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 services/app-api/src/domain-models/nullstoUndefined.ts create mode 100644 services/app-api/src/resolvers/rate/generateRateCertificationName.ts diff --git a/services/app-api/src/domain-models/nullstoUndefined.ts b/services/app-api/src/domain-models/nullstoUndefined.ts new file mode 100644 index 0000000000..f2f6ed6ebc --- /dev/null +++ b/services/app-api/src/domain-models/nullstoUndefined.ts @@ -0,0 +1,37 @@ +/* + Recursively replaces all nulls with undefineds + GQL return types are T | null instead of T | undefined which match our zod .optional() domain types + This functions allows us convert GQL to zod-friendly types to type match zod and apollo server types + and avoid manual type casting or null coalescing work + + Adapted from https://github.com/apollographql/apollo-client/issues/2412 +*/ + +type RecursivelyReplaceNullWithUndefined = T extends null + ? undefined + : T extends Date + ? T + : { + [K in keyof T]: T[K] extends (infer U)[] + ? RecursivelyReplaceNullWithUndefined[] + : RecursivelyReplaceNullWithUndefined + } + +export function nullsToUndefined( + obj: T +): RecursivelyReplaceNullWithUndefined { + if (obj === null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return undefined as any + } + + // object check based on: https://stackoverflow.com/a/51458052/6489012 + if (obj?.constructor.name === 'Object') { + for (const key in obj) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + obj[key] = nullsToUndefined(obj[key]) as any + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return obj as any +} diff --git a/services/app-api/src/resolvers/rate/generateRateCertificationName.ts b/services/app-api/src/resolvers/rate/generateRateCertificationName.ts new file mode 100644 index 0000000000..732dc31ce0 --- /dev/null +++ b/services/app-api/src/resolvers/rate/generateRateCertificationName.ts @@ -0,0 +1,56 @@ +import { formatRateNameDate } from '../../../../app-web/src/common-code/dateHelpers' +import { packageName } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import type { ProgramArgType } from '../../../../app-web/src/common-code/healthPlanFormDataType' +import type { RateFormDataType } from '../../domain-models/contractAndRates' + +const generateRateCertificationName = ( + rateFormData: RateFormDataType, + stateCode: string, + stateNumber: number, + statePrograms: ProgramArgType[] +): string => { + const { + rateType, + rateProgramIDs, + amendmentEffectiveDateEnd, + amendmentEffectiveDateStart, + rateDateCertified, + rateDateEnd, + rateDateStart, + } = rateFormData + + let rateName = `${packageName( + stateCode, + stateNumber, + rateProgramIDs ?? [], + statePrograms + )}-RATE` + if (rateType === 'NEW' && rateDateStart) { + rateName = rateName.concat( + '-', + formatRateNameDate(rateDateStart), + '-', + formatRateNameDate(rateDateEnd), + '-', + 'CERTIFICATION' + ) + } + + if (rateType === 'AMENDMENT') { + rateName = rateName.concat( + '-', + formatRateNameDate(amendmentEffectiveDateStart), + '-', + formatRateNameDate(amendmentEffectiveDateEnd), + '-', + 'AMENDMENT' + ) + } + + if (rateDateCertified) { + rateName = rateName.concat('-', formatRateNameDate(rateDateCertified)) + } + return rateName +} + +export { generateRateCertificationName } diff --git a/services/app-api/src/resolvers/rate/submitRate.ts b/services/app-api/src/resolvers/rate/submitRate.ts index 550bc137a5..8af1b25199 100644 --- a/services/app-api/src/resolvers/rate/submitRate.ts +++ b/services/app-api/src/resolvers/rate/submitRate.ts @@ -4,14 +4,20 @@ import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, } from '../attributeHelper' -import type { RateFormDataType } from '../../domain-models' import { isStateUser } from '../../domain-models' import { logError } from '../../logger' import { ForbiddenError, UserInputError } from 'apollo-server-lambda' import { NotFoundError } from '../../postgres' import { GraphQLError } from 'graphql/index' import type { LDService } from '../../launchDarkly/launchDarkly' +import { generateRateCertificationName } from './generateRateCertificationName' +import { findStatePrograms } from '../../../../app-web/src/common-code/healthPlanFormDataType/findStatePrograms' +import { nullsToUndefined } from '../../domain-models/nullstoUndefined' +/* + Submit rate will change a draft revision to submitted and generate a rate name if one is missing + Also, if form data is passed in (such as on standalone rate edits) the form data itself will be updated +*/ export function submitRate( store: Store, launchDarkly: LDService @@ -86,6 +92,25 @@ export function submitRate( }) } + // prepare to generate rate cert name - either use new form data coming down on submit or unsubmitted submission data already in database + const stateCode = unsubmittedRate.stateCode + const stateNumber = unsubmittedRate.stateNumber + const statePrograms = findStatePrograms(stateCode) + const generatedRateCertName = formData + ? generateRateCertificationName( + nullsToUndefined(formData), + stateCode, + stateNumber, + statePrograms + ) + : generateRateCertificationName( + draftRateRevision.formData, + stateCode, + stateNumber, + statePrograms + ) + + // combine existing db draft data with any form data added on submit // call submit rate handler const submittedRate = await store.submitRate({ rateID, @@ -93,8 +118,7 @@ export function submitRate( submitReason: submitReason ?? 'Initial submission', formData: formData ? { - rateType: (formData.rateType ?? - undefined) as RateFormDataType['rateType'], + rateType: formData.rateType ?? undefined, rateCapitationType: formData.rateCapitationType ?? undefined, rateDocuments: formData.rateDocuments ?? [], @@ -108,8 +132,6 @@ export function submitRate( amendmentEffectiveDateEnd: formData.amendmentEffectiveDateEnd ?? undefined, rateProgramIDs: formData.rateProgramIDs ?? [], - rateCertificationName: - formData.rateCertificationName ?? undefined, certifyingActuaryContacts: formData.certifyingActuaryContacts ? formData.certifyingActuaryContacts.map( @@ -145,8 +167,15 @@ export function submitRate( packageId: pkg.packageId ?? undefined, packageStatus: pkg.packageStatus ?? undefined, })), + rateCertificationName: + formData.rateCertificationName ?? + generatedRateCertName, } - : undefined, + : { + rateCertificationName: + draftRateRevision.formData.rateCertificationName ?? + generatedRateCertName, + }, }) if (submittedRate instanceof Error) { diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index f39f27b7c4..49f6a7f122 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -975,7 +975,7 @@ type ContractFormData { } "Either new capitation rates (NEW) or updates to previously certified capitation rates (AMENDMENT)" -enum RateType { +enum RateAmendmentType { NEW AMENDMENT } @@ -1058,7 +1058,7 @@ type RateFormData { Refers to whether the state is submitting a brand new rate certification or an amendment to an existing rate certification """ - rateType: RateType + rateType: RateAmendmentType """ Can be 'RATE_CELL' or 'RATE_RANGE' These values represent on what basis the capitation rate is actuarially sound @@ -1265,7 +1265,7 @@ input RateFormDataInput { Refers to whether the state is submitting a brand new rate certification or an amendment to an existing rate certification """ - rateType: RateType + rateType: RateAmendmentType """ Can be 'RATE_CELL' or 'RATE_RANGE' These values represent on what basis the capitation rate is actuarially sound From e4d0c015e5242b2ea46f7f006933622179066705 Mon Sep 17 00:00:00 2001 From: Hana Worku Date: Mon, 12 Feb 2024 11:48:19 -0600 Subject: [PATCH 26/26] Fixup app-api tests, don't mess with id-only submit log path --- services/app-api/src/resolvers/rate/submitRate.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/app-api/src/resolvers/rate/submitRate.ts b/services/app-api/src/resolvers/rate/submitRate.ts index 8af1b25199..81791d3dee 100644 --- a/services/app-api/src/resolvers/rate/submitRate.ts +++ b/services/app-api/src/resolvers/rate/submitRate.ts @@ -171,11 +171,7 @@ export function submitRate( formData.rateCertificationName ?? generatedRateCertName, } - : { - rateCertificationName: - draftRateRevision.formData.rateCertificationName ?? - generatedRateCertName, - }, + : undefined, }) if (submittedRate instanceof Error) {