diff --git a/services/app-api/handlers/formTemplates/populateTemplatesTable.ts b/services/app-api/handlers/formTemplates/populateTemplatesTable.ts index d72ded05d..acfc8ae5b 100644 --- a/services/app-api/handlers/formTemplates/populateTemplatesTable.ts +++ b/services/app-api/handlers/formTemplates/populateTemplatesTable.ts @@ -16,7 +16,6 @@ import { import { S3 } from "aws-sdk"; import * as path from "path"; import { logger } from "../../utils/logging"; -import { AttributeValue, QueryInput } from "aws-sdk/clients/dynamodb"; import { createHash } from "crypto"; type S3ObjectRequired = SomeRequired; @@ -34,26 +33,6 @@ type FormTemplateMetaData = { hash: string; }; -/** - * - * @param reportType report type - * @param hash hash to look for - * @returns - */ -export function getTemplateVersionByHash(reportType: ReportType, hash: string) { - const queryParams: QueryInput = { - TableName: process.env.FORM_TEMPLATE_TABLE_NAME!, - IndexName: "HashIndex", - KeyConditionExpression: "reportType = :reportType AND md5Hash = :md5Hash", - Limit: 1, - ExpressionAttributeValues: { - ":md5Hash": hash as AttributeValue, - ":reportType": reportType as unknown as AttributeValue, - }, - }; - return dynamodbLib.query(queryParams); -} - /** * Retrieve template data from S3 * diff --git a/services/app-api/handlers/reports/create.ts b/services/app-api/handlers/reports/create.ts index b842e1e89..5ae572a7f 100644 --- a/services/app-api/handlers/reports/create.ts +++ b/services/app-api/handlers/reports/create.ts @@ -83,10 +83,14 @@ export const createReport = handler(async (event, _context) => { let formTemplate, formTemplateVersion; + const isProgramPCCM = + unvalidatedMetadata?.programIsPCCM?.[0]?.value === "Yes"; + try { ({ formTemplate, formTemplateVersion } = await getOrCreateFormTemplate( reportBucket, - reportType + reportType, + isProgramPCCM )); } catch (err) { logger.error(err, "Error getting or creating template"); diff --git a/services/app-api/utils/formTemplates/formTemplates.test.ts b/services/app-api/utils/formTemplates/formTemplates.test.ts index 61d2c7e86..eb6b006af 100644 --- a/services/app-api/utils/formTemplates/formTemplates.test.ts +++ b/services/app-api/utils/formTemplates/formTemplates.test.ts @@ -2,6 +2,7 @@ import { compileValidationJsonFromRoutes, flattenReportRoutesArray, formTemplateForReportType, + generatePCCMTemplate, getOrCreateFormTemplate, getValidationFromFormTemplate, isFieldElement, @@ -15,6 +16,11 @@ import { mockDocumentClient, mockReportJson } from "../testing/setupJest"; import s3Lib from "../s3/s3-lib"; import dynamodbLib from "../dynamo/dynamodb-lib"; +const programIsPCCM = true; +const programIsNotPCCM = false; + +global.structuredClone = (val: any) => JSON.parse(JSON.stringify(val)); + const currentMLRFormHash = createHash("md5") .update(JSON.stringify(mlr)) .digest("hex"); @@ -23,11 +29,21 @@ const currentMCPARFormHash = createHash("md5") .update(JSON.stringify(mcpar)) .digest("hex"); +const pccmTemplate = generatePCCMTemplate(mcpar); +const currentPCCMFormHash = createHash("md5") + .update(JSON.stringify(pccmTemplate)) + .digest("hex"); + describe("Test getOrCreateFormTemplate MCPAR", () => { beforeEach(() => { jest.restoreAllMocks(); }); it("should create a new form template if none exist", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [], }); @@ -35,7 +51,8 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mcpar-reports", - ReportType.MCPAR + ReportType.MCPAR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); @@ -47,7 +64,34 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { expect(result.formTemplateVersion?.md5Hash).toEqual(currentMCPARFormHash); }); + it("should create a new form template for PCCM if none exist", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + const dynamoPutSpy = jest.spyOn(dynamodbLib, "put"); + const s3PutSpy = jest.spyOn(s3Lib, "put"); + const result = await getOrCreateFormTemplate( + "local-mcpar-reports", + ReportType.MCPAR, + programIsPCCM + ); + expect(dynamoPutSpy).toHaveBeenCalled(); + expect(s3PutSpy).toHaveBeenCalled(); + expect(result.formTemplate).toEqual({ + ...pccmTemplate, + validationJson: getValidationFromFormTemplate(pccmTemplate as ReportJson), + }); + expect(result.formTemplateVersion?.versionNumber).toEqual(1); + expect(result.formTemplateVersion?.md5Hash).toEqual(currentPCCMFormHash); + }); + it("should return the right form and formTemplateVersion if it matches the most recent form", async () => { + // mocked once for search by hash mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { @@ -56,19 +100,14 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { md5Hash: currentMCPARFormHash, versionNumber: 3, }, - { - formTemplateId: "foo", - id: "mockReportJson", - md5Hash: currentMCPARFormHash + "111", - versionNumber: 2, - }, ], }); const dynamoPutSpy = jest.spyOn(dynamodbLib, "put"); const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mcpar-reports", - ReportType.MCPAR + ReportType.MCPAR, + programIsNotPCCM ); expect(dynamoPutSpy).not.toHaveBeenCalled(); expect(s3PutSpy).not.toHaveBeenCalled(); @@ -77,6 +116,11 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { }); it("should create a new form if it doesn't match the most recent form", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { @@ -97,7 +141,8 @@ describe("Test getOrCreateFormTemplate MCPAR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mcpar-reports", - ReportType.MCPAR + ReportType.MCPAR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); @@ -110,6 +155,11 @@ describe("Test getOrCreateFormTemplate MLR", () => { jest.restoreAllMocks(); }); it("should create a new form template if none exist", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [], }); @@ -117,7 +167,8 @@ describe("Test getOrCreateFormTemplate MLR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mlr-reports", - ReportType.MLR + ReportType.MLR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); @@ -130,6 +181,7 @@ describe("Test getOrCreateFormTemplate MLR", () => { }); it("should return the right form and formTemplateVersion if it matches the most recent form", async () => { + // mocked once for search by hash mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { @@ -138,19 +190,14 @@ describe("Test getOrCreateFormTemplate MLR", () => { md5Hash: currentMLRFormHash, versionNumber: 3, }, - { - formTemplateId: "foo", - id: "mockReportJson", - md5Hash: currentMLRFormHash + "111", - versionNumber: 2, - }, ], }); const dynamoPutSpy = jest.spyOn(dynamodbLib, "put"); const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mlr-reports", - ReportType.MLR + ReportType.MLR, + programIsNotPCCM ); expect(dynamoPutSpy).not.toHaveBeenCalled(); expect(s3PutSpy).not.toHaveBeenCalled(); @@ -159,18 +206,23 @@ describe("Test getOrCreateFormTemplate MLR", () => { }); it("should create a new form if it doesn't match the most recent form", async () => { + // mocked once for search by hash + mockDocumentClient.query.promise.mockReturnValueOnce({ + Items: [], + }); + // mocked again for search for latest report mockDocumentClient.query.promise.mockReturnValueOnce({ Items: [ { formTemplateId: "foo", id: "mockReportJson", - md5Hash: currentMLRFormHash + "111111", + md5Hash: currentMCPARFormHash + "111111", versionNumber: 3, }, { formTemplateId: "foo", id: "mockReportJson", - md5Hash: currentMLRFormHash + "111", + md5Hash: currentMCPARFormHash + "111", versionNumber: 2, }, ], @@ -179,7 +231,8 @@ describe("Test getOrCreateFormTemplate MLR", () => { const s3PutSpy = jest.spyOn(s3Lib, "put"); const result = await getOrCreateFormTemplate( "local-mlr-reports", - ReportType.MLR + ReportType.MLR, + programIsNotPCCM ); expect(dynamoPutSpy).toHaveBeenCalled(); expect(s3PutSpy).toHaveBeenCalled(); diff --git a/services/app-api/utils/formTemplates/formTemplates.ts b/services/app-api/utils/formTemplates/formTemplates.ts index 04f48350c..ac27e6d52 100644 --- a/services/app-api/utils/formTemplates/formTemplates.ts +++ b/services/app-api/utils/formTemplates/formTemplates.ts @@ -34,6 +34,24 @@ export async function getNewestTemplateVersion(reportType: ReportType) { return result.Items?.[0]; } +export async function getTemplateVersionByHash( + reportType: ReportType, + hash: string +) { + const queryParams: QueryInput = { + TableName: process.env.FORM_TEMPLATE_TABLE_NAME!, + IndexName: "HashIndex", + KeyConditionExpression: "reportType = :reportType AND md5Hash = :md5Hash", + Limit: 1, + ExpressionAttributeValues: { + ":md5Hash": hash as AttributeValue, + ":reportType": reportType as unknown as AttributeValue, + }, + }; + const result = await dynamodbLib.query(queryParams); + return result.Items?.[0]; +} + export const formTemplateForReportType = (reportType: ReportType) => { switch (reportType) { case ReportType.MCPAR: @@ -54,25 +72,31 @@ export const formTemplateForReportType = (reportType: ReportType) => { export async function getOrCreateFormTemplate( reportBucket: string, - reportType: ReportType + reportType: ReportType, + isProgramPCCM: boolean ) { - const currentFormTemplate = formTemplateForReportType(reportType); + let currentFormTemplate = formTemplateForReportType(reportType); + if (isProgramPCCM) { + currentFormTemplate = generatePCCMTemplate(currentFormTemplate); + } const stringifiedTemplate = JSON.stringify(currentFormTemplate); const currentTemplateHash = createHash("md5") .update(stringifiedTemplate) .digest("hex"); - const mostRecentTemplateVersion = await getNewestTemplateVersion(reportType); - const mostRecentTemplateVersionHash = mostRecentTemplateVersion?.md5Hash; + const matchingTemplateMetadata = await getTemplateVersionByHash( + reportType, + currentTemplateHash + ); - if (currentTemplateHash === mostRecentTemplateVersionHash) { + if (matchingTemplateMetadata) { return { formTemplate: await getTemplate( reportBucket, - getFormTemplateKey(mostRecentTemplateVersion?.id) + getFormTemplateKey(matchingTemplateMetadata?.id) ), - formTemplateVersion: mostRecentTemplateVersion, + formTemplateVersion: matchingTemplateMetadata, }; } else { const newFormTemplateId = KSUID.randomSync().string; @@ -92,10 +116,12 @@ export async function getOrCreateFormTemplate( throw err; } + const newestTemplateMetadata = await getNewestTemplateVersion(reportType); + // If we didn't find any form templates, start version at 1. const newFormTemplateVersionItem: FormTemplate = { - versionNumber: mostRecentTemplateVersion?.versionNumber - ? (mostRecentTemplateVersion.versionNumber += 1) + versionNumber: newestTemplateMetadata?.versionNumber + ? (newestTemplateMetadata.versionNumber += 1) : 1, md5Hash: currentTemplateHash, id: newFormTemplateId, @@ -244,3 +270,43 @@ export function getValidationFromFormTemplate(reportJson: ReportJson) { export function getPossibleFieldsFromFormTemplate(reportJson: ReportJson) { return Object.keys(getValidationFromFormTemplate(reportJson)); } + +const routesToIncludeInPCCM = { + "A: Program Information": [ + "Point of Contact", + "Reporting Period", + "Add Plans", + ], + "B: State-Level Indicators": ["I: Program Characteristics"], + "C: Program-Level Indicators": ["I: Program Characteristics"], + "D: Plan-Level Indicators": ["I: Program Characteristics", "VIII: Sanctions"], + "Review & Submit": [], +} as { [key: string]: string[] }; + +const entitiesToIncludeInPCCM = ["plans", "sanctions"]; + +export const generatePCCMTemplate = (originalReportTemplate: any) => { + const reportTemplate = structuredClone(originalReportTemplate); + // remove top level sections not in include list + reportTemplate.routes = reportTemplate.routes.filter( + (route: ReportRoute) => !!routesToIncludeInPCCM[route.name] + ); + + // only include listed subsections + for (let route of reportTemplate.routes) { + if (route?.children) { + route.children = route.children.filter((childRoute: ReportRoute) => + routesToIncludeInPCCM[route.name].includes(childRoute.name) + ); + } + } + + // Any entity not in the allow list must be removed. + for (let entityType of Object.keys(reportTemplate.entities)) { + if (!entitiesToIncludeInPCCM.includes(entityType)) { + delete reportTemplate.entities[entityType]; + } + } + + return reportTemplate; +}; diff --git a/services/app-api/utils/testing/setupJest.ts b/services/app-api/utils/testing/setupJest.ts index c7fd1ff5d..3544a90fc 100644 --- a/services/app-api/utils/testing/setupJest.ts +++ b/services/app-api/utils/testing/setupJest.ts @@ -152,6 +152,12 @@ export const mockDynamoDataCompleted: MCPARReportMetadata = { reportingPeriodEndDate: 168515200000, dueDate: 168515200000, combinedData: false, + programIsPCCM: [ + { + key: "programIsPCCM-no_programIsNotPCCM", + value: "No", + }, + ], lastAlteredBy: "Thelonious States", fieldDataId: "mockReportFieldData", formTemplateId: "mockReportJson", @@ -225,6 +231,12 @@ export const mockMcparReport = { reportingPeriodEndDate: 168515200000, dueDate: 168515200000, combinedData: false, + programIsPCCM: [ + { + key: "programIsPCCM-no_programIsNotPCCM", + value: "No", + }, + ], lastAlteredBy: "Thelonious States", fieldDataId: "mockReportFieldData", formTemplateId: "mockReportJson", diff --git a/services/app-api/utils/types/reports.ts b/services/app-api/utils/types/reports.ts index 64985a4cb..c2ecc8fb2 100644 --- a/services/app-api/utils/types/reports.ts +++ b/services/app-api/utils/types/reports.ts @@ -1,4 +1,4 @@ -import { FormJson, ModalOverlayReportPageShape } from "./formFields"; +import { Choice, FormJson, ModalOverlayReportPageShape } from "./formFields"; import { AnyObject, CompletionData, CustomHtmlElement, State } from "./other"; // REPORT STRUCTURE @@ -151,6 +151,7 @@ export interface MCPARReportMetadata extends ReportMetadata { reportingPeriodEndDate: number; dueDate: number; combinedData: boolean; + programIsPCCM: Choice[]; } /** diff --git a/services/app-api/utils/validation/schemas.ts b/services/app-api/utils/validation/schemas.ts index ef11317e2..296a9bc2d 100644 --- a/services/app-api/utils/validation/schemas.ts +++ b/services/app-api/utils/validation/schemas.ts @@ -1,4 +1,5 @@ import * as yup from "yup"; +import { radioOptional } from "./completionSchemas"; export const metadataValidationSchema = yup.object().shape({ programName: yup.string(), @@ -9,6 +10,7 @@ export const metadataValidationSchema = yup.object().shape({ reportingPeriodEndDate: yup.number(), dueDate: yup.number(), combinedData: yup.boolean(), + programIsPCCM: radioOptional(), lastAlteredBy: yup.string(), submittedBy: yup.string(), submittedOnDate: yup.string(), diff --git a/services/ui-src/src/components/modals/AddEditReportModal.test.tsx b/services/ui-src/src/components/modals/AddEditReportModal.test.tsx index a21fc7129..f69cc233d 100644 --- a/services/ui-src/src/components/modals/AddEditReportModal.test.tsx +++ b/services/ui-src/src/components/modals/AddEditReportModal.test.tsx @@ -5,18 +5,21 @@ import { axe } from "jest-axe"; //components import { AddEditReportModal, ReportContext } from "components"; import { + mockLDFlags, mockMcparReport, mockMcparReportContext, + mockMlrReport, + mockMlrReportContext, RouterWrappedComponent, - mockLDFlags, } from "utils/testing/setupJest"; +import { convertDateUtcToEt } from "utils"; const mockCreateReport = jest.fn(); const mockUpdateReport = jest.fn(); const mockFetchReportsByState = jest.fn(); const mockCloseHandler = jest.fn(); -const mockedReportContext = { +const mockedMcparReportContext = { ...mockMcparReportContext, createReport: mockCreateReport, updateReport: mockUpdateReport, @@ -24,9 +27,17 @@ const mockedReportContext = { isReportPage: true, }; +const mockedMlrReportContext = { + ...mockMlrReportContext, + createReport: mockCreateReport, + updateReport: mockUpdateReport, + fetchReportsByState: mockFetchReportsByState, + isReportPage: true, +}; + const modalComponent = ( - + ); +// a similar assignment is performed in DashboardPage and is needed here to make sure the modal form hydrates +const mockSelectedMcparReport = { + ...mockMcparReport, + fieldData: { + programName: mockMcparReport.programName, + reportingPeriodEndDate: convertDateUtcToEt( + mockMcparReport.reportingPeriodEndDate + ), + reportingPeriodStartDate: convertDateUtcToEt( + mockMcparReport.reportingPeriodStartDate + ), + combinedData: mockMcparReport.combinedData, + programIsPCCM: mockMcparReport?.programIsPCCM, + }, +}; + const modalComponentWithSelectedReport = ( - + - + - + { await userEvent.type(startDateField, "1/1/2022"); const endDateField = form.querySelector("[name='reportingPeriodEndDate']")!; await userEvent.type(endDateField, "12/31/2022"); + const isPccmNo = screen.getByLabelText("No") as HTMLInputElement; + if (!isPccmNo.disabled) { + await userEvent.click(isPccmNo); + } const submitButton = screen.getByRole("button", { name: "Save" }); await userEvent.click(submitButton); }; @@ -149,14 +180,43 @@ describe("Test AddEditReportModal functionality for MCPAR", () => { await expect(mockCloseHandler).toHaveBeenCalledTimes(1); }); - test("Editing an existing report", async () => { + test("Edit modal hydrates with report info and disables fields", async () => { mockLDFlags.setDefault({ yoyCopy: true }); const result = await render(modalComponentWithSelectedReport); const form = result.getByTestId("add-edit-report-form"); const copyFieldDataSourceId = form.querySelector( "[name='copyFieldDataSourceId']" )!; + const programIsPCCMField = form.querySelectorAll("[name='programIsPCCM']")!; + // yoy copy and pccm fields are disabled expect(copyFieldDataSourceId).toHaveProperty("disabled", true); + expect(programIsPCCMField[0]).toHaveProperty("disabled", true); + expect(programIsPCCMField[1]).toHaveProperty("disabled", true); + // hydrated values are in the modal + const programNameField = form.querySelector("[name='programName']")!; + const startDateField = form.querySelector( + "[name='reportingPeriodStartDate']" + )!; + const endDateField = form.querySelector("[name='reportingPeriodEndDate']")!; + expect(programNameField).toHaveProperty( + "value", + mockMcparReport.programName + ); + expect(startDateField).toHaveProperty( + "value", + convertDateUtcToEt(mockMcparReport.reportingPeriodStartDate) + ); + expect(endDateField).toHaveProperty( + "value", + convertDateUtcToEt(mockMcparReport.reportingPeriodEndDate) + ); + userEvent.click(screen.getByText("Cancel")); + }); + + test("Editing an existing report", async () => { + mockLDFlags.setDefault({ yoyCopy: true }); + const result = await render(modalComponentWithSelectedReport); + const form = result.getByTestId("add-edit-report-form"); await fillForm(form); await expect(mockUpdateReport).toHaveBeenCalledTimes(1); await expect(mockFetchReportsByState).toHaveBeenCalledTimes(1); diff --git a/services/ui-src/src/components/modals/AddEditReportModal.tsx b/services/ui-src/src/components/modals/AddEditReportModal.tsx index e86010a53..60867713e 100644 --- a/services/ui-src/src/components/modals/AddEditReportModal.tsx +++ b/services/ui-src/src/components/modals/AddEditReportModal.tsx @@ -48,23 +48,32 @@ export const AddEditReportModal = ({ const [form, setForm] = useState(modalFormJson); useEffect(() => { + // make deep copy of baseline form for customization + let customizedModalForm: FormJson = JSON.parse( + JSON.stringify(modalFormJson) + ); // check if yoy copy field exists in form const yoyCopyFieldIndex = form.fields.findIndex( (field: FormField | FormLayoutElement) => field.id === "copyFieldDataSourceId" ); - if (yoyCopyFieldIndex > -1) { - // if not creating new report || no reports eligible for copy - if (selectedReport?.id || !copyEligibleReportsByState?.length) { - // make deep copy of baseline form, disable yoy copy field, and use copied form - let tempForm: FormJson = JSON.parse(JSON.stringify(modalFormJson)); - tempForm.fields[yoyCopyFieldIndex].props!.disabled = true; - setForm(tempForm); - } else { - // use the original baseline form - setForm(modalFormJson); - } + // if yoyCopyField is in form && (not creating new report || no reports eligible for copy) + if ( + yoyCopyFieldIndex > -1 && + (selectedReport?.id || !copyEligibleReportsByState?.length) + ) { + customizedModalForm.fields[yoyCopyFieldIndex].props!.disabled = true; + } + // check if program is PCCM field exists in form + const programIsPCCMFieldIndex = form.fields.findIndex( + (field: FormField | FormLayoutElement) => field.id === "programIsPCCM" + ); + // if programIsPCCMField is in form && not creating new report + if (programIsPCCMFieldIndex > -1 && selectedReport?.id) { + customizedModalForm.fields[programIsPCCMFieldIndex].props!.disabled = + true; } + setForm(customizedModalForm); }, [selectedReport, copyEligibleReportsByState]); // MCPAR report payload @@ -79,6 +88,7 @@ export const AddEditReportModal = ({ const reportingPeriodEndDate = convertDateEtToUtc( formData["reportingPeriodEndDate"] ); + const programIsPCCM = formData["programIsPCCM"]; return { metadata: { @@ -89,6 +99,7 @@ export const AddEditReportModal = ({ combinedData, lastAlteredBy: full_name, copyFieldDataSourceId: copyFieldDataSourceId?.value, + programIsPCCM, }, fieldData: { reportingPeriodStartDate: convertDateUtcToEt(reportingPeriodStartDate), diff --git a/services/ui-src/src/components/pages/Dashboard/DashboardPage.tsx b/services/ui-src/src/components/pages/Dashboard/DashboardPage.tsx index ac01b0273..8c0bc52c3 100644 --- a/services/ui-src/src/components/pages/Dashboard/DashboardPage.tsx +++ b/services/ui-src/src/components/pages/Dashboard/DashboardPage.tsx @@ -144,6 +144,7 @@ export const DashboardPage = ({ reportType }: Props) => { ), combinedData: report.combinedData, copyFieldDataSourceId, + programIsPCCM: report?.programIsPCCM, }, state: report.state, id: report.id, diff --git a/services/ui-src/src/forms/addEditMcparReport/addEditMcparReport.json b/services/ui-src/src/forms/addEditMcparReport/addEditMcparReport.json index 95f55db1f..376e07b08 100644 --- a/services/ui-src/src/forms/addEditMcparReport/addEditMcparReport.json +++ b/services/ui-src/src/forms/addEditMcparReport/addEditMcparReport.json @@ -56,6 +56,24 @@ "label": "Exclusion of CHIP from MCPAR", "hint": "Enrollees in separate CHIP programs funded under Title XXI should not be reported in the MCPAR. Please check this box if the state is unable to remove information about Separate CHIP enrollees from its reporting on this program." } + }, + { + "id": "programIsPCCM", + "type": "radio", + "validation": "radio", + "props": { + "label": "Is your program a Primary Care Case Management (PCCM) entity?", + "choices": [ + { + "id": "yes_programIsPCCM", + "label": "Yes" + }, + { + "id": "no_programIsNotPCCM", + "label": "No" + } + ] + } } ] } diff --git a/services/ui-src/src/forms/addEditMcparReport/addEditMcparReportWithoutYoY.json b/services/ui-src/src/forms/addEditMcparReport/addEditMcparReportWithoutYoY.json index 7024c8a93..956412b43 100644 --- a/services/ui-src/src/forms/addEditMcparReport/addEditMcparReportWithoutYoY.json +++ b/services/ui-src/src/forms/addEditMcparReport/addEditMcparReportWithoutYoY.json @@ -47,6 +47,24 @@ "label": "Exclusion of CHIP from MCPAR", "hint": "Enrollees in separate CHIP programs funded under Title XXI should not be reported in the MCPAR. Please check this box if the state is unable to remove information about Separate CHIP enrollees from its reporting on this program." } + }, + { + "id": "programIsPCCM", + "type": "radio", + "validation": "radio", + "props": { + "label": "Is your program a Primary Care Case Management (PCCM) entity?", + "choices": [ + { + "id": "yes_programIsPCCM", + "label": "Yes" + }, + { + "id": "no_programIsNotPCCM", + "label": "No" + } + ] + } } ] } diff --git a/services/ui-src/src/types/reportContext.ts b/services/ui-src/src/types/reportContext.ts index 22ab953d0..2249deb2f 100644 --- a/services/ui-src/src/types/reportContext.ts +++ b/services/ui-src/src/types/reportContext.ts @@ -1,6 +1,6 @@ // REPORT PROVIDER/CONTEXT -import { AnyObject, ReportJson } from "types"; +import { AnyObject, Choice, ReportJson } from "types"; export interface ReportKeys { reportType: string; @@ -28,6 +28,7 @@ export interface ReportMetadataShape extends ReportKeys { locked?: boolean; fieldDataId: string; copyFieldDataSourceId?: string; + programIsPCCM?: Choice[]; } export interface ReportShape extends ReportMetadataShape { diff --git a/services/ui-src/src/utils/testing/mockReport.tsx b/services/ui-src/src/utils/testing/mockReport.tsx index 7f3d99786..2feaffae6 100644 --- a/services/ui-src/src/utils/testing/mockReport.tsx +++ b/services/ui-src/src/utils/testing/mockReport.tsx @@ -307,6 +307,12 @@ export const mockMcparReport = { lastAltered: 162515200000, lastAlteredBy: "Thelonious States", combinedData: false, + programIsPCCM: [ + { + key: "programIsPCCM-no_programIsNotPCCM", + value: "No", + }, + ], submittedOnDate: Date.now(), fieldData: mockReportFieldData, fieldDataId: "mockFieldDataId", @@ -334,6 +340,12 @@ export const mockMcparReportCombinedData = { lastAltered: 162515200000, lastAlteredBy: "Thelonious States", combinedData: true, + programIsPCCM: [ + { + key: "programIsPCCM-no_programIsNotPCCM", + value: "No", + }, + ], submittedOnDate: Date.now(), fieldData: mockReportFieldData, fieldDataId: "mockFieldDataId", diff --git a/tests/cypress/tests/e2e/mcpar/autosave.feature b/tests/cypress/tests/e2e/mcpar/autosave.feature index da4f4bc76..fed7a346a 100644 --- a/tests/cypress/tests/e2e/mcpar/autosave.feature +++ b/tests/cypress/tests/e2e/mcpar/autosave.feature @@ -9,6 +9,7 @@ Feature: Autosave | programName | text | Autosave Test | | reportingPeriodStartDate | text | 07/11/2022 | | reportingPeriodEndDate | text | 11/07/2026 | + | programIsPCCM | radio| No | And I click the "Save" button Then there is an "Autosave Test" program diff --git a/tests/cypress/tests/e2e/mcpar/dashboard.feature b/tests/cypress/tests/e2e/mcpar/dashboard.feature index 4865c8632..5a7fff560 100644 --- a/tests/cypress/tests/e2e/mcpar/dashboard.feature +++ b/tests/cypress/tests/e2e/mcpar/dashboard.feature @@ -14,13 +14,14 @@ Feature: MCPAR Dashboard Page - Program Creation/Editing/Archiving | reportingPeriodStartDate | text | | | reportingPeriodEndDate | text | | | combinedData | singleCheckbox | | + | programIsPCCM | radio | | And I click the "Save" button Then there is the active program with "" Examples: #New Program Data - | title | startDate | endDate | combinedData | - | Test Program | 07/11/2022 | 11/07/2026 | true | - | Test Program 2 | 01012022 | 12212024 | false | + | title | startDate | endDate | combinedData | programIsPCCM | + | Test Program | 07/11/2022 | 11/07/2026 | true | No | + | Test Program 2 | 01012022 | 12212024 | false | No | Scenario Outline: State users can Edit programs Given I click the "Edit Report" button @@ -29,6 +30,7 @@ Feature: MCPAR Dashboard Page - Program Creation/Editing/Archiving | reportingPeriodStartDate | text | 01/01/2022 | | reportingPeriodEndDate | text | 12/21/2024 | | combinedData | singleCheckbox | false | + | programIsPCCM | radio | No | When these form elements are edited: | programName | text | <title> | diff --git a/tests/cypress/tests/e2e/mcpar/form.stepdef.js b/tests/cypress/tests/e2e/mcpar/form.stepdef.js index 9fd581bf9..60ee3a0bf 100644 --- a/tests/cypress/tests/e2e/mcpar/form.stepdef.js +++ b/tests/cypress/tests/e2e/mcpar/form.stepdef.js @@ -110,6 +110,7 @@ When("I completely fill out a {string} form", (form) => { today.toLocaleDateString("en-US") ); cy.get('input[name="combinedData"').check(); + cy.get('input[name="programIsPCCM"').check("No"); cy.get("button[type=submit]").contains("Save").click(); //Find our new program and open it @@ -159,6 +160,7 @@ When("I try to submit an incomplete {string} program", (form) => { today.toLocaleDateString("en-US") ); cy.findByRole("checkbox").focus().click(); + cy.get('input[name="programIsPCCM"').check("No"); cy.get("button[type=submit]").contains("Save").click(); //Find our new program and open it diff --git a/tests/cypress/tests/e2e/sidebar.spec.js b/tests/cypress/tests/e2e/sidebar.spec.js index b58f852f7..8ba81b23d 100644 --- a/tests/cypress/tests/e2e/sidebar.spec.js +++ b/tests/cypress/tests/e2e/sidebar.spec.js @@ -23,6 +23,7 @@ describe("Sidebar integration tests", () => { cy.get('input[name="reportingPeriodStartDate"]').type("07142023"); cy.get('input[name="reportingPeriodEndDate"]').type("07142026"); cy.findByRole("checkbox").focused().click(); + cy.get('input[name="programIsPCCM"').check("No"); cy.get("button[type=submit]").contains("Save").click(); cy.wait(2000); cy.findAllByRole("button", { name: "Edit" }).first().click(); // Timeout