diff --git a/.codeclimate.yml b/.codeclimate.yml index 8641ebe8a0..fcbdd16bea 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -28,7 +28,6 @@ exclude_patterns: - "src/" - "services/.sechub/" - "services/database/" - - "services/stream-functions/" - "services/ui/" - "services/ui-src/public/" - "services/ui-src/src/libs/" diff --git a/.env_example b/.env_example index ea3ba2a632..70423f3516 100644 --- a/.env_example +++ b/.env_example @@ -22,4 +22,6 @@ MEASURE_TABLE_NAME=local-measures # LAUNCHDARKLY LD_PROJECT_KEY=mdct-qmr -LD_SDK_KEY=sdk-25b0f45f-bb20-4223-aede-32d66b525721 #pragma allowlist secret +LD_SDK_KEY=sdk-25b0f45f-bb20-4223-aede-32d66b525721 #pragma: allowlist secret + +docraptorApiKey=YOUR_API_KEY_HERE #pragma: allowlist secret diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3ed3821a5a..b7882841fa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # Default repo owners -* @BearHanded @braxex @cassandradanger +* @BearHanded @braxex @ailZhou # Rate label file changes require application owner approval *rateLabelText.ts @davidkoger diff --git a/.github/branchNameValidation.sh b/.github/branchNameValidation.sh index 3f488c5fd0..e071a65aed 100755 --- a/.github/branchNameValidation.sh +++ b/.github/branchNameValidation.sh @@ -4,10 +4,18 @@ set -e local_branch=${1} -valid_branch="^[a-z][a-z-0-9-]*$" +valid_branch='^[a-z][a-z0-9-]*$' +reserved_words=( + cognito +) -if [[ ! $local_branch =~ $valid_branch ]] && [[ $local_branch -gt 128 ]]; then +join_by() { local IFS='|'; echo "$*"; } + +#creates glob match to check for reserved words used in branch names which would trigger failures +glob=$(join_by $(for i in ${reserved_words[@]}; do echo "^$i-|-$i$|-$i-|^$i$"; done;)) + +if [[ ! $local_branch =~ $valid_branch ]] || [[ $local_branch =~ $glob ]] || [[ ${#local_branch} -gt 64 ]]; then echo """ ------------------------------------------------------------------------------------------------------------------------------ ERROR: Please read below @@ -28,4 +36,4 @@ if [[ ! $local_branch =~ $valid_branch ]] && [[ $local_branch -gt 128 ]]; then exit 1 fi -exit 0 \ No newline at end of file +exit 0 diff --git a/.github/build_vars.sh b/.github/build_vars.sh index 1b5c603cba..380179a9cb 100755 --- a/.github/build_vars.sh +++ b/.github/build_vars.sh @@ -13,12 +13,14 @@ set_value() { if [ ! -z "${!varname}" ]; then echo "Setting $varname" echo "${varname}=${!varname}" >> $GITHUB_ENV + echo "${varname}=${!varname}" >> $GITHUB_OUTPUT fi } set_name() { varname=${1} echo "BRANCH_SPECIFIC_VARNAME_$varname=${branch_name//-/_}_$varname" >> $GITHUB_ENV + echo "BRANCH_SPECIFIC_VARNAME_$varname=${branch_name//-/_}_$varname" >> $GITHUB_OUTPUT } action=${1} diff --git a/.github/setBranchName.sh b/.github/setBranchName.sh new file mode 100755 index 0000000000..00ffd55ffe --- /dev/null +++ b/.github/setBranchName.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +GITHUB_REFNAME="${1}" + +[ -z "${GITHUB_REFNAME}" ] && echo "Error setting branch name. No input given." && exit 1 + +case ${GITHUB_REFNAME} in + $([[ "$GITHUB_REFNAME" =~ ^dependabot/.* ]] && echo ${GITHUB_REFNAME})) + echo ${GITHUB_REFNAME} | md5sum | head -c 10 | sed 's/^/x/' + ;; + $([[ "$GITHUB_REFNAME" =~ ^snyk-* ]] && echo ${GITHUB_REFNAME})) + echo ${GITHUB_REFNAME##*-} | head -c 10 | sed 's/^/s/' + ;; + *) + echo ${GITHUB_REFNAME} + ;; +esac \ No newline at end of file diff --git a/.github/waf-controller.sh b/.github/waf-controller.sh new file mode 100755 index 0000000000..3bdd242e38 --- /dev/null +++ b/.github/waf-controller.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +CIRCUIT_BREAKER=10 +AWS_RETRY_ERROR=254 +AWS_THROTTLING_EXCEPTION=252 +#0, 1, 2 are the levels of debug, with 0 being off +DEBUG=1 + +set -o pipefail -o nounset -u + +case ${1} in + append) + OP=append + ;; + set) + OP=set + ;; + *) + echo "Error: unkown operation" + echo "Usage: ${0} [append|set]" && exit 1 + ;; +esac + +shift +NAME="${1}" +ID="${2}" +shift; shift +RUNNER_CIDRS="${@}" + +[[ $DEBUG -ge 1 ]] && echo "Inputs: NAME ${NAME}, ID ${ID}, RUNNER_CIDRS ${RUNNER_CIDRS}" + +#Exponential backoff with jitter +jitter() { + #.5 seconds + SHORTEST=50 + #10 seconds + LONGEST=1000 + DIV=100 + EXP=$(perl -e "use bigint; print $SHORTEST**$1") + MIN=$(($EXP>$LONGEST ? $LONGEST : $EXP)) + RND=$(shuf -i$SHORTEST-$MIN -n1) + perl -e "print $RND/$DIV" +} + +#Attempt to avoid resource contention from the start +sleep $(jitter $(shuf -i1-10 -n1)) + +for ((i=1; i <= $CIRCUIT_BREAKER; i++)); do + #This loop is ONLY for retrying if the retries exceeded exception is thrown + for ((j=1; j <= $CIRCUIT_BREAKER; j++)); do + #Read WAF configuration from AWS + WAF_CONFIG=$(aws wafv2 get-ip-set --scope CLOUDFRONT --id ${ID} --name ${NAME} 2>&1) + CMD_CD=$? + [[ $DEBUG -ge 1 ]] && echo "AWS CLI Read Response Code: ${CMD_CD}" + [[ $DEBUG -ge 2 ]] && echo "AWS CLI Read Response: ${WAF_CONFIG}" + + #If the retries exceeded error code is returned, try again, otherwise exit the loop + [[ $CMD_CD -eq $AWS_RETRY_ERROR ]] || break + + SLEEP_FOR=$(jitter ${j}) + echo "CLI retries exceed. Waiting for ${SLEEP_FOR} seconds to execute read again...(${j})" + sleep ${SLEEP_FOR} + done + + #Unable to get the lock tocken and IP set so there isn't any point in attempting the write op + [[ $j -ge $CIRCUIT_BREAKER ]] && echo “Attempts to read WAF IPSet exceeded” && sleep $(jitter ${i}) && continue + + #The loop was short circuited with an error code other than 0, so something is wrong + [[ $CMD_CD -eq 0 ]] || ( echo "An unexpected read error occurred: ${CMD_CD}" && exit 2 ) + + echo "Read was successful." + + if [ ${OP} == "append" ]; then + ##If this is used to whitelist individual ips or cidrs, using an additive approach is what is required + #Parse out IP set addresses to array + IP_ADDRESSES=($(jq -r '.IPSet.Addresses | .[]' <<< ${WAF_CONFIG})) + + #If CIDR is already present in IP set, eject + grep -q $RUNNER_CIDRS <<< ${IP_ADDRESSES} + [[ $? -ne 0 ]] || ( echo "CIDR is present in IP Set." && exit 0 ) + + #Add runner CIDR to array + IP_ADDRESSES+=("$RUNNER_CIDRS") + else + ##If this is used to hard set the IP set, just clobber it + IP_ADDRESSES=("$RUNNER_CIDRS") + fi + + #Stringify IPs + STRINGIFIED=$(echo $(IFS=" " ; echo "${IP_ADDRESSES[*]}")) + [[ $DEBUG -ge 2 ]] && echo "Ip Addresses: ${STRINGIFIED}" + + #Parse out optimistic concurrency control token + OCC_TOKEN=$(jq -r '.LockToken' <<< ${WAF_CONFIG}) + [[ $DEBUG -ge 2 ]] && echo "LockToken: ${OCC_TOKEN}" + + #This loop is ONLY for retrying if the retries exceeded exception is thrown + for ((k=1; k <= $CIRCUIT_BREAKER; k++)); do + #Write updated WAF configuration to AWS + OUTPUT=$(aws wafv2 update-ip-set --scope CLOUDFRONT --id ${ID} --name ${NAME} --lock-token ${OCC_TOKEN} --addresses ${STRINGIFIED} 2>&1) + CMD_CD=$? + [[ $DEBUG -ge 1 ]] && echo "AWS CLI Write Response Code: ${CMD_CD}" + [[ $DEBUG -ge 2 ]] && echo "AWS CLI Write Response: ${OUTPUT}" + + #If the retries exceeded error code is returned, try again, otherwise exit the loop + [[ $CMD_CD -eq $AWS_RETRY_ERROR ]] || break + #If WAFOptimisticLockException error code is returned, exit the loop + [[ "$OUTPUT" =~ "WAFOptimisticLockException" ]] && break + + SLEEP_FOR=$(jitter ${k}) + echo "CLI retries exceed. Waiting for ${SLEEP_FOR} seconds to execute write again...(${k})" + sleep ${SLEEP_FOR} + done + + [[ $CMD_CD -ne 0 ]] || break + #Still not having success, so try again + + echo "Exit Code: ${CMD_CD}" + + SLEEP_FOR=$(jitter ${i}) + echo "Waiting for ${SLEEP_FOR} seconds to execute main loop again...(${i})" + sleep ${SLEEP_FOR} +done + +[[ $DEBUG -ge 1 ]] && echo "Attempts to update ip set: $i" + +[[ $i -gt $CIRCUIT_BREAKER ]] && echo “Attempts to update WAF IPSet exceeded, exiting.” && exit 2 + +echo "Applied the IP Set successfully." + +#Things should not have made it this far without being able to successfully write the IP Set +exit $CMD_CD diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6eb49a5ee7..5909a66c4c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 6297aae216..23893d3fc2 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -35,7 +35,7 @@ jobs: name: Setup Cypress Test Matrix runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: set-test-matrix run: | echo "test-matrix=$(ls -1 tests/cypress/cypress/e2e/${{ inputs.test-path }}/* | xargs -n 1 basename | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT @@ -46,14 +46,14 @@ jobs: runs-on: ubuntu-latest container: image: cypress/browsers:node16.16.0-chrome107-ff107 - options: --user 1001 + options: --user root needs: setup strategy: fail-fast: false matrix: containers: ${{ fromJson(needs.setup.outputs.test-matrix) }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: yarn install --frozen-lockfile - name: set path diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c8415d296a..29332636db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,7 @@ on: - "!skipci*" concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref_name }} permissions: id-token: write @@ -19,7 +19,7 @@ jobs: unit-tests: name: Unit Tests uses: ./.github/workflows/unittest-workflow.yml - if: github.ref == 'refs/heads/master' + if: github.ref_name == 'master' secrets: CODE_CLIMATE_ID: ${{ secrets.CODE_CLIMATE_ID }} deploy: @@ -27,17 +27,21 @@ jobs: env: SLS_DEPRECATION_DISABLE: "*" # Turn off deprecation warnings in the pipeline steps: + - uses: actions/checkout@v4 - name: set branch_name # Some integrations (Dependabot & Snyk) build very long branch names. This is a switch to make long branch names shorter. run: | - echo "GITHUB_REF=${GITHUB_REF}" - if [[ "$GITHUB_REF" =~ ^refs/heads/dependabot/.* ]]; then - echo "branch_name=`echo ${GITHUB_REF##*/*-} | md5sum | head -c 10 | sed 's/^/x/'`" >> $GITHUB_ENV - elif [[ "$GITHUB_REF" =~ ^refs/.*/snyk-* ]]; then - echo "branch_name=`echo ${GITHUB_REF##*/*-} | head -c 10 | sed 's/^/s/'`" >> $GITHUB_ENV - else - echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - fi - - uses: actions/checkout@v3 + BRANCH_NAME=$(./.github/setBranchName.sh ${{ github.ref_name }}) + echo "branch_name=${BRANCH_NAME}" >> $GITHUB_ENV + - name: "Setup jq" + uses: dcarbone/install-jq-action@v2.1.0 + with: + version: "${{ inputs.version }}" + force: "${{ inputs.force }}" + - name: "Check jq" + # language=sh + run: | + which jq + jq --version - name: Validate branch name run: ./.github/branchNameValidation.sh $STAGE_PREFIX$branch_name - name: set branch specific variable names @@ -80,10 +84,12 @@ jobs: working-directory: services outputs: application_endpoint: ${{ steps.endpoint.outputs.application_endpoint}} + BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION: ${{ steps.set_names.outputs.BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION }} + BRANCH_SPECIFIC_VARNAME_AWS_OIDC_ROLE_TO_ASSUME: ${{ steps.set_names.outputs.BRANCH_SPECIFIC_VARNAME_AWS_OIDC_ROLE_TO_ASSUME }} # run e2e tests after deploy completes e2e-tests-init: name: Initialize End To End Tests - if: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/val' && github.ref != 'refs/heads/prod' }} + if: ${{ github.ref_name != 'master' && github.ref_name != 'val' && github.ref_name != 'prod' }} needs: - deploy runs-on: ubuntu-latest @@ -93,17 +99,16 @@ jobs: run: | echo "No endpoint set, Check if the deploy workflow was successful." exit 1 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: set branch_name run: | - if [[ "$GITHUB_REF" =~ ^refs/heads/dependabot/.* ]]; then # Dependabot builds very long branch names. This is a switch to make it shorter. - echo "branch_name=`echo ${GITHUB_REF#refs/heads/} | md5sum | head -c 10 | sed 's/^/x/'`" >> $GITHUB_ENV - else - echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - fi + BRANCH_NAME=$(./.github/setBranchName.sh ${{ github.ref_name }}) + echo "branch_name=${BRANCH_NAME}" >> $GITHUB_ENV - name: set branch specific variable names + id: set_names run: ./.github/build_vars.sh set_names - name: set variable values + id: set_values run: ./.github/build_vars.sh set_values env: AWS_DEFAULT_REGION: ${{ secrets[env.BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION] || secrets.AWS_DEFAULT_REGION }} @@ -123,7 +128,6 @@ jobs: path: | services/app-api/node_modules services/uploads/node_modules - services/stream-functions/node_modules services/ui/node_modules services/ui-auth/node_modules services/ui-src/node_modules @@ -139,13 +143,63 @@ jobs: - name: set path run: | echo "PATH=$(pwd)/node_modules/.bin/:$PATH" >> $GITHUB_ENV + - name: Get Runner IP + id: get-ip + run: | + #!/bin/bash + # Get the IP address of the runner + IP_ADDRESS=$(curl https://api.ipify.org) + echo "Runner IP address: $IP_ADDRESS" + # Store the IP address as an output variable + echo "RUNNER_IP=$IP_ADDRESS/32" >> $GITHUB_OUTPUT + - name: Get Github Actions CIDR Blocks + id: get-gha-cidrs + shell: bash + run: | + #!/bin/bash + GHA_RESP=$(curl --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/meta) + echo "Response for GHA runner CIDR blocks: $GHA_RESP" + IPV4_CIDR_ARR=($(echo $GHA_RESP | jq -r '.actions | .[]' | grep -v ':')) + GHA_CIDRS_IPV4=$(echo $(IFS=" "; echo ${IPV4_CIDR_ARR[*]})) + echo "GHA_CIDRS_IPV4=$GHA_CIDRS_IPV4" >> $GITHUB_OUTPUT + - name: Generate IP Set Name + id: gen-ip-set-name + run: | + STAGE_GH_IPSET_NAME=$STAGE_PREFIX$branch_name-gh-ipset + echo "Github IP Set name: $STAGE_GH_IPSET_NAME" + echo "STAGE_GH_IPSET_NAME=$STAGE_GH_IPSET_NAME" >> $GITHUB_OUTPUT + - name: Fetch AWS IP set Metadata + id: fetch-ip-set-info + run: | + #!/bin/bash + # Fetch AWS IP set ARNs using AWS CLI and store them in a variable + AWS_IP_SET_INFO=$(aws wafv2 list-ip-sets --scope=CLOUDFRONT) + # Store the IP set ARNs in an output variable using GITHUB_OUTPUT + IPSET_NAME=${{ steps.gen-ip-set-name.outputs.STAGE_GH_IPSET_NAME }} + IPSET=$(jq '.IPSets | map(select(.Name == "'${IPSET_NAME}'")) | .[]' <<< ${AWS_IP_SET_INFO}) + [ -z "$IPSET" ] && echo "IP Set with name ${IPSET_NAME} was not located. Exiting..." && exit 1 + echo "IP Set metadata: ${IPSET}" + #Get Values from the IP SET + IPSET_ID=$(jq -r '.Id' <<< ${IPSET}) + echo "IPSET_ARN=$IPSET_ARN" >> $GITHUB_OUTPUT + echo "IPSET_NAME=$IPSET_NAME" >> $GITHUB_OUTPUT + echo "IPSET_ID=$IPSET_ID" >> $GITHUB_OUTPUT + - name: Update IP Set + id: update-ip-set + run: ./.github/waf-controller.sh set ${{ steps.fetch-ip-set-info.outputs.IPSET_NAME }} ${{ steps.fetch-ip-set-info.outputs.IPSET_ID }} ${{ steps.get-gha-cidrs.outputs.GHA_CIDRS_IPV4 }} + env: + AWS_RETRY_MODE: adaptive + AWS_MAX_ATTEMPTS: 10 outputs: application_endpoint: ${{ needs.deploy.outputs.application_endpoint }} + ipset_name: ${{ steps.fetch-ip-set-info.outputs.IPSET_NAME }} + ipset_id: ${{ steps.fetch-ip-set-info.outputs.IPSET_ID }} setup-tests: name: "Setup End To End Tests" uses: ./.github/workflows/cypress-workflow.yml - needs: e2e-tests-init + needs: + - e2e-tests-init with: test-path: "init" test-endpoint: "${{ needs.e2e-tests-init.outputs.application_endpoint }}" @@ -244,3 +298,31 @@ jobs: cypress-user3: ${{ secrets.CYPRESS_TEST_USER_3 }} cypress-user4: ${{ secrets.CYPRESS_TEST_USER_4 }} cypress-password: ${{ secrets.CYPRESS_TEST_PASSWORD_1 }} + + cleanup: + name: Deslist GHA Runner CIDR Blocks + if: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/val' && github.ref != 'refs/heads/prod' }} + runs-on: ubuntu-latest + needs: + - e2e-tests-init + - e2e-feature-tests + - child-e2e-measure-tests + - adult-e2e-measure-tests + - health-home-e2e-measure-tests + - deploy + - a11y-tests + env: + SLS_DEPRECATION_DISABLE: "*" # Turn off deprecation warnings in the pipeline + steps: + - uses: actions/checkout@v4 + - name: Configure AWS credentials for GitHub Actions + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets[env.BRANCH_SPECIFIC_VARNAME_AWS_OIDC_ROLE_TO_ASSUME] || secrets.AWS_OIDC_ROLE_TO_ASSUME }} + aws-region: ${{ secrets[env.BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION] || secrets.AWS_DEFAULT_REGION }} + - name: clean-up-iplist + id: reset-ip-set + run: ./.github/waf-controller.sh set ${{ needs.e2e-tests-init.outputs.ipset_name }} ${{ needs.e2e-tests-init.outputs.ipset_id }} '[]' + env: + AWS_RETRY_MODE: adaptive + AWS_MAX_ATTEMPTS: 10 diff --git a/.github/workflows/destroy.yml b/.github/workflows/destroy.yml index f9f5ef5066..08ee56abd1 100644 --- a/.github/workflows/destroy.yml +++ b/.github/workflows/destroy.yml @@ -1,6 +1,12 @@ name: Destroy -on: delete +on: + delete: + workflow_dispatch: + inputs: + environment: + description: "Name of the environment to destroy:" + required: true permissions: id-token: write @@ -14,17 +20,23 @@ jobs: # This conditional is a backup mechanism to help prevent mistakes from becoming disasters. # This is a list of branch names that are commonly used for protected branches/environments. # Add/remove names from this list as appropriate. - if: github.event.ref_type == 'branch' && !contains(fromJson('["master", "val", "prod"]'), github.event.ref) + if: | + ( + github.event.ref_type == 'branch' && + (!startsWith(github.event.ref, 'skipci')) && + (!contains(fromJson('["master", "val", "prod"]'), github.event.ref)) + ) || + ( + inputs.environment != '' && + (!contains(fromJson('["master", "val", "prod"]'), inputs.environment)) + ) runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - name: set branch_name run: | - if [[ "${{ github.event.ref }}" =~ ^dependabot/.* ]]; then # Dependabot builds very long branch names. This is a switch to make it shorter. - echo "branch_name=`echo ${{ github.event.ref }} | md5sum | head -c 10 | sed 's/^/x/'`" >> $GITHUB_ENV - else - echo "branch_name=${{ github.event.ref }}" >> $GITHUB_ENV - fi - - uses: actions/checkout@v3 + BRANCH_NAME=$(./.github/setBranchName.sh ${{ inputs.environment || github.event.ref }}) + echo "branch_name=${BRANCH_NAME}" >> $GITHUB_ENV - name: set branch specific variable names run: ./.github/build_vars.sh set_names - name: set variable values diff --git a/.github/workflows/git-secrets.yaml b/.github/workflows/git-secrets.yaml index e63711abea..3906e2eef1 100644 --- a/.github/workflows/git-secrets.yaml +++ b/.github/workflows/git-secrets.yaml @@ -4,7 +4,7 @@ jobs: gitleaks-scan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run gitleaks docker uses: docker://zricethezav/gitleaks with: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b8c1b44a45..8eb67279b3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -15,7 +15,7 @@ jobs: assignAuthor: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Assign PR to Creator run: | if [ "$PR_AUTHOR_TYPE" != "Bot" ] diff --git a/.github/workflows/pullrequest-precommit-check.yml b/.github/workflows/pullrequest-precommit-check.yml index b9909c03f8..51c92c7ef4 100644 --- a/.github/workflows/pullrequest-precommit-check.yml +++ b/.github/workflows/pullrequest-precommit-check.yml @@ -6,7 +6,7 @@ jobs: prettier: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v3 - uses: pre-commit/action@v3.0.0 with: diff --git a/.github/workflows/scan_security-hub-jira-integration.yml b/.github/workflows/scan_security-hub-jira-integration.yml index 89f872c57c..5f8001001c 100644 --- a/.github/workflows/scan_security-hub-jira-integration.yml +++ b/.github/workflows/scan_security-hub-jira-integration.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Check out repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: diff --git a/.github/workflows/scan_snyk-jira-integration.yml b/.github/workflows/scan_snyk-jira-integration.yml index 66e5a5f9c4..dcb08e11cc 100644 --- a/.github/workflows/scan_snyk-jira-integration.yml +++ b/.github/workflows/scan_snyk-jira-integration.yml @@ -16,7 +16,7 @@ jobs: if: github.event_name == 'pull_request' steps: - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Snyk and Run Snyk test run: | @@ -31,7 +31,7 @@ jobs: if: github.event_name == 'schedule' steps: - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Snyk and Run Snyk test run: | diff --git a/.github/workflows/unittest-workflow.yml b/.github/workflows/unittest-workflow.yml index 465ff76879..fbbe523e33 100644 --- a/.github/workflows/unittest-workflow.yml +++ b/.github/workflows/unittest-workflow.yml @@ -11,7 +11,7 @@ jobs: name: Unit Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: set variable values run: ./.github/build_vars.sh set_values env: diff --git a/README.md b/README.md index 27c332c8cf..f5fd2910ed 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ QMR is the CMCS MDCT application for collecting state data for related to measur # Table of Contents +- [MDCT QMR (Quality Measure Reporting)](#mdct-qmr-quality-measure-reporting) +- [Table of Contents](#table-of-contents) - [Getting Started](#getting-started) - [Local Development Setup](#local-development-setup) - [Prettier](#prettier) @@ -40,7 +42,6 @@ QMR is the CMCS MDCT application for collecting state data for related to measur - [Database](#database) - [Tables](#tables) - [How to set up Dynamo endpoint to view local Db](#how-to-set-up-dynamo-endpoint-to-view-local-db) - - [Stream Functions](#stream-functions) - [UI](#ui) - [Dev/Impl/Prod endpoints](#devimplprod-endpoints) - [Branch Endpoints](#branch-endpoints) @@ -413,12 +414,6 @@ To run the dynamodb gui, run `DYNAMO_ENDPOINT=http://localhost:8000 dynamodb-adm From here you can view the tables and perform operations on the local tables. -### Stream Functions - ---- - -The stream functions fire deltas when updates to its table happens. These changes are picked up in the API where these changes are communicated to the kafka streams for the application. - ## UI The UI Service creates the URL's associated with the application and the cloudfront logs that monitor traffic. @@ -539,9 +534,13 @@ to make in order to get that year working. ![After](./.images/afterImportUpdate.png?raw=true) -5. Copy over the `/globalValidations`, `/CommonQuestions`, and `/Qualifiers` directories to the latest year +5. Go to the `/services/ui-src/src/utils/testUtils`, create a new directory for the latest year (e.g. 2024), and copy over the `validationHelpers.ts` and `validationHelpers.test.ts` files + + **NOTE:** Remember to update the imports in these files to reflect the latest year. + +6. Copy over the `/globalValidations`, `/CommonQuestions`, and `/Qualifiers` directories to the latest year -6. Similar to Step 4, update import names from the previous year to the most recent year +7. Similar to Step 4, update import names from the previous year to the most recent year Before @@ -551,9 +550,9 @@ to make in order to get that year working. ![After](./.images/afterCommonComponentUpdate.png?raw=true) -7. In `services/ui-src/src/libs/spaLib.ts`, copy over the prior year's entry into the array. +8. In `services/ui-src/src/libs/spaLib.ts`, copy over the prior year's entry into the array. -8. In `services/ui-src/src/measures/measureDescriptions.ts` , copy over the prior year's entry into the array. +9. In `services/ui-src/src/measures/measureDescriptions.ts` , copy over the prior year's entry into the array. ## Things to Look Out For (Gotchas) diff --git a/deploy.sh b/deploy.sh index 71cfaa96cd..7942b5ede7 100755 --- a/deploy.sh +++ b/deploy.sh @@ -8,7 +8,6 @@ services=( 'database' 'app-api' 'uploads' - 'stream-functions' 'ui' 'ui-auth' 'ui-src' diff --git a/destroy.sh b/destroy.sh index 6ada321564..615ec57db0 100755 --- a/destroy.sh +++ b/destroy.sh @@ -35,6 +35,16 @@ set -e # Find cloudformation stacks associated with stage stackList=(`aws cloudformation describe-stacks | jq -r ".Stacks[] | select(.Tags[] | select(.Key==\"STAGE\") | select(.Value==\"$stage\")) | .StackName"`) +if [ ${#stackList[@]} -eq 0 ]; then + echo """ + --------------------------------------------------------------------------------------------- + ERROR: No stacks were identified for destruction + --------------------------------------------------------------------------------------------- + Please verify the stage name: $stage + """ + exit 1 +fi + # Find buckets attached to any of the stages, so we can empty them before removal. bucketList=() set +e diff --git a/package.json b/package.json index ae7a3006f5..1895093a6d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "yargs": "^16.1.1" }, "dependencies": { + "@enterprise-cmcs/serverless-waf-plugin": "^1.3.2", "xml2js": "0.6.0" }, "resolutions": { diff --git a/services/app-api/handlers/dynamoUtils/measureList.ts b/services/app-api/handlers/dynamoUtils/measureList.ts index e1c6a0e22c..09b3a3b16f 100644 --- a/services/app-api/handlers/dynamoUtils/measureList.ts +++ b/services/app-api/handlers/dynamoUtils/measureList.ts @@ -1034,4 +1034,342 @@ export const measures: Measure = { placeholder: true, }, ], + 2024: [ + { + type: "A", + measure: "CSQ", + }, + { + type: "C", + measure: "CSQ", + }, + { + type: "H", + measure: "CSQ", + }, + { + type: "A", + measure: "AAB-AD", + }, + { + type: "A", + measure: "AMM-AD", + }, + { + type: "A", + measure: "AMR-AD", + }, + { + type: "A", + measure: "BCS-AD", + }, + { + type: "A", + measure: "CBP-AD", + }, + { + type: "A", + measure: "CCP-AD", + }, + { + type: "A", + measure: "CCS-AD", + }, + { + type: "A", + measure: "CCW-AD", + }, + { + type: "A", + measure: "CDF-AD", + }, + { + type: "A", + measure: "CHL-AD", + }, + { + type: "A", + measure: "COB-AD", + }, + { + type: "A", + measure: "COL-AD", + }, + { + type: "A", + measure: "CPA-AD", + }, + { + type: "A", + measure: "CPU-AD", + }, + { + type: "A", + measure: "FUA-AD", + }, + { + type: "A", + measure: "FUH-AD", + }, + { + type: "A", + measure: "FUM-AD", + }, + { + type: "A", + measure: "FVA-AD", + }, + { + type: "A", + measure: "HBD-AD", + }, + { + type: "A", + measure: "HPCMI-AD", + }, + { + type: "A", + measure: "HVL-AD", + }, + { + type: "A", + measure: "IET-AD", + }, + { + type: "A", + measure: "MSC-AD", + }, + { + type: "A", + measure: "NCIDDS-AD", + autocompleteOnCreation: true, + }, + { + type: "A", + measure: "OHD-AD", + }, + { + type: "A", + measure: "OUD-AD", + }, + { + type: "A", + measure: "PCR-AD", + }, + { + type: "A", + measure: "PPC-AD", + }, + { + type: "A", + measure: "PQI01-AD", + }, + { + type: "A", + measure: "PQI05-AD", + }, + { + type: "A", + measure: "PQI08-AD", + }, + { + type: "A", + measure: "PQI15-AD", + }, + { + type: "A", + measure: "SAA-AD", + }, + { + type: "A", + measure: "SSD-AD", + }, + { + type: "C", + measure: "AAB-CH", + }, + { + type: "C", + measure: "ADD-CH", + }, + { + type: "C", + measure: "AMB-CH", + }, + { + type: "C", + measure: "AMR-CH", + }, + { + type: "C", + measure: "APM-CH", + }, + { + type: "C", + measure: "APP-CH", + }, + { + type: "C", + measure: "CCP-CH", + }, + { + type: "C", + measure: "CCW-CH", + }, + { + type: "C", + measure: "CDF-CH", + }, + { + type: "C", + measure: "CHL-CH", + }, + { + type: "C", + measure: "CIS-CH", + }, + { + type: "C", + measure: "CPC-CH", + }, + { + type: "C", + measure: "DEV-CH", + }, + { + type: "C", + measure: "FUA-CH", + }, + { + type: "C", + measure: "FUH-CH", + }, + { + type: "C", + measure: "FUM-CH", + }, + { + type: "C", + measure: "IMA-CH", + }, + { + type: "C", + measure: "LSC-CH", + }, + { + type: "C", + measure: "LBW-CH", + autocompleteOnCreation: true, + }, + { + type: "C", + measure: "LRCD-CH", + autocompleteOnCreation: true, + }, + { + type: "C", + measure: "OEV-CH", + }, + { + type: "C", + measure: "PPC2-CH", + }, + { + type: "C", + measure: "SFM-CH", + }, + { + type: "C", + measure: "TFL-CH", + }, + { + type: "C", + measure: "W30-CH", + }, + { + type: "C", + measure: "WCC-CH", + }, + { + type: "C", + measure: "WCV-CH", + }, + { + type: "H", + measure: "AIF-HH", + }, + { + type: "H", + measure: "AMB-HH", + }, + { + type: "H", + measure: "CBP-HH", + }, + { + type: "H", + measure: "CDF-HH", + }, + { + type: "H", + measure: "COL-HH", + }, + { + type: "H", + measure: "FUA-HH", + }, + { + type: "H", + measure: "FUH-HH", + }, + { + type: "H", + measure: "FUM-HH", + }, + { + type: "H", + measure: "IET-HH", + }, + { + type: "H", + measure: "IU-HH", + }, + { + type: "H", + measure: "OUD-HH", + }, + { + type: "H", + measure: "PCR-HH", + }, + { + type: "H", + measure: "PQI92-HH", + }, + { + type: "H", + measure: "SS-1-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-2-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-3-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-4-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-5-HH", + placeholder: true, + }, + ], }; diff --git a/services/app-api/handlers/measures/tests/get.test.ts b/services/app-api/handlers/measures/tests/get.test.ts index 3312c39019..882864d599 100644 --- a/services/app-api/handlers/measures/tests/get.test.ts +++ b/services/app-api/handlers/measures/tests/get.test.ts @@ -150,7 +150,7 @@ describe("Test Get Measure Handlers", () => { }; const res = await getReportingYears(event, null); expect(res.statusCode).toBe(StatusCodes.SUCCESS); - expect(res.body).toBe('["2021","2022","2023"]'); + expect(res.body).toBe('["2021","2022","2023","2024"]'); }); test("Test getMeasureListInfo works when called with an empty object", async () => { diff --git a/services/app-api/handlers/prince/pdf.ts b/services/app-api/handlers/prince/pdf.ts index 94292d7cb4..29c330aee7 100644 --- a/services/app-api/handlers/prince/pdf.ts +++ b/services/app-api/handlers/prince/pdf.ts @@ -1,62 +1,143 @@ +import { fetch } from "cross-fetch"; // TODO delete this line and uninstall this package, once QMR is running on Nodejs 18+ +import createDOMPurify from "dompurify"; +import { JSDOM } from "jsdom"; import handler from "../../libs/handler-lib"; import { StatusCodes } from "../../utils/constants/constants"; -import { URL } from "url"; -import { SignatureV4 } from "@smithy/signature-v4"; -import { Sha256 } from "@aws-crypto/sha256-js"; -import { fetch } from "cross-fetch"; // TODO remove this polyfill once QMR is on Node 18+ + +const windowEmulator: any = new JSDOM("").window; +const DOMPurify = createDOMPurify(windowEmulator); export const getPDF = handler(async (event, _context) => { - const body = event.body; // will be base64-encoded HTML, like "PGh0bWw..." - if (!body) { + const rawBody = event.body; // will be base64-encoded HTML, like "PGh0bWw..." + if (!rawBody) { throw new Error("Missing request body"); } - const { - // princeApiHost: hostname, // JUST the host name, no protocol, ex: "my-site.cms.gov" - // princeApiPath: path, // Needs leading slash, ex: "/doc-conv/508html-to-508pdf" - princeUrl, - AWS_ACCESS_KEY_ID: accessKeyId, - AWS_SECRET_ACCESS_KEY: secretAccessKey, - AWS_SESSION_TOKEN: sessionToken, - } = process.env; - - if ( - princeUrl === undefined || - accessKeyId === undefined || - secretAccessKey === undefined || - sessionToken === undefined - ) { + let sanitizedBody; + if (DOMPurify.isSupported && typeof rawBody === "string") { + // decode body from base64, sanitize dangerous html + const decodedBody = Buffer.from(rawBody, "base64").toString(); + sanitizedBody = DOMPurify.sanitize(decodedBody); + } + if (!sanitizedBody) { + throw new Error("Could not process request"); + } + + const { docraptorApiKey, stage } = process.env; + if (!docraptorApiKey) { throw new Error("No config found to make request to PDF API"); } - const { hostname, pathname: path } = new URL(princeUrl); + const requestBody = { + user_credentials: docraptorApiKey, + doc: { + document_content: sanitizedBody, + type: "pdf" as const, + // This tag differentiates QMR and CARTS requests in DocRaptor's logs. + tag: "QMR", + test: stage !== "prod", + prince_options: { + profile: "PDF/UA-1" as const, + }, + }, + }; + + const arrayBuffer = await sendDocRaptorRequest(requestBody); + const base64PdfData = Buffer.from(arrayBuffer).toString("base64"); + return { + status: StatusCodes.SUCCESS, + body: base64PdfData, + }; +}); - const request = { +async function sendDocRaptorRequest(request: DocRaptorRequestBody) { + const response = await fetch("https://docraptor.com/docs", { method: "POST", - protocol: "https", - hostname, - path, headers: { - host: hostname, // Prince requires this to be signed + "content-type": "application/json", }, - body, - }; - - const signer = new SignatureV4({ - service: "execute-api", - region: "us-east-1", - credentials: { accessKeyId, secretAccessKey, sessionToken }, - sha256: Sha256, + body: JSON.stringify(request), }); - const signedRequest = await signer.sign(request); + await handlePdfStatusCode(response); - const response = await fetch(`https://${hostname}${path}`, signedRequest); + const pdfPageCount = response.headers.get("X-DocRaptor-Num-Pages"); + console.debug(`Successfully generated a ${pdfPageCount}-page PDF.`); - const base64EncodedPdfData = await response.json(); + return response.arrayBuffer(); +} - return { - status: StatusCodes.SUCCESS, - body: base64EncodedPdfData, +/** + * If PDF generation was not successful, log the reason and throw an error. + * + * For more details see https://docraptor.com/documentation/api/status_codes + */ +async function handlePdfStatusCode(response: Response) { + if (response.status === 200) { + return; + } + + const xmlErrorMessage = await response.text(); + console.warn("DocRaptor Error Message:\n" + xmlErrorMessage); + + switch (response.status) { + case 400: // Bad Request + case 422: // Unprocessable Entity + throw new Error("PDF generation failed - possibly an HTML issue"); + case 401: // Unauthorized + case 403: // Forbidden + throw new Error( + "PDF generation failed - possibly a configuration or throttle issue" + ); + default: + throw new Error( + `Received status code ${response.status} from PDF generation service` + ); + } +} + +type DocRaptorRequestBody = { + /** Your DocRaptor API key */ + user_credentials: string; + doc: DocRaptorParameters; +}; + +/** + * Here is some in-band documentation for the more common DocRaptor options. + * There also options for JS handling, asset handling, PDF metadata, and more. + * Note that we do not use DocRaptor's hosting; we return the PDF directly. + * For more details see https://docraptor.com/documentation/api + */ +type DocRaptorParameters = { + /** Test documents are watermarked, but don't count against API limits. */ + test?: boolean; + /** We only use `pdf`. */ + type: "pdf" | "xls" | "xlsx"; + /** The HTML to render. Either this or `document_url` is required. */ + document_content?: string; + /** The URL to fetch and render. Either this or `document_content` is required. */ + document_url?: string; + /** Synchronous calls have a 60s limit. Callbacks are required for longer-running docs. */ + async?: false; + /** This name will show up in the logs: https://docraptor.com/doc_logs */ + name?: string; + /** This tag will also show up in DocRaptor's logs. */ + tag?: string; + /** Should DocRaptor run JS embedded in your HTML? Default is `false`. */ + javascript?: boolean; + prince_options: { + /** + * In theory we can choose a different PDF version, but UA-1 is the only accessible one. + * https://docraptor.com/documentation/article/6637003-accessible-tagged-pdfs + */ + profile: "PDF/UA-1"; + /** The default is `print`. */ + media?: "print" | "screen"; + /** May be needed to load relative urls. Alternatively, use the `` tag. */ + baseurl?: string; + /** The title of your PDF. By default this is the `` of your HTML. */ + pdf_title?: string; + /** This may be used to override the default DPI of `96`. */ + css_dpi?: number; }; -}); +}; diff --git a/services/app-api/handlers/prince/tests/pdf.test.ts b/services/app-api/handlers/prince/tests/pdf.test.ts new file mode 100644 index 0000000000..44702af492 --- /dev/null +++ b/services/app-api/handlers/prince/tests/pdf.test.ts @@ -0,0 +1,113 @@ +import { getPDF } from "../pdf"; +import { fetch } from "cross-fetch"; +import { testEvent } from "../../../test-util/testEvents"; + +jest.spyOn(console, "error").mockImplementation(); +jest.spyOn(console, "warn").mockImplementation(); + +jest.mock("../../../libs/authorization", () => ({ + isAuthenticated: jest.fn().mockReturnValue(true), +})); + +jest.mock("cross-fetch", () => ({ + fetch: jest.fn().mockResolvedValue({ + status: 200, + headers: { + get: jest.fn().mockResolvedValue("3"), + }, + arrayBuffer: jest.fn().mockResolvedValue( + // An ArrayBuffer containing `%PDF-1.7` + new Uint8Array([37, 80, 68, 70, 45, 49, 46, 55]).buffer + ), + }), +})); + +const dangerousHtml = "<p>abc<iframe//src=jAva script:alert(3)>def</p>"; +const sanitizedHtml = "<p>abc</p>"; +const base64EncodedDangerousHtml = + Buffer.from(dangerousHtml).toString("base64"); + +const noBodyEvent = { + ...testEvent, + body: null, +}; + +const dangerousHtmlBodyEvent = { + ...testEvent, + body: base64EncodedDangerousHtml, +}; + +describe("Test GetPDF handler", () => { + beforeEach(() => { + process.env = { + docraptorApiKey: "mock api key", // pragma: allowlist secret + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should throw error when no body provided", async () => { + const res = await getPDF(noBodyEvent, null); + + expect(res.statusCode).toBe(500); + expect(res.body).toContain("Missing request body"); + }); + + it("should throw error when body is not type string", async () => { + const res = await getPDF(testEvent, null); + + expect(res.statusCode).toBe(500); + expect(res.body).toContain("Could not process request"); + }); + + it("should throw error when config not defined", async () => { + delete process.env.docraptorApiKey; + + const res = await getPDF(dangerousHtmlBodyEvent, null); + + expect(res.statusCode).toBe(500); + expect(res.body).toContain("No config found to make request to PDF API"); + }); + + it("should call PDF API with sanitized html", async () => { + const res = await getPDF(dangerousHtmlBodyEvent, null); + + expect(res.statusCode).toBe(200); + + expect(fetch).toHaveBeenCalled(); + const [url, request] = (fetch as jest.Mock).mock.calls[0]; + const body = JSON.parse(request.body); + expect(url).toBe("https://docraptor.com/docs"); + expect(request).toEqual({ + method: "POST", + headers: { "content-type": "application/json" }, + body: expect.stringMatching(/^\{.*\}$/), + }); + expect(body).toEqual({ + user_credentials: "mock api key", // pragma: allowlist secret + doc: expect.objectContaining({ + document_content: sanitizedHtml, + type: "pdf", + prince_options: expect.objectContaining({ + profile: "PDF/UA-1", + }), + }), + }); + }); + + it("should handle an error response from the PDF API", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 500, + text: jest.fn().mockResolvedValue("<error>It broke.</error>"), + }); + + const res = await getPDF(dangerousHtmlBodyEvent, null); + + expect(res.statusCode).toBe(500); + + // eslint-disable-next-line no-console + expect(console.warn).toBeCalledWith(expect.stringContaining("It broke.")); + }); +}); diff --git a/services/app-api/package.json b/services/app-api/package.json index a3f7f671e4..a59aa8f408 100644 --- a/services/app-api/package.json +++ b/services/app-api/package.json @@ -19,7 +19,9 @@ "devDependencies": { "@types/aws-lambda": "^8.10.88", "@types/aws4": "^1.11.2", + "@types/dompurify": "^3.0.5", "@types/jest": "^27.4.0", + "@types/jsdom": "^21.1.6", "@types/prompt-sync": "^4.1.1", "aws-lambda": "^1.0.7", "jest": "^27.4.7", @@ -33,11 +35,11 @@ "typescript": "^4.6.4" }, "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@smithy/signature-v4": "^2.0.18", "aws-sdk": "^2.1310.0", "aws4": "^1.11.0", "cross-fetch": "^4.0.0", + "dompurify": "^3.0.9", + "jsdom": "^22.1.0", "jwt-decode": "^3.1.2", "kafkajs": "^2.2.4", "prompt-sync": "^4.2.0", diff --git a/services/app-api/serverless.yml b/services/app-api/serverless.yml index 04749d6421..a78491cd7a 100644 --- a/services/app-api/serverless.yml +++ b/services/app-api/serverless.yml @@ -11,6 +11,7 @@ plugins: - serverless-plugin-typescript - serverless-plugin-warmup - serverless-associate-waf + - "@enterprise-cmcs/serverless-waf-plugin" - serverless-offline-ssm - serverless-offline - serverless-stack-termination-protection @@ -34,6 +35,11 @@ custom: tsConfigFileLocation: "./tsconfig.json" stage: ${opt:stage, self:provider.stage} region: ${opt:region, self:provider.region} + wafPlugin: + name: ${self:service}-${self:custom.stage}-webacl-waf + wafExcludeRules: + awsCommon: + - "SizeRestrictions_BODY" serverlessTerminationProtection: stages: - master @@ -41,7 +47,7 @@ custom: - prod dotenv: path: ../../.env - princeUrl: ${ssm:/configuration/default/princeurl, "https://macpro-platform-dev.cms.gov/doc-conv/508html-to-508pdf"} + docraptorApiKey: ${env:docraptorApiKey, ssm:/${self:custom.stage}/pdf/docraptorApiKey, ssm:/default/pdf/docraptorApiKey} bootstrapBrokerStringTls: ${ssm:/configuration/${self:custom.stage}/qmr/bootstrapBrokerStringTls, ssm:/configuration/default/qmr/bootstrapBrokerStringTls, ''} coreSetTableName: ${env:coreSetTableName, cf:database-${self:custom.stage}.CoreSetTableName} coreSetTableArn: ${env:DYNAMO_TABLE_ARN, cf:database-${self:custom.stage}.CoreSetTableArn} @@ -51,7 +57,7 @@ custom: measureTableStreamArn: ${env:DYNAMO_TABLE_ARN, cf:database-${self:custom.stage}.MeasureTableStreamArn} bannerTableName: ${env:bannerTableName, cf:database-${self:custom.stage}.BannerTableName} bannerTableArn: ${env:DYNAMO_TABLE_ARN, cf:database-${self:custom.stage}.BannerTableArn} - webAclName: ${self:service}-${self:custom.stage}-webacl + webAclName: ${self:service}-${self:custom.stage}-webacl-waf vpcId: ${ssm:/configuration/${self:custom.stage}/vpc/id, ssm:/configuration/default/vpc/id, ''} privateSubnets: - ${ssm:/configuration/${self:custom.stage}/vpc/subnets/private/a/id, ssm:/configuration/default/vpc/subnets/private/a/id, ''} @@ -114,7 +120,7 @@ provider: uploadS3BucketName: ${ssm:/s3bucket/uploads, cf:uploads-${self:custom.stage}.AttachmentsBucketName, "local-uploads"} dynamoSnapshotS3BucketName: ${ssm:/s3bucket/snapshots, cf:uploads-${self:custom.stage}.DynamoSnapshotBucketName, "local-dynamo-snapshots"} stage: ${opt:stage, self:provider.stage} - princeUrl: ${self:custom.princeUrl} + docraptorApiKey: ${self:custom.docraptorApiKey} functions: listMeasures: @@ -309,35 +315,6 @@ resources: gatewayresponse.header.Access-Control-Allow-Headers: "'*'" ResponseType: DEFAULT_5XX RestApiId: !Ref ApiGatewayRestApi - ApiGwWebAcl: - Type: AWS::WAFv2::WebACL - Properties: - Name: ${self:custom.webAclName} - DefaultAction: - Block: {} - Rules: - - Action: - Allow: {} - Name: ${self:custom.webAclName}-allow-usa-plus-territories - Priority: 0 - Statement: - GeoMatchStatement: - CountryCodes: - - GU # Guam - - PR # Puerto Rico - - US # USA - - UM # US Minor Outlying Islands - - VI # US Virgin Islands - - MP # Northern Mariana Islands - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: WafWebAcl - Scope: REGIONAL - VisibilityConfig: - CloudWatchMetricsEnabled: true - SampledRequestsEnabled: true - MetricName: ${self:custom.stage}-webacl Outputs: ApiGatewayRestApiName: Value: !Ref ApiGatewayRestApi diff --git a/services/app-api/yarn.lock b/services/app-api/yarn.lock index 3e3f9e46d6..75987d1177 100644 --- a/services/app-api/yarn.lock +++ b/services/app-api/yarn.lock @@ -18,57 +18,6 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@aws-crypto/crc32@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" - integrity sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA== - dependencies: - "@aws-crypto/util" "^3.0.0" - "@aws-sdk/types" "^3.222.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz#c4fdb773fdbed9a664fc1a95724e206cf3860042" - integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== - dependencies: - "@aws-crypto/util" "^5.2.0" - "@aws-sdk/types" "^3.222.0" - tslib "^2.6.2" - -"@aws-crypto/util@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-3.0.0.tgz#1c7ca90c29293f0883468ad48117937f0fe5bfb0" - integrity sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w== - dependencies: - "@aws-sdk/types" "^3.222.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/util@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" - integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== - dependencies: - "@aws-sdk/types" "^3.222.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.6.2" - -"@aws-sdk/types@^3.222.0": - version "3.468.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.468.0.tgz#f97b34fc92a800d1d8b866f47693ae8f3d46517b" - integrity sha512-rx/9uHI4inRbp2tw3Y4Ih4PNZkVj32h7WneSg3MVgVjAoVD5Zti9KhS5hkvsBxfgmQmg0AQbE+b1sy5WGAgntA== - dependencies: - "@smithy/types" "^2.7.0" - tslib "^2.5.0" - -"@aws-sdk/util-utf8-browser@^3.0.0": - version "3.55.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.55.0.tgz#a045bf1a93f6e0ff9c846631b168ea55bbb37668" - integrity sha512-ljzqJcyjfJpEVSIAxwtIS8xMRUly84BdjlBXyp6cu4G8TUufgjNS31LWdhyGhgmW5vYBNr+LTz0Kwf6J+ou7Ug== - dependencies: - tslib "^2.3.1" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" @@ -822,82 +771,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@smithy/eventstream-codec@^2.0.15": - version "2.0.15" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-2.0.15.tgz#733e638fd38e7e264bc0429dbda139bab950bd25" - integrity sha512-crjvz3j1gGPwA0us6cwS7+5gAn35CTmqu/oIxVbYJo2Qm/sGAye6zGJnMDk3BKhWZw5kcU1G4MxciTkuBpOZPg== - dependencies: - "@aws-crypto/crc32" "3.0.0" - "@smithy/types" "^2.7.0" - "@smithy/util-hex-encoding" "^2.0.0" - tslib "^2.5.0" - -"@smithy/is-array-buffer@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz#8fa9b8040651e7ba0b2f6106e636a91354ff7d34" - integrity sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug== - dependencies: - tslib "^2.5.0" - -"@smithy/signature-v4@^2.0.18": - version "2.0.18" - resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-2.0.18.tgz#53b78b238edaa84cc8d61faf67d2b3c926cdd698" - integrity sha512-SJRAj9jT/l9ocm8D0GojMbnA1sp7I4JeStOQ4lEXI8A5eHE73vbjlzlqIFB7cLvIgau0oUl4cGVpF9IGCrvjlw== - dependencies: - "@smithy/eventstream-codec" "^2.0.15" - "@smithy/is-array-buffer" "^2.0.0" - "@smithy/types" "^2.7.0" - "@smithy/util-hex-encoding" "^2.0.0" - "@smithy/util-middleware" "^2.0.8" - "@smithy/util-uri-escape" "^2.0.0" - "@smithy/util-utf8" "^2.0.2" - tslib "^2.5.0" - -"@smithy/types@^2.7.0": - version "2.7.0" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.7.0.tgz#6ed9ba5bff7c4d28c980cff967e6d8456840a4f3" - integrity sha512-1OIFyhK+vOkMbu4aN2HZz/MomREkrAC/HqY5mlJMUJfGrPRwijJDTeiN8Rnj9zUaB8ogXAfIOtZrrgqZ4w7Wnw== - dependencies: - tslib "^2.5.0" - -"@smithy/util-buffer-from@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz#7eb75d72288b6b3001bc5f75b48b711513091deb" - integrity sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw== - dependencies: - "@smithy/is-array-buffer" "^2.0.0" - tslib "^2.5.0" - -"@smithy/util-hex-encoding@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz#0aa3515acd2b005c6d55675e377080a7c513b59e" - integrity sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA== - dependencies: - tslib "^2.5.0" - -"@smithy/util-middleware@^2.0.8": - version "2.0.8" - resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-2.0.8.tgz#2ec1da1190d09b69512ce0248ebd5e819e3c8a92" - integrity sha512-qkvqQjM8fRGGA8P2ydWylMhenCDP8VlkPn8kiNuFEaFz9xnUKC2irfqsBSJrfrOB9Qt6pQsI58r3zvvumhFMkw== - dependencies: - "@smithy/types" "^2.7.0" - tslib "^2.5.0" - -"@smithy/util-uri-escape@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz#19955b1a0f517a87ae77ac729e0e411963dfda95" - integrity sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw== - dependencies: - tslib "^2.5.0" - -"@smithy/util-utf8@^2.0.0", "@smithy/util-utf8@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.0.2.tgz#626b3e173ad137208e27ed329d6bea70f4a1a7f7" - integrity sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA== - dependencies: - "@smithy/util-buffer-from" "^2.0.0" - tslib "^2.5.0" - "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -915,6 +788,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -990,6 +868,13 @@ "@types/node" "*" "@types/responselike" "*" +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== + dependencies: + "@types/trusted-types" "*" + "@types/glob@^7.1.1": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" @@ -1037,6 +922,15 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/jsdom@^21.1.6": + version "21.1.6" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.6.tgz#bcbc7b245787ea863f3da1ef19aa1dcfb9271a1b" + integrity sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + "@types/json-buffer@~3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/json-buffer/-/json-buffer-3.0.0.tgz#85c1ff0f0948fc159810d4b5be35bf8c20875f64" @@ -1086,6 +980,16 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +"@types/trusted-types@*": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -1098,7 +1002,7 @@ dependencies: "@types/yargs-parser" "*" -abab@^2.0.3, abab@^2.0.5: +abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== @@ -1915,6 +1819,13 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +cssstyle@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-3.0.0.tgz#17ca9c87d26eac764bb8cfd00583cff21ce0277a" + integrity sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg== + dependencies: + rrweb-cssom "^0.6.0" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -1932,6 +1843,15 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-urls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" + integrity sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^12.0.0" + dayjs@^1.11.7: version "1.11.7" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" @@ -1956,6 +1876,11 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -2093,6 +2018,18 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + +dompurify@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e" + integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ== + dotenv-expand@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-9.0.0.tgz#1fd37e2cd63ea0b5f7389fb87256efc38b035b26" @@ -2133,6 +2070,11 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2140,22 +2082,14 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es5-ext@^0.10.12, es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.47, es5-ext@^0.10.49, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.61" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.61.tgz#311de37949ef86b6b0dcea894d1ffedb909d3269" - integrity sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA== - dependencies: - es6-iterator "^2.0.3" - es6-symbol "^3.1.3" - next-tick "^1.1.0" - -es5-ext@^0.10.61, es5-ext@^0.10.62: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== +es5-ext@^0.10.12, es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.47, es5-ext@^0.10.49, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@^0.10.61, es5-ext@^0.10.62, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.63" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.63.tgz#9c222a63b6a332ac80b1e373b426af723b895bd6" + integrity sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ== dependencies: es6-iterator "^2.0.3" es6-symbol "^3.1.3" + esniff "^2.0.1" next-tick "^1.1.0" es6-iterator@^2.0.3, es6-iterator@~2.0.3: @@ -2232,6 +2166,16 @@ esniff@^1.1.0: d "1" es5-ext "^0.10.12" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -2505,6 +2449,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + formidable@^1.2.0: version "1.2.6" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" @@ -2763,6 +2716,13 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -2782,6 +2742,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -2810,6 +2779,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -3531,6 +3507,35 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" +jsdom@^22.1.0: + version "22.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8" + integrity sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw== + dependencies: + abab "^2.0.6" + cssstyle "^3.0.0" + data-urls "^4.0.0" + decimal.js "^10.4.3" + domexception "^4.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.4" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^12.0.1" + ws "^8.13.0" + xml-name-validator "^4.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -4006,6 +4011,11 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +nwsapi@^2.2.4: + version "2.2.7" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" + integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== + object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -4147,6 +4157,13 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^7.0.0, parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4316,6 +4333,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qs@^6.5.1: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" @@ -4469,6 +4491,11 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +rrweb-cssom@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" + integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -4505,7 +4532,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -4527,6 +4554,13 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + seek-bzip@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" @@ -5021,7 +5055,7 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" -tough-cookie@^4.0.0: +tough-cookie@^4.0.0, tough-cookie@^4.1.2: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== @@ -5038,6 +5072,13 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-4.1.1.tgz#281a758dcc82aeb4fe38c7dfe4d11a395aac8469" + integrity sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw== + dependencies: + punycode "^2.3.0" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -5088,26 +5129,11 @@ ts-node@^10.7.0: v8-compile-cache-lib "^3.0.0" yn "3.1.1" -tslib@^1.11.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.1.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tslib@^2.3.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -tslib@^2.5.0, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -5286,6 +5312,13 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + walker@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -5323,6 +5356,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -5330,11 +5368,31 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^12.0.0, whatwg-url@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-12.0.1.tgz#fd7bcc71192e7c3a2a97b9a8d6b094853ed8773c" + integrity sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ== + dependencies: + tr46 "^4.1.1" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -5420,11 +5478,21 @@ ws@^7.4.6, ws@^7.5.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== +ws@^8.13.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + xml2js@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" diff --git a/services/stream-functions/.gitignore b/services/stream-functions/.gitignore deleted file mode 100644 index 546c3da2a7..0000000000 --- a/services/stream-functions/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# package directories -node_modules -jspm_packages - -# Serverless directories -.serverless -.webpack -.repack diff --git a/services/stream-functions/README.md b/services/stream-functions/README.md deleted file mode 100644 index fdb3b12cf9..0000000000 --- a/services/stream-functions/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# stream-functions - -## Configuration - AWS Systems Manager Parameter Store (SSM) - -The following values are used to configure the deployment of this service (see below for more background and context). -| Parameter | Required? | Accepts a default? | Accepts a branch override? | Purpose | -| --- | :---: | :---: | :---: | --- | -| .../iam/path | N | Y | Y | Specifies the [IAM Path](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-friendly-names) at which all IAM objects should be created. The default value is "/". The path variable in IAM is used for grouping related users and groups in a unique namespace, usually for organizational purposes.| -| .../iam/permissionsBoundaryPolicy | N | Y | Y | Specifies the [IAM Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) that should be attached to all IAM objects. A permissions boundary is an advanced feature for using a managed policy to set the maximum permissions that an identity-based policy can grant to an IAM entity. If set, this parmeter should contain the full ARN to the policy.| -| sesSourceEmailAddress | N | Y | Y | The email address with which the apllication sends the email. This email address must be verified in SES.| -| reviewerEmailAddress | N | Y | Y | Email address of the submissions reviewer.| - -This project uses [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html), often referred to as simply SSM, to inject environment specific, project specific, and/or sensitive information into the deployment. -In short, SSM is an AWS service that allows users to store (optionally) encrypted strings in a directory like hierarchy. For example, "/my/first/ssm/param" is a valid path for a parameter. Access to this service and even individual paramters is granted via AWS IAM. - -An example of environment specific information is the id of a VPC into which we want to deploy. This VPC id should not be checked in to git, as it may vary from environment to environment, so we would use SSM to store the id information and use the [Serverless Framework's SSM lookup capability](https://www.serverless.com/framework/docs/providers/aws/guide/variables/#reference-variables-using-the-ssm-parameter-store) to fetcn the information at deploy time. - -This project has also implemented a pattern for specifying defaults for variables, while allowing for branch (environment specific overrides). That pattern looks like this: - -``` -sesSourceEmailAddress: ${ssm:/configuration/${self:custom.stage}/sesSourceEmailAddress~true, ssm:/configuration/default/sesSourceEmailAddress~true} -``` - -The above syntax says "look for an ssm parameter at /configuration/<branch name>/sesSourceEmailAddress; if there isn't one, look for a parameter at /configuration/default/sesSourceEmailAddress". With this logic, we can specify a generic value for this variable that would apply to all environments deployed to a given account, but if we wish to set a different value for a specific environment (branch), we can create a parameter at the branch specific path and it will take precedence. - -In the above tabular documentation, you will see columns for "Accepts default?" and "Accepts a branch override?". These columns relate to the above convention of searching for a branch specific override but falling back to a default parameter. It's important to note if a parameter can accept a default or can accept an override, because not all can do both. For example, a parameter used to specify Okta App information cannot be set as a default, because Okta can only support one environment (branch) at a time; so, okta_metadata_url is a good example of a parameter that can only be specified on a branch by branch basis, and never as a default. - -In the above documentation, you will also see the Parameter value denoted as ".../iam/path", for example. This notation is meant to represent the core of the parameter's expected path. The "..." prefix is meant to be a placeholder for either "/configuration/default" (in the case of a default value) or "/configuration/myfavoritebranch" (in the case of specifying a branch specific override for the myfavoritebranch branch. diff --git a/services/stream-functions/handlers/emailReviewer.js b/services/stream-functions/handlers/emailReviewer.js deleted file mode 100644 index b3dfacadee..0000000000 --- a/services/stream-functions/handlers/emailReviewer.js +++ /dev/null @@ -1,64 +0,0 @@ -import * as ses from "./../libs/ses-lib"; - -exports.handler = function (event, context, callback) { - console.log("Received event:", JSON.stringify(event, null, 2)); - event.Records.forEach(function (record) { - var params = (function (eventName) { - switch (eventName) { - case "INSERT": - return ses.getSESEmailParams({ - ToAddresses: [process.env.reviewerEmail], - Source: process.env.emailSource, - Subject: `New APS Submission - ${record.dynamodb.NewImage.transmittalNumber.S}`, - Text: getReviewerEmailBody( - record.dynamodb.NewImage, - "A new APS submission has been received." - ), - }); - case "MODIFY": - return ses.getSESEmailParams({ - ToAddresses: [process.env.reviewerEmail], - Source: process.env.emailSource, - Subject: `Updated APS Submission - ${record.dynamodb.NewImage.transmittalNumber.S}`, - Text: getReviewerEmailBody( - record.dynamodb.NewImage, - "An update to an existing APS submission has been received." - ), - }); - case "REMOVE": - return ses.getSESEmailParams({ - ToAddresses: [process.env.reviewerEmail], - Source: process.env.emailSource, - Subject: `Updated APS Submission - ${record.dynamodb.OldImage.transmittalNumber.S}`, - Text: getReviewerEmailBody( - record.dynamodb.OldImage, - "A request to delete the below APS request has been processed." - ), - }); - default: - return 30; - } - })(record.eventName); - - ses.sendEmail(params); - }); - callback(null, "message"); -}; - -function getReviewerEmailBody(image, summary) { - return ` -Hi, - -${summary} - -Details: -- APS ID (Transmittal Number): ${image.transmittalNumber.S} -State: ${image.territory.S} -Submitter Name: ${image.firstName.S} ${image.lastName.S} -Submitter Contact Email: ${image.email.S} - -Regards, -APS Submission App - -`; -} diff --git a/services/stream-functions/handlers/emailSubmitter.js b/services/stream-functions/handlers/emailSubmitter.js deleted file mode 100644 index e6ac0f9f63..0000000000 --- a/services/stream-functions/handlers/emailSubmitter.js +++ /dev/null @@ -1,79 +0,0 @@ -import * as ses from "./../libs/ses-lib"; - -exports.handler = function (event, context, callback) { - console.log("Received event:", JSON.stringify(event, null, 2)); - event.Records.forEach(function (record) { - var params = (function (eventName) { - switch (eventName) { - case "INSERT": - return ses.getSESEmailParams({ - ToAddresses: [record.dynamodb.NewImage.email.S], - Source: process.env.emailSource, - Subject: `New ACME APS submission received! - ${record.dynamodb.NewImage.transmittalNumber.S}`, - Text: ` -Hi ${record.dynamodb.NewImage.firstName.S}, - -We are writing to let you know we've received your Amendment to Planned Settlement (APS) submission! -It is under review. -No additional action is needed on your part. - -APS ID: ${record.dynamodb.NewImage.transmittalNumber.S} - -Thank you for using our APS submission system! - -Regards, -APS Team - -`, - }); - case "MODIFY": - return ses.getSESEmailParams({ - ToAddresses: [record.dynamodb.NewImage.email.S], - Source: process.env.emailSource, - Subject: `Updated ACME APS submission received! - ${record.dynamodb.NewImage.transmittalNumber.S}`, - Text: ` - Hi ${record.dynamodb.NewImage.firstName.S}, - - We are writing to let you know we've received an update to your Amendment to Planned Settlement (APS) submission! - It is under review. - No additional action is needed on your part. - - APS ID: ${record.dynamodb.NewImage.transmittalNumber.S} - - Thank you for using our APS submission system! - - Regards, - APS Team - - `, - }); - case "REMOVE": - return ses.getSESEmailParams({ - ToAddresses: [record.dynamodb.OldImage.email.S], - Source: process.env.emailSource, - Subject: `Your ACME APS submission has been deleted - ${record.dynamodb.OldImage.transmittalNumber.S}`, - Text: ` - Hi ${record.dynamodb.OldImage.firstName.S}, - - We received a request to delete your Amendment to Planned Settlement (APS) submission. - We are writing to let you know that we have processed that request. - No additional action is needed on your part. - - APS ID: ${record.dynamodb.OldImage.transmittalNumber.S} - - Thank you for using our APS submission system! - - Regards, - APS Team - - `, - }); - default: - return 30; - } - })(record.eventName); - - ses.sendEmail(params); - }); - callback(null, "message"); -}; diff --git a/services/stream-functions/libs/ses-lib.js b/services/stream-functions/libs/ses-lib.js deleted file mode 100644 index 89ddd3c6db..0000000000 --- a/services/stream-functions/libs/ses-lib.js +++ /dev/null @@ -1,35 +0,0 @@ -const AWS = require("aws-sdk"); -var ses = new AWS.SES({ region: "us-east-1" }); - -export function getSESEmailParams(email) { - let emailParams = { - Destination: { - ToAddresses: email.ToAddresses, - }, - Message: { - Body: { - Text: { - Charset: "UTF-8", - Data: email.Text, - }, - }, - Subject: { - Charset: "UTF-8", - Data: email.Subject, - }, - }, - Source: email.Source, - }; - - return emailParams; -} - -export function sendEmail(params) { - ses.sendEmail(params, function (err, data) { - if (err) { - console.error(err); - } else { - console.log(data); - } - }); -} diff --git a/services/stream-functions/package.json b/services/stream-functions/package.json deleted file mode 100644 index cf7f782e4d..0000000000 --- a/services/stream-functions/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "stream-functions", - "description": "", - "version": "1.0.0", - "dependencies": {}, - "devDependencies": { - "serverless-s3-bucket-helper": "Enterprise-CMCS/serverless-s3-bucket-helper#0.1.1" - } -} diff --git a/services/stream-functions/serverless.yml b/services/stream-functions/serverless.yml deleted file mode 100644 index 09cba88af6..0000000000 --- a/services/stream-functions/serverless.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Refer to the README.md file in within this service directory to configure all ssm parameters required for this service. -service: stream-functions - -frameworkVersion: "3" - -package: - individually: true - -plugins: - - serverless-bundle - - serverless-dotenv-plugin - - serverless-stack-termination-protection - - serverless-idempotency-helper - - serverless-s3-bucket-helper - -provider: - name: aws - runtime: nodejs16.x - region: us-east-1 - iam: - role: - path: ${ssm:/configuration/${self:custom.stage}/iam/path, ssm:/configuration/default/iam/path, "/"} - permissionsBoundary: ${ssm:/configuration/${self:custom.stage}/iam/permissionsBoundaryPolicy, ssm:/configuration/default/iam/permissionsBoundaryPolicy, ""} - statements: - - Effect: "Allow" - Action: - - dynamodb:DescribeStream - - dynamodb:GetRecords - - dynamodb:GetShardIterator - - dynamodb:ListStreams - Resource: ${self:custom.tableStreamArn} - - Effect: "Allow" - Action: - - ses:SendEmail - - ses:SendRawEmail - Resource: "*" - -custom: - stage: ${opt:stage, self:provider.stage} - region: ${opt:region, self:provider.region} - serverlessTerminationProtection: - stages: - - master - - val - - prod - tableStreamArn: ${cf:database-${self:custom.stage}.CoreSetTableStreamArn} - sesSourceEmailAddress: ${ssm:/configuration/${self:custom.stage}/sesSourceEmailAddress, ssm:/configuration/default/sesSourceEmailAddress, "admin@example.com"} - reviewerEmailAddress: ${ssm:/configuration/${self:custom.stage}/reviewerEmailAddress, ssm:/configuration/default/reviewerEmailAddress, "reviewteam@example.com"} - -functions: - emailSubmitter: - handler: handlers/emailSubmitter.handler - events: - - stream: - arn: ${self:custom.tableStreamArn} - startingPosition: LATEST - maximumRetryAttempts: 2 - environment: - emailSource: ${self:custom.sesSourceEmailAddress} - maximumRetryAttempts: 2 - emailReviewer: - handler: handlers/emailReviewer.handler - events: - - stream: - arn: ${self:custom.tableStreamArn} - startingPosition: LATEST - maximumRetryAttempts: 2 - environment: - emailSource: ${self:custom.sesSourceEmailAddress} - reviewerEmail: ${self:custom.reviewerEmailAddress} - maximumRetryAttempts: 2 diff --git a/services/stream-functions/yarn.lock b/services/stream-functions/yarn.lock deleted file mode 100644 index 8886796bb4..0000000000 --- a/services/stream-functions/yarn.lock +++ /dev/null @@ -1,7 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -serverless-s3-bucket-helper@Enterprise-CMCS/serverless-s3-bucket-helper#0.1.1: - version "1.0.0" - resolved "https://codeload.github.com/Enterprise-CMCS/serverless-s3-bucket-helper/tar.gz/f0f6d6a1ffe54e292f0afc93777764bce16a4037" diff --git a/services/ui-auth/handlers/createUsers.js b/services/ui-auth/handlers/createUsers.js index 0f176630ab..77c2cadd42 100644 --- a/services/ui-auth/handlers/createUsers.js +++ b/services/ui-auth/handlers/createUsers.js @@ -12,6 +12,7 @@ async function myHandler(event, context, callback) { UserPoolId: userPoolId, Username: users[i].username, DesiredDeliveryMediums: ["EMAIL"], + MessageAction: "SUPPRESS", UserAttributes: users[i].attributes, }; var passwordData = { diff --git a/services/ui-auth/package.json b/services/ui-auth/package.json index ddc79cf9be..f5372e61e2 100644 --- a/services/ui-auth/package.json +++ b/services/ui-auth/package.json @@ -12,7 +12,7 @@ "serverless-s3-bucket-helper": "Enterprise-CMCS/serverless-s3-bucket-helper#0.1.1" }, "dependencies": { - "aws-sdk": "^2.1310.0", + "aws-sdk": "^2.1531.0", "xml2js": "0.6.0" } } diff --git a/services/ui-auth/serverless.yml b/services/ui-auth/serverless.yml index 108ece315e..1b264622df 100644 --- a/services/ui-auth/serverless.yml +++ b/services/ui-auth/serverless.yml @@ -36,7 +36,6 @@ custom: - master - val - prod - sesSourceEmailAddress: ${ssm:/configuration/${self:custom.stage}/sesSourceEmailAddress, ssm:/configuration/default/sesSourceEmailAddress, ""} attachments_bucket_arn: ${cf:uploads-${self:custom.stage}.AttachmentsBucketArn} api_gateway_rest_api_name: ${cf:app-api-${self:custom.stage}.ApiGatewayRestApiName} okta_metadata_url: ${ssm:/configuration/${self:custom.stage}/okta_metadata_url, ""} @@ -61,11 +60,6 @@ functions: resources: Conditions: - CreateEmailConfiguration: - Fn::Not: - - Fn::Equals: - - "" - - ${self:custom.sesSourceEmailAddress} BackWithOkta: Fn::Not: - Fn::Equals: @@ -80,12 +74,6 @@ resources: - email AutoVerifiedAttributes: - email - EmailConfiguration: - Fn::If: - - CreateEmailConfiguration - - EmailSendingAccount: DEVELOPER - SourceArn: !Sub arn:aws:ses:us-east-1:${AWS::AccountId}:identity/${self:custom.sesSourceEmailAddress} - - !Ref AWS::NoValue Schema: - Name: given_name AttributeDataType: String diff --git a/services/ui-auth/yarn.lock b/services/ui-auth/yarn.lock index d4877c03b2..f44e75cdd7 100644 --- a/services/ui-auth/yarn.lock +++ b/services/ui-auth/yarn.lock @@ -7,10 +7,10 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-sdk@^2.1310.0: - version "2.1326.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1326.0.tgz#91da60f08d10e4e1db0640c97bc7d2298fde8f17" - integrity sha512-LSGiO4RSooupHnkvYbPOuOYqwAxmcnYinwIxBz4P1YI8ulhZZ/pypOj/HKqC629UyhY1ndSMtlM1l56U74UclA== +aws-sdk@^2.1531.0: + version "2.1545.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1545.0.tgz#8678ae8117a426d4a6602408e7f47f176356d7ca" + integrity sha512-iDUv6ksG7lTA0l/HlOgYdO6vfYFA1D2/JzAEXSdgKY0C901WgJqBtfs2CncOkCgDe2CjmlMuqciBzAfxCIiKFA== dependencies: buffer "4.9.2" events "1.1.1" @@ -21,7 +21,7 @@ aws-sdk@^2.1310.0: url "0.10.3" util "^0.12.4" uuid "8.0.0" - xml2js "0.4.19" + xml2js "0.6.2" base64-js@^1.0.2: version "1.5.1" @@ -213,14 +213,6 @@ which-typed-array@^1.1.2: has-tostringtag "^1.0.0" is-typed-array "^1.1.10" -xml2js@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - xml2js@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282" @@ -229,12 +221,15 @@ xml2js@0.6.0: sax ">=0.6.0" xmlbuilder "~11.0.0" +xml2js@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xmlbuilder@~11.0.0: version "11.0.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - -xmlbuilder@~9.0.1: - version "9.0.7" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ== diff --git a/services/ui-src/package.json b/services/ui-src/package.json index efe6ca2f53..781d82659d 100644 --- a/services/ui-src/package.json +++ b/services/ui-src/package.json @@ -20,7 +20,7 @@ "node-fetch": "3.3.1", "node-forge": "1.3.1", "object-path": "^0.11.8", - "pac-resolver": "6.0.2", + "pac-resolver": "7.0.1", "react": "^17.0.1", "react-bootstrap": "^2.0.1", "react-dom": "^17.0.1", diff --git a/services/ui-src/public/index.html b/services/ui-src/public/index.html index f5556f0f70..4448c12327 100644 --- a/services/ui-src/public/index.html +++ b/services/ui-src/public/index.html @@ -33,13 +33,12 @@ /> <!-- Tealium Analytics Tag Manager --> <script> - var nodeEnv = window._env_.STAGE; var tealiumEnvMap = { - val: "qa", - production: "prod", + "mdctqmr.cms.gov": "production", + "mdctqmrval.cms.gov": "qa", }; - var tealiumEnv = tealiumEnvMap[nodeEnv] || "dev"; - var tealiumUrl = `https://tags.tiqcdn.com/utag/cmsgov/cms-mdctqmr/${tealiumEnv}/utag.sync.js`; + var tealiumEnv = tealiumEnvMap[window.location.hostname] || "dev"; + var tealiumUrl = `https://tags.tiqcdn.com/utag/cmsgov/cms-general/${tealiumEnv}/utag.sync.js`; document.write(`<script src="${tealiumUrl}" async><\/script>`); </script> </head> @@ -51,7 +50,7 @@ </script> <script> (function (t, e, a, l, i, u, m) { - t = "cmsgov/cms-mdctqmr"; + t = "cmsgov/cms-general"; e = tealiumEnv; a = "/" + t + "/" + e + "/utag.js"; l = "//tags.tiqcdn.com/utag" + a; diff --git a/services/ui-src/src/components/MeasureWrapper/index.tsx b/services/ui-src/src/components/MeasureWrapper/index.tsx index 44ab18a363..db034f6297 100644 --- a/services/ui-src/src/components/MeasureWrapper/index.tsx +++ b/services/ui-src/src/components/MeasureWrapper/index.tsx @@ -17,13 +17,20 @@ import { import { v4 as uuidv4 } from "uuid"; import * as QMR from "components"; import { useEditCoreSet, useGetMeasure, useUpdateMeasure } from "hooks/api"; -import { AutoCompletedMeasures, CoreSetAbbr, MeasureStatus } from "types"; +import { + AnyObject, + AutoCompletedMeasures, + CoreSetAbbr, + MeasureStatus, +} from "types"; import { areSomeRatesCompleted } from "utils/form"; import * as DC from "dataConstants"; import { CoreSetTableItem } from "components/Table/types"; import { useUser } from "hooks/authHooks"; import { measureDescriptions } from "measures/measureDescriptions"; import { CompleteCoreSets } from "./complete"; +import SharedContext from "shared/SharedContext"; +import * as Labels from "labels/Labels"; const LastModifiedBy = ({ user }: { user: string | undefined }) => { if (!user) return null; @@ -142,6 +149,10 @@ export const MeasureWrapper = ({ [] ); + //WIP: this code will be replaced with a dynamic import onces we refactored enough files + const shared: AnyObject = + Labels[`CQ${year}` as "CQ2021" | "CQ2022" | "CQ2023" | "CQ2024"]; + // setup default values for core set, as delivery system uses this to pregen the labeled portion of the table const coreSet = (params.coreSetId?.split("_")?.[0] ?? params.coreSetId ?? @@ -451,16 +462,17 @@ export const MeasureWrapper = ({ to MACQualityTA@cms.hhs.gov. </CUI.Text> )} - <Measure - measure={measure} - name={name} - detailedDescription={detailedDescription} - year={year} - measureId={measureId} - setValidationFunctions={setValidationFunctions} - handleSave={handleSave} - /> - + <SharedContext.Provider value={shared}> + <Measure + measure={measure} + name={name} + detailedDescription={detailedDescription} + year={year} + measureId={measureId} + setValidationFunctions={setValidationFunctions} + handleSave={handleSave} + /> + </SharedContext.Provider> {/* Core set qualifiers use a slightly different submission button layout */} {!!(!autocompleteOnCreation && !defaultData) && ( <QMR.CompleteMeasureFooter diff --git a/services/ui-src/src/config.ts b/services/ui-src/src/config.ts index 5d429e925c..3497e65808 100644 --- a/services/ui-src/src/config.ts +++ b/services/ui-src/src/config.ts @@ -32,7 +32,7 @@ const configToExport = { REDIRECT_SIGNOUT: window._env_.COGNITO_REDIRECT_SIGNOUT, }, POST_SIGNOUT_REDIRECT: window._env_.POST_SIGNOUT_REDIRECT, - currentReportingYear: "2023", + currentReportingYear: "2024", REACT_APP_LD_SDK_CLIENT: window._env_.REACT_APP_LD_SDK_CLIENT, }; diff --git a/services/ui-src/src/dataConstants.ts b/services/ui-src/src/dataConstants.ts index 1db44642f0..1c885164f8 100644 --- a/services/ui-src/src/dataConstants.ts +++ b/services/ui-src/src/dataConstants.ts @@ -13,6 +13,7 @@ export const AHRQ_NCQA = "AHRQ-NCQA"; export const AMOUNT_OF_POP_NOT_COVERED = "AmountOfPopulationNotCovered"; export const BUDGET_CONSTRAINTS = "BudgetConstraints"; export const CASE_MANAGEMENT_RECORD_REVIEW = "Case management record review"; +export const CASE_MANAGEMENT_RECORD_REVIEW_DATA = "Casemanagementrecordreview"; export const CATEGORIES = "categories"; export const CDC = "CDC"; export const CHANGE_IN_POP_EXPLANATION = "ChangeInPopulationExplanation"; @@ -113,6 +114,7 @@ export const HEDIS_2020 = "HEDIS 2020"; export const HEDIS_MY_2020 = "HEDIS MY 2020"; export const HEDIS_MY_2021 = "HEDIS MY 2021"; export const HEDIS_MY_2022 = "HEDIS MY 2022"; +export const HEDIS_MY_2023 = "HEDIS MY 2023"; export const HRSA = "HRSA"; export const HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA = "HybridAdministrativeandMedicalRecordsData"; diff --git a/services/ui-src/src/hooks/api/useGetCoreSets.tsx b/services/ui-src/src/hooks/api/useGetCoreSets.tsx index 8a792ed5f7..af408e19d4 100644 --- a/services/ui-src/src/hooks/api/useGetCoreSets.tsx +++ b/services/ui-src/src/hooks/api/useGetCoreSets.tsx @@ -14,9 +14,9 @@ const getCoreSets = async ({ state, year }: GetCoreSets) => { }); }; -export const useGetCoreSets = (releasedTwentyTwentyThree: boolean) => { +export const useGetCoreSets = (releasedTwentyTwentyFour: boolean) => { const { state, year } = useParams(); - if (state && year && (releasedTwentyTwentyThree || year !== "2023")) { + if (state && year && (releasedTwentyTwentyFour || year !== "2024")) { return useQuery(["coreSets", state, year], () => getCoreSets({ state, year }) ); diff --git a/services/ui-src/src/labels/2021/commonQuestionsLabel.tsx b/services/ui-src/src/labels/2021/commonQuestionsLabel.tsx new file mode 100644 index 0000000000..2bc1c308dd --- /dev/null +++ b/services/ui-src/src/labels/2021/commonQuestionsLabel.tsx @@ -0,0 +1,11 @@ +export const commonQuestionsLabel = { + AdditonalNotes: { + header: "Additional Notes/Comments on the measure (optional)", + section: + "Please add any additional notes or comments on the measure not otherwise captured above:", + upload: + "If you need additional space to include comments or supplemental information, please attach further documentation below.", + }, +}; + +export default commonQuestionsLabel; diff --git a/services/ui-src/src/labels/2022/commonQuestionsLabel.tsx b/services/ui-src/src/labels/2022/commonQuestionsLabel.tsx new file mode 100644 index 0000000000..2bc1c308dd --- /dev/null +++ b/services/ui-src/src/labels/2022/commonQuestionsLabel.tsx @@ -0,0 +1,11 @@ +export const commonQuestionsLabel = { + AdditonalNotes: { + header: "Additional Notes/Comments on the measure (optional)", + section: + "Please add any additional notes or comments on the measure not otherwise captured above:", + upload: + "If you need additional space to include comments or supplemental information, please attach further documentation below.", + }, +}; + +export default commonQuestionsLabel; diff --git a/services/ui-src/src/labels/2023/commonQuestionsLabel.tsx b/services/ui-src/src/labels/2023/commonQuestionsLabel.tsx new file mode 100644 index 0000000000..ee7520b70c --- /dev/null +++ b/services/ui-src/src/labels/2023/commonQuestionsLabel.tsx @@ -0,0 +1,11 @@ +export const commonQuestionsLabel = { + AdditonalNotes: { + header: "Additional Notes/Comments on the measure (optional)", + section: + "Please add any additional notes or comments on the measure not otherwise captured above (<em>text in this field is included in publicly-reported state-specific comments</em>):", + upload: + "If you need additional space to include comments or supplemental information, please attach further documentation below.", + }, +}; + +export default commonQuestionsLabel; diff --git a/services/ui-src/src/labels/2024/commonQuestionsLabel.tsx b/services/ui-src/src/labels/2024/commonQuestionsLabel.tsx new file mode 100644 index 0000000000..ee7520b70c --- /dev/null +++ b/services/ui-src/src/labels/2024/commonQuestionsLabel.tsx @@ -0,0 +1,11 @@ +export const commonQuestionsLabel = { + AdditonalNotes: { + header: "Additional Notes/Comments on the measure (optional)", + section: + "Please add any additional notes or comments on the measure not otherwise captured above (<em>text in this field is included in publicly-reported state-specific comments</em>):", + upload: + "If you need additional space to include comments or supplemental information, please attach further documentation below.", + }, +}; + +export default commonQuestionsLabel; diff --git a/services/ui-src/src/labels/Labels.tsx b/services/ui-src/src/labels/Labels.tsx new file mode 100644 index 0000000000..1b78012cc0 --- /dev/null +++ b/services/ui-src/src/labels/Labels.tsx @@ -0,0 +1,4 @@ +export { commonQuestionsLabel as CQ2021 } from "labels/2021/commonQuestionsLabel"; +export { commonQuestionsLabel as CQ2022 } from "labels/2022/commonQuestionsLabel"; +export { commonQuestionsLabel as CQ2023 } from "labels/2023/commonQuestionsLabel"; +export { commonQuestionsLabel as CQ2024 } from "labels/2024/commonQuestionsLabel"; diff --git a/services/ui-src/src/libs/spaLib.ts b/services/ui-src/src/libs/spaLib.ts index 5f6dc5cdc8..6db634e41b 100644 --- a/services/ui-src/src/libs/spaLib.ts +++ b/services/ui-src/src/libs/spaLib.ts @@ -376,4 +376,129 @@ export const SPA: { [year: string]: SPAi[] } = { { id: "12-008", state: "WI", name: "Individuals with HIV/AIDS" }, { id: "21-0012", state: "WI", name: "Substance Use Disorder Health Home" }, ], + "2024": [ + { + id: "19-0037", + state: "CA", + name: "California Health Home Program (HHP)", + }, + { id: "20-0002", state: "CA", name: "California Behavioral Health Home" }, + { + id: "15-014", + state: "CT", + name: "Serious and Persistent Mental Illness", + }, + { + id: "18-006", + state: "DE", + name: "Delaware Assertive Community Integration Support Team (ACIST) Health Home", + }, + { + id: "18-0006", + state: "DC", + name: "Chronic Care Management for Individuals with Serious and Persistent Mental Health Conditions", + }, + { id: "18-0007", state: "DC", name: "Chronic Conditions" }, + { id: "22-0004", state: "IA", name: "Iowa Health Home Services" }, + { + id: "20-0011", + state: "IA", + name: "Iowa Severe and Persistent Mental Illness Health Home ", + }, + { + id: "20-0004", + state: "KS", + name: "Health Home Serving Serious Mental Illness (SMI)", + }, + { + id: "20-0005", + state: "KS", + name: "Health Home Chronic Conditions - Asthma", + }, + { + id: "22-0033-CT", + state: "ME", + name: "Maine Stage A Health Home Targeting Individuals with Chronic Conditions", + }, + { id: "23-0014-BHH", state: "ME", name: "Maine Behavioral Health Home" }, + { + id: "22-0018", + state: "ME", + name: "Maine Health Home for Beneficiaries Receiving Medication Assisted Treatment for Opioid Addiction", + }, + { id: "21-0005", state: "MD", name: "Maryland Health Home Services" }, + { + id: "20-1500", + state: "MI", + name: "Chronic Care Model for Individuals with Serious and Persistent Mental Health Conditions", + }, + { id: "15-2000", state: "MI", name: "Michigan Care Team" }, + { id: "23-1500", state: "MI", name: "Opioid Health Home" }, + { id: "22-0036", state: "MN", name: "Minnesota Behavioral Health Homes" }, + { + id: "19-0020", + state: "MO", + name: "Community Mental Health Center Health Home", + }, + { id: "19-0003", state: "MO", name: "Primary Care Clinic Health Home" }, + { + id: "16-0001", + state: "NJ", + name: "New Jersey Behavioral Health Home (Adults)", + }, + { + id: "16-0002", + state: "NJ", + name: "New Jersey Behavioral Health Home (Children)", + }, + { id: "18-0002", state: "NM", name: "CareLink New Mexico" }, + { + id: "22-0088", + state: "NY", + name: "Health Home for High-Cost, High-Needs Enrollees ", + }, + { id: "23-0062", state: "NY", name: "New York I/DD Health Home Services" }, + { id: "22-0024", state: "NC", name: "Tailored Care Management" }, + { id: "14-0012", state: "OK", name: "Oklahoma Health Home (Adults)" }, + { id: "14-0011", state: "OK", name: "Oklahoma Health Home (Children)" }, + { + id: "18-009", + state: "RI", + name: "Comprehensive Evaluation, Diagnosis, Assessment, Referral, & Reevaluation (CEDARR) Family Centers Health Home", + }, + { + id: "18-006", + state: "RI", + name: "Community Mental Health Organizations Health Home", + }, + { + id: "21-0025", + state: "RI", + name: "Rhode Island Opioid Treatment Program Health Home Services", + }, + { id: "22-0009", state: "SD", name: "South Dakota Health Home" }, + { + id: "16-004", + state: "TN", + name: "Tennessee Health Link Health Home Program ", + }, + { + id: "14-007", + state: "VT", + name: "Vermont Health Home for Beneficiaries Receiving Medication Assisted Treatment for Opioid Addiction ", + }, + { id: "23-0027", state: "WA", name: "Washington Health Home Services" }, + { + id: "16-0007", + state: "WV", + name: "West Virginia Health Home for Individuals with Bipolar Disorder at Risk for Hepatitis Type B and C", + }, + { + id: "16-0008", + state: "WV", + name: "West Virginia Health Home for Individuals with Chronic Conditions", + }, + { id: "12-008", state: "WI", name: "Individuals with HIV/AIDS" }, + { id: "22-0013", state: "WI", name: "Substance Use Disorder Health Home" }, + ], }; diff --git a/services/ui-src/src/measures/2021/ADDCH/index.tsx b/services/ui-src/src/measures/2021/ADDCH/index.tsx index f0fa15d1c3..8a3d6c5dc0 100644 --- a/services/ui-src/src/measures/2021/ADDCH/index.tsx +++ b/services/ui-src/src/measures/2021/ADDCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const ADDCH = ({ name, diff --git a/services/ui-src/src/measures/2021/ADDCH/types.ts b/services/ui-src/src/measures/2021/ADDCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/ADDCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/ADDCH/validation.ts b/services/ui-src/src/measures/2021/ADDCH/validation.ts index 54e9f06e1b..fc1a8b8725 100644 --- a/services/ui-src/src/measures/2021/ADDCH/validation.ts +++ b/services/ui-src/src/measures/2021/ADDCH/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "../globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const ADDCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/AIFHH/index.tsx b/services/ui-src/src/measures/2021/AIFHH/index.tsx index 99bfe9b07e..84b86a81a0 100644 --- a/services/ui-src/src/measures/2021/AIFHH/index.tsx +++ b/services/ui-src/src/measures/2021/AIFHH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AIFHH = ({ name, diff --git a/services/ui-src/src/measures/2021/AIFHH/types.ts b/services/ui-src/src/measures/2021/AIFHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/AIFHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AIFHH/validation.ts b/services/ui-src/src/measures/2021/AIFHH/validation.ts index dd4b40f238..c113422b2e 100644 --- a/services/ui-src/src/measures/2021/AIFHH/validation.ts +++ b/services/ui-src/src/measures/2021/AIFHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; // Rate structure by index in row const ndrFormulas = [ diff --git a/services/ui-src/src/measures/2021/AMBCH/index.tsx b/services/ui-src/src/measures/2021/AMBCH/index.tsx index ebe09b6ae4..9588b06f0f 100644 --- a/services/ui-src/src/measures/2021/AMBCH/index.tsx +++ b/services/ui-src/src/measures/2021/AMBCH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AMBCH = ({ name, diff --git a/services/ui-src/src/measures/2021/AMBCH/types.ts b/services/ui-src/src/measures/2021/AMBCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/AMBCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AMBCH/validation.ts b/services/ui-src/src/measures/2021/AMBCH/validation.ts index ec15a2a3a2..d42e28598b 100644 --- a/services/ui-src/src/measures/2021/AMBCH/validation.ts +++ b/services/ui-src/src/measures/2021/AMBCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const AMBCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/AMBHH/index.tsx b/services/ui-src/src/measures/2021/AMBHH/index.tsx index 88c68f6d82..0dd696a91b 100644 --- a/services/ui-src/src/measures/2021/AMBHH/index.tsx +++ b/services/ui-src/src/measures/2021/AMBHH/index.tsx @@ -5,8 +5,9 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AMBHH = ({ name, diff --git a/services/ui-src/src/measures/2021/AMBHH/types.ts b/services/ui-src/src/measures/2021/AMBHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/AMBHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AMBHH/validation.ts b/services/ui-src/src/measures/2021/AMBHH/validation.ts index 15dabe0e24..bc875282fc 100644 --- a/services/ui-src/src/measures/2021/AMBHH/validation.ts +++ b/services/ui-src/src/measures/2021/AMBHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const AMBHHValidation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2021/AMMAD/index.tsx b/services/ui-src/src/measures/2021/AMMAD/index.tsx index d30c82e651..d8b320e1c1 100644 --- a/services/ui-src/src/measures/2021/AMMAD/index.tsx +++ b/services/ui-src/src/measures/2021/AMMAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AMMAD = ({ name, diff --git a/services/ui-src/src/measures/2021/AMMAD/types.ts b/services/ui-src/src/measures/2021/AMMAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/AMMAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AMMAD/validation.ts b/services/ui-src/src/measures/2021/AMMAD/validation.ts index 47ccb157be..8d337d5488 100644 --- a/services/ui-src/src/measures/2021/AMMAD/validation.ts +++ b/services/ui-src/src/measures/2021/AMMAD/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; import { cleanString } from "utils/cleanString"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const sameDenominatorSets: GV.Types.OmsValidationCallback = ({ rateData, diff --git a/services/ui-src/src/measures/2021/AMRAD/index.tsx b/services/ui-src/src/measures/2021/AMRAD/index.tsx index 8ba3d5cf1f..3cfb0968d7 100644 --- a/services/ui-src/src/measures/2021/AMRAD/index.tsx +++ b/services/ui-src/src/measures/2021/AMRAD/index.tsx @@ -2,10 +2,12 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as QMR from "components"; import * as PMD from "./data"; import { useFormContext, useWatch } from "react-hook-form"; -import { FormData, Measure } from "./types"; +import { Measure } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { useEffect } from "react"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AMRAD = ({ name, diff --git a/services/ui-src/src/measures/2021/AMRAD/types.ts b/services/ui-src/src/measures/2021/AMRAD/types.ts index ec0b5a069c..9457b66f24 100644 --- a/services/ui-src/src/measures/2021/AMRAD/types.ts +++ b/services/ui-src/src/measures/2021/AMRAD/types.ts @@ -102,5 +102,3 @@ export namespace Measure { ACAGroupRate: AggregateRate; } } - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AMRAD/validation.ts b/services/ui-src/src/measures/2021/AMRAD/validation.ts index f199106fa8..c1de4054ff 100644 --- a/services/ui-src/src/measures/2021/AMRAD/validation.ts +++ b/services/ui-src/src/measures/2021/AMRAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const AMRADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/AMRCH/index.tsx b/services/ui-src/src/measures/2021/AMRCH/index.tsx index f871cce695..f553fddff3 100644 --- a/services/ui-src/src/measures/2021/AMRCH/index.tsx +++ b/services/ui-src/src/measures/2021/AMRCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AMRCH = ({ name, diff --git a/services/ui-src/src/measures/2021/AMRCH/types.ts b/services/ui-src/src/measures/2021/AMRCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/AMRCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AMRCH/validation.ts b/services/ui-src/src/measures/2021/AMRCH/validation.ts index e7b17e9d68..9635bec5b4 100644 --- a/services/ui-src/src/measures/2021/AMRCH/validation.ts +++ b/services/ui-src/src/measures/2021/AMRCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const AMRCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/APMCH/index.tsx b/services/ui-src/src/measures/2021/APMCH/index.tsx index 73a59d3b1f..54e694b6d7 100644 --- a/services/ui-src/src/measures/2021/APMCH/index.tsx +++ b/services/ui-src/src/measures/2021/APMCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const APMCH = ({ name, diff --git a/services/ui-src/src/measures/2021/APMCH/types.ts b/services/ui-src/src/measures/2021/APMCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/APMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/APMCH/validation.ts b/services/ui-src/src/measures/2021/APMCH/validation.ts index 0c22f3358f..26524c8774 100644 --- a/services/ui-src/src/measures/2021/APMCH/validation.ts +++ b/services/ui-src/src/measures/2021/APMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const APMCHValidation = (data: FormData) => { const dateRange = data[DC.DATE_RANGE]; diff --git a/services/ui-src/src/measures/2021/APPCH/index.tsx b/services/ui-src/src/measures/2021/APPCH/index.tsx index 0891acb285..3225bcde7d 100644 --- a/services/ui-src/src/measures/2021/APPCH/index.tsx +++ b/services/ui-src/src/measures/2021/APPCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const APPCH = ({ name, diff --git a/services/ui-src/src/measures/2021/APPCH/types.ts b/services/ui-src/src/measures/2021/APPCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/APPCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/APPCH/validation.ts b/services/ui-src/src/measures/2021/APPCH/validation.ts index 47eb5c525e..5c43659387 100644 --- a/services/ui-src/src/measures/2021/APPCH/validation.ts +++ b/services/ui-src/src/measures/2021/APPCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const APPCHValidation = (data: FormData) => { const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; diff --git a/services/ui-src/src/measures/2021/AUDCH/index.tsx b/services/ui-src/src/measures/2021/AUDCH/index.tsx index e52375fd0b..34b23b85e2 100644 --- a/services/ui-src/src/measures/2021/AUDCH/index.tsx +++ b/services/ui-src/src/measures/2021/AUDCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const AUDCH = ({ name, diff --git a/services/ui-src/src/measures/2021/AUDCH/types.ts b/services/ui-src/src/measures/2021/AUDCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/AUDCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/AUDCH/validation.ts b/services/ui-src/src/measures/2021/AUDCH/validation.ts index 27ccedd745..a7c79bf792 100644 --- a/services/ui-src/src/measures/2021/AUDCH/validation.ts +++ b/services/ui-src/src/measures/2021/AUDCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const AUDCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/BCSAD/index.tsx b/services/ui-src/src/measures/2021/BCSAD/index.tsx index 7284358921..9f8fde457e 100644 --- a/services/ui-src/src/measures/2021/BCSAD/index.tsx +++ b/services/ui-src/src/measures/2021/BCSAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const BCSAD = ({ name, diff --git a/services/ui-src/src/measures/2021/BCSAD/types.ts b/services/ui-src/src/measures/2021/BCSAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/BCSAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/BCSAD/validation.ts b/services/ui-src/src/measures/2021/BCSAD/validation.ts index ed96d5241d..1fce9bcf30 100644 --- a/services/ui-src/src/measures/2021/BCSAD/validation.ts +++ b/services/ui-src/src/measures/2021/BCSAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const BCSValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CBPAD/index.tsx b/services/ui-src/src/measures/2021/CBPAD/index.tsx index 2962d704b7..ef995136e3 100644 --- a/services/ui-src/src/measures/2021/CBPAD/index.tsx +++ b/services/ui-src/src/measures/2021/CBPAD/index.tsx @@ -5,7 +5,8 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CBPAD = ({ name, diff --git a/services/ui-src/src/measures/2021/CBPAD/types.ts b/services/ui-src/src/measures/2021/CBPAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CBPAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CBPAD/validation.ts b/services/ui-src/src/measures/2021/CBPAD/validation.ts index 37b980ef6f..c6da515c40 100644 --- a/services/ui-src/src/measures/2021/CBPAD/validation.ts +++ b/services/ui-src/src/measures/2021/CBPAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CBPValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CBPHH/index.tsx b/services/ui-src/src/measures/2021/CBPHH/index.tsx index 6d98bf9a27..329e162af4 100644 --- a/services/ui-src/src/measures/2021/CBPHH/index.tsx +++ b/services/ui-src/src/measures/2021/CBPHH/index.tsx @@ -5,7 +5,8 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CBPHH = ({ name, diff --git a/services/ui-src/src/measures/2021/CBPHH/types.ts b/services/ui-src/src/measures/2021/CBPHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CBPHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CBPHH/validation.ts b/services/ui-src/src/measures/2021/CBPHH/validation.ts index e955c9c4bc..d260fced70 100644 --- a/services/ui-src/src/measures/2021/CBPHH/validation.ts +++ b/services/ui-src/src/measures/2021/CBPHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CBPValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CCPAD/index.tsx b/services/ui-src/src/measures/2021/CCPAD/index.tsx index 5a45d042fc..c1628b76b6 100644 --- a/services/ui-src/src/measures/2021/CCPAD/index.tsx +++ b/services/ui-src/src/measures/2021/CCPAD/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "../CommonQuestions"; import * as QMR from "components"; -import { FormData } from "./types"; import { useEffect } from "react"; import { validationFunctions } from "./validation"; import * as PMD from "./data"; import { useFormContext } from "react-hook-form"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CCPAD = ({ name, diff --git a/services/ui-src/src/measures/2021/CCPAD/types.ts b/services/ui-src/src/measures/2021/CCPAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CCPAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CCPAD/validation.ts b/services/ui-src/src/measures/2021/CCPAD/validation.ts index d6c48efda4..0129addb95 100644 --- a/services/ui-src/src/measures/2021/CCPAD/validation.ts +++ b/services/ui-src/src/measures/2021/CCPAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CCPADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CCPCH/index.tsx b/services/ui-src/src/measures/2021/CCPCH/index.tsx index e49def9f45..559db46f03 100644 --- a/services/ui-src/src/measures/2021/CCPCH/index.tsx +++ b/services/ui-src/src/measures/2021/CCPCH/index.tsx @@ -2,11 +2,12 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CCPCH = ({ name, diff --git a/services/ui-src/src/measures/2021/CCPCH/types.ts b/services/ui-src/src/measures/2021/CCPCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CCPCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CCPCH/validation.ts b/services/ui-src/src/measures/2021/CCPCH/validation.ts index 0aa059e199..770debfef0 100644 --- a/services/ui-src/src/measures/2021/CCPCH/validation.ts +++ b/services/ui-src/src/measures/2021/CCPCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CCPCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CCSAD/index.tsx b/services/ui-src/src/measures/2021/CCSAD/index.tsx index b3db816bbe..15210c5103 100644 --- a/services/ui-src/src/measures/2021/CCSAD/index.tsx +++ b/services/ui-src/src/measures/2021/CCSAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CCSAD = ({ name, diff --git a/services/ui-src/src/measures/2021/CCSAD/types.ts b/services/ui-src/src/measures/2021/CCSAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CCSAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CCSAD/validation.ts b/services/ui-src/src/measures/2021/CCSAD/validation.ts index 668c347aaf..0918cb95fd 100644 --- a/services/ui-src/src/measures/2021/CCSAD/validation.ts +++ b/services/ui-src/src/measures/2021/CCSAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CCSADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CCWAD/index.tsx b/services/ui-src/src/measures/2021/CCWAD/index.tsx index 2aa51d85a9..011ad44cf2 100644 --- a/services/ui-src/src/measures/2021/CCWAD/index.tsx +++ b/services/ui-src/src/measures/2021/CCWAD/index.tsx @@ -4,8 +4,9 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CCWAD = ({ name, diff --git a/services/ui-src/src/measures/2021/CCWAD/types.ts b/services/ui-src/src/measures/2021/CCWAD/types.ts deleted file mode 100644 index 98004183a7..0000000000 --- a/services/ui-src/src/measures/2021/CCWAD/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type DeviationCheckBoxOptions = - | "moderate-method-deviation-Numerator" - | "moderate-method-deviation-Denominator" - | "moderate-method-deviation-Other" - | "reversible-method-deviation-Numerator" - | "reversible-method-deviation-Denominator" - | "reversible-method-deviation-Other"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CCWAD/validation.ts b/services/ui-src/src/measures/2021/CCWAD/validation.ts index 3108abf909..3b28511ac2 100644 --- a/services/ui-src/src/measures/2021/CCWAD/validation.ts +++ b/services/ui-src/src/measures/2021/CCWAD/validation.ts @@ -1,9 +1,10 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { cleanString } from "utils"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CCWADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CCWCH/index.tsx b/services/ui-src/src/measures/2021/CCWCH/index.tsx index b4b90e2724..7aee43d7b1 100644 --- a/services/ui-src/src/measures/2021/CCWCH/index.tsx +++ b/services/ui-src/src/measures/2021/CCWCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CCWCH = ({ name, diff --git a/services/ui-src/src/measures/2021/CCWCH/types.ts b/services/ui-src/src/measures/2021/CCWCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CCWCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CCWCH/validation.ts b/services/ui-src/src/measures/2021/CCWCH/validation.ts index 47c5f3e56b..09c4c5f71d 100644 --- a/services/ui-src/src/measures/2021/CCWCH/validation.ts +++ b/services/ui-src/src/measures/2021/CCWCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CCWCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CDFAD/index.tsx b/services/ui-src/src/measures/2021/CDFAD/index.tsx index fb86fa5228..ef038a899d 100644 --- a/services/ui-src/src/measures/2021/CDFAD/index.tsx +++ b/services/ui-src/src/measures/2021/CDFAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CDFAD = ({ name, diff --git a/services/ui-src/src/measures/2021/CDFAD/types.ts b/services/ui-src/src/measures/2021/CDFAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CDFAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CDFAD/validation.ts b/services/ui-src/src/measures/2021/CDFAD/validation.ts index df1f7c6add..47ad07967f 100644 --- a/services/ui-src/src/measures/2021/CDFAD/validation.ts +++ b/services/ui-src/src/measures/2021/CDFAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CDFADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CDFCH/index.tsx b/services/ui-src/src/measures/2021/CDFCH/index.tsx index 9522d107af..d52f158584 100644 --- a/services/ui-src/src/measures/2021/CDFCH/index.tsx +++ b/services/ui-src/src/measures/2021/CDFCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CDFCH = ({ name, diff --git a/services/ui-src/src/measures/2021/CDFCH/types.ts b/services/ui-src/src/measures/2021/CDFCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CDFCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CDFCH/validation.ts b/services/ui-src/src/measures/2021/CDFCH/validation.ts index a2d0cde738..104d7747b2 100644 --- a/services/ui-src/src/measures/2021/CDFCH/validation.ts +++ b/services/ui-src/src/measures/2021/CDFCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CDFCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CDFHH/index.tsx b/services/ui-src/src/measures/2021/CDFHH/index.tsx index fa1612fc75..19af05ecd7 100644 --- a/services/ui-src/src/measures/2021/CDFHH/index.tsx +++ b/services/ui-src/src/measures/2021/CDFHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CDFHH = ({ name, diff --git a/services/ui-src/src/measures/2021/CDFHH/types.ts b/services/ui-src/src/measures/2021/CDFHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CDFHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CDFHH/validation.ts b/services/ui-src/src/measures/2021/CDFHH/validation.ts index dbe78654ff..b55bef82f8 100644 --- a/services/ui-src/src/measures/2021/CDFHH/validation.ts +++ b/services/ui-src/src/measures/2021/CDFHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CDFHHValidation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2021/CHLAD/index.tsx b/services/ui-src/src/measures/2021/CHLAD/index.tsx index cb0158f23e..6a0c225008 100644 --- a/services/ui-src/src/measures/2021/CHLAD/index.tsx +++ b/services/ui-src/src/measures/2021/CHLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CHLAD = ({ name, diff --git a/services/ui-src/src/measures/2021/CHLAD/types.ts b/services/ui-src/src/measures/2021/CHLAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CHLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CHLAD/validation.ts b/services/ui-src/src/measures/2021/CHLAD/validation.ts index 2480e7743b..d41ef11eb0 100644 --- a/services/ui-src/src/measures/2021/CHLAD/validation.ts +++ b/services/ui-src/src/measures/2021/CHLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CHLValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CHLCH/index.tsx b/services/ui-src/src/measures/2021/CHLCH/index.tsx index 3b6bf07612..a462005904 100644 --- a/services/ui-src/src/measures/2021/CHLCH/index.tsx +++ b/services/ui-src/src/measures/2021/CHLCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CHLCH = ({ name, diff --git a/services/ui-src/src/measures/2021/CHLCH/types.ts b/services/ui-src/src/measures/2021/CHLCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CHLCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CHLCH/validation.ts b/services/ui-src/src/measures/2021/CHLCH/validation.ts index 2480e7743b..d41ef11eb0 100644 --- a/services/ui-src/src/measures/2021/CHLCH/validation.ts +++ b/services/ui-src/src/measures/2021/CHLCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CHLValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CISCH/index.tsx b/services/ui-src/src/measures/2021/CISCH/index.tsx index 32d2d985ff..97d8ae8dc7 100644 --- a/services/ui-src/src/measures/2021/CISCH/index.tsx +++ b/services/ui-src/src/measures/2021/CISCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const CISCH = ({ name, diff --git a/services/ui-src/src/measures/2021/CISCH/types.ts b/services/ui-src/src/measures/2021/CISCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/CISCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/CISCH/validation.ts b/services/ui-src/src/measures/2021/CISCH/validation.ts index d66538407e..93ab35048b 100644 --- a/services/ui-src/src/measures/2021/CISCH/validation.ts +++ b/services/ui-src/src/measures/2021/CISCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const CISCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/COBAD/index.tsx b/services/ui-src/src/measures/2021/COBAD/index.tsx index 8dd5a23461..8e92ea766d 100644 --- a/services/ui-src/src/measures/2021/COBAD/index.tsx +++ b/services/ui-src/src/measures/2021/COBAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const COBAD = ({ name, diff --git a/services/ui-src/src/measures/2021/COBAD/types.ts b/services/ui-src/src/measures/2021/COBAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/COBAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/COBAD/validation.ts b/services/ui-src/src/measures/2021/COBAD/validation.ts index f174973aa5..0032b1c0ca 100644 --- a/services/ui-src/src/measures/2021/COBAD/validation.ts +++ b/services/ui-src/src/measures/2021/COBAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const IEDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/CommonQuestions/AdditionalNotes/index.test.tsx b/services/ui-src/src/measures/2021/CommonQuestions/AdditionalNotes/index.test.tsx deleted file mode 100644 index 4ff2ff08ff..0000000000 --- a/services/ui-src/src/measures/2021/CommonQuestions/AdditionalNotes/index.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import fireEvent from "@testing-library/user-event"; -import { AdditionalNotes } from "."; -import { screen } from "@testing-library/react"; -import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; - -describe("Test AdditionalNotes component", () => { - beforeEach(() => { - renderWithHookForm(<AdditionalNotes />); - }); - - it("component renders", () => { - expect( - screen.getByText("Additional Notes/Comments on the measure (optional)") - ).toBeInTheDocument(); - expect( - screen.getByText( - "If you need additional space to include comments or supplemental information, please attach further documentation below." - ) - ).toBeInTheDocument(); - }); - - it("accepts input", async () => { - const textArea = await screen.findByLabelText( - "Please add any additional notes or comments on the measure not otherwise captured above:" - ); - fireEvent.type(textArea, "This is the test text"); - expect(textArea).toHaveDisplayValue("This is the test text"); - }); -}); diff --git a/services/ui-src/src/measures/2021/CommonQuestions/AdditionalNotes/index.tsx b/services/ui-src/src/measures/2021/CommonQuestions/AdditionalNotes/index.tsx deleted file mode 100644 index 329265f03c..0000000000 --- a/services/ui-src/src/measures/2021/CommonQuestions/AdditionalNotes/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as QMR from "components"; -import * as CUI from "@chakra-ui/react"; -import { useCustomRegister } from "hooks/useCustomRegister"; -import { Upload } from "components/Upload"; -import * as Types from "../types"; -import * as DC from "dataConstants"; - -export const AdditionalNotes = () => { - const register = useCustomRegister<Types.AdditionalNotes>(); - - return ( - <QMR.CoreQuestionWrapper label="Additional Notes/Comments on the measure (optional)"> - <QMR.TextArea - label="Please add any additional notes or comments on the measure not otherwise captured above:" - {...register(DC.ADDITIONAL_NOTES)} - /> - <CUI.Box marginTop={10}> - <Upload - label="If you need additional space to include comments or supplemental information, please attach further documentation below." - {...register(DC.ADDITIONAL_NOTES_UPLOAD)} - /> - </CUI.Box> - </QMR.CoreQuestionWrapper> - ); -}; diff --git a/services/ui-src/src/measures/2021/CommonQuestions/index.tsx b/services/ui-src/src/measures/2021/CommonQuestions/index.tsx index 3b75284a67..63b075e014 100644 --- a/services/ui-src/src/measures/2021/CommonQuestions/index.tsx +++ b/services/ui-src/src/measures/2021/CommonQuestions/index.tsx @@ -3,7 +3,7 @@ export * from "./DateRange"; export * from "./DefinitionsOfPopulation"; export * from "./DataSource"; export * from "./DataSourceCahps"; -export * from "./AdditionalNotes"; +export * from "shared/commonQuestions/AdditionalNotes"; export * from "./OtherPerformanceMeasure"; export * from "./Reporting"; export * from "./StatusOfData"; diff --git a/services/ui-src/src/measures/2021/DEVCH/index.tsx b/services/ui-src/src/measures/2021/DEVCH/index.tsx index 56218b9847..67df2790e6 100644 --- a/services/ui-src/src/measures/2021/DEVCH/index.tsx +++ b/services/ui-src/src/measures/2021/DEVCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const DEVCH = ({ name, diff --git a/services/ui-src/src/measures/2021/DEVCH/types.ts b/services/ui-src/src/measures/2021/DEVCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/DEVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/DEVCH/validation.ts b/services/ui-src/src/measures/2021/DEVCH/validation.ts index b21670c854..c3da6ce9f6 100644 --- a/services/ui-src/src/measures/2021/DEVCH/validation.ts +++ b/services/ui-src/src/measures/2021/DEVCH/validation.ts @@ -1,8 +1,9 @@ import * as PMD from "./data"; import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const DEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FUAAD/index.tsx b/services/ui-src/src/measures/2021/FUAAD/index.tsx index c93de7c1b8..245a5aa676 100644 --- a/services/ui-src/src/measures/2021/FUAAD/index.tsx +++ b/services/ui-src/src/measures/2021/FUAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FUAAD = ({ name, diff --git a/services/ui-src/src/measures/2021/FUAAD/types.ts b/services/ui-src/src/measures/2021/FUAAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FUAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FUAAD/validation.ts b/services/ui-src/src/measures/2021/FUAAD/validation.ts index a0d755c3e5..f69280eb9d 100644 --- a/services/ui-src/src/measures/2021/FUAAD/validation.ts +++ b/services/ui-src/src/measures/2021/FUAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FUAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FUAHH/index.tsx b/services/ui-src/src/measures/2021/FUAHH/index.tsx index 8029ede51d..65788bde68 100644 --- a/services/ui-src/src/measures/2021/FUAHH/index.tsx +++ b/services/ui-src/src/measures/2021/FUAHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FUAHH = ({ name, diff --git a/services/ui-src/src/measures/2021/FUAHH/types.ts b/services/ui-src/src/measures/2021/FUAHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FUAHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FUAHH/validation.ts b/services/ui-src/src/measures/2021/FUAHH/validation.ts index 54b0c067af..a6fc76cc2c 100644 --- a/services/ui-src/src/measures/2021/FUAHH/validation.ts +++ b/services/ui-src/src/measures/2021/FUAHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FUAHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FUHAD/index.tsx b/services/ui-src/src/measures/2021/FUHAD/index.tsx index 276be5ef32..8580059322 100644 --- a/services/ui-src/src/measures/2021/FUHAD/index.tsx +++ b/services/ui-src/src/measures/2021/FUHAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FUHAD = ({ name, diff --git a/services/ui-src/src/measures/2021/FUHAD/types.ts b/services/ui-src/src/measures/2021/FUHAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FUHAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FUHAD/validation.ts b/services/ui-src/src/measures/2021/FUHAD/validation.ts index 3155f1f10b..115686a8b8 100644 --- a/services/ui-src/src/measures/2021/FUHAD/validation.ts +++ b/services/ui-src/src/measures/2021/FUHAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as PMD from "./data"; import * as GV from "../globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FUHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FUHCH/index.tsx b/services/ui-src/src/measures/2021/FUHCH/index.tsx index df5c547f1d..2b7497d698 100644 --- a/services/ui-src/src/measures/2021/FUHCH/index.tsx +++ b/services/ui-src/src/measures/2021/FUHCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FUHCH = ({ name, diff --git a/services/ui-src/src/measures/2021/FUHCH/types.ts b/services/ui-src/src/measures/2021/FUHCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FUHCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FUHCH/validation.ts b/services/ui-src/src/measures/2021/FUHCH/validation.ts index 9d783b6643..a19fb224db 100644 --- a/services/ui-src/src/measures/2021/FUHCH/validation.ts +++ b/services/ui-src/src/measures/2021/FUHCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FUHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FUHHH/index.tsx b/services/ui-src/src/measures/2021/FUHHH/index.tsx index ef154aad46..adb825a9d9 100644 --- a/services/ui-src/src/measures/2021/FUHHH/index.tsx +++ b/services/ui-src/src/measures/2021/FUHHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FUHHH = ({ name, diff --git a/services/ui-src/src/measures/2021/FUHHH/types.ts b/services/ui-src/src/measures/2021/FUHHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FUHHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FUHHH/validation.ts b/services/ui-src/src/measures/2021/FUHHH/validation.ts index c3fd7cc3fa..ad244e5eb5 100644 --- a/services/ui-src/src/measures/2021/FUHHH/validation.ts +++ b/services/ui-src/src/measures/2021/FUHHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FUHHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FUMAD/index.tsx b/services/ui-src/src/measures/2021/FUMAD/index.tsx index 062a09daee..b7c9e6e892 100644 --- a/services/ui-src/src/measures/2021/FUMAD/index.tsx +++ b/services/ui-src/src/measures/2021/FUMAD/index.tsx @@ -5,7 +5,8 @@ import * as QMR from "components"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { validationFunctions } from "./validation"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FUMAD = ({ name, diff --git a/services/ui-src/src/measures/2021/FUMAD/types.ts b/services/ui-src/src/measures/2021/FUMAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FUMAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FUMAD/validation.ts b/services/ui-src/src/measures/2021/FUMAD/validation.ts index 3a25fa21bf..0ec959cc67 100644 --- a/services/ui-src/src/measures/2021/FUMAD/validation.ts +++ b/services/ui-src/src/measures/2021/FUMAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FUMADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/FVAAD/index.tsx b/services/ui-src/src/measures/2021/FVAAD/index.tsx index 2c3ba1bac8..643f8c43ff 100644 --- a/services/ui-src/src/measures/2021/FVAAD/index.tsx +++ b/services/ui-src/src/measures/2021/FVAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const FVAAD = ({ name, diff --git a/services/ui-src/src/measures/2021/FVAAD/types.ts b/services/ui-src/src/measures/2021/FVAAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/FVAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/FVAAD/validation.ts b/services/ui-src/src/measures/2021/FVAAD/validation.ts index 65c810a2d9..574d80f195 100644 --- a/services/ui-src/src/measures/2021/FVAAD/validation.ts +++ b/services/ui-src/src/measures/2021/FVAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const FVAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/HPCAD/index.tsx b/services/ui-src/src/measures/2021/HPCAD/index.tsx index d1ebf97a73..2f2e698af8 100644 --- a/services/ui-src/src/measures/2021/HPCAD/index.tsx +++ b/services/ui-src/src/measures/2021/HPCAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const HPCAD = ({ name, diff --git a/services/ui-src/src/measures/2021/HPCAD/types.ts b/services/ui-src/src/measures/2021/HPCAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/HPCAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/HPCAD/validation.ts b/services/ui-src/src/measures/2021/HPCAD/validation.ts index db68ba18b3..c429358088 100644 --- a/services/ui-src/src/measures/2021/HPCAD/validation.ts +++ b/services/ui-src/src/measures/2021/HPCAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const HPCADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/HPCMIAD/index.tsx b/services/ui-src/src/measures/2021/HPCMIAD/index.tsx index c036cc2e6c..1189fa6f08 100644 --- a/services/ui-src/src/measures/2021/HPCMIAD/index.tsx +++ b/services/ui-src/src/measures/2021/HPCMIAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const HPCMIAD = ({ name, diff --git a/services/ui-src/src/measures/2021/HPCMIAD/types.ts b/services/ui-src/src/measures/2021/HPCMIAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/HPCMIAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/HPCMIAD/validation.ts b/services/ui-src/src/measures/2021/HPCMIAD/validation.ts index 22f7037390..e468c26225 100644 --- a/services/ui-src/src/measures/2021/HPCMIAD/validation.ts +++ b/services/ui-src/src/measures/2021/HPCMIAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const HPCMIADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/HVLAD/index.tsx b/services/ui-src/src/measures/2021/HVLAD/index.tsx index 8fb8541a3b..3ace12b8b0 100644 --- a/services/ui-src/src/measures/2021/HVLAD/index.tsx +++ b/services/ui-src/src/measures/2021/HVLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const HVLAD = ({ name, diff --git a/services/ui-src/src/measures/2021/HVLAD/types.ts b/services/ui-src/src/measures/2021/HVLAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/HVLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/HVLAD/validation.ts b/services/ui-src/src/measures/2021/HVLAD/validation.ts index b550a242bd..369a7afc4c 100644 --- a/services/ui-src/src/measures/2021/HVLAD/validation.ts +++ b/services/ui-src/src/measures/2021/HVLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const HVLADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/IETAD/index.tsx b/services/ui-src/src/measures/2021/IETAD/index.tsx index d457b5b21c..04ea231641 100644 --- a/services/ui-src/src/measures/2021/IETAD/index.tsx +++ b/services/ui-src/src/measures/2021/IETAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const IETAD = ({ name, diff --git a/services/ui-src/src/measures/2021/IETAD/types.ts b/services/ui-src/src/measures/2021/IETAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/IETAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/IETAD/validation.ts b/services/ui-src/src/measures/2021/IETAD/validation.ts index da058e6d19..9d45742be8 100644 --- a/services/ui-src/src/measures/2021/IETAD/validation.ts +++ b/services/ui-src/src/measures/2021/IETAD/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; import { cleanString } from "utils/cleanString"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; /** For each qualifier the denominators neeed to be the same for both Initiaion and Engagement of the same category. */ const sameDenominatorSets: GV.Types.OmsValidationCallback = ({ diff --git a/services/ui-src/src/measures/2021/IETHH/index.tsx b/services/ui-src/src/measures/2021/IETHH/index.tsx index dc1e1fe80d..4f0f1a4f50 100644 --- a/services/ui-src/src/measures/2021/IETHH/index.tsx +++ b/services/ui-src/src/measures/2021/IETHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const IETHH = ({ name, diff --git a/services/ui-src/src/measures/2021/IETHH/types.ts b/services/ui-src/src/measures/2021/IETHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/IETHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/IETHH/validation.ts b/services/ui-src/src/measures/2021/IETHH/validation.ts index 76e1b8552b..9734219223 100644 --- a/services/ui-src/src/measures/2021/IETHH/validation.ts +++ b/services/ui-src/src/measures/2021/IETHH/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "../globalValidations"; import * as PMD from "./data"; import { cleanString } from "utils/cleanString"; -import { FormData } from "./types"; import { OMSData } from "../CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; /** For each qualifier the denominators neeed to be the same for both Initiaion and Engagement of the same category. */ const sameDenominatorSets: GV.Types.OmsValidationCallback = ({ diff --git a/services/ui-src/src/measures/2021/IMACH/index.tsx b/services/ui-src/src/measures/2021/IMACH/index.tsx index 91445e33f7..0e22b4186f 100644 --- a/services/ui-src/src/measures/2021/IMACH/index.tsx +++ b/services/ui-src/src/measures/2021/IMACH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const IMACH = ({ name, diff --git a/services/ui-src/src/measures/2021/IMACH/types.ts b/services/ui-src/src/measures/2021/IMACH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/IMACH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/IMACH/validation.ts b/services/ui-src/src/measures/2021/IMACH/validation.ts index 7a3046abb5..33fbfdd9cd 100644 --- a/services/ui-src/src/measures/2021/IMACH/validation.ts +++ b/services/ui-src/src/measures/2021/IMACH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const DEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/IUHH/index.tsx b/services/ui-src/src/measures/2021/IUHH/index.tsx index 4b182cce5c..cbd5838413 100644 --- a/services/ui-src/src/measures/2021/IUHH/index.tsx +++ b/services/ui-src/src/measures/2021/IUHH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const IUHH = ({ name, diff --git a/services/ui-src/src/measures/2021/IUHH/types.ts b/services/ui-src/src/measures/2021/IUHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/IUHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/IUHH/validation.ts b/services/ui-src/src/measures/2021/IUHH/validation.ts index c34ace1453..2d06b8a7a0 100644 --- a/services/ui-src/src/measures/2021/IUHH/validation.ts +++ b/services/ui-src/src/measures/2021/IUHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; // Rate structure by index in row const ndrForumlas = [ diff --git a/services/ui-src/src/measures/2021/OHDAD/index.tsx b/services/ui-src/src/measures/2021/OHDAD/index.tsx index 045250ac3c..92b9ae30ac 100644 --- a/services/ui-src/src/measures/2021/OHDAD/index.tsx +++ b/services/ui-src/src/measures/2021/OHDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const OHDAD = ({ name, diff --git a/services/ui-src/src/measures/2021/OHDAD/types.ts b/services/ui-src/src/measures/2021/OHDAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/OHDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/OHDAD/validation.ts b/services/ui-src/src/measures/2021/OHDAD/validation.ts index 76a758dc91..2c8822a9a3 100644 --- a/services/ui-src/src/measures/2021/OHDAD/validation.ts +++ b/services/ui-src/src/measures/2021/OHDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const OHDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/OUDAD/index.tsx b/services/ui-src/src/measures/2021/OUDAD/index.tsx index c35b637f61..beed0486fc 100644 --- a/services/ui-src/src/measures/2021/OUDAD/index.tsx +++ b/services/ui-src/src/measures/2021/OUDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const OUDAD = ({ name, diff --git a/services/ui-src/src/measures/2021/OUDAD/types.ts b/services/ui-src/src/measures/2021/OUDAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/OUDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/OUDAD/validation.ts b/services/ui-src/src/measures/2021/OUDAD/validation.ts index d953d9e249..0506faa541 100644 --- a/services/ui-src/src/measures/2021/OUDAD/validation.ts +++ b/services/ui-src/src/measures/2021/OUDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const OUDValidation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2021/OUDHH/index.tsx b/services/ui-src/src/measures/2021/OUDHH/index.tsx index 71da624bb4..31b92f652a 100644 --- a/services/ui-src/src/measures/2021/OUDHH/index.tsx +++ b/services/ui-src/src/measures/2021/OUDHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const OUDHH = ({ name, diff --git a/services/ui-src/src/measures/2021/OUDHH/types.ts b/services/ui-src/src/measures/2021/OUDHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/OUDHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/OUDHH/validation.ts b/services/ui-src/src/measures/2021/OUDHH/validation.ts index c8570f8f85..d48b0faad2 100644 --- a/services/ui-src/src/measures/2021/OUDHH/validation.ts +++ b/services/ui-src/src/measures/2021/OUDHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const OUDValidation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2021/PC01AD/index.tsx b/services/ui-src/src/measures/2021/PC01AD/index.tsx index 5be704d727..3372d0b413 100644 --- a/services/ui-src/src/measures/2021/PC01AD/index.tsx +++ b/services/ui-src/src/measures/2021/PC01AD/index.tsx @@ -4,9 +4,10 @@ import { useFormContext } from "react-hook-form"; import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PC01AD = ({ name, diff --git a/services/ui-src/src/measures/2021/PC01AD/types.ts b/services/ui-src/src/measures/2021/PC01AD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PC01AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PC01AD/validation.ts b/services/ui-src/src/measures/2021/PC01AD/validation.ts index ceec2a2bd8..c71011c6c5 100644 --- a/services/ui-src/src/measures/2021/PC01AD/validation.ts +++ b/services/ui-src/src/measures/2021/PC01AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PC01ADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/PCRAD/index.tsx b/services/ui-src/src/measures/2021/PCRAD/index.tsx index 8dfdf7e47c..f24022bf26 100644 --- a/services/ui-src/src/measures/2021/PCRAD/index.tsx +++ b/services/ui-src/src/measures/2021/PCRAD/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2021/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { PCRADPerformanceMeasure } from "./questions/PerformanceMeasure"; import { useFormContext } from "react-hook-form"; import { useEffect } from "react"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PCRAD = ({ name, diff --git a/services/ui-src/src/measures/2021/PCRAD/types.ts b/services/ui-src/src/measures/2021/PCRAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PCRAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PCRAD/validation.ts b/services/ui-src/src/measures/2021/PCRAD/validation.ts index 7b8abfcba0..ace98355f5 100644 --- a/services/ui-src/src/measures/2021/PCRAD/validation.ts +++ b/services/ui-src/src/measures/2021/PCRAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const ndrForumlas = [ { diff --git a/services/ui-src/src/measures/2021/PCRHH/index.tsx b/services/ui-src/src/measures/2021/PCRHH/index.tsx index b424862ed9..003991e4a3 100644 --- a/services/ui-src/src/measures/2021/PCRHH/index.tsx +++ b/services/ui-src/src/measures/2021/PCRHH/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import { validationFunctions } from "./validation"; import { PCRHHPerformanceMeasure } from "./questions/PerformanceMeasure"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PCRHH = ({ name, diff --git a/services/ui-src/src/measures/2021/PCRHH/types.ts b/services/ui-src/src/measures/2021/PCRHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PCRHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PCRHH/validation.ts b/services/ui-src/src/measures/2021/PCRHH/validation.ts index cbbd51d693..fbfe295b43 100644 --- a/services/ui-src/src/measures/2021/PCRHH/validation.ts +++ b/services/ui-src/src/measures/2021/PCRHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const ndrForumlas = [ { diff --git a/services/ui-src/src/measures/2021/PPCAD/index.tsx b/services/ui-src/src/measures/2021/PPCAD/index.tsx index 338fc078a4..1434888323 100644 --- a/services/ui-src/src/measures/2021/PPCAD/index.tsx +++ b/services/ui-src/src/measures/2021/PPCAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PPCAD = ({ name, diff --git a/services/ui-src/src/measures/2021/PPCAD/types.ts b/services/ui-src/src/measures/2021/PPCAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PPCAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PPCAD/validation.ts b/services/ui-src/src/measures/2021/PPCAD/validation.ts index 6a5ff68bcc..f5ed61ff25 100644 --- a/services/ui-src/src/measures/2021/PPCAD/validation.ts +++ b/services/ui-src/src/measures/2021/PPCAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PPCADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/PPCCH/index.tsx b/services/ui-src/src/measures/2021/PPCCH/index.tsx index 7a7ca54490..ea666996b8 100644 --- a/services/ui-src/src/measures/2021/PPCCH/index.tsx +++ b/services/ui-src/src/measures/2021/PPCCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PPCCH = ({ name, diff --git a/services/ui-src/src/measures/2021/PPCCH/types.ts b/services/ui-src/src/measures/2021/PPCCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PPCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PPCCH/validation.ts b/services/ui-src/src/measures/2021/PPCCH/validation.ts index 4156ffb37d..b37f90d81d 100644 --- a/services/ui-src/src/measures/2021/PPCCH/validation.ts +++ b/services/ui-src/src/measures/2021/PPCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PPCCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/PQI01AD/index.tsx b/services/ui-src/src/measures/2021/PQI01AD/index.tsx index 7111b3d3e8..94c56b3c8d 100644 --- a/services/ui-src/src/measures/2021/PQI01AD/index.tsx +++ b/services/ui-src/src/measures/2021/PQI01AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PQI01AD = ({ name, diff --git a/services/ui-src/src/measures/2021/PQI01AD/types.ts b/services/ui-src/src/measures/2021/PQI01AD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PQI01AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PQI01AD/validation.ts b/services/ui-src/src/measures/2021/PQI01AD/validation.ts index 3f7793854d..26f0130cf3 100644 --- a/services/ui-src/src/measures/2021/PQI01AD/validation.ts +++ b/services/ui-src/src/measures/2021/PQI01AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PQI01Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2021/PQI05AD/index.tsx b/services/ui-src/src/measures/2021/PQI05AD/index.tsx index e22ab45c0c..c3a0c047ef 100644 --- a/services/ui-src/src/measures/2021/PQI05AD/index.tsx +++ b/services/ui-src/src/measures/2021/PQI05AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PQI05AD = ({ name, diff --git a/services/ui-src/src/measures/2021/PQI05AD/types.ts b/services/ui-src/src/measures/2021/PQI05AD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PQI05AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PQI05AD/validation.ts b/services/ui-src/src/measures/2021/PQI05AD/validation.ts index 36581d52e3..a7a102d396 100644 --- a/services/ui-src/src/measures/2021/PQI05AD/validation.ts +++ b/services/ui-src/src/measures/2021/PQI05AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PQI05Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2021/PQI08AD/index.tsx b/services/ui-src/src/measures/2021/PQI08AD/index.tsx index 1779c327c6..9bcffa86b3 100644 --- a/services/ui-src/src/measures/2021/PQI08AD/index.tsx +++ b/services/ui-src/src/measures/2021/PQI08AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PQI08AD = ({ name, diff --git a/services/ui-src/src/measures/2021/PQI08AD/types.ts b/services/ui-src/src/measures/2021/PQI08AD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PQI08AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PQI08AD/validation.ts b/services/ui-src/src/measures/2021/PQI08AD/validation.ts index e13ffe7ce7..be6928c3c7 100644 --- a/services/ui-src/src/measures/2021/PQI08AD/validation.ts +++ b/services/ui-src/src/measures/2021/PQI08AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PQI08Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2021/PQI15AD/index.tsx b/services/ui-src/src/measures/2021/PQI15AD/index.tsx index 5c18f1173a..1815dcffda 100644 --- a/services/ui-src/src/measures/2021/PQI15AD/index.tsx +++ b/services/ui-src/src/measures/2021/PQI15AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PQI15AD = ({ name, diff --git a/services/ui-src/src/measures/2021/PQI15AD/types.ts b/services/ui-src/src/measures/2021/PQI15AD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PQI15AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PQI15AD/validation.ts b/services/ui-src/src/measures/2021/PQI15AD/validation.ts index dbe173c2ef..2b05607aee 100644 --- a/services/ui-src/src/measures/2021/PQI15AD/validation.ts +++ b/services/ui-src/src/measures/2021/PQI15AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PQI15Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2021/PQI92HH/index.tsx b/services/ui-src/src/measures/2021/PQI92HH/index.tsx index bddc366e8f..2cb9a85e6c 100644 --- a/services/ui-src/src/measures/2021/PQI92HH/index.tsx +++ b/services/ui-src/src/measures/2021/PQI92HH/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const PQI92HH = ({ name, diff --git a/services/ui-src/src/measures/2021/PQI92HH/types.ts b/services/ui-src/src/measures/2021/PQI92HH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/PQI92HH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/PQI92HH/validation.ts b/services/ui-src/src/measures/2021/PQI92HH/validation.ts index 044d9791fb..58571a1ead 100644 --- a/services/ui-src/src/measures/2021/PQI92HH/validation.ts +++ b/services/ui-src/src/measures/2021/PQI92HH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const PQI92Validation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2021/SAAAD/index.tsx b/services/ui-src/src/measures/2021/SAAAD/index.tsx index 828af124c7..4da3e7cab8 100644 --- a/services/ui-src/src/measures/2021/SAAAD/index.tsx +++ b/services/ui-src/src/measures/2021/SAAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const SAAAD = ({ name, diff --git a/services/ui-src/src/measures/2021/SAAAD/types.ts b/services/ui-src/src/measures/2021/SAAAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/SAAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/SAAAD/validation.ts b/services/ui-src/src/measures/2021/SAAAD/validation.ts index ea163bb57d..b7cd091277 100644 --- a/services/ui-src/src/measures/2021/SAAAD/validation.ts +++ b/services/ui-src/src/measures/2021/SAAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const SAAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/SFMCH/index.tsx b/services/ui-src/src/measures/2021/SFMCH/index.tsx index 7364e5c8d4..b1dd65ad5e 100644 --- a/services/ui-src/src/measures/2021/SFMCH/index.tsx +++ b/services/ui-src/src/measures/2021/SFMCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const SFMCH = ({ name, diff --git a/services/ui-src/src/measures/2021/SFMCH/types.ts b/services/ui-src/src/measures/2021/SFMCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/SFMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/SFMCH/validation.ts b/services/ui-src/src/measures/2021/SFMCH/validation.ts index 0599774abc..b2867b8ee8 100644 --- a/services/ui-src/src/measures/2021/SFMCH/validation.ts +++ b/services/ui-src/src/measures/2021/SFMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const SFMCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/SSDAD/index.tsx b/services/ui-src/src/measures/2021/SSDAD/index.tsx index 73d8952be2..f70cdd3940 100644 --- a/services/ui-src/src/measures/2021/SSDAD/index.tsx +++ b/services/ui-src/src/measures/2021/SSDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const SSDAD = ({ name, diff --git a/services/ui-src/src/measures/2021/SSDAD/types.ts b/services/ui-src/src/measures/2021/SSDAD/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/SSDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/SSDAD/validation.ts b/services/ui-src/src/measures/2021/SSDAD/validation.ts index 7b9c8d43c9..cd37bbdf7b 100644 --- a/services/ui-src/src/measures/2021/SSDAD/validation.ts +++ b/services/ui-src/src/measures/2021/SSDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const SSDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/SSHH/types.ts b/services/ui-src/src/measures/2021/SSHH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/SSHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/SSHH/validation.ts b/services/ui-src/src/measures/2021/SSHH/validation.ts index 32fc8bad60..836da8e0db 100644 --- a/services/ui-src/src/measures/2021/SSHH/validation.ts +++ b/services/ui-src/src/measures/2021/SSHH/validation.ts @@ -1,6 +1,7 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export interface FormRateField { denominator?: string; diff --git a/services/ui-src/src/measures/2021/W30CH/index.tsx b/services/ui-src/src/measures/2021/W30CH/index.tsx index 981874a86b..369f2bb03c 100644 --- a/services/ui-src/src/measures/2021/W30CH/index.tsx +++ b/services/ui-src/src/measures/2021/W30CH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const W30CH = ({ name, diff --git a/services/ui-src/src/measures/2021/W30CH/types.ts b/services/ui-src/src/measures/2021/W30CH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/W30CH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/W30CH/validation.ts b/services/ui-src/src/measures/2021/W30CH/validation.ts index 8bfa14fa97..dfd9af9c3c 100644 --- a/services/ui-src/src/measures/2021/W30CH/validation.ts +++ b/services/ui-src/src/measures/2021/W30CH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const W30CHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2021/WCCCH/index.tsx b/services/ui-src/src/measures/2021/WCCCH/index.tsx index 2b5ccb9f5a..e9e80a51bc 100644 --- a/services/ui-src/src/measures/2021/WCCCH/index.tsx +++ b/services/ui-src/src/measures/2021/WCCCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const WCCCH = ({ name, diff --git a/services/ui-src/src/measures/2021/WCCCH/types.ts b/services/ui-src/src/measures/2021/WCCCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/WCCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/WCCCH/validation.ts b/services/ui-src/src/measures/2021/WCCCH/validation.ts index 701d114415..146d7f0a2c 100644 --- a/services/ui-src/src/measures/2021/WCCCH/validation.ts +++ b/services/ui-src/src/measures/2021/WCCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const WCCHValidation = (data: FormData) => { const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; diff --git a/services/ui-src/src/measures/2021/WCVCH/index.tsx b/services/ui-src/src/measures/2021/WCVCH/index.tsx index 26c77d04fa..22cdcd98d7 100644 --- a/services/ui-src/src/measures/2021/WCVCH/index.tsx +++ b/services/ui-src/src/measures/2021/WCVCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2021/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; export const WCVCH = ({ name, diff --git a/services/ui-src/src/measures/2021/WCVCH/types.ts b/services/ui-src/src/measures/2021/WCVCH/types.ts deleted file mode 100644 index b5f7188645..0000000000 --- a/services/ui-src/src/measures/2021/WCVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2021/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2021/WCVCH/validation.ts b/services/ui-src/src/measures/2021/WCVCH/validation.ts index 4ffeb268ec..d28d1a05ad 100644 --- a/services/ui-src/src/measures/2021/WCVCH/validation.ts +++ b/services/ui-src/src/measures/2021/WCVCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2021/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2021/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2021/CommonQuestions/types"; const WCVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/AABAD/index.tsx b/services/ui-src/src/measures/2022/AABAD/index.tsx index 8163eb1658..644133271b 100644 --- a/services/ui-src/src/measures/2022/AABAD/index.tsx +++ b/services/ui-src/src/measures/2022/AABAD/index.tsx @@ -5,10 +5,11 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; -import { FormData } from "./types"; import { validationFunctions } from "./validation"; import { AABRateCalculation } from "utils/rateFormulas"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const AABAD = ({ name, diff --git a/services/ui-src/src/measures/2022/AABAD/types.ts b/services/ui-src/src/measures/2022/AABAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/AABAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/AABAD/validation.ts b/services/ui-src/src/measures/2022/AABAD/validation.ts index bba6a303e6..2fb02698ce 100644 --- a/services/ui-src/src/measures/2022/AABAD/validation.ts +++ b/services/ui-src/src/measures/2022/AABAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const AABADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/ADDCH/index.tsx b/services/ui-src/src/measures/2022/ADDCH/index.tsx index 953de4d580..d2d7fa7b32 100644 --- a/services/ui-src/src/measures/2022/ADDCH/index.tsx +++ b/services/ui-src/src/measures/2022/ADDCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const ADDCH = ({ name, diff --git a/services/ui-src/src/measures/2022/ADDCH/types.ts b/services/ui-src/src/measures/2022/ADDCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/ADDCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/ADDCH/validation.ts b/services/ui-src/src/measures/2022/ADDCH/validation.ts index 83b25cd862..95283424c0 100644 --- a/services/ui-src/src/measures/2022/ADDCH/validation.ts +++ b/services/ui-src/src/measures/2022/ADDCH/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "../shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const ADDCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/AIFHH/index.tsx b/services/ui-src/src/measures/2022/AIFHH/index.tsx index ce1044653c..2867da9582 100644 --- a/services/ui-src/src/measures/2022/AIFHH/index.tsx +++ b/services/ui-src/src/measures/2022/AIFHH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const AIFHH = ({ name, diff --git a/services/ui-src/src/measures/2022/AIFHH/types.ts b/services/ui-src/src/measures/2022/AIFHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/AIFHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/AIFHH/validation.ts b/services/ui-src/src/measures/2022/AIFHH/validation.ts index 983084dbdb..3a4014c254 100644 --- a/services/ui-src/src/measures/2022/AIFHH/validation.ts +++ b/services/ui-src/src/measures/2022/AIFHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; // Rate structure by index in row const ndrFormulas = [ diff --git a/services/ui-src/src/measures/2022/AMBCH/index.tsx b/services/ui-src/src/measures/2022/AMBCH/index.tsx index 4ae8ba5408..10695dbfff 100644 --- a/services/ui-src/src/measures/2022/AMBCH/index.tsx +++ b/services/ui-src/src/measures/2022/AMBCH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const AMBCH = ({ name, diff --git a/services/ui-src/src/measures/2022/AMBCH/types.ts b/services/ui-src/src/measures/2022/AMBCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/AMBCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/AMBCH/validation.ts b/services/ui-src/src/measures/2022/AMBCH/validation.ts index c04fc28e74..5695833321 100644 --- a/services/ui-src/src/measures/2022/AMBCH/validation.ts +++ b/services/ui-src/src/measures/2022/AMBCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const AMBCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/AMBHH/index.tsx b/services/ui-src/src/measures/2022/AMBHH/index.tsx index de0fad968a..6e6caebce8 100644 --- a/services/ui-src/src/measures/2022/AMBHH/index.tsx +++ b/services/ui-src/src/measures/2022/AMBHH/index.tsx @@ -5,8 +5,9 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const AMBHH = ({ name, diff --git a/services/ui-src/src/measures/2022/AMBHH/types.ts b/services/ui-src/src/measures/2022/AMBHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/AMBHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/AMBHH/validation.ts b/services/ui-src/src/measures/2022/AMBHH/validation.ts index 2dbc2b9fc8..c4befe27d2 100644 --- a/services/ui-src/src/measures/2022/AMBHH/validation.ts +++ b/services/ui-src/src/measures/2022/AMBHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const AMBHHValidation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2022/AMMAD/index.tsx b/services/ui-src/src/measures/2022/AMMAD/index.tsx index 287a748374..15219e4035 100644 --- a/services/ui-src/src/measures/2022/AMMAD/index.tsx +++ b/services/ui-src/src/measures/2022/AMMAD/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const AMMAD = ({ name, diff --git a/services/ui-src/src/measures/2022/AMMAD/types.ts b/services/ui-src/src/measures/2022/AMMAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/AMMAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/AMMAD/validation.ts b/services/ui-src/src/measures/2022/AMMAD/validation.ts index ebd10a0ce3..bf3580424f 100644 --- a/services/ui-src/src/measures/2022/AMMAD/validation.ts +++ b/services/ui-src/src/measures/2022/AMMAD/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; import { cleanString } from "utils/cleanString"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const sameDenominatorSets: GV.Types.OmsValidationCallback = ({ rateData, diff --git a/services/ui-src/src/measures/2022/AMRCH/index.tsx b/services/ui-src/src/measures/2022/AMRCH/index.tsx index 986fb31733..5f3b09dccc 100644 --- a/services/ui-src/src/measures/2022/AMRCH/index.tsx +++ b/services/ui-src/src/measures/2022/AMRCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const AMRCH = ({ name, diff --git a/services/ui-src/src/measures/2022/AMRCH/types.ts b/services/ui-src/src/measures/2022/AMRCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/AMRCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/AMRCH/validation.ts b/services/ui-src/src/measures/2022/AMRCH/validation.ts index 22ce728515..32f536bce0 100644 --- a/services/ui-src/src/measures/2022/AMRCH/validation.ts +++ b/services/ui-src/src/measures/2022/AMRCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const AMRCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/APMCH/index.tsx b/services/ui-src/src/measures/2022/APMCH/index.tsx index 123eb57abb..c85fb94faa 100644 --- a/services/ui-src/src/measures/2022/APMCH/index.tsx +++ b/services/ui-src/src/measures/2022/APMCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const APMCH = ({ name, diff --git a/services/ui-src/src/measures/2022/APMCH/types.ts b/services/ui-src/src/measures/2022/APMCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/APMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/APMCH/validation.ts b/services/ui-src/src/measures/2022/APMCH/validation.ts index 30011b6f96..75fac45709 100644 --- a/services/ui-src/src/measures/2022/APMCH/validation.ts +++ b/services/ui-src/src/measures/2022/APMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const APMCHValidation = (data: FormData) => { const dateRange = data[DC.DATE_RANGE]; diff --git a/services/ui-src/src/measures/2022/APPCH/index.tsx b/services/ui-src/src/measures/2022/APPCH/index.tsx index 6b7c08eb3e..48ccb27351 100644 --- a/services/ui-src/src/measures/2022/APPCH/index.tsx +++ b/services/ui-src/src/measures/2022/APPCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const APPCH = ({ name, diff --git a/services/ui-src/src/measures/2022/APPCH/types.ts b/services/ui-src/src/measures/2022/APPCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/APPCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/APPCH/validation.ts b/services/ui-src/src/measures/2022/APPCH/validation.ts index 659221e03f..d9c0cad0c5 100644 --- a/services/ui-src/src/measures/2022/APPCH/validation.ts +++ b/services/ui-src/src/measures/2022/APPCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const APPCHValidation = (data: FormData) => { const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; diff --git a/services/ui-src/src/measures/2022/BCSAD/index.tsx b/services/ui-src/src/measures/2022/BCSAD/index.tsx index ca560f0cc3..a0c11b91bb 100644 --- a/services/ui-src/src/measures/2022/BCSAD/index.tsx +++ b/services/ui-src/src/measures/2022/BCSAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const BCSAD = ({ name, diff --git a/services/ui-src/src/measures/2022/BCSAD/types.ts b/services/ui-src/src/measures/2022/BCSAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/BCSAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/BCSAD/validation.ts b/services/ui-src/src/measures/2022/BCSAD/validation.ts index 4ee556fa1f..76ba190f8d 100644 --- a/services/ui-src/src/measures/2022/BCSAD/validation.ts +++ b/services/ui-src/src/measures/2022/BCSAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const BCSValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CBPAD/index.tsx b/services/ui-src/src/measures/2022/CBPAD/index.tsx index 825ca932e9..cf11248c8a 100644 --- a/services/ui-src/src/measures/2022/CBPAD/index.tsx +++ b/services/ui-src/src/measures/2022/CBPAD/index.tsx @@ -5,7 +5,8 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CBPAD = ({ name, diff --git a/services/ui-src/src/measures/2022/CBPAD/types.ts b/services/ui-src/src/measures/2022/CBPAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CBPAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CBPAD/validation.ts b/services/ui-src/src/measures/2022/CBPAD/validation.ts index 55c7e533c9..99cf7cb7b5 100644 --- a/services/ui-src/src/measures/2022/CBPAD/validation.ts +++ b/services/ui-src/src/measures/2022/CBPAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CBPValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CBPHH/index.tsx b/services/ui-src/src/measures/2022/CBPHH/index.tsx index 9696bb688b..df28f99c50 100644 --- a/services/ui-src/src/measures/2022/CBPHH/index.tsx +++ b/services/ui-src/src/measures/2022/CBPHH/index.tsx @@ -5,7 +5,8 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CBPHH = ({ name, diff --git a/services/ui-src/src/measures/2022/CBPHH/types.ts b/services/ui-src/src/measures/2022/CBPHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CBPHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CBPHH/validation.ts b/services/ui-src/src/measures/2022/CBPHH/validation.ts index 75b79584d2..f3807d17cc 100644 --- a/services/ui-src/src/measures/2022/CBPHH/validation.ts +++ b/services/ui-src/src/measures/2022/CBPHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CBPValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CCPAD/index.tsx b/services/ui-src/src/measures/2022/CCPAD/index.tsx index ee63b05e89..1b728b6ecc 100644 --- a/services/ui-src/src/measures/2022/CCPAD/index.tsx +++ b/services/ui-src/src/measures/2022/CCPAD/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "../shared/CommonQuestions"; import * as QMR from "components"; -import { FormData } from "./types"; import { useEffect } from "react"; import { validationFunctions } from "./validation"; import * as PMD from "./data"; import { useFormContext } from "react-hook-form"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CCPAD = ({ name, diff --git a/services/ui-src/src/measures/2022/CCPAD/types.ts b/services/ui-src/src/measures/2022/CCPAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CCPAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CCPAD/validation.ts b/services/ui-src/src/measures/2022/CCPAD/validation.ts index 5efb9063f5..7a4912da60 100644 --- a/services/ui-src/src/measures/2022/CCPAD/validation.ts +++ b/services/ui-src/src/measures/2022/CCPAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CCPADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CCPCH/index.tsx b/services/ui-src/src/measures/2022/CCPCH/index.tsx index 7fcbacf948..85e8e5ddf5 100644 --- a/services/ui-src/src/measures/2022/CCPCH/index.tsx +++ b/services/ui-src/src/measures/2022/CCPCH/index.tsx @@ -2,11 +2,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CCPCH = ({ name, diff --git a/services/ui-src/src/measures/2022/CCPCH/types.ts b/services/ui-src/src/measures/2022/CCPCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CCPCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CCPCH/validation.ts b/services/ui-src/src/measures/2022/CCPCH/validation.ts index 1dbe6c7219..c1853b6a7e 100644 --- a/services/ui-src/src/measures/2022/CCPCH/validation.ts +++ b/services/ui-src/src/measures/2022/CCPCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CCPCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CCSAD/index.tsx b/services/ui-src/src/measures/2022/CCSAD/index.tsx index a4eedccb34..86e8d293ec 100644 --- a/services/ui-src/src/measures/2022/CCSAD/index.tsx +++ b/services/ui-src/src/measures/2022/CCSAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CCSAD = ({ name, diff --git a/services/ui-src/src/measures/2022/CCSAD/types.ts b/services/ui-src/src/measures/2022/CCSAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CCSAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CCSAD/validation.ts b/services/ui-src/src/measures/2022/CCSAD/validation.ts index 9223391a78..08ce5a16aa 100644 --- a/services/ui-src/src/measures/2022/CCSAD/validation.ts +++ b/services/ui-src/src/measures/2022/CCSAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CCSADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CCWAD/index.tsx b/services/ui-src/src/measures/2022/CCWAD/index.tsx index 476d9bf3a1..5f035641c0 100644 --- a/services/ui-src/src/measures/2022/CCWAD/index.tsx +++ b/services/ui-src/src/measures/2022/CCWAD/index.tsx @@ -4,8 +4,9 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CCWAD = ({ name, diff --git a/services/ui-src/src/measures/2022/CCWAD/types.ts b/services/ui-src/src/measures/2022/CCWAD/types.ts deleted file mode 100644 index 1bdc04b1a6..0000000000 --- a/services/ui-src/src/measures/2022/CCWAD/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type DeviationCheckBoxOptions = - | "moderate-method-deviation-Numerator" - | "moderate-method-deviation-Denominator" - | "moderate-method-deviation-Other" - | "reversible-method-deviation-Numerator" - | "reversible-method-deviation-Denominator" - | "reversible-method-deviation-Other"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CCWAD/validation.ts b/services/ui-src/src/measures/2022/CCWAD/validation.ts index f51e08d9cb..eb85074c4f 100644 --- a/services/ui-src/src/measures/2022/CCWAD/validation.ts +++ b/services/ui-src/src/measures/2022/CCWAD/validation.ts @@ -1,9 +1,10 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { cleanString } from "utils"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CCWADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CCWCH/index.tsx b/services/ui-src/src/measures/2022/CCWCH/index.tsx index 00152d4ab8..7d34b2638e 100644 --- a/services/ui-src/src/measures/2022/CCWCH/index.tsx +++ b/services/ui-src/src/measures/2022/CCWCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CCWCH = ({ name, diff --git a/services/ui-src/src/measures/2022/CCWCH/types.ts b/services/ui-src/src/measures/2022/CCWCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CCWCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CCWCH/validation.ts b/services/ui-src/src/measures/2022/CCWCH/validation.ts index 2f44aa7a17..d14cf1b19a 100644 --- a/services/ui-src/src/measures/2022/CCWCH/validation.ts +++ b/services/ui-src/src/measures/2022/CCWCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CCWCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CDFAD/index.tsx b/services/ui-src/src/measures/2022/CDFAD/index.tsx index 48be1bda03..1bd3b5be6a 100644 --- a/services/ui-src/src/measures/2022/CDFAD/index.tsx +++ b/services/ui-src/src/measures/2022/CDFAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CDFAD = ({ name, diff --git a/services/ui-src/src/measures/2022/CDFAD/types.ts b/services/ui-src/src/measures/2022/CDFAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CDFAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CDFAD/validation.ts b/services/ui-src/src/measures/2022/CDFAD/validation.ts index d47977f3d9..25002105cd 100644 --- a/services/ui-src/src/measures/2022/CDFAD/validation.ts +++ b/services/ui-src/src/measures/2022/CDFAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CDFADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CDFCH/index.tsx b/services/ui-src/src/measures/2022/CDFCH/index.tsx index 744e840048..37b7522079 100644 --- a/services/ui-src/src/measures/2022/CDFCH/index.tsx +++ b/services/ui-src/src/measures/2022/CDFCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CDFCH = ({ name, diff --git a/services/ui-src/src/measures/2022/CDFCH/types.ts b/services/ui-src/src/measures/2022/CDFCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CDFCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CDFCH/validation.ts b/services/ui-src/src/measures/2022/CDFCH/validation.ts index 35e833600e..39665cd35b 100644 --- a/services/ui-src/src/measures/2022/CDFCH/validation.ts +++ b/services/ui-src/src/measures/2022/CDFCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CDFCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CDFHH/index.tsx b/services/ui-src/src/measures/2022/CDFHH/index.tsx index 529f15d5cd..69e01f5b37 100644 --- a/services/ui-src/src/measures/2022/CDFHH/index.tsx +++ b/services/ui-src/src/measures/2022/CDFHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CDFHH = ({ name, diff --git a/services/ui-src/src/measures/2022/CDFHH/types.ts b/services/ui-src/src/measures/2022/CDFHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CDFHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CDFHH/validation.ts b/services/ui-src/src/measures/2022/CDFHH/validation.ts index 26a3ca8ed5..bb62fbaf70 100644 --- a/services/ui-src/src/measures/2022/CDFHH/validation.ts +++ b/services/ui-src/src/measures/2022/CDFHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CDFHHValidation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2022/CHLAD/index.tsx b/services/ui-src/src/measures/2022/CHLAD/index.tsx index ccab0dac6a..11c9de786d 100644 --- a/services/ui-src/src/measures/2022/CHLAD/index.tsx +++ b/services/ui-src/src/measures/2022/CHLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CHLAD = ({ name, diff --git a/services/ui-src/src/measures/2022/CHLAD/types.ts b/services/ui-src/src/measures/2022/CHLAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CHLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CHLAD/validation.ts b/services/ui-src/src/measures/2022/CHLAD/validation.ts index 8270b8f09e..cf1bc3657b 100644 --- a/services/ui-src/src/measures/2022/CHLAD/validation.ts +++ b/services/ui-src/src/measures/2022/CHLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CHLValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CHLCH/index.tsx b/services/ui-src/src/measures/2022/CHLCH/index.tsx index 31b975510c..eb355e8c48 100644 --- a/services/ui-src/src/measures/2022/CHLCH/index.tsx +++ b/services/ui-src/src/measures/2022/CHLCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CHLCH = ({ name, diff --git a/services/ui-src/src/measures/2022/CHLCH/types.ts b/services/ui-src/src/measures/2022/CHLCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CHLCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CHLCH/validation.ts b/services/ui-src/src/measures/2022/CHLCH/validation.ts index 8270b8f09e..cf1bc3657b 100644 --- a/services/ui-src/src/measures/2022/CHLCH/validation.ts +++ b/services/ui-src/src/measures/2022/CHLCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CHLValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/CISCH/index.tsx b/services/ui-src/src/measures/2022/CISCH/index.tsx index b72b8f89bd..edc8668b41 100644 --- a/services/ui-src/src/measures/2022/CISCH/index.tsx +++ b/services/ui-src/src/measures/2022/CISCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const CISCH = ({ name, diff --git a/services/ui-src/src/measures/2022/CISCH/types.ts b/services/ui-src/src/measures/2022/CISCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/CISCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/CISCH/validation.ts b/services/ui-src/src/measures/2022/CISCH/validation.ts index cf44566655..77713ddbe8 100644 --- a/services/ui-src/src/measures/2022/CISCH/validation.ts +++ b/services/ui-src/src/measures/2022/CISCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const CISCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/COBAD/index.tsx b/services/ui-src/src/measures/2022/COBAD/index.tsx index baca753079..78c8488ff1 100644 --- a/services/ui-src/src/measures/2022/COBAD/index.tsx +++ b/services/ui-src/src/measures/2022/COBAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const COBAD = ({ name, diff --git a/services/ui-src/src/measures/2022/COBAD/types.ts b/services/ui-src/src/measures/2022/COBAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/COBAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/COBAD/validation.ts b/services/ui-src/src/measures/2022/COBAD/validation.ts index 2486e5bb95..d1695f0f31 100644 --- a/services/ui-src/src/measures/2022/COBAD/validation.ts +++ b/services/ui-src/src/measures/2022/COBAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const COBADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/COLAD/index.tsx b/services/ui-src/src/measures/2022/COLAD/index.tsx index bc873ff21e..b9f5fe6a19 100644 --- a/services/ui-src/src/measures/2022/COLAD/index.tsx +++ b/services/ui-src/src/measures/2022/COLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const COLAD = ({ name, diff --git a/services/ui-src/src/measures/2022/COLAD/types.ts b/services/ui-src/src/measures/2022/COLAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/COLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/COLAD/validation.ts b/services/ui-src/src/measures/2022/COLAD/validation.ts index a534bb4a72..5152c42ec6 100644 --- a/services/ui-src/src/measures/2022/COLAD/validation.ts +++ b/services/ui-src/src/measures/2022/COLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const COLADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/COLHH/index.tsx b/services/ui-src/src/measures/2022/COLHH/index.tsx index 2083b59745..99f2fae040 100644 --- a/services/ui-src/src/measures/2022/COLHH/index.tsx +++ b/services/ui-src/src/measures/2022/COLHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const COLHH = ({ name, diff --git a/services/ui-src/src/measures/2022/COLHH/types.ts b/services/ui-src/src/measures/2022/COLHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/COLHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/COLHH/validation.ts b/services/ui-src/src/measures/2022/COLHH/validation.ts index ea9630ea02..3803d39b23 100644 --- a/services/ui-src/src/measures/2022/COLHH/validation.ts +++ b/services/ui-src/src/measures/2022/COLHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const COLHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/DEVCH/index.tsx b/services/ui-src/src/measures/2022/DEVCH/index.tsx index 93579f2bfc..5a35f07a8d 100644 --- a/services/ui-src/src/measures/2022/DEVCH/index.tsx +++ b/services/ui-src/src/measures/2022/DEVCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const DEVCH = ({ name, diff --git a/services/ui-src/src/measures/2022/DEVCH/types.ts b/services/ui-src/src/measures/2022/DEVCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/DEVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/DEVCH/validation.ts b/services/ui-src/src/measures/2022/DEVCH/validation.ts index 9af49e6158..d187b92418 100644 --- a/services/ui-src/src/measures/2022/DEVCH/validation.ts +++ b/services/ui-src/src/measures/2022/DEVCH/validation.ts @@ -1,8 +1,9 @@ import * as PMD from "./data"; import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const DEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUAAD/index.tsx b/services/ui-src/src/measures/2022/FUAAD/index.tsx index 9501bb36cd..4d55a82d09 100644 --- a/services/ui-src/src/measures/2022/FUAAD/index.tsx +++ b/services/ui-src/src/measures/2022/FUAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUAAD = ({ name, diff --git a/services/ui-src/src/measures/2022/FUAAD/types.ts b/services/ui-src/src/measures/2022/FUAAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUAAD/validation.ts b/services/ui-src/src/measures/2022/FUAAD/validation.ts index 06e00e268c..deb1111552 100644 --- a/services/ui-src/src/measures/2022/FUAAD/validation.ts +++ b/services/ui-src/src/measures/2022/FUAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUACH/index.tsx b/services/ui-src/src/measures/2022/FUACH/index.tsx index 8ebe2c052e..4ad06257f6 100644 --- a/services/ui-src/src/measures/2022/FUACH/index.tsx +++ b/services/ui-src/src/measures/2022/FUACH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUACH = ({ name, diff --git a/services/ui-src/src/measures/2022/FUACH/types.ts b/services/ui-src/src/measures/2022/FUACH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUACH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUACH/validation.ts b/services/ui-src/src/measures/2022/FUACH/validation.ts index f286e554e2..d844b7caeb 100644 --- a/services/ui-src/src/measures/2022/FUACH/validation.ts +++ b/services/ui-src/src/measures/2022/FUACH/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "../shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUACHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUAHH/index.tsx b/services/ui-src/src/measures/2022/FUAHH/index.tsx index c8cebf49da..83ea7ec407 100644 --- a/services/ui-src/src/measures/2022/FUAHH/index.tsx +++ b/services/ui-src/src/measures/2022/FUAHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUAHH = ({ name, diff --git a/services/ui-src/src/measures/2022/FUAHH/types.ts b/services/ui-src/src/measures/2022/FUAHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUAHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUAHH/validation.ts b/services/ui-src/src/measures/2022/FUAHH/validation.ts index 0707706e77..544815fb95 100644 --- a/services/ui-src/src/measures/2022/FUAHH/validation.ts +++ b/services/ui-src/src/measures/2022/FUAHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUAHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUHAD/index.tsx b/services/ui-src/src/measures/2022/FUHAD/index.tsx index 390f04b2ac..5295140074 100644 --- a/services/ui-src/src/measures/2022/FUHAD/index.tsx +++ b/services/ui-src/src/measures/2022/FUHAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUHAD = ({ name, diff --git a/services/ui-src/src/measures/2022/FUHAD/types.ts b/services/ui-src/src/measures/2022/FUHAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUHAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUHAD/validation.ts b/services/ui-src/src/measures/2022/FUHAD/validation.ts index bbe80eba45..15af5e6b30 100644 --- a/services/ui-src/src/measures/2022/FUHAD/validation.ts +++ b/services/ui-src/src/measures/2022/FUHAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as PMD from "./data"; import * as GV from "../shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUHCH/index.tsx b/services/ui-src/src/measures/2022/FUHCH/index.tsx index 74d665c0cf..1becf49f8c 100644 --- a/services/ui-src/src/measures/2022/FUHCH/index.tsx +++ b/services/ui-src/src/measures/2022/FUHCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUHCH = ({ name, diff --git a/services/ui-src/src/measures/2022/FUHCH/types.ts b/services/ui-src/src/measures/2022/FUHCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUHCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUHCH/validation.ts b/services/ui-src/src/measures/2022/FUHCH/validation.ts index fa8787e1cb..d3e834f566 100644 --- a/services/ui-src/src/measures/2022/FUHCH/validation.ts +++ b/services/ui-src/src/measures/2022/FUHCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUHHH/index.tsx b/services/ui-src/src/measures/2022/FUHHH/index.tsx index a783bc3c87..dcaf6f96f3 100644 --- a/services/ui-src/src/measures/2022/FUHHH/index.tsx +++ b/services/ui-src/src/measures/2022/FUHHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUHHH = ({ name, diff --git a/services/ui-src/src/measures/2022/FUHHH/types.ts b/services/ui-src/src/measures/2022/FUHHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUHHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUHHH/validation.ts b/services/ui-src/src/measures/2022/FUHHH/validation.ts index d78bc0b05c..71107a8023 100644 --- a/services/ui-src/src/measures/2022/FUHHH/validation.ts +++ b/services/ui-src/src/measures/2022/FUHHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUHHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUMAD/index.tsx b/services/ui-src/src/measures/2022/FUMAD/index.tsx index 08a72e79db..b619a50121 100644 --- a/services/ui-src/src/measures/2022/FUMAD/index.tsx +++ b/services/ui-src/src/measures/2022/FUMAD/index.tsx @@ -5,7 +5,8 @@ import * as QMR from "components"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { validationFunctions } from "./validation"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUMAD = ({ name, diff --git a/services/ui-src/src/measures/2022/FUMAD/types.ts b/services/ui-src/src/measures/2022/FUMAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUMAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUMAD/validation.ts b/services/ui-src/src/measures/2022/FUMAD/validation.ts index 88a72786a4..f0edb14c11 100644 --- a/services/ui-src/src/measures/2022/FUMAD/validation.ts +++ b/services/ui-src/src/measures/2022/FUMAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUMADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUMCH/index.tsx b/services/ui-src/src/measures/2022/FUMCH/index.tsx index 4e437c4c9c..a7b138fbc4 100644 --- a/services/ui-src/src/measures/2022/FUMCH/index.tsx +++ b/services/ui-src/src/measures/2022/FUMCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUMCH = ({ name, diff --git a/services/ui-src/src/measures/2022/FUMCH/types.ts b/services/ui-src/src/measures/2022/FUMCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUMCH/validation.ts b/services/ui-src/src/measures/2022/FUMCH/validation.ts index 06ac03cbb0..ed16c9a500 100644 --- a/services/ui-src/src/measures/2022/FUMCH/validation.ts +++ b/services/ui-src/src/measures/2022/FUMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUMCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FUMHH/index.tsx b/services/ui-src/src/measures/2022/FUMHH/index.tsx index 8a70028748..6972a06449 100644 --- a/services/ui-src/src/measures/2022/FUMHH/index.tsx +++ b/services/ui-src/src/measures/2022/FUMHH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FUMHH = ({ isNotReportingData, diff --git a/services/ui-src/src/measures/2022/FUMHH/types.ts b/services/ui-src/src/measures/2022/FUMHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FUMHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FUMHH/validation.ts b/services/ui-src/src/measures/2022/FUMHH/validation.ts index 0728988126..610c2a8dd9 100644 --- a/services/ui-src/src/measures/2022/FUMHH/validation.ts +++ b/services/ui-src/src/measures/2022/FUMHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FUMHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/FVAAD/index.tsx b/services/ui-src/src/measures/2022/FVAAD/index.tsx index 5e96e8ec18..67c16bcec7 100644 --- a/services/ui-src/src/measures/2022/FVAAD/index.tsx +++ b/services/ui-src/src/measures/2022/FVAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const FVAAD = ({ name, diff --git a/services/ui-src/src/measures/2022/FVAAD/types.ts b/services/ui-src/src/measures/2022/FVAAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/FVAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/FVAAD/validation.ts b/services/ui-src/src/measures/2022/FVAAD/validation.ts index 8ea17c4d7f..f6fcc7c745 100644 --- a/services/ui-src/src/measures/2022/FVAAD/validation.ts +++ b/services/ui-src/src/measures/2022/FVAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const FVAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/HPCAD/index.tsx b/services/ui-src/src/measures/2022/HPCAD/index.tsx index ea5e7e0a3b..1b53699f5b 100644 --- a/services/ui-src/src/measures/2022/HPCAD/index.tsx +++ b/services/ui-src/src/measures/2022/HPCAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const HPCAD = ({ name, diff --git a/services/ui-src/src/measures/2022/HPCAD/types.ts b/services/ui-src/src/measures/2022/HPCAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/HPCAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/HPCAD/validation.ts b/services/ui-src/src/measures/2022/HPCAD/validation.ts index 693281ab8e..b70b790861 100644 --- a/services/ui-src/src/measures/2022/HPCAD/validation.ts +++ b/services/ui-src/src/measures/2022/HPCAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const HPCADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/HPCMIAD/index.tsx b/services/ui-src/src/measures/2022/HPCMIAD/index.tsx index 37bce1f15e..867f21016e 100644 --- a/services/ui-src/src/measures/2022/HPCMIAD/index.tsx +++ b/services/ui-src/src/measures/2022/HPCMIAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const HPCMIAD = ({ name, diff --git a/services/ui-src/src/measures/2022/HPCMIAD/types.ts b/services/ui-src/src/measures/2022/HPCMIAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/HPCMIAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/HPCMIAD/validation.ts b/services/ui-src/src/measures/2022/HPCMIAD/validation.ts index d53e95d026..7291a43712 100644 --- a/services/ui-src/src/measures/2022/HPCMIAD/validation.ts +++ b/services/ui-src/src/measures/2022/HPCMIAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const HPCMIADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/HVLAD/index.tsx b/services/ui-src/src/measures/2022/HVLAD/index.tsx index eb4b79973f..52e2c061d6 100644 --- a/services/ui-src/src/measures/2022/HVLAD/index.tsx +++ b/services/ui-src/src/measures/2022/HVLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const HVLAD = ({ name, diff --git a/services/ui-src/src/measures/2022/HVLAD/types.ts b/services/ui-src/src/measures/2022/HVLAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/HVLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/HVLAD/validation.ts b/services/ui-src/src/measures/2022/HVLAD/validation.ts index 9cdbe1d0b0..388a59e6b3 100644 --- a/services/ui-src/src/measures/2022/HVLAD/validation.ts +++ b/services/ui-src/src/measures/2022/HVLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const HVLADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/IETAD/index.tsx b/services/ui-src/src/measures/2022/IETAD/index.tsx index 3ed66139c8..bebb85159e 100644 --- a/services/ui-src/src/measures/2022/IETAD/index.tsx +++ b/services/ui-src/src/measures/2022/IETAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const IETAD = ({ name, diff --git a/services/ui-src/src/measures/2022/IETAD/types.ts b/services/ui-src/src/measures/2022/IETAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/IETAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/IETAD/validation.ts b/services/ui-src/src/measures/2022/IETAD/validation.ts index 2912947f81..c068b5ccab 100644 --- a/services/ui-src/src/measures/2022/IETAD/validation.ts +++ b/services/ui-src/src/measures/2022/IETAD/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; import { cleanString } from "utils/cleanString"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; /** For each qualifier the denominators neeed to be the same for both Initiaion and Engagement of the same category. */ const sameDenominatorSets: GV.Types.OmsValidationCallback = ({ diff --git a/services/ui-src/src/measures/2022/IETHH/index.tsx b/services/ui-src/src/measures/2022/IETHH/index.tsx index 04789317b8..1ec09921a5 100644 --- a/services/ui-src/src/measures/2022/IETHH/index.tsx +++ b/services/ui-src/src/measures/2022/IETHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const IETHH = ({ name, diff --git a/services/ui-src/src/measures/2022/IETHH/types.ts b/services/ui-src/src/measures/2022/IETHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/IETHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/IETHH/validation.ts b/services/ui-src/src/measures/2022/IETHH/validation.ts index 93309792e0..861bc086d1 100644 --- a/services/ui-src/src/measures/2022/IETHH/validation.ts +++ b/services/ui-src/src/measures/2022/IETHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "../shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "../shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const cleanString = (s: string) => s.replace(/[^\w]/g, ""); diff --git a/services/ui-src/src/measures/2022/IMACH/index.tsx b/services/ui-src/src/measures/2022/IMACH/index.tsx index 3f56aa9d6e..2f60012849 100644 --- a/services/ui-src/src/measures/2022/IMACH/index.tsx +++ b/services/ui-src/src/measures/2022/IMACH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const IMACH = ({ name, diff --git a/services/ui-src/src/measures/2022/IMACH/types.ts b/services/ui-src/src/measures/2022/IMACH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/IMACH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/IMACH/validation.ts b/services/ui-src/src/measures/2022/IMACH/validation.ts index 3d8a2b9383..d1e500ea42 100644 --- a/services/ui-src/src/measures/2022/IMACH/validation.ts +++ b/services/ui-src/src/measures/2022/IMACH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const DEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/IUHH/index.tsx b/services/ui-src/src/measures/2022/IUHH/index.tsx index fd87fc3710..cd1147c47b 100644 --- a/services/ui-src/src/measures/2022/IUHH/index.tsx +++ b/services/ui-src/src/measures/2022/IUHH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const IUHH = ({ name, diff --git a/services/ui-src/src/measures/2022/IUHH/types.ts b/services/ui-src/src/measures/2022/IUHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/IUHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/IUHH/validation.ts b/services/ui-src/src/measures/2022/IUHH/validation.ts index b0a03274bd..5c44f580d8 100644 --- a/services/ui-src/src/measures/2022/IUHH/validation.ts +++ b/services/ui-src/src/measures/2022/IUHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; // Rate structure by index in row const ndrForumlas = [ diff --git a/services/ui-src/src/measures/2022/OEVCH/index.tsx b/services/ui-src/src/measures/2022/OEVCH/index.tsx index 1705d3141c..cd3baf669b 100644 --- a/services/ui-src/src/measures/2022/OEVCH/index.tsx +++ b/services/ui-src/src/measures/2022/OEVCH/index.tsx @@ -2,11 +2,12 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as DC from "dataConstants"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const OEVCH = ({ name, diff --git a/services/ui-src/src/measures/2022/OEVCH/types.ts b/services/ui-src/src/measures/2022/OEVCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/OEVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/OEVCH/validation.ts b/services/ui-src/src/measures/2022/OEVCH/validation.ts index e809b16ef0..ecd27db32a 100644 --- a/services/ui-src/src/measures/2022/OEVCH/validation.ts +++ b/services/ui-src/src/measures/2022/OEVCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const OEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/OHDAD/index.tsx b/services/ui-src/src/measures/2022/OHDAD/index.tsx index 60f6eb35f8..8a89991c19 100644 --- a/services/ui-src/src/measures/2022/OHDAD/index.tsx +++ b/services/ui-src/src/measures/2022/OHDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const OHDAD = ({ name, diff --git a/services/ui-src/src/measures/2022/OHDAD/types.ts b/services/ui-src/src/measures/2022/OHDAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/OHDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/OHDAD/validation.ts b/services/ui-src/src/measures/2022/OHDAD/validation.ts index 11a91073f2..c63a07153d 100644 --- a/services/ui-src/src/measures/2022/OHDAD/validation.ts +++ b/services/ui-src/src/measures/2022/OHDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const OHDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/OUDAD/index.tsx b/services/ui-src/src/measures/2022/OUDAD/index.tsx index b269bf1be4..0b068c2984 100644 --- a/services/ui-src/src/measures/2022/OUDAD/index.tsx +++ b/services/ui-src/src/measures/2022/OUDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const OUDAD = ({ name, diff --git a/services/ui-src/src/measures/2022/OUDAD/types.ts b/services/ui-src/src/measures/2022/OUDAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/OUDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/OUDAD/validation.ts b/services/ui-src/src/measures/2022/OUDAD/validation.ts index 1e235f4d34..74b376e9e1 100644 --- a/services/ui-src/src/measures/2022/OUDAD/validation.ts +++ b/services/ui-src/src/measures/2022/OUDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const OUDValidation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2022/OUDHH/index.tsx b/services/ui-src/src/measures/2022/OUDHH/index.tsx index ea84c5dc25..9d8d9ec6be 100644 --- a/services/ui-src/src/measures/2022/OUDHH/index.tsx +++ b/services/ui-src/src/measures/2022/OUDHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const OUDHH = ({ name, diff --git a/services/ui-src/src/measures/2022/OUDHH/types.ts b/services/ui-src/src/measures/2022/OUDHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/OUDHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/OUDHH/validation.ts b/services/ui-src/src/measures/2022/OUDHH/validation.ts index c2b80bc967..0775ab9b56 100644 --- a/services/ui-src/src/measures/2022/OUDHH/validation.ts +++ b/services/ui-src/src/measures/2022/OUDHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const OUDValidation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2022/PCRAD/index.tsx b/services/ui-src/src/measures/2022/PCRAD/index.tsx index 7fa80808e3..e6f1359339 100644 --- a/services/ui-src/src/measures/2022/PCRAD/index.tsx +++ b/services/ui-src/src/measures/2022/PCRAD/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2022/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { PCRADPerformanceMeasure } from "./questions/PerformanceMeasure"; import { useFormContext } from "react-hook-form"; import { useEffect } from "react"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PCRAD = ({ name, diff --git a/services/ui-src/src/measures/2022/PCRAD/types.ts b/services/ui-src/src/measures/2022/PCRAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PCRAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PCRAD/validation.ts b/services/ui-src/src/measures/2022/PCRAD/validation.ts index 4cad34eb06..1773fc2ef7 100644 --- a/services/ui-src/src/measures/2022/PCRAD/validation.ts +++ b/services/ui-src/src/measures/2022/PCRAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const ndrForumlas = [ { diff --git a/services/ui-src/src/measures/2022/PCRHH/index.tsx b/services/ui-src/src/measures/2022/PCRHH/index.tsx index 065eef0bed..cd209a4390 100644 --- a/services/ui-src/src/measures/2022/PCRHH/index.tsx +++ b/services/ui-src/src/measures/2022/PCRHH/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import { validationFunctions } from "./validation"; import { PCRHHPerformanceMeasure } from "./questions/PerformanceMeasure"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PCRHH = ({ name, diff --git a/services/ui-src/src/measures/2022/PCRHH/types.ts b/services/ui-src/src/measures/2022/PCRHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PCRHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PCRHH/validation.ts b/services/ui-src/src/measures/2022/PCRHH/validation.ts index 10cf722af0..1c7146b4b5 100644 --- a/services/ui-src/src/measures/2022/PCRHH/validation.ts +++ b/services/ui-src/src/measures/2022/PCRHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const ndrForumlas = [ { diff --git a/services/ui-src/src/measures/2022/PPCAD/index.tsx b/services/ui-src/src/measures/2022/PPCAD/index.tsx index 3828fe934a..8948cfef3a 100644 --- a/services/ui-src/src/measures/2022/PPCAD/index.tsx +++ b/services/ui-src/src/measures/2022/PPCAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PPCAD = ({ name, diff --git a/services/ui-src/src/measures/2022/PPCAD/types.ts b/services/ui-src/src/measures/2022/PPCAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PPCAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PPCAD/validation.ts b/services/ui-src/src/measures/2022/PPCAD/validation.ts index 9470645c1d..d0ff94eadc 100644 --- a/services/ui-src/src/measures/2022/PPCAD/validation.ts +++ b/services/ui-src/src/measures/2022/PPCAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PPCADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/PPCCH/index.tsx b/services/ui-src/src/measures/2022/PPCCH/index.tsx index 5ad0f899a5..a384efbad0 100644 --- a/services/ui-src/src/measures/2022/PPCCH/index.tsx +++ b/services/ui-src/src/measures/2022/PPCCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PPCCH = ({ name, diff --git a/services/ui-src/src/measures/2022/PPCCH/types.ts b/services/ui-src/src/measures/2022/PPCCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PPCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PPCCH/validation.ts b/services/ui-src/src/measures/2022/PPCCH/validation.ts index 48b6e1b2ed..8977ea2207 100644 --- a/services/ui-src/src/measures/2022/PPCCH/validation.ts +++ b/services/ui-src/src/measures/2022/PPCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PPCCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/PQI01AD/index.tsx b/services/ui-src/src/measures/2022/PQI01AD/index.tsx index e6aad7a3bf..5a6aa830ce 100644 --- a/services/ui-src/src/measures/2022/PQI01AD/index.tsx +++ b/services/ui-src/src/measures/2022/PQI01AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PQI01AD = ({ name, diff --git a/services/ui-src/src/measures/2022/PQI01AD/types.ts b/services/ui-src/src/measures/2022/PQI01AD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PQI01AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PQI01AD/validation.ts b/services/ui-src/src/measures/2022/PQI01AD/validation.ts index 40ccedcdd9..731a5123b3 100644 --- a/services/ui-src/src/measures/2022/PQI01AD/validation.ts +++ b/services/ui-src/src/measures/2022/PQI01AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PQI01Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2022/PQI05AD/index.tsx b/services/ui-src/src/measures/2022/PQI05AD/index.tsx index 8d5a42f0eb..5581a05d88 100644 --- a/services/ui-src/src/measures/2022/PQI05AD/index.tsx +++ b/services/ui-src/src/measures/2022/PQI05AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PQI05AD = ({ name, diff --git a/services/ui-src/src/measures/2022/PQI05AD/types.ts b/services/ui-src/src/measures/2022/PQI05AD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PQI05AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PQI05AD/validation.ts b/services/ui-src/src/measures/2022/PQI05AD/validation.ts index 3c6de12b11..eebd04e8b6 100644 --- a/services/ui-src/src/measures/2022/PQI05AD/validation.ts +++ b/services/ui-src/src/measures/2022/PQI05AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PQI05Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2022/PQI08AD/index.tsx b/services/ui-src/src/measures/2022/PQI08AD/index.tsx index 9f2f7147fe..5ae520a94d 100644 --- a/services/ui-src/src/measures/2022/PQI08AD/index.tsx +++ b/services/ui-src/src/measures/2022/PQI08AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PQI08AD = ({ name, diff --git a/services/ui-src/src/measures/2022/PQI08AD/types.ts b/services/ui-src/src/measures/2022/PQI08AD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PQI08AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PQI08AD/validation.ts b/services/ui-src/src/measures/2022/PQI08AD/validation.ts index 6d4ad6103f..7190bede47 100644 --- a/services/ui-src/src/measures/2022/PQI08AD/validation.ts +++ b/services/ui-src/src/measures/2022/PQI08AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PQI08Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2022/PQI15AD/index.tsx b/services/ui-src/src/measures/2022/PQI15AD/index.tsx index 8b89b942bc..5b7ed187f0 100644 --- a/services/ui-src/src/measures/2022/PQI15AD/index.tsx +++ b/services/ui-src/src/measures/2022/PQI15AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PQI15AD = ({ name, diff --git a/services/ui-src/src/measures/2022/PQI15AD/types.ts b/services/ui-src/src/measures/2022/PQI15AD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PQI15AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PQI15AD/validation.ts b/services/ui-src/src/measures/2022/PQI15AD/validation.ts index d4e0e518d0..dcee0c46f4 100644 --- a/services/ui-src/src/measures/2022/PQI15AD/validation.ts +++ b/services/ui-src/src/measures/2022/PQI15AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PQI15Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2022/PQI92HH/index.tsx b/services/ui-src/src/measures/2022/PQI92HH/index.tsx index 073bc6b135..b19649444a 100644 --- a/services/ui-src/src/measures/2022/PQI92HH/index.tsx +++ b/services/ui-src/src/measures/2022/PQI92HH/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const PQI92HH = ({ name, diff --git a/services/ui-src/src/measures/2022/PQI92HH/types.ts b/services/ui-src/src/measures/2022/PQI92HH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/PQI92HH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/PQI92HH/validation.ts b/services/ui-src/src/measures/2022/PQI92HH/validation.ts index 8db515242f..87b828876a 100644 --- a/services/ui-src/src/measures/2022/PQI92HH/validation.ts +++ b/services/ui-src/src/measures/2022/PQI92HH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const PQI92Validation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2022/SAAAD/index.tsx b/services/ui-src/src/measures/2022/SAAAD/index.tsx index 31161743ad..9c59e2583e 100644 --- a/services/ui-src/src/measures/2022/SAAAD/index.tsx +++ b/services/ui-src/src/measures/2022/SAAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const SAAAD = ({ name, diff --git a/services/ui-src/src/measures/2022/SAAAD/types.ts b/services/ui-src/src/measures/2022/SAAAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/SAAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/SAAAD/validation.ts b/services/ui-src/src/measures/2022/SAAAD/validation.ts index 48ea2a0626..ec2dd604ca 100644 --- a/services/ui-src/src/measures/2022/SAAAD/validation.ts +++ b/services/ui-src/src/measures/2022/SAAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const SAAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/SFMCH/index.tsx b/services/ui-src/src/measures/2022/SFMCH/index.tsx index affd04f6e1..2fd0584657 100644 --- a/services/ui-src/src/measures/2022/SFMCH/index.tsx +++ b/services/ui-src/src/measures/2022/SFMCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const SFMCH = ({ name, diff --git a/services/ui-src/src/measures/2022/SFMCH/types.ts b/services/ui-src/src/measures/2022/SFMCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/SFMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/SFMCH/validation.ts b/services/ui-src/src/measures/2022/SFMCH/validation.ts index 3e6627c5ac..3bfc37fa2a 100644 --- a/services/ui-src/src/measures/2022/SFMCH/validation.ts +++ b/services/ui-src/src/measures/2022/SFMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const SFMCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/SSDAD/index.tsx b/services/ui-src/src/measures/2022/SSDAD/index.tsx index dc2b0f9dfa..5930510c48 100644 --- a/services/ui-src/src/measures/2022/SSDAD/index.tsx +++ b/services/ui-src/src/measures/2022/SSDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const SSDAD = ({ name, diff --git a/services/ui-src/src/measures/2022/SSDAD/types.ts b/services/ui-src/src/measures/2022/SSDAD/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/SSDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/SSDAD/validation.ts b/services/ui-src/src/measures/2022/SSDAD/validation.ts index 95724fbf62..3262e2f3ca 100644 --- a/services/ui-src/src/measures/2022/SSDAD/validation.ts +++ b/services/ui-src/src/measures/2022/SSDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const SSDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/SSHH/types.ts b/services/ui-src/src/measures/2022/SSHH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/SSHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/SSHH/validation.ts b/services/ui-src/src/measures/2022/SSHH/validation.ts index b97e8f9012..f23004e28a 100644 --- a/services/ui-src/src/measures/2022/SSHH/validation.ts +++ b/services/ui-src/src/measures/2022/SSHH/validation.ts @@ -1,6 +1,7 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export interface FormRateField { denominator?: string; diff --git a/services/ui-src/src/measures/2022/TFLCH/index.tsx b/services/ui-src/src/measures/2022/TFLCH/index.tsx index 54e438945e..0ca92ac554 100644 --- a/services/ui-src/src/measures/2022/TFLCH/index.tsx +++ b/services/ui-src/src/measures/2022/TFLCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const TFLCH = ({ name, diff --git a/services/ui-src/src/measures/2022/TFLCH/types.ts b/services/ui-src/src/measures/2022/TFLCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/TFLCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/TFLCH/validation.ts b/services/ui-src/src/measures/2022/TFLCH/validation.ts index 1fd60893e7..778fba7692 100644 --- a/services/ui-src/src/measures/2022/TFLCH/validation.ts +++ b/services/ui-src/src/measures/2022/TFLCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const TFLCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/W30CH/index.tsx b/services/ui-src/src/measures/2022/W30CH/index.tsx index 90cc5a6477..7491364b03 100644 --- a/services/ui-src/src/measures/2022/W30CH/index.tsx +++ b/services/ui-src/src/measures/2022/W30CH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const W30CH = ({ name, diff --git a/services/ui-src/src/measures/2022/W30CH/types.ts b/services/ui-src/src/measures/2022/W30CH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/W30CH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/W30CH/validation.ts b/services/ui-src/src/measures/2022/W30CH/validation.ts index 998c0d221c..fb89de7d32 100644 --- a/services/ui-src/src/measures/2022/W30CH/validation.ts +++ b/services/ui-src/src/measures/2022/W30CH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const W30CHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/WCCCH/index.tsx b/services/ui-src/src/measures/2022/WCCCH/index.tsx index dcbb357d21..9c8d6a49c9 100644 --- a/services/ui-src/src/measures/2022/WCCCH/index.tsx +++ b/services/ui-src/src/measures/2022/WCCCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const WCCCH = ({ name, diff --git a/services/ui-src/src/measures/2022/WCCCH/types.ts b/services/ui-src/src/measures/2022/WCCCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/WCCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/WCCCH/validation.ts b/services/ui-src/src/measures/2022/WCCCH/validation.ts index 6ae9996bab..7c58b9170e 100644 --- a/services/ui-src/src/measures/2022/WCCCH/validation.ts +++ b/services/ui-src/src/measures/2022/WCCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const WCCHValidation = (data: FormData) => { const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; diff --git a/services/ui-src/src/measures/2022/WCVCH/index.tsx b/services/ui-src/src/measures/2022/WCVCH/index.tsx index 868265e8e8..dd181b9bff 100644 --- a/services/ui-src/src/measures/2022/WCVCH/index.tsx +++ b/services/ui-src/src/measures/2022/WCVCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2022/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; export const WCVCH = ({ name, diff --git a/services/ui-src/src/measures/2022/WCVCH/types.ts b/services/ui-src/src/measures/2022/WCVCH/types.ts deleted file mode 100644 index 6db300349d..0000000000 --- a/services/ui-src/src/measures/2022/WCVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2022/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2022/WCVCH/validation.ts b/services/ui-src/src/measures/2022/WCVCH/validation.ts index 316768e297..045de19d74 100644 --- a/services/ui-src/src/measures/2022/WCVCH/validation.ts +++ b/services/ui-src/src/measures/2022/WCVCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2022/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2022/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2022/shared/CommonQuestions/types"; const WCVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2022/shared/CommonQuestions/AdditionalNotes/index.test.tsx b/services/ui-src/src/measures/2022/shared/CommonQuestions/AdditionalNotes/index.test.tsx deleted file mode 100644 index 4ff2ff08ff..0000000000 --- a/services/ui-src/src/measures/2022/shared/CommonQuestions/AdditionalNotes/index.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import fireEvent from "@testing-library/user-event"; -import { AdditionalNotes } from "."; -import { screen } from "@testing-library/react"; -import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; - -describe("Test AdditionalNotes component", () => { - beforeEach(() => { - renderWithHookForm(<AdditionalNotes />); - }); - - it("component renders", () => { - expect( - screen.getByText("Additional Notes/Comments on the measure (optional)") - ).toBeInTheDocument(); - expect( - screen.getByText( - "If you need additional space to include comments or supplemental information, please attach further documentation below." - ) - ).toBeInTheDocument(); - }); - - it("accepts input", async () => { - const textArea = await screen.findByLabelText( - "Please add any additional notes or comments on the measure not otherwise captured above:" - ); - fireEvent.type(textArea, "This is the test text"); - expect(textArea).toHaveDisplayValue("This is the test text"); - }); -}); diff --git a/services/ui-src/src/measures/2022/shared/CommonQuestions/AdditionalNotes/index.tsx b/services/ui-src/src/measures/2022/shared/CommonQuestions/AdditionalNotes/index.tsx deleted file mode 100644 index 66b5713b70..0000000000 --- a/services/ui-src/src/measures/2022/shared/CommonQuestions/AdditionalNotes/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as QMR from "components"; -import * as CUI from "@chakra-ui/react"; -import { useCustomRegister } from "hooks/useCustomRegister"; -import { Upload } from "components/Upload"; -import * as Types from "../types"; -import * as DC from "dataConstants"; - -export const AdditionalNotes = () => { - const register = useCustomRegister<Types.AdditionalNotes>(); - - return ( - <QMR.CoreQuestionWrapper - testid="additional-notes" - label="Additional Notes/Comments on the measure (optional)" - > - <QMR.TextArea - label="Please add any additional notes or comments on the measure not otherwise captured above:" - {...register(DC.ADDITIONAL_NOTES)} - /> - <CUI.Box marginTop={10}> - <Upload - label="If you need additional space to include comments or supplemental information, please attach further documentation below." - {...register(DC.ADDITIONAL_NOTES_UPLOAD)} - /> - </CUI.Box> - </QMR.CoreQuestionWrapper> - ); -}; diff --git a/services/ui-src/src/measures/2022/shared/CommonQuestions/index.tsx b/services/ui-src/src/measures/2022/shared/CommonQuestions/index.tsx index 3b75284a67..63b075e014 100644 --- a/services/ui-src/src/measures/2022/shared/CommonQuestions/index.tsx +++ b/services/ui-src/src/measures/2022/shared/CommonQuestions/index.tsx @@ -3,7 +3,7 @@ export * from "./DateRange"; export * from "./DefinitionsOfPopulation"; export * from "./DataSource"; export * from "./DataSourceCahps"; -export * from "./AdditionalNotes"; +export * from "shared/commonQuestions/AdditionalNotes"; export * from "./OtherPerformanceMeasure"; export * from "./Reporting"; export * from "./StatusOfData"; diff --git a/services/ui-src/src/measures/2023/AABAD/index.tsx b/services/ui-src/src/measures/2023/AABAD/index.tsx index 7023b6204d..3483260d98 100644 --- a/services/ui-src/src/measures/2023/AABAD/index.tsx +++ b/services/ui-src/src/measures/2023/AABAD/index.tsx @@ -5,10 +5,11 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; -import { FormData } from "./types"; import { validationFunctions } from "./validation"; import { AABRateCalculation } from "utils/rateFormulas"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AABAD = ({ name, diff --git a/services/ui-src/src/measures/2023/AABAD/types.ts b/services/ui-src/src/measures/2023/AABAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AABAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AABAD/validation.ts b/services/ui-src/src/measures/2023/AABAD/validation.ts index a81089a836..a76dd00099 100644 --- a/services/ui-src/src/measures/2023/AABAD/validation.ts +++ b/services/ui-src/src/measures/2023/AABAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const AABADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/AABCH/index.tsx b/services/ui-src/src/measures/2023/AABCH/index.tsx index a6f6bbfbb8..ce851e58ab 100644 --- a/services/ui-src/src/measures/2023/AABCH/index.tsx +++ b/services/ui-src/src/measures/2023/AABCH/index.tsx @@ -1,13 +1,14 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { AABRateCalculation } from "utils/rateFormulas"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AABCH = ({ name, diff --git a/services/ui-src/src/measures/2023/AABCH/types.ts b/services/ui-src/src/measures/2023/AABCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AABCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AABCH/validation.ts b/services/ui-src/src/measures/2023/AABCH/validation.ts index fbfd842fac..53d1d66509 100644 --- a/services/ui-src/src/measures/2023/AABCH/validation.ts +++ b/services/ui-src/src/measures/2023/AABCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const AABCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/ADDCH/index.tsx b/services/ui-src/src/measures/2023/ADDCH/index.tsx index 6f36043b17..2a80e9bd74 100644 --- a/services/ui-src/src/measures/2023/ADDCH/index.tsx +++ b/services/ui-src/src/measures/2023/ADDCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const ADDCH = ({ name, diff --git a/services/ui-src/src/measures/2023/ADDCH/types.ts b/services/ui-src/src/measures/2023/ADDCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/ADDCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/ADDCH/validation.ts b/services/ui-src/src/measures/2023/ADDCH/validation.ts index ce41192a41..25092b3e85 100644 --- a/services/ui-src/src/measures/2023/ADDCH/validation.ts +++ b/services/ui-src/src/measures/2023/ADDCH/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "../shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const ADDCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/AIFHH/index.tsx b/services/ui-src/src/measures/2023/AIFHH/index.tsx index c7fe58c852..e5d1ef5740 100644 --- a/services/ui-src/src/measures/2023/AIFHH/index.tsx +++ b/services/ui-src/src/measures/2023/AIFHH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AIFHH = ({ name, diff --git a/services/ui-src/src/measures/2023/AIFHH/types.ts b/services/ui-src/src/measures/2023/AIFHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AIFHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AIFHH/validation.ts b/services/ui-src/src/measures/2023/AIFHH/validation.ts index 446b8ea127..743f58124a 100644 --- a/services/ui-src/src/measures/2023/AIFHH/validation.ts +++ b/services/ui-src/src/measures/2023/AIFHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; // Rate structure by index in row const ndrFormulas = [ diff --git a/services/ui-src/src/measures/2023/AMBCH/index.tsx b/services/ui-src/src/measures/2023/AMBCH/index.tsx index 94ee12129e..fe4a45277f 100644 --- a/services/ui-src/src/measures/2023/AMBCH/index.tsx +++ b/services/ui-src/src/measures/2023/AMBCH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AMBCH = ({ name, diff --git a/services/ui-src/src/measures/2023/AMBCH/types.ts b/services/ui-src/src/measures/2023/AMBCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AMBCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AMBCH/validation.ts b/services/ui-src/src/measures/2023/AMBCH/validation.ts index d45078867a..ecab530ce5 100644 --- a/services/ui-src/src/measures/2023/AMBCH/validation.ts +++ b/services/ui-src/src/measures/2023/AMBCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const AMBCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/AMBHH/index.tsx b/services/ui-src/src/measures/2023/AMBHH/index.tsx index f5e5d43601..4b69889b77 100644 --- a/services/ui-src/src/measures/2023/AMBHH/index.tsx +++ b/services/ui-src/src/measures/2023/AMBHH/index.tsx @@ -5,8 +5,9 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AMBHH = ({ name, diff --git a/services/ui-src/src/measures/2023/AMBHH/types.ts b/services/ui-src/src/measures/2023/AMBHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AMBHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AMBHH/validation.ts b/services/ui-src/src/measures/2023/AMBHH/validation.ts index 4eb3161c35..bb46b2f620 100644 --- a/services/ui-src/src/measures/2023/AMBHH/validation.ts +++ b/services/ui-src/src/measures/2023/AMBHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const AMBHHValidation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2023/AMMAD/index.tsx b/services/ui-src/src/measures/2023/AMMAD/index.tsx index 2a635e6851..b30637feb1 100644 --- a/services/ui-src/src/measures/2023/AMMAD/index.tsx +++ b/services/ui-src/src/measures/2023/AMMAD/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AMMAD = ({ name, diff --git a/services/ui-src/src/measures/2023/AMMAD/types.ts b/services/ui-src/src/measures/2023/AMMAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AMMAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AMMAD/validation.ts b/services/ui-src/src/measures/2023/AMMAD/validation.ts index 760aadf573..46fa86de6f 100644 --- a/services/ui-src/src/measures/2023/AMMAD/validation.ts +++ b/services/ui-src/src/measures/2023/AMMAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const AMMADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/AMRCH/index.tsx b/services/ui-src/src/measures/2023/AMRCH/index.tsx index 46f6c4e05a..bccdb96e14 100644 --- a/services/ui-src/src/measures/2023/AMRCH/index.tsx +++ b/services/ui-src/src/measures/2023/AMRCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const AMRCH = ({ name, diff --git a/services/ui-src/src/measures/2023/AMRCH/types.ts b/services/ui-src/src/measures/2023/AMRCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/AMRCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/AMRCH/validation.ts b/services/ui-src/src/measures/2023/AMRCH/validation.ts index cdca9d165e..c54119dad1 100644 --- a/services/ui-src/src/measures/2023/AMRCH/validation.ts +++ b/services/ui-src/src/measures/2023/AMRCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const AMRCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/APMCH/index.tsx b/services/ui-src/src/measures/2023/APMCH/index.tsx index c7c2a4033e..9e31f2a679 100644 --- a/services/ui-src/src/measures/2023/APMCH/index.tsx +++ b/services/ui-src/src/measures/2023/APMCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const APMCH = ({ name, diff --git a/services/ui-src/src/measures/2023/APMCH/types.ts b/services/ui-src/src/measures/2023/APMCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/APMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/APMCH/validation.ts b/services/ui-src/src/measures/2023/APMCH/validation.ts index 1e94789dc6..20b1c24f4a 100644 --- a/services/ui-src/src/measures/2023/APMCH/validation.ts +++ b/services/ui-src/src/measures/2023/APMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const APMCHValidation = (data: FormData) => { const dateRange = data[DC.DATE_RANGE]; diff --git a/services/ui-src/src/measures/2023/APPCH/index.tsx b/services/ui-src/src/measures/2023/APPCH/index.tsx index 2b320d38ce..4156aa8fd8 100644 --- a/services/ui-src/src/measures/2023/APPCH/index.tsx +++ b/services/ui-src/src/measures/2023/APPCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const APPCH = ({ name, diff --git a/services/ui-src/src/measures/2023/APPCH/types.ts b/services/ui-src/src/measures/2023/APPCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/APPCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/APPCH/validation.ts b/services/ui-src/src/measures/2023/APPCH/validation.ts index f1f840502c..968f09ec88 100644 --- a/services/ui-src/src/measures/2023/APPCH/validation.ts +++ b/services/ui-src/src/measures/2023/APPCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const APPCHValidation = (data: FormData) => { const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; diff --git a/services/ui-src/src/measures/2023/BCSAD/index.tsx b/services/ui-src/src/measures/2023/BCSAD/index.tsx index 83926c35d3..3481076431 100644 --- a/services/ui-src/src/measures/2023/BCSAD/index.tsx +++ b/services/ui-src/src/measures/2023/BCSAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const BCSAD = ({ name, diff --git a/services/ui-src/src/measures/2023/BCSAD/types.ts b/services/ui-src/src/measures/2023/BCSAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/BCSAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/BCSAD/validation.ts b/services/ui-src/src/measures/2023/BCSAD/validation.ts index c5c3155175..65072f67f3 100644 --- a/services/ui-src/src/measures/2023/BCSAD/validation.ts +++ b/services/ui-src/src/measures/2023/BCSAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const BCSValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CBPAD/index.tsx b/services/ui-src/src/measures/2023/CBPAD/index.tsx index ef5265e1bc..030819f59c 100644 --- a/services/ui-src/src/measures/2023/CBPAD/index.tsx +++ b/services/ui-src/src/measures/2023/CBPAD/index.tsx @@ -5,7 +5,8 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CBPAD = ({ name, diff --git a/services/ui-src/src/measures/2023/CBPAD/types.ts b/services/ui-src/src/measures/2023/CBPAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CBPAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CBPAD/validation.ts b/services/ui-src/src/measures/2023/CBPAD/validation.ts index 5b99eb23fe..47f52b5ed6 100644 --- a/services/ui-src/src/measures/2023/CBPAD/validation.ts +++ b/services/ui-src/src/measures/2023/CBPAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CBPValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CBPHH/index.tsx b/services/ui-src/src/measures/2023/CBPHH/index.tsx index 92068b20ad..37c62ec50d 100644 --- a/services/ui-src/src/measures/2023/CBPHH/index.tsx +++ b/services/ui-src/src/measures/2023/CBPHH/index.tsx @@ -5,7 +5,8 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CBPHH = ({ name, diff --git a/services/ui-src/src/measures/2023/CBPHH/types.ts b/services/ui-src/src/measures/2023/CBPHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CBPHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CBPHH/validation.ts b/services/ui-src/src/measures/2023/CBPHH/validation.ts index 52e09064a1..c9d346520e 100644 --- a/services/ui-src/src/measures/2023/CBPHH/validation.ts +++ b/services/ui-src/src/measures/2023/CBPHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CBPValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CCPAD/index.tsx b/services/ui-src/src/measures/2023/CCPAD/index.tsx index 8f65899d48..506ed0eb32 100644 --- a/services/ui-src/src/measures/2023/CCPAD/index.tsx +++ b/services/ui-src/src/measures/2023/CCPAD/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "../shared/CommonQuestions"; import * as QMR from "components"; -import { FormData } from "./types"; import { useEffect } from "react"; import { validationFunctions } from "./validation"; import * as PMD from "./data"; import { useFormContext } from "react-hook-form"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CCPAD = ({ name, diff --git a/services/ui-src/src/measures/2023/CCPAD/types.ts b/services/ui-src/src/measures/2023/CCPAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CCPAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CCPAD/validation.ts b/services/ui-src/src/measures/2023/CCPAD/validation.ts index a2aa467d20..328694059b 100644 --- a/services/ui-src/src/measures/2023/CCPAD/validation.ts +++ b/services/ui-src/src/measures/2023/CCPAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CCPADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CCPCH/index.tsx b/services/ui-src/src/measures/2023/CCPCH/index.tsx index b6af665a27..8ae82a5591 100644 --- a/services/ui-src/src/measures/2023/CCPCH/index.tsx +++ b/services/ui-src/src/measures/2023/CCPCH/index.tsx @@ -2,11 +2,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CCPCH = ({ name, diff --git a/services/ui-src/src/measures/2023/CCPCH/types.ts b/services/ui-src/src/measures/2023/CCPCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CCPCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CCPCH/validation.ts b/services/ui-src/src/measures/2023/CCPCH/validation.ts index b05831d0b9..ff70cd024b 100644 --- a/services/ui-src/src/measures/2023/CCPCH/validation.ts +++ b/services/ui-src/src/measures/2023/CCPCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CCPCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CCSAD/index.tsx b/services/ui-src/src/measures/2023/CCSAD/index.tsx index 7e83d20472..4b0027ec63 100644 --- a/services/ui-src/src/measures/2023/CCSAD/index.tsx +++ b/services/ui-src/src/measures/2023/CCSAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CCSAD = ({ name, diff --git a/services/ui-src/src/measures/2023/CCSAD/types.ts b/services/ui-src/src/measures/2023/CCSAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CCSAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CCSAD/validation.ts b/services/ui-src/src/measures/2023/CCSAD/validation.ts index 49872bc58f..3127e94535 100644 --- a/services/ui-src/src/measures/2023/CCSAD/validation.ts +++ b/services/ui-src/src/measures/2023/CCSAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CCSADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CCWAD/index.tsx b/services/ui-src/src/measures/2023/CCWAD/index.tsx index c5da64ee51..f50522a14b 100644 --- a/services/ui-src/src/measures/2023/CCWAD/index.tsx +++ b/services/ui-src/src/measures/2023/CCWAD/index.tsx @@ -4,8 +4,9 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CCWAD = ({ name, diff --git a/services/ui-src/src/measures/2023/CCWAD/types.ts b/services/ui-src/src/measures/2023/CCWAD/types.ts deleted file mode 100644 index 25e32b8193..0000000000 --- a/services/ui-src/src/measures/2023/CCWAD/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type DeviationCheckBoxOptions = - | "moderate-method-deviation-Numerator" - | "moderate-method-deviation-Denominator" - | "moderate-method-deviation-Other" - | "reversible-method-deviation-Numerator" - | "reversible-method-deviation-Denominator" - | "reversible-method-deviation-Other"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CCWAD/validation.ts b/services/ui-src/src/measures/2023/CCWAD/validation.ts index b9ffbbd50c..b756192c3c 100644 --- a/services/ui-src/src/measures/2023/CCWAD/validation.ts +++ b/services/ui-src/src/measures/2023/CCWAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CCWADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CCWCH/index.tsx b/services/ui-src/src/measures/2023/CCWCH/index.tsx index 179111f933..c17804fd03 100644 --- a/services/ui-src/src/measures/2023/CCWCH/index.tsx +++ b/services/ui-src/src/measures/2023/CCWCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CCWCH = ({ name, diff --git a/services/ui-src/src/measures/2023/CCWCH/types.ts b/services/ui-src/src/measures/2023/CCWCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CCWCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CCWCH/validation.ts b/services/ui-src/src/measures/2023/CCWCH/validation.ts index 5e7208a4ce..2d45600476 100644 --- a/services/ui-src/src/measures/2023/CCWCH/validation.ts +++ b/services/ui-src/src/measures/2023/CCWCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CCWCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CDFAD/index.tsx b/services/ui-src/src/measures/2023/CDFAD/index.tsx index 1100cd2674..ae395f4b27 100644 --- a/services/ui-src/src/measures/2023/CDFAD/index.tsx +++ b/services/ui-src/src/measures/2023/CDFAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CDFAD = ({ name, diff --git a/services/ui-src/src/measures/2023/CDFAD/types.ts b/services/ui-src/src/measures/2023/CDFAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CDFAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CDFAD/validation.ts b/services/ui-src/src/measures/2023/CDFAD/validation.ts index f75f0ce025..618f860f02 100644 --- a/services/ui-src/src/measures/2023/CDFAD/validation.ts +++ b/services/ui-src/src/measures/2023/CDFAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CDFADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CDFCH/index.tsx b/services/ui-src/src/measures/2023/CDFCH/index.tsx index 14f23c431f..ab6f4d2c93 100644 --- a/services/ui-src/src/measures/2023/CDFCH/index.tsx +++ b/services/ui-src/src/measures/2023/CDFCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CDFCH = ({ name, diff --git a/services/ui-src/src/measures/2023/CDFCH/types.ts b/services/ui-src/src/measures/2023/CDFCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CDFCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CDFCH/validation.ts b/services/ui-src/src/measures/2023/CDFCH/validation.ts index 341f52290f..c5adeec48d 100644 --- a/services/ui-src/src/measures/2023/CDFCH/validation.ts +++ b/services/ui-src/src/measures/2023/CDFCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CDFCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CDFHH/index.tsx b/services/ui-src/src/measures/2023/CDFHH/index.tsx index 4d7a3fd29c..f48395c3a3 100644 --- a/services/ui-src/src/measures/2023/CDFHH/index.tsx +++ b/services/ui-src/src/measures/2023/CDFHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CDFHH = ({ name, diff --git a/services/ui-src/src/measures/2023/CDFHH/types.ts b/services/ui-src/src/measures/2023/CDFHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CDFHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CDFHH/validation.ts b/services/ui-src/src/measures/2023/CDFHH/validation.ts index 3cb77e920d..857c4355d7 100644 --- a/services/ui-src/src/measures/2023/CDFHH/validation.ts +++ b/services/ui-src/src/measures/2023/CDFHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CDFHHValidation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2023/CHLAD/index.tsx b/services/ui-src/src/measures/2023/CHLAD/index.tsx index c5b94a7b2d..471352d4fe 100644 --- a/services/ui-src/src/measures/2023/CHLAD/index.tsx +++ b/services/ui-src/src/measures/2023/CHLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CHLAD = ({ name, diff --git a/services/ui-src/src/measures/2023/CHLAD/types.ts b/services/ui-src/src/measures/2023/CHLAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CHLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CHLAD/validation.ts b/services/ui-src/src/measures/2023/CHLAD/validation.ts index bd21bd820d..b1e36d8527 100644 --- a/services/ui-src/src/measures/2023/CHLAD/validation.ts +++ b/services/ui-src/src/measures/2023/CHLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CHLValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CHLCH/index.tsx b/services/ui-src/src/measures/2023/CHLCH/index.tsx index 1e04dac86d..4df7cf300f 100644 --- a/services/ui-src/src/measures/2023/CHLCH/index.tsx +++ b/services/ui-src/src/measures/2023/CHLCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CHLCH = ({ name, diff --git a/services/ui-src/src/measures/2023/CHLCH/types.ts b/services/ui-src/src/measures/2023/CHLCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CHLCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CHLCH/validation.ts b/services/ui-src/src/measures/2023/CHLCH/validation.ts index 7352f40d28..59b668760f 100644 --- a/services/ui-src/src/measures/2023/CHLCH/validation.ts +++ b/services/ui-src/src/measures/2023/CHLCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CHLValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CISCH/index.tsx b/services/ui-src/src/measures/2023/CISCH/index.tsx index c94a2d140d..5e898195c5 100644 --- a/services/ui-src/src/measures/2023/CISCH/index.tsx +++ b/services/ui-src/src/measures/2023/CISCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const CISCH = ({ name, diff --git a/services/ui-src/src/measures/2023/CISCH/types.ts b/services/ui-src/src/measures/2023/CISCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CISCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CISCH/validation.ts b/services/ui-src/src/measures/2023/CISCH/validation.ts index e203580703..16ebb25467 100644 --- a/services/ui-src/src/measures/2023/CISCH/validation.ts +++ b/services/ui-src/src/measures/2023/CISCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CISCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/COBAD/index.tsx b/services/ui-src/src/measures/2023/COBAD/index.tsx index 3206dd4907..623889f3c4 100644 --- a/services/ui-src/src/measures/2023/COBAD/index.tsx +++ b/services/ui-src/src/measures/2023/COBAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const COBAD = ({ name, diff --git a/services/ui-src/src/measures/2023/COBAD/types.ts b/services/ui-src/src/measures/2023/COBAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/COBAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/COBAD/validation.ts b/services/ui-src/src/measures/2023/COBAD/validation.ts index 631fd3ab91..3ef25bc30e 100644 --- a/services/ui-src/src/measures/2023/COBAD/validation.ts +++ b/services/ui-src/src/measures/2023/COBAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const COBADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/COLAD/index.tsx b/services/ui-src/src/measures/2023/COLAD/index.tsx index a051317ee8..0195b59690 100644 --- a/services/ui-src/src/measures/2023/COLAD/index.tsx +++ b/services/ui-src/src/measures/2023/COLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const COLAD = ({ name, diff --git a/services/ui-src/src/measures/2023/COLAD/types.ts b/services/ui-src/src/measures/2023/COLAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/COLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/COLAD/validation.ts b/services/ui-src/src/measures/2023/COLAD/validation.ts index b4fc0c6334..513dcded1b 100644 --- a/services/ui-src/src/measures/2023/COLAD/validation.ts +++ b/services/ui-src/src/measures/2023/COLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const COLADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/COLHH/index.tsx b/services/ui-src/src/measures/2023/COLHH/index.tsx index 84ac5b9a61..fa26068919 100644 --- a/services/ui-src/src/measures/2023/COLHH/index.tsx +++ b/services/ui-src/src/measures/2023/COLHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const COLHH = ({ name, diff --git a/services/ui-src/src/measures/2023/COLHH/types.ts b/services/ui-src/src/measures/2023/COLHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/COLHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/COLHH/validation.ts b/services/ui-src/src/measures/2023/COLHH/validation.ts index f8c7da4c2e..6e3b8ec7fd 100644 --- a/services/ui-src/src/measures/2023/COLHH/validation.ts +++ b/services/ui-src/src/measures/2023/COLHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const COLHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/CPUAD/types.ts b/services/ui-src/src/measures/2023/CPUAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/CPUAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/CPUAD/validation.ts b/services/ui-src/src/measures/2023/CPUAD/validation.ts index 8ac39ce29b..0ae8409400 100644 --- a/services/ui-src/src/measures/2023/CPUAD/validation.ts +++ b/services/ui-src/src/measures/2023/CPUAD/validation.ts @@ -1,7 +1,8 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const CPUADValidation = (data: FormData) => { const carePlans = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/DEVCH/index.tsx b/services/ui-src/src/measures/2023/DEVCH/index.tsx index 313b9a7db5..02d1146578 100644 --- a/services/ui-src/src/measures/2023/DEVCH/index.tsx +++ b/services/ui-src/src/measures/2023/DEVCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const DEVCH = ({ name, diff --git a/services/ui-src/src/measures/2023/DEVCH/types.ts b/services/ui-src/src/measures/2023/DEVCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/DEVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/DEVCH/validation.ts b/services/ui-src/src/measures/2023/DEVCH/validation.ts index d003c0ee01..98133edb5d 100644 --- a/services/ui-src/src/measures/2023/DEVCH/validation.ts +++ b/services/ui-src/src/measures/2023/DEVCH/validation.ts @@ -1,8 +1,9 @@ import * as PMD from "./data"; import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const DEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUAAD/index.tsx b/services/ui-src/src/measures/2023/FUAAD/index.tsx index e5015f8bb1..4ac514d6f8 100644 --- a/services/ui-src/src/measures/2023/FUAAD/index.tsx +++ b/services/ui-src/src/measures/2023/FUAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUAAD = ({ name, diff --git a/services/ui-src/src/measures/2023/FUAAD/types.ts b/services/ui-src/src/measures/2023/FUAAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUAAD/validation.ts b/services/ui-src/src/measures/2023/FUAAD/validation.ts index 79be820860..1e6fa03d2b 100644 --- a/services/ui-src/src/measures/2023/FUAAD/validation.ts +++ b/services/ui-src/src/measures/2023/FUAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUACH/index.tsx b/services/ui-src/src/measures/2023/FUACH/index.tsx index 76ee319e37..7048478f02 100644 --- a/services/ui-src/src/measures/2023/FUACH/index.tsx +++ b/services/ui-src/src/measures/2023/FUACH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUACH = ({ name, diff --git a/services/ui-src/src/measures/2023/FUACH/types.ts b/services/ui-src/src/measures/2023/FUACH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUACH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUACH/validation.ts b/services/ui-src/src/measures/2023/FUACH/validation.ts index 710c1055d7..36c814ecd8 100644 --- a/services/ui-src/src/measures/2023/FUACH/validation.ts +++ b/services/ui-src/src/measures/2023/FUACH/validation.ts @@ -2,8 +2,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "../shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUACHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUAHH/index.tsx b/services/ui-src/src/measures/2023/FUAHH/index.tsx index b1845ae19c..caadb568f1 100644 --- a/services/ui-src/src/measures/2023/FUAHH/index.tsx +++ b/services/ui-src/src/measures/2023/FUAHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUAHH = ({ name, diff --git a/services/ui-src/src/measures/2023/FUAHH/types.ts b/services/ui-src/src/measures/2023/FUAHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUAHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUAHH/validation.ts b/services/ui-src/src/measures/2023/FUAHH/validation.ts index 8fc30e5513..8fe6015a6a 100644 --- a/services/ui-src/src/measures/2023/FUAHH/validation.ts +++ b/services/ui-src/src/measures/2023/FUAHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUAHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUHAD/index.tsx b/services/ui-src/src/measures/2023/FUHAD/index.tsx index 40162e1981..95a37eac26 100644 --- a/services/ui-src/src/measures/2023/FUHAD/index.tsx +++ b/services/ui-src/src/measures/2023/FUHAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUHAD = ({ name, diff --git a/services/ui-src/src/measures/2023/FUHAD/types.ts b/services/ui-src/src/measures/2023/FUHAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUHAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUHAD/validation.ts b/services/ui-src/src/measures/2023/FUHAD/validation.ts index ed225baa3a..79a1b4fbcb 100644 --- a/services/ui-src/src/measures/2023/FUHAD/validation.ts +++ b/services/ui-src/src/measures/2023/FUHAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as PMD from "./data"; import * as GV from "../shared/globalValidations"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUHCH/index.tsx b/services/ui-src/src/measures/2023/FUHCH/index.tsx index 0526d623ea..57fb120cdb 100644 --- a/services/ui-src/src/measures/2023/FUHCH/index.tsx +++ b/services/ui-src/src/measures/2023/FUHCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUHCH = ({ name, diff --git a/services/ui-src/src/measures/2023/FUHCH/types.ts b/services/ui-src/src/measures/2023/FUHCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUHCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUHCH/validation.ts b/services/ui-src/src/measures/2023/FUHCH/validation.ts index b1f4478783..98fa4de957 100644 --- a/services/ui-src/src/measures/2023/FUHCH/validation.ts +++ b/services/ui-src/src/measures/2023/FUHCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUHHH/index.tsx b/services/ui-src/src/measures/2023/FUHHH/index.tsx index 1723192851..d14ee4b647 100644 --- a/services/ui-src/src/measures/2023/FUHHH/index.tsx +++ b/services/ui-src/src/measures/2023/FUHHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUHHH = ({ name, diff --git a/services/ui-src/src/measures/2023/FUHHH/types.ts b/services/ui-src/src/measures/2023/FUHHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUHHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUHHH/validation.ts b/services/ui-src/src/measures/2023/FUHHH/validation.ts index a5e43b9315..23b89d4dc0 100644 --- a/services/ui-src/src/measures/2023/FUHHH/validation.ts +++ b/services/ui-src/src/measures/2023/FUHHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUHHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUMAD/index.tsx b/services/ui-src/src/measures/2023/FUMAD/index.tsx index 7e2258058f..565b85637f 100644 --- a/services/ui-src/src/measures/2023/FUMAD/index.tsx +++ b/services/ui-src/src/measures/2023/FUMAD/index.tsx @@ -5,7 +5,8 @@ import * as QMR from "components"; import * as PMD from "./data"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { validationFunctions } from "./validation"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUMAD = ({ name, diff --git a/services/ui-src/src/measures/2023/FUMAD/types.ts b/services/ui-src/src/measures/2023/FUMAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUMAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUMAD/validation.ts b/services/ui-src/src/measures/2023/FUMAD/validation.ts index 02e4294940..36d0666217 100644 --- a/services/ui-src/src/measures/2023/FUMAD/validation.ts +++ b/services/ui-src/src/measures/2023/FUMAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUMADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUMCH/index.tsx b/services/ui-src/src/measures/2023/FUMCH/index.tsx index 568cceab09..0cb62cf33d 100644 --- a/services/ui-src/src/measures/2023/FUMCH/index.tsx +++ b/services/ui-src/src/measures/2023/FUMCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUMCH = ({ name, diff --git a/services/ui-src/src/measures/2023/FUMCH/types.ts b/services/ui-src/src/measures/2023/FUMCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUMCH/validation.ts b/services/ui-src/src/measures/2023/FUMCH/validation.ts index f7d83be7bf..adaf86fd13 100644 --- a/services/ui-src/src/measures/2023/FUMCH/validation.ts +++ b/services/ui-src/src/measures/2023/FUMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUMCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FUMHH/index.tsx b/services/ui-src/src/measures/2023/FUMHH/index.tsx index c189b63ece..537225d901 100644 --- a/services/ui-src/src/measures/2023/FUMHH/index.tsx +++ b/services/ui-src/src/measures/2023/FUMHH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const FUMHH = ({ isNotReportingData, diff --git a/services/ui-src/src/measures/2023/FUMHH/types.ts b/services/ui-src/src/measures/2023/FUMHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FUMHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FUMHH/validation.ts b/services/ui-src/src/measures/2023/FUMHH/validation.ts index dfb8a0d92c..04743466d3 100644 --- a/services/ui-src/src/measures/2023/FUMHH/validation.ts +++ b/services/ui-src/src/measures/2023/FUMHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FUMHHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/FVAAD/types.ts b/services/ui-src/src/measures/2023/FVAAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/FVAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/FVAAD/validation.ts b/services/ui-src/src/measures/2023/FVAAD/validation.ts index f3c9b62f9a..79d5b0aa08 100644 --- a/services/ui-src/src/measures/2023/FVAAD/validation.ts +++ b/services/ui-src/src/measures/2023/FVAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const FVAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/HBDAD/index.tsx b/services/ui-src/src/measures/2023/HBDAD/index.tsx index 3b25674955..2873af1381 100644 --- a/services/ui-src/src/measures/2023/HBDAD/index.tsx +++ b/services/ui-src/src/measures/2023/HBDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const HBDAD = ({ name, diff --git a/services/ui-src/src/measures/2023/HBDAD/types.ts b/services/ui-src/src/measures/2023/HBDAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/HBDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/HBDAD/validation.ts b/services/ui-src/src/measures/2023/HBDAD/validation.ts index 9bcbe07370..f0b6b9442e 100644 --- a/services/ui-src/src/measures/2023/HBDAD/validation.ts +++ b/services/ui-src/src/measures/2023/HBDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const HBDADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/HPCMIAD/index.tsx b/services/ui-src/src/measures/2023/HPCMIAD/index.tsx index e5c2d07150..6579c42321 100644 --- a/services/ui-src/src/measures/2023/HPCMIAD/index.tsx +++ b/services/ui-src/src/measures/2023/HPCMIAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const HPCMIAD = ({ name, diff --git a/services/ui-src/src/measures/2023/HPCMIAD/types.ts b/services/ui-src/src/measures/2023/HPCMIAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/HPCMIAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/HPCMIAD/validation.ts b/services/ui-src/src/measures/2023/HPCMIAD/validation.ts index 9c3af98875..6bfc6ac714 100644 --- a/services/ui-src/src/measures/2023/HPCMIAD/validation.ts +++ b/services/ui-src/src/measures/2023/HPCMIAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const HPCMIADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/HVLAD/index.tsx b/services/ui-src/src/measures/2023/HVLAD/index.tsx index 039bdca309..551e7725b4 100644 --- a/services/ui-src/src/measures/2023/HVLAD/index.tsx +++ b/services/ui-src/src/measures/2023/HVLAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const HVLAD = ({ name, diff --git a/services/ui-src/src/measures/2023/HVLAD/types.ts b/services/ui-src/src/measures/2023/HVLAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/HVLAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/HVLAD/validation.ts b/services/ui-src/src/measures/2023/HVLAD/validation.ts index 15d412dc49..31b4f8183e 100644 --- a/services/ui-src/src/measures/2023/HVLAD/validation.ts +++ b/services/ui-src/src/measures/2023/HVLAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const HVLADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/IETAD/index.tsx b/services/ui-src/src/measures/2023/IETAD/index.tsx index a33a067db5..ed0dbb565d 100644 --- a/services/ui-src/src/measures/2023/IETAD/index.tsx +++ b/services/ui-src/src/measures/2023/IETAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const IETAD = ({ name, diff --git a/services/ui-src/src/measures/2023/IETAD/types.ts b/services/ui-src/src/measures/2023/IETAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/IETAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/IETAD/validation.ts b/services/ui-src/src/measures/2023/IETAD/validation.ts index b4393d3094..17e860e201 100644 --- a/services/ui-src/src/measures/2023/IETAD/validation.ts +++ b/services/ui-src/src/measures/2023/IETAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const IETValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/IETHH/index.tsx b/services/ui-src/src/measures/2023/IETHH/index.tsx index 8d3f04c215..5eda3dd981 100644 --- a/services/ui-src/src/measures/2023/IETHH/index.tsx +++ b/services/ui-src/src/measures/2023/IETHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const IETHH = ({ name, diff --git a/services/ui-src/src/measures/2023/IETHH/types.ts b/services/ui-src/src/measures/2023/IETHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/IETHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/IETHH/validation.ts b/services/ui-src/src/measures/2023/IETHH/validation.ts index 63bba4e43e..a3ab57a1d2 100644 --- a/services/ui-src/src/measures/2023/IETHH/validation.ts +++ b/services/ui-src/src/measures/2023/IETHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "../shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "../shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const IETValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/IMACH/index.tsx b/services/ui-src/src/measures/2023/IMACH/index.tsx index 239513c18b..d76bf285e9 100644 --- a/services/ui-src/src/measures/2023/IMACH/index.tsx +++ b/services/ui-src/src/measures/2023/IMACH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const IMACH = ({ name, diff --git a/services/ui-src/src/measures/2023/IMACH/types.ts b/services/ui-src/src/measures/2023/IMACH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/IMACH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/IMACH/validation.ts b/services/ui-src/src/measures/2023/IMACH/validation.ts index b6e78ab78b..16b0d2661b 100644 --- a/services/ui-src/src/measures/2023/IMACH/validation.ts +++ b/services/ui-src/src/measures/2023/IMACH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const DEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/IUHH/index.tsx b/services/ui-src/src/measures/2023/IUHH/index.tsx index 5b4edff2f8..22b7355299 100644 --- a/services/ui-src/src/measures/2023/IUHH/index.tsx +++ b/services/ui-src/src/measures/2023/IUHH/index.tsx @@ -1,12 +1,13 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const IUHH = ({ name, diff --git a/services/ui-src/src/measures/2023/IUHH/types.ts b/services/ui-src/src/measures/2023/IUHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/IUHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/IUHH/validation.ts b/services/ui-src/src/measures/2023/IUHH/validation.ts index 0001f1c3ef..3b9709f1cb 100644 --- a/services/ui-src/src/measures/2023/IUHH/validation.ts +++ b/services/ui-src/src/measures/2023/IUHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; // Rate structure by index in row const ndrForumlas = [ diff --git a/services/ui-src/src/measures/2023/LSCCH/index.tsx b/services/ui-src/src/measures/2023/LSCCH/index.tsx index 0d33ff08db..cde6751232 100644 --- a/services/ui-src/src/measures/2023/LSCCH/index.tsx +++ b/services/ui-src/src/measures/2023/LSCCH/index.tsx @@ -1,11 +1,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const LSCCH = ({ name, diff --git a/services/ui-src/src/measures/2023/LSCCH/types.ts b/services/ui-src/src/measures/2023/LSCCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/LSCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/LSCCH/validation.ts b/services/ui-src/src/measures/2023/LSCCH/validation.ts index c032453296..fdaaff9d9d 100644 --- a/services/ui-src/src/measures/2023/LSCCH/validation.ts +++ b/services/ui-src/src/measures/2023/LSCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const LSCCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/OEVCH/index.tsx b/services/ui-src/src/measures/2023/OEVCH/index.tsx index 2a2a44c05a..b690f37e28 100644 --- a/services/ui-src/src/measures/2023/OEVCH/index.tsx +++ b/services/ui-src/src/measures/2023/OEVCH/index.tsx @@ -2,11 +2,12 @@ import * as CMQ from "measures/2023/shared/CommonQuestions"; import * as DC from "dataConstants"; import * as PMD from "./data"; import * as QMR from "components"; -import { FormData } from "./types"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const OEVCH = ({ name, diff --git a/services/ui-src/src/measures/2023/OEVCH/types.ts b/services/ui-src/src/measures/2023/OEVCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/OEVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/OEVCH/validation.ts b/services/ui-src/src/measures/2023/OEVCH/validation.ts index 6b2dddf615..1a8c2237b4 100644 --- a/services/ui-src/src/measures/2023/OEVCH/validation.ts +++ b/services/ui-src/src/measures/2023/OEVCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const OEVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/OHDAD/index.tsx b/services/ui-src/src/measures/2023/OHDAD/index.tsx index 6da1b9fa16..18836072e5 100644 --- a/services/ui-src/src/measures/2023/OHDAD/index.tsx +++ b/services/ui-src/src/measures/2023/OHDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const OHDAD = ({ name, diff --git a/services/ui-src/src/measures/2023/OHDAD/types.ts b/services/ui-src/src/measures/2023/OHDAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/OHDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/OHDAD/validation.ts b/services/ui-src/src/measures/2023/OHDAD/validation.ts index 30dac90929..2ddd1d392d 100644 --- a/services/ui-src/src/measures/2023/OHDAD/validation.ts +++ b/services/ui-src/src/measures/2023/OHDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const OHDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/OUDAD/index.tsx b/services/ui-src/src/measures/2023/OUDAD/index.tsx index 88f63eb916..8a3c525270 100644 --- a/services/ui-src/src/measures/2023/OUDAD/index.tsx +++ b/services/ui-src/src/measures/2023/OUDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const OUDAD = ({ name, diff --git a/services/ui-src/src/measures/2023/OUDAD/types.ts b/services/ui-src/src/measures/2023/OUDAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/OUDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/OUDAD/validation.ts b/services/ui-src/src/measures/2023/OUDAD/validation.ts index 88e7db8153..66d572c0e7 100644 --- a/services/ui-src/src/measures/2023/OUDAD/validation.ts +++ b/services/ui-src/src/measures/2023/OUDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const OUDValidation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2023/OUDHH/index.tsx b/services/ui-src/src/measures/2023/OUDHH/index.tsx index f55ab05528..abb919b91c 100644 --- a/services/ui-src/src/measures/2023/OUDHH/index.tsx +++ b/services/ui-src/src/measures/2023/OUDHH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const OUDHH = ({ name, diff --git a/services/ui-src/src/measures/2023/OUDHH/types.ts b/services/ui-src/src/measures/2023/OUDHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/OUDHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/OUDHH/validation.ts b/services/ui-src/src/measures/2023/OUDHH/validation.ts index d71ceec7c9..63c68d0a8d 100644 --- a/services/ui-src/src/measures/2023/OUDHH/validation.ts +++ b/services/ui-src/src/measures/2023/OUDHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const OUDValidation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2023/PCRAD/types.ts b/services/ui-src/src/measures/2023/PCRAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PCRAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PCRAD/validation.ts b/services/ui-src/src/measures/2023/PCRAD/validation.ts index af45287193..f4539b76ec 100644 --- a/services/ui-src/src/measures/2023/PCRAD/validation.ts +++ b/services/ui-src/src/measures/2023/PCRAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const ndrForumlas = [ { diff --git a/services/ui-src/src/measures/2023/PCRHH/types.ts b/services/ui-src/src/measures/2023/PCRHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PCRHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PCRHH/validation.ts b/services/ui-src/src/measures/2023/PCRHH/validation.ts index ed8c03098a..48bf1f2b01 100644 --- a/services/ui-src/src/measures/2023/PCRHH/validation.ts +++ b/services/ui-src/src/measures/2023/PCRHH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const ndrForumlas = [ { diff --git a/services/ui-src/src/measures/2023/PPCAD/index.tsx b/services/ui-src/src/measures/2023/PPCAD/index.tsx index 9a648348ce..bd16283792 100644 --- a/services/ui-src/src/measures/2023/PPCAD/index.tsx +++ b/services/ui-src/src/measures/2023/PPCAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PPCAD = ({ name, diff --git a/services/ui-src/src/measures/2023/PPCAD/types.ts b/services/ui-src/src/measures/2023/PPCAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PPCAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PPCAD/validation.ts b/services/ui-src/src/measures/2023/PPCAD/validation.ts index 3ab20c8f50..79d450fb88 100644 --- a/services/ui-src/src/measures/2023/PPCAD/validation.ts +++ b/services/ui-src/src/measures/2023/PPCAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PPCADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/PPCCH/index.tsx b/services/ui-src/src/measures/2023/PPCCH/index.tsx index 8d28660cc5..1fdf6f5ae2 100644 --- a/services/ui-src/src/measures/2023/PPCCH/index.tsx +++ b/services/ui-src/src/measures/2023/PPCCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PPCCH = ({ name, diff --git a/services/ui-src/src/measures/2023/PPCCH/types.ts b/services/ui-src/src/measures/2023/PPCCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PPCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PPCCH/validation.ts b/services/ui-src/src/measures/2023/PPCCH/validation.ts index c4c0b58eb6..0a1f839435 100644 --- a/services/ui-src/src/measures/2023/PPCCH/validation.ts +++ b/services/ui-src/src/measures/2023/PPCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PPCCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/PQI01AD/index.tsx b/services/ui-src/src/measures/2023/PQI01AD/index.tsx index 1dc2b028d2..484f1fbba4 100644 --- a/services/ui-src/src/measures/2023/PQI01AD/index.tsx +++ b/services/ui-src/src/measures/2023/PQI01AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PQI01AD = ({ name, diff --git a/services/ui-src/src/measures/2023/PQI01AD/types.ts b/services/ui-src/src/measures/2023/PQI01AD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PQI01AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PQI01AD/validation.ts b/services/ui-src/src/measures/2023/PQI01AD/validation.ts index e89660a3f4..9b0aeb84a5 100644 --- a/services/ui-src/src/measures/2023/PQI01AD/validation.ts +++ b/services/ui-src/src/measures/2023/PQI01AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PQI01Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2023/PQI05AD/index.tsx b/services/ui-src/src/measures/2023/PQI05AD/index.tsx index 35a2e3bd6f..cb526a77f6 100644 --- a/services/ui-src/src/measures/2023/PQI05AD/index.tsx +++ b/services/ui-src/src/measures/2023/PQI05AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PQI05AD = ({ name, diff --git a/services/ui-src/src/measures/2023/PQI05AD/types.ts b/services/ui-src/src/measures/2023/PQI05AD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PQI05AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PQI05AD/validation.ts b/services/ui-src/src/measures/2023/PQI05AD/validation.ts index dafca2aaa1..7e29063f8a 100644 --- a/services/ui-src/src/measures/2023/PQI05AD/validation.ts +++ b/services/ui-src/src/measures/2023/PQI05AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PQI05Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2023/PQI08AD/index.tsx b/services/ui-src/src/measures/2023/PQI08AD/index.tsx index b86b52392e..17b25e6e37 100644 --- a/services/ui-src/src/measures/2023/PQI08AD/index.tsx +++ b/services/ui-src/src/measures/2023/PQI08AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PQI08AD = ({ name, diff --git a/services/ui-src/src/measures/2023/PQI08AD/types.ts b/services/ui-src/src/measures/2023/PQI08AD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PQI08AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PQI08AD/validation.ts b/services/ui-src/src/measures/2023/PQI08AD/validation.ts index 40e22d543b..781f10fcb9 100644 --- a/services/ui-src/src/measures/2023/PQI08AD/validation.ts +++ b/services/ui-src/src/measures/2023/PQI08AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PQI08Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2023/PQI15AD/index.tsx b/services/ui-src/src/measures/2023/PQI15AD/index.tsx index ea2da8fbef..52473cddb8 100644 --- a/services/ui-src/src/measures/2023/PQI15AD/index.tsx +++ b/services/ui-src/src/measures/2023/PQI15AD/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PQI15AD = ({ name, diff --git a/services/ui-src/src/measures/2023/PQI15AD/types.ts b/services/ui-src/src/measures/2023/PQI15AD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PQI15AD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PQI15AD/validation.ts b/services/ui-src/src/measures/2023/PQI15AD/validation.ts index 2f9727ff68..77d5283ae5 100644 --- a/services/ui-src/src/measures/2023/PQI15AD/validation.ts +++ b/services/ui-src/src/measures/2023/PQI15AD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PQI15Validation = (data: FormData) => { const OPM = data[DC.OPM_RATES]; diff --git a/services/ui-src/src/measures/2023/PQI92HH/index.tsx b/services/ui-src/src/measures/2023/PQI92HH/index.tsx index fcd89f677e..83b0515742 100644 --- a/services/ui-src/src/measures/2023/PQI92HH/index.tsx +++ b/services/ui-src/src/measures/2023/PQI92HH/index.tsx @@ -6,7 +6,8 @@ import * as QMR from "components"; import { validationFunctions } from "./validation"; import { positiveNumbersWithMaxDecimalPlaces } from "utils"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const PQI92HH = ({ name, diff --git a/services/ui-src/src/measures/2023/PQI92HH/types.ts b/services/ui-src/src/measures/2023/PQI92HH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/PQI92HH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/PQI92HH/validation.ts b/services/ui-src/src/measures/2023/PQI92HH/validation.ts index e953567bef..b4a4aadeda 100644 --- a/services/ui-src/src/measures/2023/PQI92HH/validation.ts +++ b/services/ui-src/src/measures/2023/PQI92HH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const PQI92Validation = (data: FormData) => { const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; diff --git a/services/ui-src/src/measures/2023/SAAAD/index.tsx b/services/ui-src/src/measures/2023/SAAAD/index.tsx index caeeacd0c6..77b7cbf617 100644 --- a/services/ui-src/src/measures/2023/SAAAD/index.tsx +++ b/services/ui-src/src/measures/2023/SAAAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const SAAAD = ({ name, diff --git a/services/ui-src/src/measures/2023/SAAAD/types.ts b/services/ui-src/src/measures/2023/SAAAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/SAAAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/SAAAD/validation.ts b/services/ui-src/src/measures/2023/SAAAD/validation.ts index 2b26a77047..374f6a451e 100644 --- a/services/ui-src/src/measures/2023/SAAAD/validation.ts +++ b/services/ui-src/src/measures/2023/SAAAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const SAAADValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/SFMCH/index.tsx b/services/ui-src/src/measures/2023/SFMCH/index.tsx index fff49bd2f5..339217ad35 100644 --- a/services/ui-src/src/measures/2023/SFMCH/index.tsx +++ b/services/ui-src/src/measures/2023/SFMCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const SFMCH = ({ name, diff --git a/services/ui-src/src/measures/2023/SFMCH/types.ts b/services/ui-src/src/measures/2023/SFMCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/SFMCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/SFMCH/validation.ts b/services/ui-src/src/measures/2023/SFMCH/validation.ts index 1687975ae4..394956519b 100644 --- a/services/ui-src/src/measures/2023/SFMCH/validation.ts +++ b/services/ui-src/src/measures/2023/SFMCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const SFMCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/SSDAD/index.tsx b/services/ui-src/src/measures/2023/SSDAD/index.tsx index 054eb8ffb9..c6f55a3884 100644 --- a/services/ui-src/src/measures/2023/SSDAD/index.tsx +++ b/services/ui-src/src/measures/2023/SSDAD/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const SSDAD = ({ name, diff --git a/services/ui-src/src/measures/2023/SSDAD/types.ts b/services/ui-src/src/measures/2023/SSDAD/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/SSDAD/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/SSDAD/validation.ts b/services/ui-src/src/measures/2023/SSDAD/validation.ts index e79da61b5e..2ef15b8cb6 100644 --- a/services/ui-src/src/measures/2023/SSDAD/validation.ts +++ b/services/ui-src/src/measures/2023/SSDAD/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const SSDValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/SSHH/types.ts b/services/ui-src/src/measures/2023/SSHH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/SSHH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/SSHH/validation.ts b/services/ui-src/src/measures/2023/SSHH/validation.ts index a0f1188cba..9d7e878441 100644 --- a/services/ui-src/src/measures/2023/SSHH/validation.ts +++ b/services/ui-src/src/measures/2023/SSHH/validation.ts @@ -1,6 +1,7 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export interface FormRateField { denominator?: string; diff --git a/services/ui-src/src/measures/2023/TFLCH/index.tsx b/services/ui-src/src/measures/2023/TFLCH/index.tsx index cb97309615..91eed24c47 100644 --- a/services/ui-src/src/measures/2023/TFLCH/index.tsx +++ b/services/ui-src/src/measures/2023/TFLCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const TFLCH = ({ name, diff --git a/services/ui-src/src/measures/2023/TFLCH/types.ts b/services/ui-src/src/measures/2023/TFLCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/TFLCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/TFLCH/validation.ts b/services/ui-src/src/measures/2023/TFLCH/validation.ts index 39d85f829a..60731bea95 100644 --- a/services/ui-src/src/measures/2023/TFLCH/validation.ts +++ b/services/ui-src/src/measures/2023/TFLCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const TFLCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/W30CH/index.tsx b/services/ui-src/src/measures/2023/W30CH/index.tsx index 401a766ea2..160dadc74c 100644 --- a/services/ui-src/src/measures/2023/W30CH/index.tsx +++ b/services/ui-src/src/measures/2023/W30CH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const W30CH = ({ name, diff --git a/services/ui-src/src/measures/2023/W30CH/types.ts b/services/ui-src/src/measures/2023/W30CH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/W30CH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/W30CH/validation.ts b/services/ui-src/src/measures/2023/W30CH/validation.ts index d2af5537b4..16a4a02a8a 100644 --- a/services/ui-src/src/measures/2023/W30CH/validation.ts +++ b/services/ui-src/src/measures/2023/W30CH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const W30CHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/WCCCH/index.tsx b/services/ui-src/src/measures/2023/WCCCH/index.tsx index 436c438546..a3a90c7ac1 100644 --- a/services/ui-src/src/measures/2023/WCCCH/index.tsx +++ b/services/ui-src/src/measures/2023/WCCCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; import * as QMR from "components"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const WCCCH = ({ name, diff --git a/services/ui-src/src/measures/2023/WCCCH/types.ts b/services/ui-src/src/measures/2023/WCCCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/WCCCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/WCCCH/validation.ts b/services/ui-src/src/measures/2023/WCCCH/validation.ts index 7e6eddb01c..a319e9a65d 100644 --- a/services/ui-src/src/measures/2023/WCCCH/validation.ts +++ b/services/ui-src/src/measures/2023/WCCCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const WCCHValidation = (data: FormData) => { const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; diff --git a/services/ui-src/src/measures/2023/WCVCH/index.tsx b/services/ui-src/src/measures/2023/WCVCH/index.tsx index de04f5143b..99daeaf4ad 100644 --- a/services/ui-src/src/measures/2023/WCVCH/index.tsx +++ b/services/ui-src/src/measures/2023/WCVCH/index.tsx @@ -5,7 +5,8 @@ import * as PMD from "./data"; import * as QMR from "components"; import { validationFunctions } from "./validation"; import { getPerfMeasureRateArray } from "measures/2023/shared/globalValidations"; -import { FormData } from "./types"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; export const WCVCH = ({ name, diff --git a/services/ui-src/src/measures/2023/WCVCH/types.ts b/services/ui-src/src/measures/2023/WCVCH/types.ts deleted file mode 100644 index 7b8b8b9ad8..0000000000 --- a/services/ui-src/src/measures/2023/WCVCH/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Types from "measures/2023/shared/CommonQuestions/types"; - -export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2023/WCVCH/validation.ts b/services/ui-src/src/measures/2023/WCVCH/validation.ts index d351457c59..77475ac2b3 100644 --- a/services/ui-src/src/measures/2023/WCVCH/validation.ts +++ b/services/ui-src/src/measures/2023/WCVCH/validation.ts @@ -1,8 +1,9 @@ import * as DC from "dataConstants"; import * as GV from "measures/2023/shared/globalValidations"; import * as PMD from "./data"; -import { FormData } from "./types"; import { OMSData } from "measures/2023/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2023/shared/CommonQuestions/types"; const WCVCHValidation = (data: FormData) => { const ageGroups = PMD.qualifiers; diff --git a/services/ui-src/src/measures/2023/shared/CommonQuestions/index.ts b/services/ui-src/src/measures/2023/shared/CommonQuestions/index.ts index b678ceaeee..a776e83af0 100644 --- a/services/ui-src/src/measures/2023/shared/CommonQuestions/index.ts +++ b/services/ui-src/src/measures/2023/shared/CommonQuestions/index.ts @@ -3,7 +3,7 @@ export * from "./DateRange"; export * from "./DefinitionsOfPopulation"; export * from "./DataSource"; export * from "./DataSourceCahps"; -export * from "./AdditionalNotes"; +export * from "shared/commonQuestions/AdditionalNotes"; export * from "./OtherPerformanceMeasure"; export * from "./Reporting"; export * from "./StatusOfData"; diff --git a/services/ui-src/src/measures/2024/AABAD/data.ts b/services/ui-src/src/measures/2024/AABAD/data.ts new file mode 100644 index 0000000000..25b8ebee47 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/data.ts @@ -0,0 +1,15 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AAB-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + customPrompt: + "Enter a number for the numerator and the denominator. The measure is reported as an inverted rate. The formula for the Rate = (1 - (Numerator/Denominator)) x 100", + questionText: [ + "The percentage of episodes for beneficiaries age 18 and older with a diagnosis of acute bronchitis/bronchiolitis that did not result in an antibiotic dispensing event.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AABAD/index.test.tsx b/services/ui-src/src/measures/2024/AABAD/index.test.tsx new file mode 100644 index 0000000000..3dbd6c11bd --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/index.test.tsx @@ -0,0 +1,244 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AAB-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "0.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and Older", + rate: "0.0", + numerator: "44", + denominator: "44", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AABAD/index.tsx b/services/ui-src/src/measures/2024/AABAD/index.tsx new file mode 100644 index 0000000000..7712abf676 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/index.tsx @@ -0,0 +1,81 @@ +import * as PMD from "./data"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; + +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; + +import { validationFunctions } from "./validation"; +import { AABRateCalculation } from "utils/rateFormulas"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AABAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateCalc={AABRateCalculation} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + data={PMD.data} + rateCalc={AABRateCalculation} + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + rateCalc={AABRateCalculation} + customPrompt={PMD.data.customPrompt} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AABAD/validation.ts b/services/ui-src/src/measures/2024/AABAD/validation.ts new file mode 100644 index 0000000000..f73afc84e8 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/validation.ts @@ -0,0 +1,78 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const AABADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [GV.validateNumeratorLessThanDenominatorOMS()], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AABADValidation]; diff --git a/services/ui-src/src/measures/2024/AABCH/data.ts b/services/ui-src/src/measures/2024/AABCH/data.ts new file mode 100644 index 0000000000..73afe4b456 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/data.ts @@ -0,0 +1,14 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AAB-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + customPrompt: + "Enter a number for the numerator and the denominator. The measure is reported as an inverted rate. The formula for the Rate = (1 - (Numerator/Denominator)) x 100", + questionText: [ + "The percentage of episodes for beneficiaries ages 3 months to 17 years with a diagnosis of acute bronchitis/bronchiolitis that did not result in an antibiotic dispensing event.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AABCH/index.test.tsx b/services/ui-src/src/measures/2024/AABCH/index.test.tsx new file mode 100644 index 0000000000..3cd25850fa --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/index.test.tsx @@ -0,0 +1,239 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AAB-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 3 months to 17", + rate: "0.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AABCH/index.tsx b/services/ui-src/src/measures/2024/AABCH/index.tsx new file mode 100644 index 0000000000..cd94f193dd --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/index.tsx @@ -0,0 +1,86 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { AABRateCalculation } from "utils/rateFormulas"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AABCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + let mask: RegExp = positiveNumbersWithMaxDecimalPlaces(1); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const rateScale = 100; + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateCalc={AABRateCalculation} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + customMask={mask} + rateMultiplicationValue={rateScale} + allowNumeratorGreaterThanDenominator + rateCalc={AABRateCalculation} + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + adultMeasure={false} + calcTotal + categories={PMD.categories} + customMask={mask} + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={rateScale} + allowNumeratorGreaterThanDenominator + rateCalc={AABRateCalculation} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AABCH/validation.ts b/services/ui-src/src/measures/2024/AABCH/validation.ts new file mode 100644 index 0000000000..399153860b --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const AABCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const deviationReason = data[DC.DEVIATION_REASON]; + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AABCHValidation]; diff --git a/services/ui-src/src/measures/2024/ADDCH/data.ts b/services/ui-src/src/measures/2024/ADDCH/data.ts new file mode 100644 index 0000000000..4ec728f805 --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/data.ts @@ -0,0 +1,58 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("ADD-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children newly prescribed attention-deficit/hyperactivity disorder (ADHD) medication who had at least three follow-up care visits within a 10-month period, one of which was within 30 days of when the first ADHD medication was dispensed. Two rates are reported.", + ], + questionListTitles: [ + "Initiation Phase", + "Continuation and Maintenance (C&M) Phase", + ], + questionListOrderedItems: [ + "Percentage of children ages 6 to 12 with a prescription dispensed for ADHD medication, who had one follow-up visit with a practitioner with prescribing authority during the 30-day Initiation Phase.", + "Percentage of children ages 6 to 12 with a prescription dispensed for ADHD medication who remained on the medication for at least 210 days and who, in addition to the visit in the Initiation Phase, had at least two follow-up visits with a practitioner within 270 days (9 months) after the Initiation Phase ended.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/ADDCH/index.test.tsx b/services/ui-src/src/measures/2024/ADDCH/index.test.tsx new file mode 100644 index 0000000000..64b29589db --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/index.test.tsx @@ -0,0 +1,251 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "ADD-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect( + V.validateOneQualDenomHigherThanOtherDenomOMS + ).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateOneQualDenomHigherThanOtherDenomOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Initiation Phase", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Continuation and Maintenance (C&M) Phase", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/ADDCH/index.tsx b/services/ui-src/src/measures/2024/ADDCH/index.tsx new file mode 100644 index 0000000000..733c0cf452 --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const ADDCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/ADDCH/validation.ts b/services/ui-src/src/measures/2024/ADDCH/validation.ts new file mode 100644 index 0000000000..6569a540be --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { getPerfMeasureRateArray } from "../shared/globalValidations"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const ADDCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...errorArray, + ...GV.validateOneQualDenomHigherThanOtherDenomPM(data, PMD), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOneQualDenomHigherThanOtherDenomOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [ADDCHValidation]; diff --git a/services/ui-src/src/measures/2024/AIFHH/data.ts b/services/ui-src/src/measures/2024/AIFHH/data.ts new file mode 100644 index 0000000000..74c176b04c --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/data.ts @@ -0,0 +1,89 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AIF-HH"); + +const measureName = "AIFHH"; + +const inputFieldNames = [ + { + label: "Number of Enrollee Months", + text: "Number of Enrollee Months", + id: "QFSYON", + }, + { + label: "Number of Short-Term Admissions", + text: "Number of Short-Term Admissions", + id: "tMeLfq", + }, + { + label: "Short-Term Admissions per 1,000 Enrollee Months", + text: "Short-Term Admissions per 1,000 Enrollee Months", + id: "bxkVCC", + }, + { + label: "Number of Medium-Term Admissions", + text: "Number of Medium-Term Admissions", + id: "KBOnkQ", + }, + { + label: "Medium-Term Admissions per 1,000 Enrollee Months", + text: "Medium-Term Admissions per 1,000 Enrollee Months", + id: "5RO62J", + }, + { + label: "Number of Long-Term Admissions", + text: "Number of Long-Term Admissions", + id: "m3HvMS", + }, + { + label: "Long-Term Admissions per 1,000 Enrollee Months", + text: "Long-Term Admissions per 1,000 Enrollee Months", + id: "dFMGFi", + }, +]; + +// Rate structure by index in row +const ndrFormulas = [ + // Short-Term Admissions per 1,000 Enrollee Months + { + num: 1, + denom: 0, + rate: 2, + mult: 1000, + }, + // Medium-Term Admissions per 1,000 Enrollee Months + { + num: 3, + denom: 0, + rate: 4, + mult: 1000, + }, + // Long-Term Admissions per 1,000 Enrollee Months + { + num: 5, + denom: 0, + rate: 6, + mult: 1000, + }, +]; + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The number of admissions to a facility among enrollees age 18 and older residing in the community for at least one month. The number of short-term, medium-term, or long-term admissions is reported per 1,000 enrollee months. Enrollee months reflect the total number of months each enrollee is enrolled in the program and residing in the community for at least one day of the month.", + ], + questionListItems: [ + " The rate of admissions resulting in a short-term stay (1 to 20 days) per 1,000 enrollee months.", + " The rate of admissions resulting in a medium-term stay (21 to 100 days) per 1,000 enrollee months.", + " The rate of admissions resulting in a long-term stay (greater than or equal to 101 days) per 1,000 enrollee months.", + ], + questionListTitles: ["Short-Term Stay", "Medium-Term Stay", "Long-Term Stay"], + questionSubtext: [ + "The following three performance rates are reported across four age groups (ages 18 to 64, ages 65 to 74, ages 75 to 84, and age 85 and older):", + ], + measureName, + inputFieldNames, + ndrFormulas, + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AIFHH/index.test.tsx b/services/ui-src/src/measures/2024/AIFHH/index.test.tsx new file mode 100644 index 0000000000..631ac571a2 --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/index.test.tsx @@ -0,0 +1,383 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AIF-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + afterEach(() => { + screen.debug(); + }); + it.skip("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it.skip("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it.skip("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it.skip("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it.skip("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it.skip("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it.skip("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexValidateDualPopInformation).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it.skip("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.ComplexAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it.skip("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: "1", + label: "Number of Short-Term Admissions", + }, + { + value: "1000.0", + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Medium-Term Admissions", + }, + { + value: "1000.0", + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Long-Term Admissions", + }, + { + value: "1000.0", + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: "1", + label: "Number of Short-Term Admissions", + }, + { + value: "1000.0", + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Medium-Term Admissions", + }, + { + value: "1000.0", + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Long-Term Admissions", + }, + { + value: "1000.0", + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Ages 65 to 74", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Number of Short-Term Admissions", + }, + { + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Medium-Term Admissions", + }, + { + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Long-Term Admissions", + }, + { + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Ages 75 to 84", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Number of Short-Term Admissions", + }, + { + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Medium-Term Admissions", + }, + { + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Long-Term Admissions", + }, + { + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Age 85 and older", + }, + { + fields: [ + { + value: "2", + label: "Number of Enrollee Months", + }, + { + value: 2, + label: "Number of Short-Term Admissions", + }, + { + value: "1000.0", + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + value: 2, + label: "Number of Medium-Term Admissions", + }, + { + value: "1000.0", + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + value: 2, + label: "Number of Long-Term Admissions", + }, + { + value: "1000.0", + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + isTotal: true, + label: "Total (Age 18 and older)", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AIFHH/index.tsx b/services/ui-src/src/measures/2024/AIFHH/index.tsx new file mode 100644 index 0000000000..d293bfbc90 --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/index.tsx @@ -0,0 +1,86 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AIFHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + RateComponent={QMR.ComplexRate} + calcTotal={true} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + allowNumeratorGreaterThanDenominator + customMask={xNumbersYDecimals(12, 1)} + /> + )} + <CMQ.CombinedRates healthHomeMeasure={true} /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + measureName={PMD.data.measureName} + inputFieldNames={PMD.data.inputFieldNames} + ndrFormulas={PMD.data.ndrFormulas} + allowNumeratorGreaterThanDenominator + adultMeasure={false} + calcTotal={true} + customMask={xNumbersYDecimals(12, 1)} + AIFHHPerformanceMeasureArray={performanceMeasureArray} + componentFlag={"AIF"} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AIFHH/validation.ts b/services/ui-src/src/measures/2024/AIFHH/validation.ts new file mode 100644 index 0000000000..108446b390 --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/validation.ts @@ -0,0 +1,125 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +// Rate structure by index in row +const ndrFormulas = [ + // Short-Term Admissions per 1,000 Enrollee Months + { + numerator: 1, + denominator: 0, + rateIndex: 2, + mult: 1000, + }, + // Medium-Term Admissions per 1,000 Enrollee Months + { + numerator: 3, + denominator: 0, + rateIndex: 4, + mult: 1000, + }, + // Long-Term Admissions per 1,000 Enrollee Months + { + numerator: 5, + denominator: 0, + rateIndex: 6, + mult: 1000, + }, +]; + +let OPM: any; + +const AIFHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.ComplexValidateDualPopInformation( + performanceMeasureArray, + OPM, + definitionOfDenominator + ), + + // Performance Measure Validations + ...GV.ComplexAtLeastOneRateComplete(performanceMeasureArray, OPM), + ...GV.ComplexNoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrFormulas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.ComplexValidateNDRTotals( + performanceMeasureArray, + PMD.categories, + ndrFormulas + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x]?.OPM?.[0]] }; + }); + return OPM === undefined + ? [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.["aifhh-rate"]?.rates ?? {}, + rates ?? [], + ndrFormulas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ] + : [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.rates, + rates ?? [], + ndrFormulas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ]; +}; + +export const validationFunctions = [AIFHHValidation]; diff --git a/services/ui-src/src/measures/2024/AMBCH/data.ts b/services/ui-src/src/measures/2024/AMBCH/data.ts new file mode 100644 index 0000000000..2551bf4525 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMB-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Rate of emergency department (ED) visits per 1,000 beneficiary months among children up to age 19.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMBCH/index.test.tsx b/services/ui-src/src/measures/2024/AMBCH/index.test.tsx new file mode 100644 index 0000000000..1ab8051d79 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/index.test.tsx @@ -0,0 +1,274 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMB-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "< Age 1", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 1 to 9", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 10 to 19", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages unknown", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total", + isTotal: true, + rate: "1000.0", + numerator: "4", + denominator: "4", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMBCH/index.tsx b/services/ui-src/src/measures/2024/AMBCH/index.tsx new file mode 100644 index 0000000000..808aabd1ec --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/index.tsx @@ -0,0 +1,86 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AMBCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + let mask: RegExp = positiveNumbersWithMaxDecimalPlaces(1); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const rateScale = 1000; + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + calcTotal + customMask={mask} + rateScale={rateScale} + allowNumeratorGreaterThanDenominator + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + customMask={mask} + rateMultiplicationValue={rateScale} + allowNumeratorGreaterThanDenominator + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + adultMeasure={false} + calcTotal + categories={PMD.categories} + customMask={mask} + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={rateScale} + allowNumeratorGreaterThanDenominator + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AMBCH/validation.ts b/services/ui-src/src/measures/2024/AMBCH/validation.ts new file mode 100644 index 0000000000..424e6c0348 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/validation.ts @@ -0,0 +1,72 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const AMBCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMBCHValidation]; diff --git a/services/ui-src/src/measures/2024/AMBHH/data.ts b/services/ui-src/src/measures/2024/AMBHH/data.ts new file mode 100644 index 0000000000..5e8aea9eec --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMB-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Rate of emergency department (ED) visits per 1,000 enrollee months among health home enrollees.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMBHH/index.test.tsx b/services/ui-src/src/measures/2024/AMBHH/index.test.tsx new file mode 100644 index 0000000000..f172d2a291 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/index.test.tsx @@ -0,0 +1,257 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMB-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMBHH/index.tsx b/services/ui-src/src/measures/2024/AMBHH/index.tsx new file mode 100644 index 0000000000..2ecffeafa1 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/index.tsx @@ -0,0 +1,85 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AMBHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + allowNumeratorGreaterThanDenominator + data={PMD.data} + rateScale={1000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + calcTotal + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + allowNumeratorGreaterThanDenominator + rateMultiplicationValue={1000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + )} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={1000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + calcTotal + allowNumeratorGreaterThanDenominator + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AMBHH/validation.ts b/services/ui-src/src/measures/2024/AMBHH/validation.ts new file mode 100644 index 0000000000..46e9ade737 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/validation.ts @@ -0,0 +1,86 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const AMBHHValidation = (data: FormData) => { + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMBHHValidation]; diff --git a/services/ui-src/src/measures/2024/AMMAD/data.ts b/services/ui-src/src/measures/2024/AMMAD/data.ts new file mode 100644 index 0000000000..f09b161fde --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/data.ts @@ -0,0 +1,48 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMM-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older who were treated with antidepressant medication, had a diagnosis of major depression, and who remained on an antidepressant medication treatment. Two rates are reported:", + ], + questionListItems: [ + "Effective Acute Phase Treatment: Percentage of beneficiaries who remained on an antidepressant medication for at least 84 days (12 weeks).", + "Effective Continuation Phase Treatment: Percentage of beneficiaries who remained on an antidepressant medication for at least 180 days (6 months).", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: "Administrative Data", + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: "Medicaid Management Information System (MMIS)", + }, + { + value: "Administrative Data Other", + description: true, + }, + ], + }, + ], + }, + { + value: "Electronic Health Records", + description: true, + }, + { + value: "Other Data Source", + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/AMMAD/index.test.tsx b/services/ui-src/src/measures/2024/AMMAD/index.test.tsx new file mode 100644 index 0000000000..d609c23ff4 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/index.test.tsx @@ -0,0 +1,260 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMM-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + EffectiveAcutePhaseTreatment: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMMAD/index.tsx b/services/ui-src/src/measures/2024/AMMAD/index.tsx new file mode 100644 index 0000000000..8de5b10e64 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/index.tsx @@ -0,0 +1,69 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AMMAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + // Conditional check to let rate be readonly when administrative data is the only option or no option is selected + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AMMAD/validation.ts b/services/ui-src/src/measures/2024/AMMAD/validation.ts new file mode 100644 index 0000000000..32b488be98 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/validation.ts @@ -0,0 +1,106 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const AMMADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateSameDenominatorSets(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMMADValidation]; diff --git a/services/ui-src/src/measures/2024/AMRAD/data.ts b/services/ui-src/src/measures/2024/AMRAD/data.ts new file mode 100644 index 0000000000..fb14331192 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMR-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of beneficiaries ages 19 to 64 who were identified as having persistent asthma and had a ratio of controller medications to total asthma medications of 0.50 or greater during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMRAD/index.test.tsx b/services/ui-src/src/measures/2024/AMRAD/index.test.tsx new file mode 100644 index 0000000000..59e3e67574 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +import * as DC from "dataConstants"; + +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMR-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")).toBeInTheDocument(); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { + MeasurementSpecification: "Other", + DidReport: "yes", + [DC.OPM_RATES]: [ + { + rate: [{ denominator: "", numerator: "", rate: "" }], + description: "", + }, + ], +}; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMRAD/index.tsx b/services/ui-src/src/measures/2024/AMRAD/index.tsx new file mode 100644 index 0000000000..898d7cd049 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/index.tsx @@ -0,0 +1,115 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as QMR from "components"; +import * as PMD from "./data"; +import { useFormContext, useWatch } from "react-hook-form"; +import { FormData, Measure } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; + +export const AMRAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const { getValues } = useFormContext<Measure.Form>(); + + // Watch Values of Form Questions + const watchOtherPerformanceMeasureRates = useWatch({ + name: "OtherPerformanceMeasure-Rates", + }); + + // Age Conditionals for Deviations from Measure Specifications/Optional Measure Stratification + const showOtherPerformanceMeasureRates = !!watchOtherPerformanceMeasureRates; + + const watchPersistentAsthma = useWatch({ + name: `PerformanceMeasure.rates.singleCategory`, + }); + + const showPersistentAsthma19To50 = !!watchPersistentAsthma?.[0]?.rate; + const showPersistentAsthma51To64 = !!watchPersistentAsthma?.[1]?.rate; + const showPersistentAsthmaTotal = !!watchPersistentAsthma?.[2]?.rate; + + // Logic to conditionally show age groups in Deviations from Measure Specifications/Optional Measure Stratification + const ageGroups = []; + + if (showPersistentAsthma19To50) { + ageGroups[0] = { label: "Ages 19 to 50", id: 0, isTotal: false }; + } + + if (showPersistentAsthma51To64) { + ageGroups[1] = { label: "Ages 51 to 64", id: 1, isTotal: false }; + } + + // Total field should show in OMS section if any of the NDRs in Performance Measures have been filled out. + if ( + showPersistentAsthma19To50 || + showPersistentAsthma51To64 || + showPersistentAsthmaTotal + ) { + ageGroups[2] = { label: "Total", id: 2, isTotal: true }; + } + + if (showOtherPerformanceMeasureRates) { + let otherRates = getValues("OtherPerformanceMeasure-Rates"); + otherRates.forEach((rate) => { + if (rate.description) { + ageGroups.push({ label: rate.description, id: ageGroups.length }); + } + }); + } + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {/* Show Performance Measure when HEDIS is selected from DataSource */} + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal={true} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + calcTotal={true} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AMRAD/types.ts b/services/ui-src/src/measures/2024/AMRAD/types.ts new file mode 100644 index 0000000000..2da252c887 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/types.ts @@ -0,0 +1,108 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as Type from "shared/types"; + +export namespace Measure { + export interface Props { + name: string; + year: string; + measureId: string; + handleSubmit?: any; + handleValidation?: any; + setValidationFunctions?: React.Dispatch<React.SetStateAction<any>>; + } + + interface RateFields { + numerator: string; + denominator: string; + rate: string; + } + + interface AggregateRate { + subRate: RateFields[]; + total: RateFields[]; + } + + export interface Form + extends Types.MeasurementSpecification, + Type.AdditionalNotes, + Types.CombinedRates, + Types.DateRange, + Types.DefinitionOfPopulation, + Types.StatusOfData, + Types.DidReport, + Types.WhyAreYouNotReporting, + Types.DataSource, + Types.PerformanceMeasure, + Types.DeviationFromMeasureSpecification, + Types.OtherPerformanceMeasure { + "PerformanceMeasure-Explanation": string; + "PerformanceMeasure-AgeRates-Persistent-Asthma": { + denominator: string; + numerator: string; + other: string; + id: string; + label: string; + rate: string; + }[]; + + AddtnlEthnicity: string[]; + AddtnlEthnicityRates: AggregateRate[]; + + AddtnlNonHispanicRace: string[]; + AddtnlNonHispanicRaceRates: AggregateRate[]; + AddtnlNonHispanicRaceSubCatTitle: { titles: string[] }[]; + AddtnlNonHispanicRaceSubCatOptions: string[][]; + AddtnlNonHispanicRaceSubCatRates: { rates: AggregateRate[] }[]; + + AddtnlNonHispanicSubCat: string[]; + AddtnlNonHispanicSubCatRates: AggregateRate[]; + + NonHispanicRacialCategories: string[]; + "NHRC-WhiteRates": AggregateRate; + "NHRC-BlackOrAfricanAmericanRates": AggregateRate; + "NHRC-AmericanIndianOrAlaskaNativeRates": AggregateRate; + "NHRC-AggregateAsianRates": AggregateRate; + "NHRC-IndependentAsianRates": AggregateRate[]; + "NHRC-AggregateHawaiianOrPacificIslanderRates": AggregateRate; + "NHRC-IndependentHawaiianOrPacificIslanderRates": AggregateRate[]; + + EthnicityCategories: string[]; + EthnicitySubCategories: string[]; + NonHispanicEthnicityRates: AggregateRate; + HispanicIndependentReporting: string; + HispanicEthnicityAggregateRate: AggregateRate; + IndependentHispanicOptions: string[]; + IndependentHispanicRates: AggregateRate[]; + + AsianIndependentReporting: string; + IndependentAsianOptions: string[]; + NativeHawaiianIndependentReporting: string; + IndependentNativeHawaiianOptions: string[]; + + SexOptions: string[]; + MaleSexRates: AggregateRate; + FemaleSexRates: AggregateRate; + + PrimaryLanguageOptions: string[]; + AddtnlPrimaryLanguage: string[]; + AddtnlPrimaryLanguageRates: AggregateRate[]; + EnglishLanguageRate: AggregateRate; + SpanishLanguageRate: AggregateRate; + + DisabilityStatusOptions: string[]; + DisabilitySSIRate: AggregateRate; + DisabilityNonSSIRate: AggregateRate; + AddtnlDisabilityStatusDesc: string; + AddtnlDisabilityRate: AggregateRate; + + GeographyOptions: string[]; + UrbanGeographyRate: AggregateRate; + RuralGeographyRate: AggregateRate; + AddtnlGeographyDesc: string; + AddtnlGeographyRate: AggregateRate; + + ACAGroupRate: AggregateRate; + } +} + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AMRAD/validation.ts b/services/ui-src/src/measures/2024/AMRAD/validation.ts new file mode 100644 index 0000000000..65c9d3365f --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/validation.ts @@ -0,0 +1,73 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AMRADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMRADValidation]; diff --git a/services/ui-src/src/measures/2024/AMRCH/data.ts b/services/ui-src/src/measures/2024/AMRCH/data.ts new file mode 100644 index 0000000000..1010929e66 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMR-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of children and adolescents ages 5 to 18 who were identified as having persistent asthma and had a ratio of controller medications to total asthma medications of 0.50 or greater during the measurement year.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMRCH/index.test.tsx b/services/ui-src/src/measures/2024/AMRCH/index.test.tsx new file mode 100644 index 0000000000..73410c0e37 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMR-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 5 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 18", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMRCH/index.tsx b/services/ui-src/src/measures/2024/AMRCH/index.tsx new file mode 100644 index 0000000000..dd068f69eb --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const AMRCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/AMRCH/validation.ts b/services/ui-src/src/measures/2024/AMRCH/validation.ts new file mode 100644 index 0000000000..6fad87278b --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const AMRCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMRCHValidation]; diff --git a/services/ui-src/src/measures/2024/APMCH/data.ts b/services/ui-src/src/measures/2024/APMCH/data.ts new file mode 100644 index 0000000000..71b68e39ab --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/data.ts @@ -0,0 +1,50 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("APM-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children and adolescents ages 1 to 17 who had two or more antipsychotic prescriptions and had metabolic testing. Three rates are reported:", + ], + questionListItems: [ + "Percentage of children and adolescents on antipsychotics who received blood glucose testing", + "Percentage of children and adolescents on antipsychotics who received cholesterol testing", + "Percentage of children and adolescents on antipsychotics who received blood glucose and cholesterol testing", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/APMCH/index.test.tsx b/services/ui-src/src/measures/2024/APMCH/index.test.tsx new file mode 100644 index 0000000000..3e17ee0a9e --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/index.test.tsx @@ -0,0 +1,304 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "APM-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Cholesterol: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + denominator: "1", + numerator: "1", + }, + { + label: "Total (Ages 1 to 17)", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + BloodGlucoseandCholesterol: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total (Ages 1 to 17)", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + BloodGlucose: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total (Ages 1 to 17)", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/APMCH/index.tsx b/services/ui-src/src/measures/2024/APMCH/index.tsx new file mode 100644 index 0000000000..124b5fa7d2 --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/index.tsx @@ -0,0 +1,69 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const APMCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/APMCH/validation.ts b/services/ui-src/src/measures/2024/APMCH/validation.ts new file mode 100644 index 0000000000..36bccea508 --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/validation.ts @@ -0,0 +1,109 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const APMCHValidation = (data: FormData) => { + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateEqualQualifierDenominatorsErrorMessage = ( + qualifier: string + ) => { + const isTotal = qualifier.split(" ")[0] === "Total"; + return `${ + isTotal ? "" : "The " + }${qualifier} denominator must be the same for each indicator.`; + }; + + const validateTotalNDRErrorMessage = ( + qualifier: string, + fieldType: string + ) => { + return `${fieldType} for the ${qualifier} Total rate is not equal to the sum of the ${qualifier} age-specific ${fieldType.toLowerCase()}s.`; + }; + + errorArray = [ + // Performance Measure and OPM Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR( + performanceMeasureArray, + undefined, + PMD.categories, + validateTotalNDRErrorMessage + ), + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + PMD.qualifiers, + undefined, + validateEqualQualifierDenominatorsErrorMessage + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [APMCHValidation]; diff --git a/services/ui-src/src/measures/2024/APPCH/data.ts b/services/ui-src/src/measures/2024/APPCH/data.ts new file mode 100644 index 0000000000..9cd3663f08 --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("APP-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children and adolescents ages 1 to 17 who had a new prescription for an antipsychotic medication and had documentation of psychosocial care as first-line treatment.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/APPCH/index.test.tsx b/services/ui-src/src/measures/2024/APPCH/index.test.tsx new file mode 100644 index 0000000000..d7b5023f5c --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "APP-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/APPCH/index.tsx b/services/ui-src/src/measures/2024/APPCH/index.tsx new file mode 100644 index 0000000000..77fd64f642 --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/index.tsx @@ -0,0 +1,69 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const APPCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/APPCH/validation.ts b/services/ui-src/src/measures/2024/APPCH/validation.ts new file mode 100644 index 0000000000..2876999f6d --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const APPCHValidation = (data: FormData) => { + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [APPCHValidation]; diff --git a/services/ui-src/src/measures/2024/BCSAD/data.ts b/services/ui-src/src/measures/2024/BCSAD/data.ts new file mode 100644 index 0000000000..e07f42f647 --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/data.ts @@ -0,0 +1,50 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; +import * as DC from "dataConstants"; + +export const { categories, qualifiers } = getCatQualLabels("BCS-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 50 to 74 who had a mammogram to screen for breast cancer.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/BCSAD/index.test.tsx b/services/ui-src/src/measures/2024/BCSAD/index.test.tsx new file mode 100644 index 0000000000..51f66c364f --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "BCS-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 50 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 74", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/BCSAD/index.tsx b/services/ui-src/src/measures/2024/BCSAD/index.tsx new file mode 100644 index 0000000000..11c9df5c17 --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const BCSAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/BCSAD/validation.ts b/services/ui-src/src/measures/2024/BCSAD/validation.ts new file mode 100644 index 0000000000..f4a8b4b06c --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/validation.ts @@ -0,0 +1,82 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const BCSValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [BCSValidation]; diff --git a/services/ui-src/src/measures/2024/CBPAD/data.ts b/services/ui-src/src/measures/2024/CBPAD/data.ts new file mode 100644 index 0000000000..05edfd1841 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CBP-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 85 who had a diagnosis of hypertension and whose blood pressure (BP) was adequately controlled (< 140/90 mm Hg) during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: "Medicaid Management Information System (MMIS)", + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CBPAD/index.test.tsx b/services/ui-src/src/measures/2024/CBPAD/index.test.tsx new file mode 100644 index 0000000000..549fef742d --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CBP-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 85", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CBPAD/index.tsx b/services/ui-src/src/measures/2024/CBPAD/index.tsx new file mode 100644 index 0000000000..3a8a1543c1 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CBPAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CBPAD/validation.ts b/services/ui-src/src/measures/2024/CBPAD/validation.ts new file mode 100644 index 0000000000..1a7a00dc76 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/validation.ts @@ -0,0 +1,83 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CBPValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + PMD.qualifiers[1].label + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CBPValidation]; diff --git a/services/ui-src/src/measures/2024/CBPHH/data.ts b/services/ui-src/src/measures/2024/CBPHH/data.ts new file mode 100644 index 0000000000..87ee74b1c1 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CBP-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of health home enrollees ages 18 to 85 who had a diagnosis of hypertension and whose blood pressure (BP) was adequately controlled (< 140/90 mm Hg) during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CBPHH/index.test.tsx b/services/ui-src/src/measures/2024/CBPHH/index.test.tsx new file mode 100644 index 0000000000..5747d87aa2 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CBP-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CBPHH/index.tsx b/services/ui-src/src/measures/2024/CBPHH/index.tsx new file mode 100644 index 0000000000..b146b68181 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/index.tsx @@ -0,0 +1,70 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CBPHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation hybridMeasure healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CBPHH/validation.ts b/services/ui-src/src/measures/2024/CBPHH/validation.ts new file mode 100644 index 0000000000..0303274d1c --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CBPValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + PMD.qualifiers[1].label + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CBPValidation]; diff --git a/services/ui-src/src/measures/2024/CCPAD/data.ts b/services/ui-src/src/measures/2024/CCPAD/data.ts new file mode 100644 index 0000000000..3aa27c9d30 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCP-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 21 to 44 who had a live birth, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception within 3 and 90 days of delivery", + "Were provided a long-acting reversible method of contraception (LARC) within 3 and 90 days of delivery", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCPAD/index.test.tsx b/services/ui-src/src/measures/2024/CCPAD/index.test.tsx new file mode 100644 index 0000000000..692883c69b --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/index.test.tsx @@ -0,0 +1,262 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCP-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Sixty Days Postpartum Rate", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ninety Days Postpartum Rate", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCPAD/index.tsx b/services/ui-src/src/measures/2024/CCPAD/index.tsx new file mode 100644 index 0000000000..3ff120d935 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/index.tsx @@ -0,0 +1,69 @@ +import * as CMQ from "../shared/CommonQuestions"; +import * as QMR from "components"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; +import * as PMD from "./data"; +import { useFormContext } from "react-hook-form"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CCPAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="OPA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CCPAD/validation.ts b/services/ui-src/src/measures/2024/CCPAD/validation.ts new file mode 100644 index 0000000000..0f8d3e01a9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CCPADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + // Performance Measure and OPM Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD.data, 1, 0), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Specific Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneQualRateHigherThanOtherQualOMS(1, 0), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCPADValidation]; diff --git a/services/ui-src/src/measures/2024/CCPCH/data.ts b/services/ui-src/src/measures/2024/CCPCH/data.ts new file mode 100644 index 0000000000..832103e113 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCP-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 15 to 20 who had a live birth, the percentage that:", + ], + questionListOrderedItems: [ + "Were provided a most effective or moderately effective method of contraception within 3 days of delivery and within 90 days of delivery", + "Were provided a long-acting reversible method of contraception (LARC) within 3 days of delivery and within 90 days of delivery", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCPCH/index.test.tsx b/services/ui-src/src/measures/2024/CCPCH/index.test.tsx new file mode 100644 index 0000000000..f637c34fd5 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCP-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCPCH/index.tsx b/services/ui-src/src/measures/2024/CCPCH/index.tsx new file mode 100644 index 0000000000..8638606b55 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/index.tsx @@ -0,0 +1,70 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; + +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CCPCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="OPA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + adultMeasure={false} + categories={PMD.categories} + isSingleSex={true} + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CCPCH/validation.ts b/services/ui-src/src/measures/2024/CCPCH/validation.ts new file mode 100644 index 0000000000..4b92314082 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CCPCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + // Performance Measure and OPM Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD.data, 1, 0), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Specific Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneQualRateHigherThanOtherQualOMS(1, 0), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + return errorArray; +}; + +export const validationFunctions = [CCPCHValidation]; diff --git a/services/ui-src/src/measures/2024/CCSAD/data.ts b/services/ui-src/src/measures/2024/CCSAD/data.ts new file mode 100644 index 0000000000..031f219d16 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/data.ts @@ -0,0 +1,79 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCS-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 21 to 64 who were screened for cervical cancer using either of the following criteria:", + ], + questionListItems: [ + "Women ages 21 to 64 who had cervical cytology performed within the last 3 years", + "Women ages 30 to 64 who had cervical high-risk human papillomavirus (hrHPV) testing performed within the last 5 years", + "Women ages 30 to 64 who had cervical cytology/high-risk human papillomavirus (hrHPV) cotesting within the last 5 years", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CCSAD/index.test.tsx b/services/ui-src/src/measures/2024/CCSAD/index.test.tsx new file mode 100644 index 0000000000..06b2dfc636 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/index.test.tsx @@ -0,0 +1,239 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCS-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Percentage of women ages 21 to 64 screened", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCSAD/index.tsx b/services/ui-src/src/measures/2024/CCSAD/index.tsx new file mode 100644 index 0000000000..407f441c71 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CCSAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CCSAD/validation.ts b/services/ui-src/src/measures/2024/CCSAD/validation.ts new file mode 100644 index 0000000000..8618be912d --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CCSADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCSADValidation]; diff --git a/services/ui-src/src/measures/2024/CCWAD/data.ts b/services/ui-src/src/measures/2024/CCWAD/data.ts new file mode 100644 index 0000000000..cc1dc4839a --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCW-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 21 to 44 at risk of unintended pregnancy, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception", + "Were provided a long-acting reversible method of contraception (LARC)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCWAD/index.test.tsx b/services/ui-src/src/measures/2024/CCWAD/index.test.tsx new file mode 100644 index 0000000000..857d1d838c --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/index.test.tsx @@ -0,0 +1,255 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCW-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "All Women Ages 21 to 44", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "All Women Ages 21 to 44", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCWAD/index.tsx b/services/ui-src/src/measures/2024/CCWAD/index.tsx new file mode 100644 index 0000000000..e864bae2a0 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CCWAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + measureAbbreviation={measureId} + measureName={name} + reportingYear={year} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="OPA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CCWAD/validation.ts b/services/ui-src/src/measures/2024/CCWAD/validation.ts new file mode 100644 index 0000000000..b364d39315 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/validation.ts @@ -0,0 +1,77 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CCWADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCWADValidation]; diff --git a/services/ui-src/src/measures/2024/CCWCH/data.ts b/services/ui-src/src/measures/2024/CCWCH/data.ts new file mode 100644 index 0000000000..ff2553830c --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCW-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 15 to 20 at risk of unintended pregnancy, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception", + "Were provided a long-acting reversible method of contraception (LARC)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCWCH/index.test.tsx b/services/ui-src/src/measures/2024/CCWCH/index.test.tsx new file mode 100644 index 0000000000..2eb411f577 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCW-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateOne); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCWCH/index.tsx b/services/ui-src/src/measures/2024/CCWCH/index.tsx new file mode 100644 index 0000000000..9ae6d6212f --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CCWCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="OPA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CCWCH/validation.ts b/services/ui-src/src/measures/2024/CCWCH/validation.ts new file mode 100644 index 0000000000..ef81cd00c9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CCWCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneQualRateHigherThanOtherQualOMS(), + ], + }), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCWCHValidation]; diff --git a/services/ui-src/src/measures/2024/CDFAD/data.ts b/services/ui-src/src/measures/2024/CDFAD/data.ts new file mode 100644 index 0000000000..f0eefd7bc3 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CDF-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older screened for depression on the date of the encounter or 14 days prior to the date of the encounter using an age-appropriate standardized depression screening tool, and if positive, a follow-up plan is documented on the date of the eligible encounter.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CDFAD/index.test.tsx b/services/ui-src/src/measures/2024/CDFAD/index.test.tsx new file mode 100644 index 0000000000..0f63f6d763 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CDF-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CDFAD/index.tsx b/services/ui-src/src/measures/2024/CDFAD/index.tsx new file mode 100644 index 0000000000..58b038cd81 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CDFAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CDFAD/validation.ts b/services/ui-src/src/measures/2024/CDFAD/validation.ts new file mode 100644 index 0000000000..056ae7e7f9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CDFADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CDFADValidation]; diff --git a/services/ui-src/src/measures/2024/CDFCH/data.ts b/services/ui-src/src/measures/2024/CDFCH/data.ts new file mode 100644 index 0000000000..6aad98c3f7 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CDF-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 12 to 17 screened for depression on the date of the encounter or 14 days prior to the date of the encounter using an age-appropriate standardized depression screening tool, and if positive, a follow-up plan is documented on the date of the qualifying encounter.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CDFCH/index.test.tsx b/services/ui-src/src/measures/2024/CDFCH/index.test.tsx new file mode 100644 index 0000000000..28b32a92da --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CDF-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateOne); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CDFCH/index.tsx b/services/ui-src/src/measures/2024/CDFCH/index.tsx new file mode 100644 index 0000000000..a4b19f2fb1 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CDFCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CDFCH/validation.ts b/services/ui-src/src/measures/2024/CDFCH/validation.ts new file mode 100644 index 0000000000..3e8c873c0b --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CDFCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CDFCHValidation]; diff --git a/services/ui-src/src/measures/2024/CDFHH/data.ts b/services/ui-src/src/measures/2024/CDFHH/data.ts new file mode 100644 index 0000000000..463c5bd591 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/data.ts @@ -0,0 +1,45 @@ +import * as DC from "dataConstants"; +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CDF-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of health home enrollees age 12 and older screened for depression on the date of the encounter or 14 days prior to the date of the encounter using an age-appropriate standardized depression screening tool, and if positive, a follow-up plan is documented on the date of the eligible encounter.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CDFHH/index.test.tsx b/services/ui-src/src/measures/2024/CDFHH/index.test.tsx new file mode 100644 index 0000000000..17efbfa03b --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CDF-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CDFHH/index.tsx b/services/ui-src/src/measures/2024/CDFHH/index.tsx new file mode 100644 index 0000000000..f4dcece783 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/index.tsx @@ -0,0 +1,76 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CDFHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateScale={100} + calcTotal + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure rateMultiplicationValue={100} /> + )} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={100} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CDFHH/validation.ts b/services/ui-src/src/measures/2024/CDFHH/validation.ts new file mode 100644 index 0000000000..bd98dd4b0c --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/validation.ts @@ -0,0 +1,90 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CDFHHValidation = (data: FormData) => { + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [CDFHHValidation]; diff --git a/services/ui-src/src/measures/2024/CHLAD/data.ts b/services/ui-src/src/measures/2024/CHLAD/data.ts new file mode 100644 index 0000000000..b190da3abc --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CHL-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 21 to 24 who were identified as sexually active and who had at least one test for chlamydia during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CHLAD/index.test.tsx b/services/ui-src/src/measures/2024/CHLAD/index.test.tsx new file mode 100644 index 0000000000..d7012d2650 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/index.test.tsx @@ -0,0 +1,240 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CHL-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 21 to 24", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CHLAD/index.tsx b/services/ui-src/src/measures/2024/CHLAD/index.tsx new file mode 100644 index 0000000000..436233f3ae --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CHLAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CHLAD/validation.ts b/services/ui-src/src/measures/2024/CHLAD/validation.ts new file mode 100644 index 0000000000..ade4d7d6a0 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/validation.ts @@ -0,0 +1,73 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CHLValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CHLValidation]; diff --git a/services/ui-src/src/measures/2024/CHLCH/data.ts b/services/ui-src/src/measures/2024/CHLCH/data.ts new file mode 100644 index 0000000000..78e4d80c4f --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CHL-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 16 to 20 who were identified as sexually active and who had at least one test for chlamydia during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CHLCH/index.test.tsx b/services/ui-src/src/measures/2024/CHLCH/index.test.tsx new file mode 100644 index 0000000000..b23ab6cd45 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CHL-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateOne); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 16 to 20", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CHLCH/index.tsx b/services/ui-src/src/measures/2024/CHLCH/index.tsx new file mode 100644 index 0000000000..b6ea7bac50 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CHLCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CHLCH/validation.ts b/services/ui-src/src/measures/2024/CHLCH/validation.ts new file mode 100644 index 0000000000..fc99ec9e07 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/validation.ts @@ -0,0 +1,73 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CHLValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CHLValidation]; diff --git a/services/ui-src/src/measures/2024/CISCH/data.ts b/services/ui-src/src/measures/2024/CISCH/data.ts new file mode 100644 index 0000000000..888e252841 --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/data.ts @@ -0,0 +1,84 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CIS-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children age 2 who had four diphtheria, tetanus and acellular pertussis (DTaP); three polio (IPV); one measles, mumps and rubella (MMR); three haemophilus influenza type B (HiB); three hepatitis B (Hep B), one chicken pox (VZV); four pneumococcal conjugate (PCV); one hepatitis A (HepA); two or three rotavirus (RV); and two influenza (flu) vaccines by their second birthday. The measure calculates a rate for each vaccine and three combination rates.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CISCH/index.test.tsx b/services/ui-src/src/measures/2024/CISCH/index.test.tsx new file mode 100644 index 0000000000..907342c349 --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/index.test.tsx @@ -0,0 +1,287 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CIS-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(44000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "DTaP", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "IPV", + }, + { + label: "MMR", + }, + { + label: "HiB", + }, + { + label: "Hep B", + }, + { + label: "VZV", + }, + { + label: "PCV", + }, + { + label: "Hep A", + }, + { + label: "RV", + }, + { + label: "Flu", + }, + { + label: "Combo 3", + }, + { + label: "Combo 7", + }, + { + label: "Combo 10", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CISCH/index.tsx b/services/ui-src/src/measures/2024/CISCH/index.tsx new file mode 100644 index 0000000000..657750858d --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const CISCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CISCH/validation.ts b/services/ui-src/src/measures/2024/CISCH/validation.ts new file mode 100644 index 0000000000..74e51512a0 --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/validation.ts @@ -0,0 +1,77 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CISCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ]; + + return errorArray; +}; + +export const validationFunctions = [CISCHValidation]; diff --git a/services/ui-src/src/measures/2024/COBAD/data.ts b/services/ui-src/src/measures/2024/COBAD/data.ts new file mode 100644 index 0000000000..84a6399741 --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("COB-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older with concurrent use of prescription opioids and benzodiazepines. Beneficiaries with a cancer diagnosis, sickle cell disease diagnosis, or in hospice or palliative care are excluded.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/COBAD/index.test.tsx b/services/ui-src/src/measures/2024/COBAD/index.test.tsx new file mode 100644 index 0000000000..ad01119e1b --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/index.test.tsx @@ -0,0 +1,250 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "COB-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "PQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/COBAD/index.tsx b/services/ui-src/src/measures/2024/COBAD/index.tsx new file mode 100644 index 0000000000..ffb402bcb3 --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const COBAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="PQA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/COBAD/validation.ts b/services/ui-src/src/measures/2024/COBAD/validation.ts new file mode 100644 index 0000000000..e4dab72b33 --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/validation.ts @@ -0,0 +1,83 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const COBADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + "Ages 65 to 85" + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [COBADValidation]; diff --git a/services/ui-src/src/measures/2024/COLAD/data.ts b/services/ui-src/src/measures/2024/COLAD/data.ts new file mode 100644 index 0000000000..23abb7471d --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/data.ts @@ -0,0 +1,50 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("COL-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 45 to 75 who had appropriate screening for colorectal cancer.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/COLAD/index.test.tsx b/services/ui-src/src/measures/2024/COLAD/index.test.tsx new file mode 100644 index 0000000000..06203e5d9b --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "COL-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 50 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 75", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/COLAD/index.tsx b/services/ui-src/src/measures/2024/COLAD/index.tsx new file mode 100644 index 0000000000..f5661a3b50 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const COLAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/COLAD/validation.ts b/services/ui-src/src/measures/2024/COLAD/validation.ts new file mode 100644 index 0000000000..b13853ec75 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const COLADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const errorReplacementText = "Ages 65 to 75"; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + errorReplacementText + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [COLADValidation]; diff --git a/services/ui-src/src/measures/2024/COLHH/data.ts b/services/ui-src/src/measures/2024/COLHH/data.ts new file mode 100644 index 0000000000..ca3c402387 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/data.ts @@ -0,0 +1,49 @@ +import * as DC from "dataConstants"; +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("COL-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of health home enrollees ages 46 to 75 who had appropriate screening for colorectal cancer.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/COLHH/index.test.tsx b/services/ui-src/src/measures/2024/COLHH/index.test.tsx new file mode 100644 index 0000000000..e0a5438f0f --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "COL-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/COLHH/index.tsx b/services/ui-src/src/measures/2024/COLHH/index.tsx new file mode 100644 index 0000000000..98c6d1b749 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const COLHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/COLHH/validation.ts b/services/ui-src/src/measures/2024/COLHH/validation.ts new file mode 100644 index 0000000000..060ee35919 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const COLHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const errorReplacementText = "Ages 65 to 75"; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + errorReplacementText + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [COLHHValidation]; diff --git a/services/ui-src/src/measures/2024/CPAAD/index.test.tsx b/services/ui-src/src/measures/2024/CPAAD/index.test.tsx new file mode 100644 index 0000000000..c74eae0357 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/index.test.tsx @@ -0,0 +1,158 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CPA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Did you collect question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByText("Did you collect this measure?")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which administrative protocol was used to administer the survey?" + ) + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which administrative protocol was used to administer the survey?" + ) + ).not.toBeInTheDocument(); + expect( + screen.getByText("Why did you not collect this measure") + ).toBeInTheDocument(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidCollect: "no", +}; + +const completedMeasureData = { + MeasurementSpecification: "AHRQ-NCQA", + DidCollect: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/index.tsx b/services/ui-src/src/measures/2024/CPAAD/index.tsx new file mode 100644 index 0000000000..33dd613db9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/index.tsx @@ -0,0 +1,43 @@ +import * as Q from "./questions"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import { useParams } from "react-router-dom"; +import * as QMR from "components"; +import { useFormContext } from "react-hook-form"; +import { FormData } from "./types"; +import { validationFunctions } from "./validation"; +import { useEffect } from "react"; + +export const CPAAD = ({ + name, + year, + setValidationFunctions, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const { coreSetId } = useParams(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <Q.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={coreSetId as string} + /> + {data["DidCollect"] !== "no" && ( + <> + <Q.HowDidYouReport /> + <CMQ.MeasurementSpecification type="AHRQ-NCQA" /> + <Q.DataSource /> + <Q.DefinitionOfPopulation /> + <Q.PerformanceMeasure /> + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx new file mode 100644 index 0000000000..b9ae0d7be2 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx @@ -0,0 +1,95 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DataSource = () => { + const register = useCustomRegister<FormData>(); + + return ( + <QMR.CoreQuestionWrapper testid="data-source" label="Data Source"> + <QMR.RadioButton + formControlProps={{ paddingBottom: 4 }} + label="Which version of the CAHPS survey was used for reporting?" + formLabelProps={{ fontWeight: 700 }} + {...register("DataSource-CAHPS-Version")} + options={[ + { displayValue: "CAHPS 5.1H", value: "CAHPS 5.1H" }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label={ + <> + Describe the data source ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + {...register("DataSource-CAHPS-Version-Other")} + />, + ], + }, + ]} + /> + <CUI.Heading size="sm"> + Which Supplemental Item Sets were included in the Survey + </CUI.Heading> + <QMR.Checkbox + formControlProps={{ paddingBottom: 4 }} + {...register("DataSource-Included-ItemSets")} + options={[ + { + displayValue: "No Supplemental Item Sets were included", + value: "No Supplemental Item Sets were included", + }, + { + displayValue: "Supplemental Items for Adult Survey 5.1H", + value: "Supplemental Items for Adult Survey 5.1H", + }, + { + displayValue: "Other CAHPS Item Set", + value: "Other CAHPS Item Set", + children: [ + <QMR.TextArea + label="Explain:" + {...register("DataSource-Included-ItemSets-Other")} + />, + ], + }, + ]} + label="Select all that apply:" + /> + <QMR.RadioButton + label="Which administrative protocol was used to administer the survey?" + formLabelProps={{ fontWeight: 700 }} + {...register("DataSource-Admin-Protocol")} + options={[ + { + displayValue: "NCQA/HEDIS CAHPS 5.1H administrative protocol", + value: "NCQA/HEDIS CAHPS 5.1H administrative protocol", + }, + + { + displayValue: "AHRQ CAHPS administrative protocol", + value: "AHRQ CAHPS administrative protocol", + }, + { + displayValue: "Other administrative protocol", + value: "Other administrative protocol", + children: [ + <QMR.TextArea + label="Explain:" + {...register("DataSource-Admin-Protocol-Other")} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx new file mode 100644 index 0000000000..dbe7d5f963 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx @@ -0,0 +1,66 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DefinitionOfPopulation = () => { + const register = useCustomRegister<FormData>(); + + return ( + <QMR.CoreQuestionWrapper + testid="definition-of-population" + label="Definition of Population Included in the Measure" + > + <CUI.Heading size="sm" as="h3"> + Definition of population included in the survey sample + </CUI.Heading> + <CUI.Text mt="3"> + Please select all populations that are included. For example, if your + data include both non-dual Medicaid beneficiaries and Medicare and + Medicaid Dual Eligibles, select both: + </CUI.Text> + <CUI.UnorderedList m="5" ml="10"> + <CUI.ListItem>Survey sample includes Medicaid population</CUI.ListItem> + <CUI.ListItem> + Survey sample includes Medicare and Medicaid Dually-Eligible + population + </CUI.ListItem> + </CUI.UnorderedList> + <QMR.Checkbox + {...register("DefinitionOfSurveySample")} + options={[ + { + displayValue: "Survey sample includes Medicaid population", + value: "SurveySampleIncMedicaidPop", + }, + { + displayValue: + "Survey sample includes CHIP population (e.g. pregnant women)", + value: "SurveySampleIncCHIP", + }, + { + displayValue: + "Survey sample includes Medicare and Medicaid Dually-Eligible population", + value: "SurveySampleIncMedicareMedicaidDualEligible", + }, + { + displayValue: "Other", + value: "SurveySampleIncOther", + children: [ + <QMR.TextInput + formLabelProps={{ fontWeight: "400" }} + label="Specify:" + {...register("DefinitionOfSurveySample-Other")} + />, + ], + }, + ]} + /> + <QMR.TextArea + label="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + formControlProps={{ paddingTop: "15px" }} + {...register("DefinitionOfSurveySample-Changes")} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx new file mode 100644 index 0000000000..afeb83a2c4 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx @@ -0,0 +1,38 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const HowDidYouReport = () => { + const register = useCustomRegister<FormData>(); + return ( + <QMR.CoreQuestionWrapper + testid="how-did-you-report" + label="How did you report this measure?" + > + <QMR.RadioButton + {...register("HowDidYouReport")} + options={[ + { + displayValue: "Submitted raw data to AHRQ (CAHPS Database)", + value: "Submitted raw data to AHRQ (CAHPS Database)", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + {...register("HowDidYouReport-Explanation")} + label="Explain" + formLabelProps={{ + fontWeight: "normal", + fontSize: "normal", + }} + />, + ], + }, + ]} + formLabelProps={{ fontWeight: "bold" }} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..10408e9884 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx @@ -0,0 +1,16 @@ +import * as QMR from "components"; + +export const PerformanceMeasure = () => { + return ( + <QMR.CoreQuestionWrapper + testid="performance-measure" + label="Performance Measure" + > + This measure provides information on the experiences of beneficiaries with + their health care and gives a general indication of how well the health + care meets the beneficiaries’ expectations. Results summarize + beneficiaries’ experiences through ratings, composites, and question + summary rates. + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx new file mode 100644 index 0000000000..6fa4b9a0b8 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx @@ -0,0 +1,41 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useFormContext } from "react-hook-form"; +import { WhyDidYouNotCollect } from "."; +import { FormData } from "../types"; + +interface Props { + measureName: string; + measureAbbreviation: string; + reportingYear: string; +} + +export const Reporting = ({ reportingYear }: Props) => { + const register = useCustomRegister<FormData>(); + const { watch } = useFormContext<FormData>(); + const watchRadioStatus = watch("DidCollect"); + + return ( + <> + <QMR.CoreQuestionWrapper + testid="reporting" + label="Did you collect this measure?" + > + <QMR.RadioButton + {...register("DidCollect")} + options={[ + { + displayValue: `Yes, we did collect data for the Consumer Assessment of Healthcare Providers and Systems (CAHPS®) Health Plan Survey 5.1H, Adult Version (Medicaid) (CPA-AD) for FFY ${reportingYear} quality measure reporting.`, + value: "yes", + }, + { + displayValue: `No, we did not collect data for the Consumer Assessment of Healthcare Providers and Systems (CAHPS®) Health Plan Survey 5.1H, Adult Version (Medicaid) (CPA-AD) for FFY ${reportingYear} quality measure reporting.`, + value: "no", + }, + ]} + /> + </QMR.CoreQuestionWrapper> + {watchRadioStatus?.includes("no") && <WhyDidYouNotCollect />} + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx new file mode 100644 index 0000000000..6e4ebc943b --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx @@ -0,0 +1,192 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; +import { useFlags } from "launchdarkly-react-client-sdk"; + +export const WhyDidYouNotCollect = () => { + const pheIsCurrent = useFlags()?.["periodOfHealthEmergency2024"]; + const register = useCustomRegister<FormData>(); + return ( + <QMR.CoreQuestionWrapper + testid="why-did-you-not-collect" + label="Why did you not collect this measure" + > + <QMR.Checkbox + {...register("WhyDidYouNotCollect")} + helperText="Select all that apply:" + renderHelperTextAbove + options={[ + { + displayValue: `Service not covered`, + value: "ServiceNotCovered", + }, + { + displayValue: `Population not covered`, + value: "PopulationNotCovered", + children: [ + <QMR.RadioButton + {...register("AmountOfPopulationNotCovered")} + options={[ + { + displayValue: "Entire population not covered", + value: "EntirePopulationNotCovered", + }, + { + displayValue: "Partial population not covered", + value: "PartialPopulationNotCovered", + children: [ + <QMR.TextArea + label="Explain the partial population not covered:" + {...register("PartialPopulationNotCoveredExplanation")} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: `Data not available`, + value: "DataNotAvailable", + children: [ + <QMR.Checkbox + {...register("WhyIsDataNotAvailable")} + label="Why is data not available?" + renderHelperTextAbove + helperText="Select all that apply:" + options={[ + { + displayValue: "Budget constraints", + value: "BudgetConstraints", + }, + { + displayValue: "Staff Constraints", + value: "StaffConstraints", + }, + { + displayValue: "Data inconsistencies/Accuracy", + value: "DataInconsistenciesAccuracyIssues", + children: [ + <QMR.TextArea + label="Explain the Data inconsistencies/Accuracy issues:" + {...register("DataInconsistenciesAccuracyIssues")} + />, + ], + }, + { + displayValue: "Data source not easily accessible", + value: "DataSourceNotEasilyAccessible", + children: [ + <QMR.Checkbox + label="Select all that apply:" + {...register("DataSourceNotEasilyAccessible")} + options={[ + { + displayValue: "Requires medical record review", + value: "RequiresMedicalRecordReview", + }, + { + displayValue: + "Requires data linkage which does not currently exist", + value: "RequireDataLinkage", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register( + "DataSourceNotEasilyAccessible-Other" + )} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Information not collected", + value: "InformationNotCollected", + children: [ + <QMR.Checkbox + label="Select all that apply:" + {...register("InformationNotCollected")} + options={[ + { + displayValue: + "Not Collected by Provider (Hospital/Health Plan)", + value: "NotCollectedByProviderHospitalHealthPlan", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register("InformationNotCollected-Other")} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register("WhyIsDataNotAvailable-Other")} + />, + ], + }, + ]} + />, + ], + }, + ...(pheIsCurrent + ? [ + { + displayValue: + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic", + value: "LimitationWithDatCollecitonReportAccuracyCovid", + children: [ + <QMR.TextArea + label="Describe your state's limitations with regard to collection, reporting, or accuracy of data for this measure:" + {...register( + "LimitationWithDatCollecitonReportAccuracyCovid" + )} + />, + ], + }, + ] + : []), + { + displayValue: "Small sample size (less than 30)", + value: "SmallSampleSizeLessThan30", + children: [ + <QMR.NumberInput + {...register("SmallSampleSizeLessThan30")} + label="Enter specific sample size:" + mask={/^([1-2]?\d)?$/i} + />, + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register("WhyDidYouNotCollect-Other")} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/index.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/index.tsx new file mode 100644 index 0000000000..3d81873b8a --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/index.tsx @@ -0,0 +1,6 @@ +export * from "./DataSource"; +export * from "./DefinitionOfPopulation"; +export * from "./HowDidYouReport"; +export * from "./PerformanceMeasure"; +export * from "./Reporting"; +export * from "./WhyDidYouNotCollect"; diff --git a/services/ui-src/src/measures/2024/CPAAD/types.ts b/services/ui-src/src/measures/2024/CPAAD/types.ts new file mode 100644 index 0000000000..cbba4c105d --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/types.ts @@ -0,0 +1,40 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as Type from "shared/types"; + +export interface FormData + extends Type.AdditionalNotes, + Types.MeasurementSpecification, + Types.DidCollect { + //HowDidYouReport + HowDidYouReport: string[]; + "HowDidYouReport-Explanation": string; + + //DataSource + "DataSource-Included-ItemSets": string[]; + "DataSource-Included-ItemSets-Other": string; + "DataSource-CAHPS-Version": string; + "DataSource-CAHPS-Version-Other": string; + "DataSource-Admin-Protocol": string; + "DataSource-Admin-Protocol-Other": string; + + //WhyDidYouNotCollect + WhyDidYouNotCollect: string[]; + AmountOfPopulationNotCovered: string; + PopulationNotCovered: string; + PartialPopulationNotCoveredExplanation: string; + WhyIsDataNotAvailable: string; + "WhyIsDataNotAvailable-Other": string; + DataInconsistenciesAccuracyIssues: string; + DataSourceNotEasilyAccessible: string; + "DataSourceNotEasilyAccessible-Other": string; + InformationNotCollected: string; + "InformationNotCollected-Other": string; + LimitationWithDatCollecitonReportAccuracyCovid: string; + SmallSampleSizeLessThan30: string; + "WhyDidYouNotCollect-Other": string; + + //DefinitionOfPopulation + DefinitionOfSurveySample: string[]; + "DefinitionOfSurveySample-Other": string; + "DefinitionOfSurveySample-Changes": string; +} diff --git a/services/ui-src/src/measures/2024/CPAAD/validation.ts b/services/ui-src/src/measures/2024/CPAAD/validation.ts new file mode 100644 index 0000000000..6e3919edd8 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/validation.ts @@ -0,0 +1,25 @@ +import { FormData } from "./types"; +import { validateReasonForNotReporting } from "measures/2024/shared/globalValidations"; +import * as DC from "dataConstants"; + +const CPAADValidation = (data: FormData) => { + let errorArray: any[] = []; + const whyDidYouNotCollect = data["WhyDidYouNotCollect"]; + + if (data["DidCollect"] === undefined) { + errorArray.push({ + errorLocation: "Did you collect this measure", + errorMessage: + "You must select at least one option for Did you collect this measure?", + }); + } + + if (data["DidCollect"] === DC.NO) { + errorArray = [...validateReasonForNotReporting(whyDidYouNotCollect, true)]; + return errorArray; + } + + return errorArray; +}; + +export const validationFunctions = [CPAADValidation]; diff --git a/services/ui-src/src/measures/2024/CPCCH/index.test.tsx b/services/ui-src/src/measures/2024/CPCCH/index.test.tsx new file mode 100644 index 0000000000..cd3562f317 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/index.test.tsx @@ -0,0 +1,158 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CPC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Did you collect question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByText("Did you collect this measure?")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which administrative protocol was used to administer the survey?" + ) + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which administrative protocol was used to administer the survey?" + ) + ).not.toBeInTheDocument(); + expect( + screen.getByText("Why did you not collect this measure") + ).toBeInTheDocument(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidCollect: "no", +}; + +const completedMeasureData = { + MeasurementSpecification: "AHRQ-NCQA", + DidCollect: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/index.tsx b/services/ui-src/src/measures/2024/CPCCH/index.tsx new file mode 100644 index 0000000000..b3e5829273 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/index.tsx @@ -0,0 +1,44 @@ +import * as Q from "./questions"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import { useParams } from "react-router-dom"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { useEffect } from "react"; + +export const CPCCH = ({ + name, + year, + setValidationFunctions, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<Types.DefaultFormData>(); + const { coreSetId } = useParams(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <Q.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={coreSetId as string} + /> + {data["DidCollect"] !== "no" && ( + <> + <Q.HowDidYouReport /> + <CMQ.MeasurementSpecification type="AHRQ-NCQA" /> + <Q.DataSource /> + <Q.DefinitionOfPopulation /> + <Q.PerformanceMeasure /> + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx new file mode 100644 index 0000000000..733718fced --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx @@ -0,0 +1,86 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DataSource = () => { + const register = useCustomRegister<FormData>(); + + return ( + <QMR.CoreQuestionWrapper testid="data-source" label="Data Source"> + <QMR.RadioButton + formControlProps={{ paddingBottom: 4 }} + label="Which version of the CAHPS survey was used for reporting?" + formLabelProps={{ fontWeight: 700 }} + {...register("DataSource-CAHPS-Version")} + options={[ + { displayValue: "CAHPS 5.1H", value: "CAHPS 5.1H" }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Describe the Data Soure:" + {...register("DataSource-CAHPS-Version-Other")} + />, + ], + }, + ]} + /> + <CUI.Heading size="sm"> + Which Supplemental Item Sets were included in the Survey + </CUI.Heading> + <QMR.Checkbox + formControlProps={{ paddingBottom: 4 }} + {...register("DataSource-Included-ItemSets")} + options={[ + { + displayValue: "No Supplemental Item Sets were included", + value: "No Supplemental Item Sets were included", + }, + { + displayValue: "CAHPS Item Set for Children with Chronic Conditions", + value: "CAHPS Item Set for Children with Chronic Conditions", + }, + { + displayValue: "Other CAHPS Item Set", + value: "Other CAHPS Item Set", + children: [ + <QMR.TextArea + label="Explain:" + {...register("DataSource-Included-ItemSets-Other")} + />, + ], + }, + ]} + label="Select all that apply:" + /> + <QMR.RadioButton + label="Which administrative protocol was used to administer the survey?" + formLabelProps={{ fontWeight: 700 }} + {...register("DataSource-Admin-Protocol")} + options={[ + { + displayValue: "NCQA/HEDIS CAHPS 5.1H administrative protocol", + value: "NCQA/HEDIS CAHPS 5.1H administrative protocol", + }, + + { + displayValue: "AHRQ CAHPS administrative protocol", + value: "AHRQ CAHPS administrative protocol", + }, + { + displayValue: "Other administrative protocol", + value: "Other administrative protocol", + children: [ + <QMR.TextArea + label="Explain:" + {...register("DataSource-Admin-Protocol-Other")} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx new file mode 100644 index 0000000000..9c8c01ad12 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx @@ -0,0 +1,49 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DefinitionOfPopulation = () => { + const register = useCustomRegister<FormData>(); + + return ( + <QMR.CoreQuestionWrapper + testid="definition-of-population" + label="Definition of Population Included in the Measure" + > + <CUI.Heading size="sm" as="h3" pb="3"> + Definition of population included in the survey sample + </CUI.Heading> + <QMR.RadioButton + {...register("DefinitionOfSurveySample")} + options={[ + { + displayValue: + "Survey sample includes CHIP (Title XXI) population only", + value: "SurveySampleiIncludesCHIPOnly", + }, + { + displayValue: + "Survey sample includes Medicaid (Title XIX) population only", + value: "SurveySampleIncludesMedicaidOnly", + }, + { + displayValue: + "Survey sample includes CHIP (Title XXI) and Medicaid (Title XIX) populations, combined", + value: "SurveySampleIncludesCHIPMedicaidCombined", + }, + { + displayValue: + "Two sets of survey results submitted; survey samples include CHIP and Medicaid (Title XIX) populations, separately", + value: "SurveySamplesIncludeCHIPAndMedicaidSeparately", + }, + ]} + /> + <QMR.TextArea + label="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + formControlProps={{ paddingTop: "15px" }} + {...register("DefinitionOfSurveySample-Changes")} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx new file mode 100644 index 0000000000..afeb83a2c4 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx @@ -0,0 +1,38 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const HowDidYouReport = () => { + const register = useCustomRegister<FormData>(); + return ( + <QMR.CoreQuestionWrapper + testid="how-did-you-report" + label="How did you report this measure?" + > + <QMR.RadioButton + {...register("HowDidYouReport")} + options={[ + { + displayValue: "Submitted raw data to AHRQ (CAHPS Database)", + value: "Submitted raw data to AHRQ (CAHPS Database)", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + {...register("HowDidYouReport-Explanation")} + label="Explain" + formLabelProps={{ + fontWeight: "normal", + fontSize: "normal", + }} + />, + ], + }, + ]} + formLabelProps={{ fontWeight: "bold" }} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..b3d4c84430 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx @@ -0,0 +1,22 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; + +export const PerformanceMeasure = () => { + return ( + <QMR.CoreQuestionWrapper + testid="performance-measure" + label="Performance Measure" + > + <CUI.Text> + This measure provides information on parents’ experiences with their + child’s health care. Results summarize children’s experiences through + ratings, composites, and individual question summary rates. + </CUI.Text> + <CUI.Text py="4"> + The Children with Chronic Conditions Supplemental Items provides + information on parents’ experience with their child’s health care for + the population of children with chronic conditions. + </CUI.Text> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx new file mode 100644 index 0000000000..c0cea60ced --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx @@ -0,0 +1,41 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useFormContext } from "react-hook-form"; +import { WhyDidYouNotCollect } from "."; +import { FormData } from "../types"; + +interface Props { + measureName: string; + measureAbbreviation: string; + reportingYear: string; +} + +export const Reporting = ({ reportingYear }: Props) => { + const register = useCustomRegister<FormData>(); + const { watch } = useFormContext<FormData>(); + const watchRadioStatus = watch("DidCollect"); + + return ( + <> + <QMR.CoreQuestionWrapper + testid="reporting" + label="Did you collect this measure?" + > + <QMR.RadioButton + {...register("DidCollect")} + options={[ + { + displayValue: `Yes, we did collect data for the Consumer Assessment of Healthcare Providers and Systems (CAHPS®) Health Plan Survey 5.1H - Child Version Including Medicaid and Children with Chronic Conditions Supplemental Items (CPC-CH) for FFY ${reportingYear} quality measure reporting`, + value: "yes", + }, + { + displayValue: `No, we did not collect data for the Consumer Assessment of Healthcare Providers and Systems (CAHPS®) Health Plan Survey 5.1H - Child Version Including Medicaid and Children with Chronic Conditions Supplemental Items (CPC-CH) for FFY ${reportingYear} quality measure reporting`, + value: "no", + }, + ]} + /> + </QMR.CoreQuestionWrapper> + {watchRadioStatus?.includes("no") && <WhyDidYouNotCollect />} + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx new file mode 100644 index 0000000000..ab32cd24c5 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx @@ -0,0 +1,184 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const WhyDidYouNotCollect = () => { + const register = useCustomRegister<FormData>(); + return ( + <QMR.CoreQuestionWrapper + testid="why-did-you-not-collect" + label="Why did you not collect this measure" + > + <QMR.Checkbox + {...register("WhyDidYouNotCollect")} + helperText="Select all that apply:" + renderHelperTextAbove + options={[ + { + displayValue: `Service not covered`, + value: "ServiceNotCovered", + }, + { + displayValue: `Population not covered`, + value: "PopulationNotCovered", + children: [ + <QMR.RadioButton + {...register("AmountOfPopulationNotCovered")} + options={[ + { + displayValue: "Entire population not covered", + value: "EntirePopulationNotCovered", + }, + { + displayValue: "Partial population not covered", + value: "PartialPopulationNotCovered", + children: [ + <QMR.TextArea + label="Explain the partial population not covered:" + {...register("PartialPopulationNotCoveredExplanation")} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: `Data not available`, + value: "DataNotAvailable", + children: [ + <QMR.Checkbox + {...register("WhyIsDataNotAvailable")} + label="Why is data not available?" + renderHelperTextAbove + helperText="Select all that apply:" + options={[ + { + displayValue: "Budget constraints", + value: "BudgetConstraints", + }, + { + displayValue: "Staff Constraints", + value: "StaffConstraints", + }, + { + displayValue: "Data inconsistencies/Accuracy", + value: "DataInconsistenciesAccuracyIssues", + children: [ + <QMR.TextArea + label="Explain the Data inconsistencies/Accuracy issues:" + {...register("DataInconsistenciesAccuracyIssues")} + />, + ], + }, + { + displayValue: "Data source not easily accessible", + value: "DataSourceNotEasilyAccessible", + children: [ + <QMR.Checkbox + label="Select all that apply:" + {...register("DataSourceNotEasilyAccessible")} + options={[ + { + displayValue: "Requires medical record review", + value: "RequiresMedicalRecordReview", + }, + { + displayValue: + "Requires data linkage which does not currently exist", + value: "RequireDataLinkage", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register( + "DataSourceNotEasilyAccessible-Other" + )} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Information not collected", + value: "InformationNotCollected", + children: [ + <QMR.Checkbox + label="Select all that apply:" + {...register("InformationNotCollected")} + options={[ + { + displayValue: + "Not Collected by Provider (Hospital/Health Plan)", + value: "NotCollectedByProviderHospitalHealthPlan", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register("InformationNotCollected-Other")} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register("WhyIsDataNotAvailable-Other")} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic", + value: "LimitationWithDatCollecitonReportAccuracyCovid", + children: [ + <QMR.TextArea + label="Describe your state's limitations with regard to collection, reporting, or accuracy of data for this measure:" + {...register("LimitationWithDatCollecitonReportAccuracyCovid")} + />, + ], + }, + { + displayValue: "Small sample size (less than 30)", + value: "SmallSampleSizeLessThan30", + children: [ + <QMR.NumberInput + {...register("SmallSampleSizeLessThan30")} + label="Enter specific sample size:" + mask={/^([1-2]?\d)?$/i} + />, + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Explain:" + {...register("WhyDidYouNotCollect-Other")} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/index.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/index.tsx new file mode 100644 index 0000000000..3d81873b8a --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/index.tsx @@ -0,0 +1,6 @@ +export * from "./DataSource"; +export * from "./DefinitionOfPopulation"; +export * from "./HowDidYouReport"; +export * from "./PerformanceMeasure"; +export * from "./Reporting"; +export * from "./WhyDidYouNotCollect"; diff --git a/services/ui-src/src/measures/2024/CPCCH/types.ts b/services/ui-src/src/measures/2024/CPCCH/types.ts new file mode 100644 index 0000000000..cbba4c105d --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/types.ts @@ -0,0 +1,40 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as Type from "shared/types"; + +export interface FormData + extends Type.AdditionalNotes, + Types.MeasurementSpecification, + Types.DidCollect { + //HowDidYouReport + HowDidYouReport: string[]; + "HowDidYouReport-Explanation": string; + + //DataSource + "DataSource-Included-ItemSets": string[]; + "DataSource-Included-ItemSets-Other": string; + "DataSource-CAHPS-Version": string; + "DataSource-CAHPS-Version-Other": string; + "DataSource-Admin-Protocol": string; + "DataSource-Admin-Protocol-Other": string; + + //WhyDidYouNotCollect + WhyDidYouNotCollect: string[]; + AmountOfPopulationNotCovered: string; + PopulationNotCovered: string; + PartialPopulationNotCoveredExplanation: string; + WhyIsDataNotAvailable: string; + "WhyIsDataNotAvailable-Other": string; + DataInconsistenciesAccuracyIssues: string; + DataSourceNotEasilyAccessible: string; + "DataSourceNotEasilyAccessible-Other": string; + InformationNotCollected: string; + "InformationNotCollected-Other": string; + LimitationWithDatCollecitonReportAccuracyCovid: string; + SmallSampleSizeLessThan30: string; + "WhyDidYouNotCollect-Other": string; + + //DefinitionOfPopulation + DefinitionOfSurveySample: string[]; + "DefinitionOfSurveySample-Other": string; + "DefinitionOfSurveySample-Changes": string; +} diff --git a/services/ui-src/src/measures/2024/CPCCH/validation.ts b/services/ui-src/src/measures/2024/CPCCH/validation.ts new file mode 100644 index 0000000000..57faa3c97f --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/validation.ts @@ -0,0 +1,25 @@ +import { FormData } from "./types"; +import { validateReasonForNotReporting } from "measures/2024/shared/globalValidations"; +import * as DC from "dataConstants"; + +const CPCCHValidation = (data: FormData) => { + let errorArray: any[] = []; + const whyDidYouNotCollect = data["WhyDidYouNotCollect"]; + + if (data["DidCollect"] === undefined) { + errorArray.push({ + errorLocation: "Did you collect this measure", + errorMessage: + "You must select at least one option for Did you collect this measure?", + }); + } + + if (data["DidCollect"] === DC.NO) { + errorArray = [...validateReasonForNotReporting(whyDidYouNotCollect, true)]; + return errorArray; + } + + return errorArray; +}; + +export const validationFunctions = [CPCCHValidation]; diff --git a/services/ui-src/src/measures/2024/CPUAD/data.ts b/services/ui-src/src/measures/2024/CPUAD/data.ts new file mode 100644 index 0000000000..084dcfcf13 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/data.ts @@ -0,0 +1,45 @@ +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; +import { DataDrivenTypes } from "../shared/CommonQuestions/types"; + +export const { categories, qualifiers } = getCatQualLabels("CPU-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries receiving long-term services and supports (LTSS) services ages 18 and older who have documentation of a comprehensive long-term services and supports (LTSS) care plan in a specified time frame that includes core elements. The following rates are reported:", + ], + questionListItems: [ + "Care Plan with Core Elements Documented. Beneficiaries who had a comprehensive LTSS care plan with 9 core elements documented within 120 days of enrollment (for new beneficiaries) or during the measurement year (for established beneficiaries).", + "Care Plan with Supplemental Elements Documented. Beneficiaries who had a comprehensive LTSS care plan with 9 core elements and at least 4 supplemental elements documented within 120 days of enrollment (for new beneficiaries) or during the measurement year (for established beneficiaries).", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If Reporting entities (e.g. health plans) used different data sources, please select all applicable data sources used below", + options: [ + { + value: DC.CASE_MANAGEMENT_RECORD_REVIEW, + description: false, + subOptions: [ + { + label: "What is the case management record data source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CPUAD/index.test.tsx b/services/ui-src/src/measures/2024/CPUAD/index.test.tsx new file mode 100644 index 0000000000..941b029ae9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/index.test.tsx @@ -0,0 +1,253 @@ +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CPU-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Care Plan with Core Elements Documented", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Care Plan with Supplemental Elements Documented", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CPUAD/index.tsx b/services/ui-src/src/measures/2024/CPUAD/index.tsx new file mode 100644 index 0000000000..8f27cb4edf --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/index.tsx @@ -0,0 +1,54 @@ +import * as PMD from "./data"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; + +export const CPUAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + isOtherMeasureSpecSelected, + showOptionalMeasureStrat, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation populationSampleSize /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure data={PMD.data} /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && <CMQ.NotCollectingOMS />} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/CPUAD/validation.ts b/services/ui-src/src/measures/2024/CPUAD/validation.ts new file mode 100644 index 0000000000..a5f255d041 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/validation.ts @@ -0,0 +1,61 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const CPUADValidation = (data: FormData) => { + const carePlans = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + + let errorArray: any[] = []; + const OPM = data[DC.OPM_RATES]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + carePlans, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + carePlans + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, carePlans), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, carePlans, data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, carePlans), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CPUADValidation]; diff --git a/services/ui-src/src/measures/2024/DEVCH/data.ts b/services/ui-src/src/measures/2024/DEVCH/data.ts new file mode 100644 index 0000000000..0798cb1281 --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/data.ts @@ -0,0 +1,69 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("DEV-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children screened for risk of developmental, behavioral, and social delays using a standardized screening tool in the 12 months preceding or on their first, second, or third birthday.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/DEVCH/index.test.tsx b/services/ui-src/src/measures/2024/DEVCH/index.test.tsx new file mode 100644 index 0000000000..3cff2c8e8f --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "DEV-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Children screened by 12 months of age", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Children screened by 24 months of age", + }, + { + label: "Children screened by 36 months of age", + }, + { + label: "Children ", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "OHSU", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/DEVCH/index.tsx b/services/ui-src/src/measures/2024/DEVCH/index.tsx new file mode 100644 index 0000000000..04904565d0 --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const DEVCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="OHSU" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/DEVCH/validation.ts b/services/ui-src/src/measures/2024/DEVCH/validation.ts new file mode 100644 index 0000000000..a1e26539d3 --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/validation.ts @@ -0,0 +1,77 @@ +import * as PMD from "./data"; +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const DEVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + customTotalLabel: "Children", + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ]; + + return errorArray; +}; + +export const validationFunctions = [DEVCHValidation]; diff --git a/services/ui-src/src/measures/2024/FUAAD/data.ts b/services/ui-src/src/measures/2024/FUAAD/data.ts new file mode 100644 index 0000000000..8afd9f589c --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUA-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries age 18 and Older with a principal diagnosis of Substance Use Disorder(SUD), or any diagnosis of drug overdose, for which there was follow-up. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUAAD/index.test.tsx b/services/ui-src/src/measures/2024/FUAAD/index.test.tsx new file mode 100644 index 0000000000..5215cd48cf --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Followupwithin7daysofEDvisit: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + denominator: "55", + numerator: "55", + }, + ], + Followupwithin30daysofEDvisit: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUAAD/index.tsx b/services/ui-src/src/measures/2024/FUAAD/index.tsx new file mode 100644 index 0000000000..854e393b71 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUAAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUAAD/validation.ts b/services/ui-src/src/measures/2024/FUAAD/validation.ts new file mode 100644 index 0000000000..9ff478d97d --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/validation.ts @@ -0,0 +1,88 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUAADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const sixtyDaysIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUAADValidation]; diff --git a/services/ui-src/src/measures/2024/FUACH/data.ts b/services/ui-src/src/measures/2024/FUACH/data.ts new file mode 100644 index 0000000000..f3c075bbf0 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUA-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries ages 13 to 17 years with a principal diagnosis of substance use disorder (SUD) or any diagnosis of drug overdose, for which there was follow-up. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUACH/index.test.tsx b/services/ui-src/src/measures/2024/FUACH/index.test.tsx new file mode 100644 index 0000000000..52a9b00378 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUA-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Children screened by 12 months of age", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Children screened by 24 months of age", + }, + { + label: "Children screened by 36 months of age", + }, + { + label: "Children ", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "OHSU", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUACH/index.tsx b/services/ui-src/src/measures/2024/FUACH/index.tsx new file mode 100644 index 0000000000..d9b56b5a4d --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUACH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUACH/validation.ts b/services/ui-src/src/measures/2024/FUACH/validation.ts new file mode 100644 index 0000000000..c5fab418b5 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { getPerfMeasureRateArray } from "../shared/globalValidations"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUACHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + return [...GV.validateReasonForNotReporting(whyNotReporting)]; + } + + let errorArray: any[] = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUACHValidation]; diff --git a/services/ui-src/src/measures/2024/FUAHH/data.ts b/services/ui-src/src/measures/2024/FUAHH/data.ts new file mode 100644 index 0000000000..906a7ceabc --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUA-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for health home enrollees age 13 and older with a principal diagnosis of substance use disorder (SUD), or any diagnosis of drug overdose, for which there was follow-up. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the enrollee received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the enrollee received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUAHH/index.test.tsx b/services/ui-src/src/measures/2024/FUAHH/index.test.tsx new file mode 100644 index 0000000000..4fe51ad800 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUA-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUAHH/index.tsx b/services/ui-src/src/measures/2024/FUAHH/index.tsx new file mode 100644 index 0000000000..3aaf7df513 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/index.tsx @@ -0,0 +1,70 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUAHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal={true} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure={true} /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + calcTotal={true} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUAHH/validation.ts b/services/ui-src/src/measures/2024/FUAHH/validation.ts new file mode 100644 index 0000000000..0148ee3dc9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/validation.ts @@ -0,0 +1,98 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUAHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const sixtyDaysIndex = 2; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.validateAtLeastOneDataSourceType(data), + // Performance Measure Validations + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...sameDenominatorError, + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUAHHValidation]; diff --git a/services/ui-src/src/measures/2024/FUHAD/data.ts b/services/ui-src/src/measures/2024/FUHAD/data.ts new file mode 100644 index 0000000000..65fa8059d2 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUH-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of discharges for beneficiaries age 18 and older who were hospitalized for treatment of selected mental illness or intentional self-harm diagnoses and who had a follow-up visit with a mental health provider. Two rates are reported:", + ], + questionListItems: [ + "Percentage of discharges for which the beneficiary received follow-up within 30 days after discharge", + "Percentage of discharges for which the beneficiary received follow-up within 7 days after discharge", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUHAD/index.test.tsx b/services/ui-src/src/measures/2024/FUHAD/index.test.tsx new file mode 100644 index 0000000000..8a374ef364 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/index.test.tsx @@ -0,0 +1,259 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUH-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUHAD/index.tsx b/services/ui-src/src/measures/2024/FUHAD/index.tsx new file mode 100644 index 0000000000..627ea39190 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUHAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={true} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUHAD/validation.ts b/services/ui-src/src/measures/2024/FUHAD/validation.ts new file mode 100644 index 0000000000..6a3aaed49e --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/validation.ts @@ -0,0 +1,103 @@ +import * as DC from "dataConstants"; +import * as PMD from "./data"; +import * as GV from "../shared/globalValidations"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + 1, + DefinitionOfDenominator + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUHValidation]; diff --git a/services/ui-src/src/measures/2024/FUHCH/data.ts b/services/ui-src/src/measures/2024/FUHCH/data.ts new file mode 100644 index 0000000000..c752385c1e --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUH-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of discharges for beneficiaries ages 6 to 17 who were hospitalized for treatment of selected mental illness or intentional self-harm diagnoses and who had a follow-up visit with a mental health provider. Two rates are reported:", + ], + questionListItems: [ + "Percentage of discharges for which the beneficiary received follow-up within 30 days after discharge", + "Percentage of discharges for which the beneficiary received follow-up within 7 days after discharge", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUHCH/index.test.tsx b/services/ui-src/src/measures/2024/FUHCH/index.test.tsx new file mode 100644 index 0000000000..5c5d729699 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUH-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 6 to 17", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 6 to 17", + rate: "100.0", + numerator: "65", + denominator: "65", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUHCH/index.tsx b/services/ui-src/src/measures/2024/FUHCH/index.tsx new file mode 100644 index 0000000000..c85acbdde7 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUHCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUHCH/validation.ts b/services/ui-src/src/measures/2024/FUHCH/validation.ts new file mode 100644 index 0000000000..7481e0dbc9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/validation.ts @@ -0,0 +1,96 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUHValidation]; diff --git a/services/ui-src/src/measures/2024/FUHHH/data.ts b/services/ui-src/src/measures/2024/FUHHH/data.ts new file mode 100644 index 0000000000..53c559a8f0 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUH-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of discharges for health home enrollees age 6 and older who were hospitalized for treatment of selected mental illness or intentional self-harm diagnoses and who had a follow-up visit with a mental health provider. Two rates are reported:", + ], + questionListItems: [ + "Percentage of discharges for which the enrollee received follow-up within 30 days after discharge", + "Percentage of discharges for which the enrollee received follow-up within 7 days after discharge", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUHHH/index.test.tsx b/services/ui-src/src/measures/2024/FUHHH/index.test.tsx new file mode 100644 index 0000000000..49d916afe9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUH-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUHHH/index.tsx b/services/ui-src/src/measures/2024/FUHHH/index.tsx new file mode 100644 index 0000000000..4ca9b61525 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/index.tsx @@ -0,0 +1,70 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUHHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal={true} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure={true} /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + calcTotal={true} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUHHH/validation.ts b/services/ui-src/src/measures/2024/FUHHH/validation.ts new file mode 100644 index 0000000000..eb3a347f9a --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/validation.ts @@ -0,0 +1,97 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUHHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const sixtyDaysIndex = 2; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + // Performance Measure Validations + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...sameDenominatorError, + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUHHHValidation]; diff --git a/services/ui-src/src/measures/2024/FUMAD/data.ts b/services/ui-src/src/measures/2024/FUMAD/data.ts new file mode 100644 index 0000000000..52780d8c56 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUM-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries age 18 and Older with a principal diagnosis of mental illness or intentional self-harm and who had a follow-up visit for mental illness. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for mental illness for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for mental illness for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUMAD/index.test.tsx b/services/ui-src/src/measures/2024/FUMAD/index.test.tsx new file mode 100644 index 0000000000..b12064245c --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/index.test.tsx @@ -0,0 +1,259 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUM-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUMAD/index.tsx b/services/ui-src/src/measures/2024/FUMAD/index.tsx new file mode 100644 index 0000000000..ce326afdd5 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as QMR from "components"; +import * as PMD from "./data"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUMAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUMAD/validation.ts b/services/ui-src/src/measures/2024/FUMAD/validation.ts new file mode 100644 index 0000000000..a07d152b59 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/validation.ts @@ -0,0 +1,94 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUMADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const sixtyDaysIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data["DidReport"] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...sameDenominatorError, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUMADValidation]; diff --git a/services/ui-src/src/measures/2024/FUMCH/data.ts b/services/ui-src/src/measures/2024/FUMCH/data.ts new file mode 100644 index 0000000000..e2dceae6a4 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUM-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries ages 6 to 17 with a principal diagnosis of mental illness or intentional self-harm and who had a follow-up visit for mental illness. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUMCH/index.test.tsx b/services/ui-src/src/measures/2024/FUMCH/index.test.tsx new file mode 100644 index 0000000000..cfd005e78e --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUM-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + "30dayfollowupafterEDvisitformentalillness": [ + { + label: "Ages 6 to 17", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + "7dayfollowupafterEDvisitformentalillness": [ + { + label: "Ages 6 to 17", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUMCH/index.tsx b/services/ui-src/src/measures/2024/FUMCH/index.tsx new file mode 100644 index 0000000000..eee1a702dc --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUMCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUMCH/validation.ts b/services/ui-src/src/measures/2024/FUMCH/validation.ts new file mode 100644 index 0000000000..82a10085c3 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/validation.ts @@ -0,0 +1,95 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUMCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUMCHValidation]; diff --git a/services/ui-src/src/measures/2024/FUMHH/data.ts b/services/ui-src/src/measures/2024/FUMHH/data.ts new file mode 100644 index 0000000000..e9f0ffd5ca --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUM-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for health home enrollees age 6 and older with a principal diagnosis of mental illness or intentional self-harm and who had a follow-up visit for mental illness. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for mental illness for which the enrollee received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for mental illness for which the enrollee received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUMHH/index.test.tsx b/services/ui-src/src/measures/2024/FUMHH/index.test.tsx new file mode 100644 index 0000000000..3db42b02ce --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUM-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUMHH/index.tsx b/services/ui-src/src/measures/2024/FUMHH/index.tsx new file mode 100644 index 0000000000..e0c2c9ccba --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/index.tsx @@ -0,0 +1,73 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const FUMHH = ({ + isNotReportingData, + isOtherMeasureSpecSelected, + isPrimaryMeasureSpecSelected, + measureId, + name, + setValidationFunctions, + showOptionalMeasureStrat, + year, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + measureAbbreviation={measureId} + measureName={name} + reportingYear={year} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure calcTotal data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure rateMultiplicationValue={100} /> + )} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + adultMeasure={false} + calcTotal + categories={PMD.categories} + performanceMeasureArray={performanceMeasureArray} + rateMultiplicationValue={100} + qualifiers={PMD.qualifiers} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FUMHH/validation.ts b/services/ui-src/src/measures/2024/FUMHH/validation.ts new file mode 100644 index 0000000000..263723cf9b --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/validation.ts @@ -0,0 +1,100 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FUMHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // Performance Measure Validations + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateTotalNDR(performanceMeasureArray), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUMHHValidation]; diff --git a/services/ui-src/src/measures/2024/FVAAD/data.ts b/services/ui-src/src/measures/2024/FVAAD/data.ts new file mode 100644 index 0000000000..bcf1dd1fb9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FVA-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 64 who received a flu vaccination between July 1 of the measurement year and the date when the CAHPS 5.1H Adult Survey was completed.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FVAAD/index.test.tsx b/services/ui-src/src/measures/2024/FVAAD/index.test.tsx new file mode 100644 index 0000000000..b6d44e1e57 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/index.test.tsx @@ -0,0 +1,246 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FVA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FVAAD/index.tsx b/services/ui-src/src/measures/2024/FVAAD/index.tsx new file mode 100644 index 0000000000..6990ff8b57 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/index.tsx @@ -0,0 +1,53 @@ +import { useEffect } from "react"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import * as QMR from "components"; + +export const FVAAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + removeLessThan30 + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSourceRadio /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && <CMQ.NotCollectingOMS />} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/FVAAD/validation.ts b/services/ui-src/src/measures/2024/FVAAD/validation.ts new file mode 100644 index 0000000000..77c4cad3c1 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const FVAADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FVAADValidation]; diff --git a/services/ui-src/src/measures/2024/HBDAD/data.ts b/services/ui-src/src/measures/2024/HBDAD/data.ts new file mode 100644 index 0000000000..79fb8e3a7d --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("HBD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 75 with diabetes (type 1 and type 2) whose hemoglobin A1c (HbA1c) was at the following levels during the measurement year:", + ], + questionListItems: ["HbA1c control (<8.0%)", "HbA1c poor control (>9.0%)"], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/HBDAD/index.test.tsx b/services/ui-src/src/measures/2024/HBDAD/index.test.tsx new file mode 100644 index 0000000000..1c4fcf4271 --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/index.test.tsx @@ -0,0 +1,243 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "HBD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 75", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/HBDAD/index.tsx b/services/ui-src/src/measures/2024/HBDAD/index.tsx new file mode 100644 index 0000000000..e64f45d716 --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const HBDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/HBDAD/validation.ts b/services/ui-src/src/measures/2024/HBDAD/validation.ts new file mode 100644 index 0000000000..a051bc4a60 --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const HBDADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + "Ages 65 to 75" + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [HBDADValidation]; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/data.ts b/services/ui-src/src/measures/2024/HPCMIAD/data.ts new file mode 100644 index 0000000000..afadd5e29b --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/data.ts @@ -0,0 +1,71 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("HPCMI-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 75 with a serious mental illness and diabetes (type 1 and type 2) who had hemoglobin A1c (HbA1c) in poor control (> 9.0%).", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx b/services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx new file mode 100644 index 0000000000..dfd9d8c746 --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "HPCMI-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 75", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/index.tsx b/services/ui-src/src/measures/2024/HPCMIAD/index.tsx new file mode 100644 index 0000000000..d118cc049a --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const HPCMIAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="NCQA" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/validation.ts b/services/ui-src/src/measures/2024/HPCMIAD/validation.ts new file mode 100644 index 0000000000..a32aa95b6d --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const HPCMIADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + PMD.qualifiers[1].label + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [HPCMIADValidation]; diff --git a/services/ui-src/src/measures/2024/HVLAD/data.ts b/services/ui-src/src/measures/2024/HVLAD/data.ts new file mode 100644 index 0000000000..a76b313992 --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("HVL-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older with a diagnosis of Human Immunodeficiency Virus (HIV) who had a HIV viral load less than 200 copies/mL at last HIV viral load test during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/HVLAD/index.test.tsx b/services/ui-src/src/measures/2024/HVLAD/index.test.tsx new file mode 100644 index 0000000000..11c88efbae --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/index.test.tsx @@ -0,0 +1,259 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "HVL-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/HVLAD/index.tsx b/services/ui-src/src/measures/2024/HVLAD/index.tsx new file mode 100644 index 0000000000..4faaffa7db --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const HVLAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HRSA" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/HVLAD/validation.ts b/services/ui-src/src/measures/2024/HVLAD/validation.ts new file mode 100644 index 0000000000..b5157b4dfe --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/validation.ts @@ -0,0 +1,81 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const HVLADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [HVLADValidation]; diff --git a/services/ui-src/src/measures/2024/IETAD/data.ts b/services/ui-src/src/measures/2024/IETAD/data.ts new file mode 100644 index 0000000000..7059ea5aa4 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/data.ts @@ -0,0 +1,49 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IET-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of new substance use disorder (SUD) episodes that result in treatment initiation and engagement. Two rates are reported:", + ], + questionListItems: [ + "Initiation of SUD Treatment. The percentage of new SUD episodes that result in treatment initiation through an inpatient SUD admission, outpatient visit, intensive outpatient encounter, partial hospitalization, telehealth visit, or medication treatment within 14 days.", + "Engagement of SUD Treatment. The percentage of new SUD episodes that have evidence of treatment engagement within 34 days of initiation.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/IETAD/index.test.tsx b/services/ui-src/src/measures/2024/IETAD/index.test.tsx new file mode 100644 index 0000000000..1d89e5a12b --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/index.test.tsx @@ -0,0 +1,307 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IET-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentOtherDrugAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + EngagementofAODTreatmentTotalAODAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + EngagementofAODTreatmentOtherDrugAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentOpioidAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentTotalAODAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + EngagementofAODTreatmentOpioidAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IETAD/index.tsx b/services/ui-src/src/measures/2024/IETAD/index.tsx new file mode 100644 index 0000000000..ba053e3b2c --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/index.tsx @@ -0,0 +1,72 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const IETAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + RateComponent={QMR.IETRate} + calcTotal={true} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/IETAD/validation.ts b/services/ui-src/src/measures/2024/IETAD/validation.ts new file mode 100644 index 0000000000..97fc839386 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/validation.ts @@ -0,0 +1,116 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const IETValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + const locationDictionary = GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ); + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups, + undefined, + (qual) => + `Denominators must be the same for ${locationDictionary([ + qual, + ])} for ${PMD.categories[i].label} and ${ + PMD.categories[i + 1].label + }.` + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data, 0, 1, 2), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary, + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateSameDenominatorSets(), + GV.validateOneCatRateHigherThanOtherCatOMS(0, 1, 2), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [IETValidation]; diff --git a/services/ui-src/src/measures/2024/IETHH/data.ts b/services/ui-src/src/measures/2024/IETHH/data.ts new file mode 100644 index 0000000000..03de2cdf41 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/data.ts @@ -0,0 +1,49 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IET-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of new substance use disorder (SUD) episodes that result in treatment initiation and engagement. Two rates are reported:", + ], + questionListItems: [ + "Initiation of SUD Treatment. The percentage of new SUD episodes that result in treatment initiation through an inpatient SUD admission, outpatient visit, intensive outpatient encounter, partial hospitalization, telehealth visit, or medication treatment within 14 days.", + "Engagement of SUD Treatment. The percentage of new SUD episodes that have evidence of treatment engagement within 34 days of initiation.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/IETHH/index.test.tsx b/services/ui-src/src/measures/2024/IETHH/index.test.tsx new file mode 100644 index 0000000000..227475acfa --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IET-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IETHH/index.tsx b/services/ui-src/src/measures/2024/IETHH/index.tsx new file mode 100644 index 0000000000..00391b7d11 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/index.tsx @@ -0,0 +1,74 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const IETHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + RateComponent={QMR.IETRate} + calcTotal={true} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + calcTotal + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/IETHH/validation.ts b/services/ui-src/src/measures/2024/IETHH/validation.ts new file mode 100644 index 0000000000..584a7eb97b --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/validation.ts @@ -0,0 +1,117 @@ +import * as DC from "dataConstants"; +import * as GV from "../shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "../shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const IETValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 2; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + const locationDictionary = GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ); + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups, + undefined, + (qual) => + `Denominators must be the same for ${locationDictionary([ + qual, + ])} for ${PMD.categories[i].label} and ${ + PMD.categories[i + 1].label + }.` + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data, 0, 1, 2), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary, + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateSameDenominatorSets(), + GV.validateOneCatRateHigherThanOtherCatOMS(0, 1, 2), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [IETValidation]; diff --git a/services/ui-src/src/measures/2024/IMACH/data.ts b/services/ui-src/src/measures/2024/IMACH/data.ts new file mode 100644 index 0000000000..2186f00979 --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/data.ts @@ -0,0 +1,80 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IMA-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of adolescents age 13 who had one dose of meningococcal vaccine, one tetanus, diphtheria toxoids and acellular pertussis (Tdap) vaccine, and have completed the human papillomavirus (HPV) vaccine series by their 13th birthday. The measure calculates a rate for each vaccine and two combination rates.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/IMACH/index.test.tsx b/services/ui-src/src/measures/2024/IMACH/index.test.tsx new file mode 100644 index 0000000000..464bf4c9ba --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/index.test.tsx @@ -0,0 +1,267 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IMA-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Meningococcal", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Tdap", + }, + { + label: "Human Papillomavirus (HPV)", + }, + { + label: "Combination 1 (Meningococcal, Tdap)", + }, + { + label: "Combination 2 (Meningococcal, Tdap, HPV)", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IMACH/index.tsx b/services/ui-src/src/measures/2024/IMACH/index.tsx new file mode 100644 index 0000000000..c3e884edbf --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const IMACH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure={true} hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/IMACH/validation.ts b/services/ui-src/src/measures/2024/IMACH/validation.ts new file mode 100644 index 0000000000..0a634d4b4a --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/validation.ts @@ -0,0 +1,78 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const DEVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ]; + + return errorArray; +}; + +export const validationFunctions = [DEVCHValidation]; diff --git a/services/ui-src/src/measures/2024/IUHH/data.ts b/services/ui-src/src/measures/2024/IUHH/data.ts new file mode 100644 index 0000000000..4fb35e0026 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/data.ts @@ -0,0 +1,70 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IU-HH"); + +const measureName = "IUHH"; + +const inputFieldNames = [ + { + label: "Number of Enrollee Months", + text: "Number of Enrollee Months", + id: "G3AVs1", + }, + { label: "Discharges", text: "Discharges", id: "MwGkaA" }, + { + label: "Discharges per 1,000 Enrollee Months", + text: "Discharges per 1,000 Enrollee Months", + id: "CGEK9m", + }, + { label: "Days", text: "Days", id: "jSSEHA" }, + { + label: "Days per 1,000 Enrollee Months", + text: "Days per 1,000 Enrollee Months", + id: "qjHhDk", + }, + { + label: "Average Length of Stay", + text: "Average Length of Stay", + id: "coDyWU", + }, +]; + +// Rate structure by index in row +const ndrFormulas = [ + // Discharges per 1,000 Enrollee Months + { + num: 1, + denom: 0, + rate: 2, + mult: 1000, + }, + // Days per 1,000 Enrollee Months + { + num: 3, + denom: 0, + rate: 4, + mult: 1000, + }, + // Average Length of Stay + { + num: 3, + denom: 1, + rate: 5, + mult: 1, + }, +]; + +export const data: DataDrivenTypes.PerformanceMeasure = { + customPrompt: + "Enter the appropriate data below. Completion of at least one set of Numerator/Denominator/Rate (numeric entry, other than zero) is required.", + questionText: [ + "Rate of acute inpatient care and services (total, mental and behavioral disorders, surgery, and medicine) per 1,000 enrollee months among health home enrollees.", + ], + questionListItems: [], + measureName, + inputFieldNames, + ndrFormulas, + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/IUHH/index.test.tsx b/services/ui-src/src/measures/2024/IUHH/index.test.tsx new file mode 100644 index 0000000000..efe93cbc86 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/index.test.tsx @@ -0,0 +1,783 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IU-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexValidateDualPopInformation).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.ComplexAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Inpatient: [ + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: "1", + label: "Discharges", + }, + { + value: "1000.0", + label: "Discharges per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Days", + }, + { + value: "1000.0", + label: "Days per 1,000 Enrollee Months", + }, + { + value: "1.0", + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: 1, + label: "Discharges", + }, + { + value: "1000.0", + label: "Discharges per 1,000 Enrollee Months", + }, + { + value: 1, + label: "Days", + }, + { + value: "1000.0", + label: "Days per 1,000 Enrollee Months", + }, + { + value: "1.0", + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + Maternity: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + Medicine: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + MentalandBehavioralDisorders: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + Surgery: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IUHH/index.tsx b/services/ui-src/src/measures/2024/IUHH/index.tsx new file mode 100644 index 0000000000..0ff9bce027 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/index.tsx @@ -0,0 +1,86 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { xNumbersYDecimals } from "utils"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const IUHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + RateComponent={QMR.ComplexRate} + calcTotal={true} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + allowNumeratorGreaterThanDenominator + customMask={xNumbersYDecimals(12, 1)} + /> + )} + <CMQ.CombinedRates healthHomeMeasure={true} /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + measureName={PMD.data.measureName} + inputFieldNames={PMD.data.inputFieldNames} + ndrFormulas={PMD.data.ndrFormulas} + allowNumeratorGreaterThanDenominator + adultMeasure={false} + calcTotal={true} + customMask={xNumbersYDecimals(12, 1)} + IUHHPerformanceMeasureArray={performanceMeasureArray} + componentFlag={"IU"} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/IUHH/validation.ts b/services/ui-src/src/measures/2024/IUHH/validation.ts new file mode 100644 index 0000000000..d39987f0f9 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/validation.ts @@ -0,0 +1,131 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +// Rate structure by index in row +const ndrForumlas = [ + // Discharges per 1,000 Enrollee Months + { + numerator: 1, + denominator: 0, + rateIndex: 2, + }, + // Days per 1,000 Enrollee Months + { + numerator: 3, + denominator: 0, + rateIndex: 4, + }, + // Average Length of Stay + { + numerator: 3, + denominator: 1, + rateIndex: 5, + }, +]; + +let OPM: any; + +const IUHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.ComplexValidateDualPopInformation( + performanceMeasureArray, + OPM, + definitionOfDenominator + ), + + // Performance Measure Validations + ...GV.ComplexAtLeastOneRateComplete(performanceMeasureArray, OPM), + ...GV.ComplexNoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrForumlas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.ComplexValidateNDRTotals( + performanceMeasureArray, + PMD.categories, + ndrForumlas + ), + ...GV.ComplexValueSameCrossCategory({ + rateData: performanceMeasureArray, + OPM, + }), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x]?.OPM?.[0]] }; + }); + return OPM === undefined + ? [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.["iuhh-rate"]?.rates ?? {}, + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ...GV.ComplexValueSameCrossCategoryOMS( + rateData?.["iuhh-rate"]?.rates ?? {}, + PMD.categories, + PMD.qualifiers, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ] + : [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.rates, + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ]; +}; + +export const validationFunctions = [IUHHValidation]; diff --git a/services/ui-src/src/measures/2024/LBWCH/index.test.tsx b/services/ui-src/src/measures/2024/LBWCH/index.test.tsx new file mode 100644 index 0000000000..5e24fee106 --- /dev/null +++ b/services/ui-src/src/measures/2024/LBWCH/index.test.tsx @@ -0,0 +1,90 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "LBW-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/LBWCH/index.tsx b/services/ui-src/src/measures/2024/LBWCH/index.tsx new file mode 100644 index 0000000000..ed5a2096b5 --- /dev/null +++ b/services/ui-src/src/measures/2024/LBWCH/index.tsx @@ -0,0 +1,17 @@ +import * as QMR from "components"; + +interface Props { + name: string; + year: string; +} + +export const LBWCH = ({ name, year }: Props) => { + return ( + <QMR.AutocompletedMeasureTemplate + year={year} + measureTitle={`LBW-CH - ${name}`} + performanceMeasureText="Percentage of live births that weighed less than 2,500 grams at birth during the measurement year." + performanceMeasureSubtext="To reduce state burden and streamline reporting, CMS will calculate this measure for states using state natality data obtained through the Centers for Disease Control and Prevention Wide-ranging Online Data for Epidemiologic Research (CDC WONDER)." + /> + ); +}; diff --git a/services/ui-src/src/measures/2024/LRCDCH/index.test.tsx b/services/ui-src/src/measures/2024/LRCDCH/index.test.tsx new file mode 100644 index 0000000000..e0b5d2a8d0 --- /dev/null +++ b/services/ui-src/src/measures/2024/LRCDCH/index.test.tsx @@ -0,0 +1,90 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "LRCD-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/LRCDCH/index.tsx b/services/ui-src/src/measures/2024/LRCDCH/index.tsx new file mode 100644 index 0000000000..31d02c9d91 --- /dev/null +++ b/services/ui-src/src/measures/2024/LRCDCH/index.tsx @@ -0,0 +1,20 @@ +import * as QMR from "components"; + +interface Props { + name: string; + year: string; +} + +export const LRCDCH = ({ name, year }: Props) => { + return ( + <QMR.AutocompletedMeasureTemplate + year={year} + measureTitle={`LRCD-CH - ${name}`} + performanceMeasureText="Percentage of nulliparous (first birth), term (37 or more completed weeks based on the obstetric estimate), singleton (one fetus), in a cephalic presentation (head-first) births delivered by cesarean during the measurement year." + performanceMeasureSubtext="To reduce state burden and streamline reporting, CMS will calculate + this measure for states using state natality data obtained through + the Centers for Disease Control and Prevention Wide-ranging Online + Data for Epidemiologic Research (CDC WONDER)." + /> + ); +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/data.ts b/services/ui-src/src/measures/2024/LSCCH/data.ts new file mode 100644 index 0000000000..ebb9d8dd3d --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/data.ts @@ -0,0 +1,70 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("LSC-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children 2 years of age who had one or more capillary or venous lead blood test for lead poisoning by their second birthday.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/index.test.tsx b/services/ui-src/src/measures/2024/LSCCH/index.test.tsx new file mode 100644 index 0000000000..3a3b2fdd51 --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/index.test.tsx @@ -0,0 +1,271 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "LSC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + performanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/index.tsx b/services/ui-src/src/measures/2024/LSCCH/index.tsx new file mode 100644 index 0000000000..603c666351 --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const LSCCH = ({ + name, + year, + measureId, + setValidationFunctions, + showOptionalMeasureStrat, + isNotReportingData, + isPrimaryMeasureSpecSelected, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + adultMeasure={false} + calcTotal + categories={PMD.categories} + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/validation.ts b/services/ui-src/src/measures/2024/LSCCH/validation.ts new file mode 100644 index 0000000000..c5967cf2bd --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const LSCCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateOPMRates(OPM), + + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [LSCCHValidation]; diff --git a/services/ui-src/src/measures/2024/MSCAD/data.ts b/services/ui-src/src/measures/2024/MSCAD/data.ts new file mode 100644 index 0000000000..9ea49bf5cb --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/data.ts @@ -0,0 +1,22 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("MSC-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The following components of this measure assess different facets of providing medical assistance with smoking and tobacco use cessation:", + ], + questionListItems: [ + "– A rolling average represents the percentage of beneficiaries age 18 and Older who were current smokers or tobacco users and who received advice to quit during the measurement year", + "– A rolling average represents the percentage of beneficiaries age 18 and Older who were current smokers or tobacco users and who discussed or were recommended cessation medications during the measurement year", + "– A rolling average represents the percentage of beneficiaries age 18 and Older who were current smokers or tobacco users and who discussed or were provided cessation methods or strategies during the measurement year", + ], + questionListTitles: [ + "Advising Smokers and Tobacco Users to Quit", + "Discussing Cessation Medications", + "Discussing Cessation Strategies", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/index.test.tsx b/services/ui-src/src/measures/2024/MSCAD/index.test.tsx new file mode 100644 index 0000000000..eb6af84ec6 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "MSC-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + DiscussingCessationMedications: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + AdvisingSmokersandTobaccoUserstoQuit: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + DiscussingCessationStrategies: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + PercentageofCurrentSmokersandTobaccoUsers: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/index.tsx b/services/ui-src/src/measures/2024/MSCAD/index.tsx new file mode 100644 index 0000000000..208f883eb7 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/index.tsx @@ -0,0 +1,57 @@ +import { useEffect } from "react"; +import * as Q from "./questions"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; + +export const MSCAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + removeLessThan30 + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <Q.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} rateReadOnly={false} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {/* Show Other Performance Measures when isHedis is not true */} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure rateAlwaysEditable /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && <CMQ.NotCollectingOMS />} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx b/services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx new file mode 100644 index 0000000000..b620776912 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx @@ -0,0 +1,40 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DataSource = () => { + const register = useCustomRegister<FormData>(); + + return ( + <QMR.CoreQuestionWrapper testid="data-source" label="Data Source"> + <QMR.RadioButton + formControlProps={{ paddingBottom: 4 }} + label="Which version of the CAHPS survey was used for reporting?" + formLabelProps={{ fontWeight: 700 }} + {...register("DataSource-CAHPS-Version")} + options={[ + { displayValue: "CAHPS 5.1H", value: "CAHPS 5.1H" }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label={ + <> + Describe the data source ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + {...register("DataSource-CAHPS-Version-Other")} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/questions/index.tsx b/services/ui-src/src/measures/2024/MSCAD/questions/index.tsx new file mode 100644 index 0000000000..ab26354b72 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/questions/index.tsx @@ -0,0 +1 @@ +export * from "./DataSource"; diff --git a/services/ui-src/src/measures/2024/MSCAD/types.ts b/services/ui-src/src/measures/2024/MSCAD/types.ts new file mode 100644 index 0000000000..72a21cda97 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/types.ts @@ -0,0 +1,20 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as Type from "shared/types"; + +export interface FormData + extends Types.DefinitionOfPopulation, + Types.StatusOfData, + Types.DateRange, + Types.DidReport, + Type.AdditionalNotes, + Types.WhyAreYouNotReporting, + Types.CombinedRates, + Types.OtherPerformanceMeasure, + Types.MeasurementSpecification, + Types.PerformanceMeasure, + Types.DeviationFromMeasureSpecification, + Types.OptionalMeasureStratification { + //DataSource + "DataSource-CAHPS-Version": string; + "DataSource-CAHPS-Version-Other": string; +} diff --git a/services/ui-src/src/measures/2024/MSCAD/validation.ts b/services/ui-src/src/measures/2024/MSCAD/validation.ts new file mode 100644 index 0000000000..5b66245cb6 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const MSCADValidation = (data: Types.DefaultFormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [MSCADValidation]; diff --git a/services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx b/services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx new file mode 100644 index 0000000000..51f23104f5 --- /dev/null +++ b/services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx @@ -0,0 +1,90 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "NCIDDS-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/NCIDDSAD/index.tsx b/services/ui-src/src/measures/2024/NCIDDSAD/index.tsx new file mode 100644 index 0000000000..efad50f17b --- /dev/null +++ b/services/ui-src/src/measures/2024/NCIDDSAD/index.tsx @@ -0,0 +1,24 @@ +import * as QMR from "components"; + +interface Props { + name: string; + year: string; +} + +export const NCIDDSAD = ({ name, year }: Props) => { + return ( + <QMR.AutocompletedMeasureTemplate + year={year} + measureTitle={`NCIDDS-AD - ${name}`} + performanceMeasureText="The National Core Indicators® - Intellectual and Development Disabilities (NCI-DD®) provide information on + beneficiaries’ experience and self-reported outcomes of long-term + services and supports of individuals with intellectual and/or + developmental disabilities (I/DD) and their families. NCI-DD includes + an in-person survey, family surveys for parents and guardians of + adults and children who receive I/DD supports, and a staff + stability survey. For the purpose of the Adult Core Set, only data + from the NCI-DD in-person survey will be reported." + performanceMeasureSubtext="To reduce state burden and streamline reporting, CMS will calculate this measure for states using data that states submitted to NASDDDS/HSRI (the NCI National Team) through the Online Data Entry System (ODESA). CMS will work with the NCI National Team to obtain data for all states that gave permission for data to be released to CMS for the purpose of Adult Core Set reporting." + /> + ); +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/data.ts b/services/ui-src/src/measures/2024/OEVCH/data.ts new file mode 100644 index 0000000000..333f111f9a --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OEV-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of enrolled children under age 21 who received a comprehensive or periodic oral evaluation within the measurement year. For FFY 2024 Child Core Set reporting, the following rate is required: Total ages < 1 to 20.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/index.test.tsx b/services/ui-src/src/measures/2024/OEVCH/index.test.tsx new file mode 100644 index 0000000000..8a5131cbdd --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/index.test.tsx @@ -0,0 +1,286 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OEV-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Age <1", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 1 to 2", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + label: "Total ages <1 to 20 ", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "ADA-DQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/index.tsx b/services/ui-src/src/measures/2024/OEVCH/index.tsx new file mode 100644 index 0000000000..593152fb0e --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/index.tsx @@ -0,0 +1,69 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as DC from "dataConstants"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const OEVCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type={DC.ADA_DQA} /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + adultMeasure={false} + calcTotal + categories={PMD.categories} + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/validation.ts b/services/ui-src/src/measures/2024/OEVCH/validation.ts new file mode 100644 index 0000000000..26bdbd6927 --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/validation.ts @@ -0,0 +1,77 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const OEVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [OEVCHValidation]; diff --git a/services/ui-src/src/measures/2024/OHDAD/data.ts b/services/ui-src/src/measures/2024/OHDAD/data.ts new file mode 100644 index 0000000000..b6f55dee76 --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OHD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of beneficiaries age 18 and older who received prescriptions for opioids with an average daily dosage greater than or equal to 90 morphine milligram equivalents (MME) over a period of 90 days or more. Beneficiaries with a cancer diagnosis, sickle cell disease diagnosis, or in hospice or palliative care are excluded.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OHDAD/index.test.tsx b/services/ui-src/src/measures/2024/OHDAD/index.test.tsx new file mode 100644 index 0000000000..e7758bce90 --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OHD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OHDAD/index.tsx b/services/ui-src/src/measures/2024/OHDAD/index.tsx new file mode 100644 index 0000000000..5f467373d6 --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const OHDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="PQA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/OHDAD/validation.ts b/services/ui-src/src/measures/2024/OHDAD/validation.ts new file mode 100644 index 0000000000..536e21a9d3 --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const OHDValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [OHDValidation]; diff --git a/services/ui-src/src/measures/2024/OUDAD/data.ts b/services/ui-src/src/measures/2024/OUDAD/data.ts new file mode 100644 index 0000000000..ec44784982 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/data.ts @@ -0,0 +1,20 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OUD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of Medicaid beneficiaries ages 18 to 64 with an opioid use disorder (OUD) who filled a prescription for or were administered or dispensed an FDA-approved medication for the disorder during the measurement year. Five rates are reported:", + "A total (overall) rate capturing any medications used in medication assisted treatment of opioid dependence and addiction (Rate 1)", + "Four separate rates representing the following types of FDA-approved drug products:", + ], + questionListItems: [ + "Buprenorphine (Rate 2)", + "Oral naltrexone (Rate 3)", + "Long-acting, injectable naltrexone (Rate 4)", + "Methadone (Rate 5)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OUDAD/index.test.tsx b/services/ui-src/src/measures/2024/OUDAD/index.test.tsx new file mode 100644 index 0000000000..e1bf707c59 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/index.test.tsx @@ -0,0 +1,264 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OUD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OUDAD/index.tsx b/services/ui-src/src/measures/2024/OUDAD/index.tsx new file mode 100644 index 0000000000..ec343f347d --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const OUDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/OUDAD/validation.ts b/services/ui-src/src/measures/2024/OUDAD/validation.ts new file mode 100644 index 0000000000..3e60d9b5b5 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const OUDValidation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + ], + }), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ]; + + return errorArray; +}; + +export const validationFunctions = [OUDValidation]; diff --git a/services/ui-src/src/measures/2024/OUDHH/data.ts b/services/ui-src/src/measures/2024/OUDHH/data.ts new file mode 100644 index 0000000000..6e0d3dae4d --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/data.ts @@ -0,0 +1,20 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OUD-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of Medicaid health home enrollees ages 18 to 64 with an opioid use disorder (OUD) who filled a prescription for or were administered or dispensed an FDA-approved medication for the disorder during the measurement year. Five rates are reported:", + "A total (overall) rate capturing any medications used in medication assisted treatment of opioid dependence and addiction (Rate 1)", + "Four separate rates representing the following types of FDA-approved drug products:", + ], + questionListItems: [ + "Buprenorphine (Rate 2)", + "Oral naltrexone (Rate 3)", + "Long-acting, injectable naltrexone (Rate 4)", + "Methadone (Rate 5)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OUDHH/index.test.tsx b/services/ui-src/src/measures/2024/OUDHH/index.test.tsx new file mode 100644 index 0000000000..efdc30747e --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OUD-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OUDHH/index.tsx b/services/ui-src/src/measures/2024/OUDHH/index.tsx new file mode 100644 index 0000000000..b5c4f11afa --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const OUDHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="CMS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/OUDHH/validation.ts b/services/ui-src/src/measures/2024/OUDHH/validation.ts new file mode 100644 index 0000000000..bfb9ae13c5 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const OUDValidation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [OUDValidation]; diff --git a/services/ui-src/src/measures/2024/PCRAD/data.ts b/services/ui-src/src/measures/2024/PCRAD/data.ts new file mode 100644 index 0000000000..e8ad1732a8 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/data.ts @@ -0,0 +1,17 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PCR-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "For beneficiaries ages 18 to 64, the number of acute inpatient and observation stays during the measurement year that were followed by an unplanned acute readmission for any diagnosis within 30 days and the predicted probability of an acute readmission. Data are reported in the following categories:", + ], + questionListItems: [ + "Count of Index Hospital Stays (IHS)", + "Count of Observed 30-Day Readmissions", + "Count of Expected 30-Day Readmissions", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/index.test.tsx b/services/ui-src/src/measures/2024/PCRAD/index.test.tsx new file mode 100644 index 0000000000..0b04212ae5 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/index.test.tsx @@ -0,0 +1,284 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PCR-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.PCRatLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.PCRnoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.PCRatLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.PCRnoNonZeroNumOrDenom).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + value: "1", + label: "Count of Index Hospital Stays", + id: 0, + }, + { + value: "1", + label: "Count of Observed 30-Day Readmissions", + id: 1, + }, + { + value: "100.0000", + label: "Observed Readmission Rate", + id: 2, + }, + { + value: "1.3333", + label: "Count of Expected 30-Day Readmissions", + id: 3, + }, + { + value: "133.3300", + label: "Expected Readmission Rate", + id: 4, + }, + { + value: "0.7500", + label: + "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + id: 5, + }, + { + value: "1", + label: "Count of Beneficiaries in Medicaid Population", + id: 6, + }, + { + value: "2", + label: "Number of Outliers", + id: 7, + }, + { + value: "2000.0", + label: + "Outlier Rate (Number of Outliers/Count of Beneficiaries in Medicaid Population) x 1,000", + id: 8, + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/index.tsx b/services/ui-src/src/measures/2024/PCRAD/index.tsx new file mode 100644 index 0000000000..8d087d85be --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/index.tsx @@ -0,0 +1,54 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { PCRADPerformanceMeasure } from "./questions/PerformanceMeasure"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; + +export const PCRAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + removeLessThan30 + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <PCRADPerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && <CMQ.NotCollectingOMS />} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..c55bde1720 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx @@ -0,0 +1,196 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../../shared/CommonQuestions/types"; +import { PerformanceMeasureData } from "../../shared/CommonQuestions/PerformanceMeasure/data"; +import { useWatch } from "react-hook-form"; +import { PCRRate } from "components/PCRRate"; +import { LabelData } from "utils"; +import * as DC from "dataConstants"; + +interface Props { + data: PerformanceMeasureData; + rateReadOnly?: boolean; + calcTotal?: boolean; + rateScale?: number; + customMask?: RegExp; +} + +interface NdrSetProps { + categories?: LabelData[]; + qualifiers?: LabelData[]; + rateReadOnly: boolean; + calcTotal: boolean; + rateScale?: number; + customMask?: RegExp; +} + +/** Maps over the categories given and creates rate sets based on the qualifiers, with a default of one rate */ +const CategoryNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers, + rateScale, + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + + return ( + <> + {categories.map((cat) => { + let rates: QMR.IRate[] | undefined = qualifiers?.map((qual, idx) => ({ + label: qual.label, + uid: `${cat.id}.${qual.id}`, + id: idx, + })); + + rates = rates?.length ? rates : [{ id: 0 }]; + + return ( + <> + <CUI.Text key={cat.id} fontWeight="bold" my="5"> + {cat.label} + </CUI.Text> + <QMR.Rate + readOnly={rateReadOnly} + rates={rates} + rateMultiplicationValue={rateScale} + customMask={customMask} + {...register(`PerformanceMeasure.rates.${cat.id}`)} + /> + </> + ); + })} + </> + ); +}; + +/** If no categories, we still need a rate for the PM + * 2023 and onward, categories are expected to have at least object filled for creating uid in database + */ +const QualifierNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers = [], + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + const categoryID = categories[0]?.id ? categories[0].id : DC.SINGLE_CATEGORY; + + const rates: QMR.IRate[] = qualifiers.map((item, idx) => ({ + label: item.label, + uid: `${categoryID}.${item.id}`, + id: idx, + })); + + return ( + <PCRRate + rates={rates} + readOnly={rateReadOnly} + customMask={customMask} + {...register(`${DC.PERFORMANCE_MEASURE}.${DC.RATES}.${categoryID}`)} + /> + ); +}; + +/** Creates the NDR sets based on given categories and qualifiers */ +const PerformanceMeasureNdrs = (props: NdrSetProps) => { + let ndrSets; + + //if there is a category and the category labels are filled out, create the NDR using the categories array + if ( + props.categories?.length && + props.categories?.some((item) => item.label) + ) { + ndrSets = <CategoryNdrSets {...props} />; + } else if (props.qualifiers?.length) { + ndrSets = <QualifierNdrSets {...props} />; + } + + return <CUI.Box key="PerformanceMeasureNdrSets">{ndrSets}</CUI.Box>; +}; + +/** Data Driven Performance Measure Comp */ +export const PCRADPerformanceMeasure = ({ + data, + calcTotal = false, + rateReadOnly, + rateScale, + customMask, +}: Props) => { + const register = useCustomRegister<Types.PerformanceMeasure>(); + const dataSourceWatch = useWatch<Types.DataSource>({ name: "DataSource" }) as + | string[] + | undefined; + const readOnly = + rateReadOnly ?? + dataSourceWatch?.every((source) => source === "AdministrativeData") ?? + true; + + return ( + <QMR.CoreQuestionWrapper + testid="performance-measure" + label="Performance Measure" + > + <CUI.Text mb={5}>{data.questionText}</CUI.Text> + {data.questionListItems && ( + <CUI.UnorderedList m="5" ml="10" spacing={5}> + {data.questionListItems.map((item, idx) => { + return ( + <CUI.ListItem key={`performanceMeasureListItem.${idx}`}> + {data.questionListTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionListTitles?.[idx]} + </CUI.Text> + )} + {item} + </CUI.ListItem> + ); + })} + </CUI.UnorderedList> + )} + <CUI.Text> + For beneficiaries ages 18 to 64, states should also report the rate of + beneficiaries who are identified as outliers based on high rates of + inpatient and observation stays during the measurement year. Data are + reported in the following categories: + </CUI.Text> + <CUI.UnorderedList m="5" ml="10" spacing={5}> + {[ + "Count of Beneficiaries in Medicaid Population", + "Number of Outliers", + ].map((item, idx) => { + return ( + <CUI.ListItem key={`performanceMeasureListItem.${idx}`}> + {data.questionListTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionListTitles?.[idx]} + </CUI.Text> + )} + {item} + </CUI.ListItem> + ); + })} + </CUI.UnorderedList> + <QMR.TextArea + label="If this measure has been reported by the state previously and there has been a substantial change in the rate or measure-eligible population, please provide any available context below:" + {...register("PerformanceMeasure.explanation")} + /> + <CUI.Text + fontWeight="bold" + mt={5} + data-cy="Enter a number for the numerator and the denominator" + > + Enter values below: + </CUI.Text> + <PerformanceMeasureNdrs + categories={data.categories} + qualifiers={data.qualifiers} + rateReadOnly={readOnly} + calcTotal={calcTotal} + rateScale={rateScale} + customMask={customMask} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/validation.ts b/services/ui-src/src/measures/2024/PCRAD/validation.ts new file mode 100644 index 0000000000..c2151d7513 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/validation.ts @@ -0,0 +1,110 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const ndrForumlas = [ + { + numerator: 1, + denominator: 0, + rateIndex: 2, + }, + { + numerator: 3, + denominator: 0, + rateIndex: 4, + }, + { + numerator: 1, + denominator: 3, + rateIndex: 5, + }, + { + numerator: 7, + denominator: 6, + rateIndex: 8, + }, +]; + +const PCRADValidation = (data: FormData) => { + let errorArray: any[] = []; + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + // const rateLocations = ndrForumlas.map((ndr) => ndr.rateIndex); + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // Performance Measure Validations + ...GV.PCRatLeastOneRateComplete(performanceMeasureArray, OPM, ageGroups), + ...GV.PCRnoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrForumlas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x].OPM[0]] }; + }); + return [ + ...GV.PCRnoNonZeroNumOrDenom( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ...GV.PCRatLeastOneRateComplete( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + PMD.qualifiers, + `Optional Measure Stratification: ${locationDictionary(label)}`, + true + ), + ]; +}; + +export const validationFunctions = [PCRADValidation]; diff --git a/services/ui-src/src/measures/2024/PCRHH/data.ts b/services/ui-src/src/measures/2024/PCRHH/data.ts new file mode 100644 index 0000000000..9fbc3ec16c --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/data.ts @@ -0,0 +1,17 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PCR-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "For health home enrollees ages 18 to 64, the number of acute inpatient and observation stays during the measurement year that were followed by an unplanned acute readmission for any diagnosis within 30 days and the predicted probability of an acute readmission. Data are reported in the following categories:", + ], + questionListItems: [ + "Count of Index Hospital Stays (IHS) ", + "Count of Observed 30-Day Readmissions", + "Count of Expected 30-Day Readmissions", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/index.test.tsx b/services/ui-src/src/measures/2024/PCRHH/index.test.tsx new file mode 100644 index 0000000000..eb8b93242d --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/index.test.tsx @@ -0,0 +1,276 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PCR-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexValidateDualPopInformation).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.ComplexAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + value: "1", + label: "Count of Index Hospital Stays", + id: 0, + }, + { + value: "1", + label: "Count of Observed 30-Day Readmissions", + id: 1, + }, + { + value: "100.0000", + label: "Observed Readmission Rate", + id: 2, + }, + { + value: "1.0000", + label: "Count of Expected 30-Day Readmissions", + id: 3, + }, + { + value: "100.0000", + label: "Expected Readmission Rate", + id: 4, + }, + { + value: "1.0000", + label: + "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + id: 5, + }, + { + value: "1", + label: "Count of Enrollees in Health Home Population", + id: 6, + }, + { + value: "1", + label: "Number of Outliers", + id: 7, + }, + { + value: "1000.0", + label: + "Outlier Rate (Number of Outliers/Count of Enrollees in Health Home Population) x 1,000", + id: 8, + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/index.tsx b/services/ui-src/src/measures/2024/PCRHH/index.tsx new file mode 100644 index 0000000000..7b14df872f --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/index.tsx @@ -0,0 +1,55 @@ +import { useEffect } from "react"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { PCRHHPerformanceMeasure } from "./questions/PerformanceMeasure"; + +export const PCRHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + removeLessThan30 + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <PCRHHPerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates healthHomeMeasure /> + {showOptionalMeasureStrat && <CMQ.NotCollectingOMS />} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..86248e130d --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx @@ -0,0 +1,196 @@ +import * as CUI from "@chakra-ui/react"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as QMR from "components"; +import { PerformanceMeasureData } from "measures/2024/shared/CommonQuestions/PerformanceMeasure/data"; +import { PCRRate } from "components/PCRRate"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useWatch } from "react-hook-form"; +import { LabelData } from "utils"; +import * as DC from "dataConstants"; + +interface Props { + data: PerformanceMeasureData; + rateReadOnly?: boolean; + calcTotal?: boolean; + rateScale?: number; + customMask?: RegExp; +} + +interface NdrSetProps { + categories?: LabelData[]; + qualifiers?: LabelData[]; + rateReadOnly: boolean; + calcTotal: boolean; + rateScale?: number; + customMask?: RegExp; +} + +/** Maps over the categories given and creates rate sets based on the qualifiers, with a default of one rate */ +const CategoryNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers, + rateScale, + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + + return ( + <> + {categories.map((cat) => { + let rates: QMR.IRate[] | undefined = qualifiers?.map((qual, idx) => ({ + label: qual.label, + uid: `${cat.id}.${qual.id}`, + id: idx, + })); + + rates = rates?.length ? rates : [{ id: 0 }]; + + return ( + <> + <CUI.Text key={cat.id} fontWeight="bold" my="5"> + {cat.label} + </CUI.Text> + <QMR.Rate + readOnly={rateReadOnly} + rates={rates} + rateMultiplicationValue={rateScale} + customMask={customMask} + {...register(`PerformanceMeasure.rates.${cat.id}`)} + /> + </> + ); + })} + </> + ); +}; + +/** If no categories, we still need a rate for the PM + * 2023 and onward, categories should have 1 LabelData object filled for creating uid in database + */ +const QualifierNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers = [], + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + const categoryID = categories[0]?.id ? categories[0].id : DC.SINGLE_CATEGORY; + + const rates: QMR.IRate[] = qualifiers.map((item, idx) => ({ + label: item.label, + uid: `${categoryID}.${item.id}`, + id: idx, + })); + + return ( + <PCRRate + rates={rates} + readOnly={rateReadOnly} + customMask={customMask} + {...register(`${DC.PERFORMANCE_MEASURE}.${DC.RATES}.${categoryID}`)} + /> + ); +}; + +/** Creates the NDR sets based on given categories and qualifiers */ +const PerformanceMeasureNdrs = (props: NdrSetProps) => { + let ndrSets; + + //if there is a category and the category labels are filled out, create the NDR using the categories array + if ( + props.categories?.length && + props.categories?.some((item) => item.label) + ) { + ndrSets = <CategoryNdrSets {...props} />; + } else if (props.qualifiers?.length) { + ndrSets = <QualifierNdrSets {...props} />; + } + + return <CUI.Box key="PerformanceMeasureNdrSets">{ndrSets}</CUI.Box>; +}; + +/** Data Driven Performance Measure Comp */ +export const PCRHHPerformanceMeasure = ({ + data, + calcTotal = false, + rateReadOnly, + rateScale, + customMask, +}: Props) => { + const register = useCustomRegister<Types.PerformanceMeasure>(); + const dataSourceWatch = useWatch<Types.DataSource>({ name: "DataSource" }) as + | string[] + | undefined; + const readOnly = + rateReadOnly ?? + dataSourceWatch?.every((source) => source === "AdministrativeData") ?? + true; + + return ( + <QMR.CoreQuestionWrapper + testid="performance-measure" + label="Performance Measure" + > + <CUI.Text mb={5}>{data.questionText}</CUI.Text> + {data.questionListItems && ( + <CUI.UnorderedList m="5" ml="10" spacing={5}> + {data.questionListItems.map((item, idx) => { + return ( + <CUI.ListItem key={`performanceMeasureListItem.${idx}`}> + {data.questionListTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionListTitles?.[idx]} + </CUI.Text> + )} + {item} + </CUI.ListItem> + ); + })} + </CUI.UnorderedList> + )} + <CUI.Text> + For Health Home enrollees ages 18 to 64, states should also report the + rate of enrollees who are identified as outliers based on high rates of + inpatient and observation stays during the measurement year. Data are + reported in the following categories: + </CUI.Text> + <CUI.UnorderedList m="5" ml="10" spacing={5}> + {[ + "Count of Enrollees in Health Home Population", + "Number of Outliers", + ].map((item, idx) => { + return ( + <CUI.ListItem key={`performanceMeasureListItem.${idx}`}> + {data.questionListTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionListTitles?.[idx]} + </CUI.Text> + )} + {item} + </CUI.ListItem> + ); + })} + </CUI.UnorderedList> + <QMR.TextArea + label="If this measure has been reported by the state previously and there has been a substantial change in the rate or measure-eligible population, please provide any available context below:" + {...register("PerformanceMeasure.explanation")} + /> + <CUI.Text + fontWeight="bold" + mt={5} + data-cy="Enter a number for the numerator and the denominator" + > + Enter values below: + </CUI.Text> + <PerformanceMeasureNdrs + categories={data.categories} + qualifiers={data.qualifiers} + rateReadOnly={readOnly} + calcTotal={calcTotal} + rateScale={rateScale} + customMask={customMask} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/validation.ts b/services/ui-src/src/measures/2024/PCRHH/validation.ts new file mode 100644 index 0000000000..25477d23ad --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/validation.ts @@ -0,0 +1,110 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const ndrForumlas = [ + { + numerator: 1, + denominator: 0, + rateIndex: 2, + }, + { + numerator: 3, + denominator: 0, + rateIndex: 4, + }, + { + numerator: 1, + denominator: 3, + rateIndex: 5, + }, + { + numerator: 7, + denominator: 6, + rateIndex: 8, + }, +]; + +const PCRHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + // const rateLocations = ndrForumlas.map((ndr) => ndr.rateIndex); + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + // Performance Measure Validations + ...GV.PCRatLeastOneRateComplete(performanceMeasureArray, OPM, ageGroups), + ...GV.PCRnoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrForumlas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x].OPM[0]] }; + }); + return [ + ...GV.PCRnoNonZeroNumOrDenom( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ...GV.PCRatLeastOneRateComplete( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + PMD.qualifiers, + `Optional Measure Stratification: ${locationDictionary(label)}`, + true + ), + ]; +}; + +export const validationFunctions = [PCRHHValidation]; diff --git a/services/ui-src/src/measures/2024/PPC2CH/data.ts b/services/ui-src/src/measures/2024/PPC2CH/data.ts new file mode 100644 index 0000000000..4304930d74 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPC2CH/data.ts @@ -0,0 +1,79 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PPC2-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of deliveries of live births on or between October 8 of the year prior to the measurement year and October 7 of the measurement year. For these beneficiaries, the measure assesses the following facets of prenatal and postpartum care:", + ], + questionListItems: [ + "Timeliness of Prenatal Care: Percentage of deliveries that received a prenatal care visit in the first trimester, on or before the enrollment start date or within 42 days of enrollment in Medicaid/CHIP.", + "Postpartum Care: Percentage of deliveries that had a postpartum visit on or between 7 and 84 days after delivery.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.VITAL_DATA_SOURCE, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.VITAL_DATA_SOURCE, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/PPC2CH/index.test.tsx b/services/ui-src/src/measures/2024/PPC2CH/index.test.tsx new file mode 100644 index 0000000000..db5b04d833 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPC2CH/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PPC2-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PPC2CH/index.tsx b/services/ui-src/src/measures/2024/PPC2CH/index.tsx new file mode 100644 index 0000000000..15d91fff14 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPC2CH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PPC2CH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure={true} hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PPC2CH/validation.ts b/services/ui-src/src/measures/2024/PPC2CH/validation.ts new file mode 100644 index 0000000000..07c2273d68 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPC2CH/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PPC2CHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [PPC2CHValidation]; diff --git a/services/ui-src/src/measures/2024/PPCAD/data.ts b/services/ui-src/src/measures/2024/PPCAD/data.ts new file mode 100644 index 0000000000..37d536ecca --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/data.ts @@ -0,0 +1,71 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PPC-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of deliveries of live births on or between October 8 of the year prior to the measurement year and October 7 of the measurement year that had a postpartum visit on or between 7 and 84 days after delivery.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/PPCAD/index.test.tsx b/services/ui-src/src/measures/2024/PPCAD/index.test.tsx new file mode 100644 index 0000000000..96cffc8181 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PPC-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PPCAD/index.tsx b/services/ui-src/src/measures/2024/PPCAD/index.tsx new file mode 100644 index 0000000000..1c96907498 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PPCAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + isSingleSex + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PPCAD/validation.ts b/services/ui-src/src/measures/2024/PPCAD/validation.ts new file mode 100644 index 0000000000..bcd49ef068 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PPCADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [PPCADValidation]; diff --git a/services/ui-src/src/measures/2024/PQI01AD/data.ts b/services/ui-src/src/measures/2024/PQI01AD/data.ts new file mode 100644 index 0000000000..1f69e3bbf1 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI01-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations for a principal diagnosis of diabetes with short-term complications (ketoacidosis, hyperosmolarity, or coma) per 100,000 beneficiary months for beneficiaries age 18 and older.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI01AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI01AD/index.test.tsx new file mode 100644 index 0000000000..0a14aff236 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI01-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI01AD/index.tsx b/services/ui-src/src/measures/2024/PQI01AD/index.tsx new file mode 100644 index 0000000000..eb39b053c9 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/index.tsx @@ -0,0 +1,83 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PQI01AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="AHRQ" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateScale={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + allowNumeratorGreaterThanDenominator + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + allowNumeratorGreaterThanDenominator + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + allowNumeratorGreaterThanDenominator + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI01AD/validation.ts b/services/ui-src/src/measures/2024/PQI01AD/validation.ts new file mode 100644 index 0000000000..e32c17b402 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PQI01Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const age65PlusIndex = 0; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI01Validation]; diff --git a/services/ui-src/src/measures/2024/PQI05AD/data.ts b/services/ui-src/src/measures/2024/PQI05AD/data.ts new file mode 100644 index 0000000000..153ddbc424 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI05-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations with a principal diagnosis of chronic obstructive pulmonary disease (COPD) or asthma per 100,000 beneficiary months for beneficiaries age 40 and older.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI05AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI05AD/index.test.tsx new file mode 100644 index 0000000000..f9aaa34578 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI05-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI05AD/index.tsx b/services/ui-src/src/measures/2024/PQI05AD/index.tsx new file mode 100644 index 0000000000..e7e8a087ca --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/index.tsx @@ -0,0 +1,83 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PQI05AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="AHRQ" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateScale={100000} + allowNumeratorGreaterThanDenominator + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + allowNumeratorGreaterThanDenominator + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + allowNumeratorGreaterThanDenominator + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI05AD/validation.ts b/services/ui-src/src/measures/2024/PQI05AD/validation.ts new file mode 100644 index 0000000000..7ffd494268 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/validation.ts @@ -0,0 +1,90 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PQI05Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + const age65PlusIndex = 0; + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + errorArray = [ + ...errorArray, + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + 1, + ageGroups.map((item) => item.label) + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI05Validation]; diff --git a/services/ui-src/src/measures/2024/PQI08AD/data.ts b/services/ui-src/src/measures/2024/PQI08AD/data.ts new file mode 100644 index 0000000000..07b56d8793 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI08-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations with a principal diagnosis of heart failure per 100,000 beneficiary months for beneficiaries age 18 and older.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI08AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI08AD/index.test.tsx new file mode 100644 index 0000000000..00037624cf --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI08-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI08AD/index.tsx b/services/ui-src/src/measures/2024/PQI08AD/index.tsx new file mode 100644 index 0000000000..34e61d1080 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/index.tsx @@ -0,0 +1,83 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PQI08AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="AHRQ" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateScale={100000} + allowNumeratorGreaterThanDenominator + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + allowNumeratorGreaterThanDenominator + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + allowNumeratorGreaterThanDenominator + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI08AD/validation.ts b/services/ui-src/src/measures/2024/PQI08AD/validation.ts new file mode 100644 index 0000000000..5077f08dd9 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/validation.ts @@ -0,0 +1,81 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PQI08Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const age65PlusIndex = 0; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI08Validation]; diff --git a/services/ui-src/src/measures/2024/PQI15AD/data.ts b/services/ui-src/src/measures/2024/PQI15AD/data.ts new file mode 100644 index 0000000000..3f792bacc1 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI15-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations with a principal diagnosis of asthma per 100,000 beneficiary months for beneficiaries ages 18 to 39.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI15AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI15AD/index.test.tsx new file mode 100644 index 0000000000..719cd2076b --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI15-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI15AD/index.tsx b/services/ui-src/src/measures/2024/PQI15AD/index.tsx new file mode 100644 index 0000000000..beb4d7074d --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/index.tsx @@ -0,0 +1,82 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PQI15AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="AHRQ" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateScale={100000} + allowNumeratorGreaterThanDenominator + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + allowNumeratorGreaterThanDenominator + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + )} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + performanceMeasureArray={performanceMeasureArray} + adultMeasure + allowNumeratorGreaterThanDenominator + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI15AD/validation.ts b/services/ui-src/src/measures/2024/PQI15AD/validation.ts new file mode 100644 index 0000000000..de17a09d5f --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/validation.ts @@ -0,0 +1,65 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PQI15Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI15Validation]; diff --git a/services/ui-src/src/measures/2024/PQI92HH/data.ts b/services/ui-src/src/measures/2024/PQI92HH/data.ts new file mode 100644 index 0000000000..c76af75d17 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI92-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations for ambulatory care sensitive chronic conditions per 100,000 enrollee months for health home enrollees age 18 and older. This measure includes adult hospitalizations for diabetes with short-term complications, diabetes with long-term complications, uncontrolled diabetes without complications, diabetes with lower-extremity amputation, chronic obstructive pulmonary disease, asthma, hypertension, or heart failure without a cardiac procedure.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI92HH/index.test.tsx b/services/ui-src/src/measures/2024/PQI92HH/index.test.tsx new file mode 100644 index 0000000000..7943191d76 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI92-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI92HH/index.tsx b/services/ui-src/src/measures/2024/PQI92HH/index.tsx new file mode 100644 index 0000000000..47ed2a4283 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/index.tsx @@ -0,0 +1,85 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const PQI92HH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + healthHomeMeasure + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="AHRQ" /> + <CMQ.DataSource /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure={true} /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure + data={PMD.data} + rateScale={100000} + allowNumeratorGreaterThanDenominator + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + calcTotal={true} + /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && ( + <CMQ.OtherPerformanceMeasure + allowNumeratorGreaterThanDenominator + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + /> + )} + <CMQ.CombinedRates healthHomeMeasure={true} /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + rateMultiplicationValue={100000} + customMask={positiveNumbersWithMaxDecimalPlaces(1)} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + allowNumeratorGreaterThanDenominator + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI92HH/validation.ts b/services/ui-src/src/measures/2024/PQI92HH/validation.ts new file mode 100644 index 0000000000..5e54088551 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/validation.ts @@ -0,0 +1,84 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const PQI92Validation = (data: FormData) => { + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI92Validation]; diff --git a/services/ui-src/src/measures/2024/SAAAD/data.ts b/services/ui-src/src/measures/2024/SAAAD/data.ts new file mode 100644 index 0000000000..531d5a27fc --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("SAA-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older during the measurement year with schizophrenia or schizoaffective disorder who were dispensed and remained on an antipsychotic medication for at least 80 percent of their treatment period.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/SAAAD/index.test.tsx b/services/ui-src/src/measures/2024/SAAAD/index.test.tsx new file mode 100644 index 0000000000..a5915344e7 --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "SAA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SAAAD/index.tsx b/services/ui-src/src/measures/2024/SAAAD/index.tsx new file mode 100644 index 0000000000..7fad6ca4b3 --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const SAAAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/SAAAD/validation.ts b/services/ui-src/src/measures/2024/SAAAD/validation.ts new file mode 100644 index 0000000000..5948778e5c --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const SAAADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [SAAADValidation]; diff --git a/services/ui-src/src/measures/2024/SFMCH/data.ts b/services/ui-src/src/measures/2024/SFMCH/data.ts new file mode 100644 index 0000000000..8dfcec5f31 --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("SFM-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of enrolled children who have ever received sealants on permanent first molar teeth: (1) at least one sealant and (2) all four molars sealed by the 10th birthdate.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/SFMCH/index.test.tsx b/services/ui-src/src/measures/2024/SFMCH/index.test.tsx new file mode 100644 index 0000000000..b03b986e3e --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/index.test.tsx @@ -0,0 +1,268 @@ +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "SFM-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Rate 1 - At Least One Sealant", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Rate 2 - All Four Molars Sealed", + }, + ], + }, + }, + MeasurementSpecification: "ADA-DQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SFMCH/index.tsx b/services/ui-src/src/measures/2024/SFMCH/index.tsx new file mode 100644 index 0000000000..4dd93c5e33 --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const SFMCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="ADA-DQA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} showtextbox={true} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/SFMCH/validation.ts b/services/ui-src/src/measures/2024/SFMCH/validation.ts new file mode 100644 index 0000000000..ed27e8618b --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/validation.ts @@ -0,0 +1,97 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const SFMCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneQualRateHigherThanOtherQualOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [SFMCHValidation]; diff --git a/services/ui-src/src/measures/2024/SSDAD/data.ts b/services/ui-src/src/measures/2024/SSDAD/data.ts new file mode 100644 index 0000000000..fbf15e1649 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("SSD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 64 with schizophrenia, schizoaffective disorder, or bipolar disorder, who were dispensed an antipsychotic medication and had a diabetes screening test during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/SSDAD/index.test.tsx b/services/ui-src/src/measures/2024/SSDAD/index.test.tsx new file mode 100644 index 0000000000..11fc8c0f63 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "SSD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 50 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 74", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SSDAD/index.tsx b/services/ui-src/src/measures/2024/SSDAD/index.tsx new file mode 100644 index 0000000000..310f588e9d --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const SSDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/SSDAD/validation.ts b/services/ui-src/src/measures/2024/SSDAD/validation.ts new file mode 100644 index 0000000000..237d8f312b --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const SSDValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [SSDValidation]; diff --git a/services/ui-src/src/measures/2024/SSHH/data.ts b/services/ui-src/src/measures/2024/SSHH/data.ts new file mode 100644 index 0000000000..b966ed5889 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/data.ts @@ -0,0 +1,62 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/SSHH/index.test.tsx b/services/ui-src/src/measures/2024/SSHH/index.test.tsx new file mode 100644 index 0000000000..2f6eeda2e5 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/index.test.tsx @@ -0,0 +1,168 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { axe, toHaveNoViolations } from "jest-axe"; +import { mockLDFlags } from "../../../../setupJest"; +import { clearMocks } from "../shared/util/validationsMock"; + +expect.extend(toHaveNoViolations); + +mockLDFlags.setDefault({ periodOfHealthEmergency2024: false }); + +// Test Setup +const measureAbbr = "SS-1-HH"; +const coreSet = "HHCS"; +const state = "CT"; +const year = 2024; +const description = "test"; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + year={`${year}`} + name={description} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("SS-1-HH measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + it("Always shows What is the status of the data being reported? question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + const firstQuestion = screen.getByText( + "What is the status of the data being reported?" + ); + expect(firstQuestion).toBeVisible(); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByText("Status of Data Reported")).toBeInTheDocument(); + expect(screen.queryByText("Data Source")).toBeInTheDocument(); + expect(screen.queryByText("Date Range")).toBeInTheDocument(); + expect( + screen.queryByText("Definition of Population Included in the Measure") + ).toBeInTheDocument(); + expect(screen.queryByText("Performance Measure")).toBeInTheDocument(); + }); + + it("does not show covid text if periodOfHealthEmergency2024 flag is disabled", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByText( + "Describe any COVID-related difficulties encountered while collecting this data:" + ) + ).not.toBeInTheDocument(); + }); + + it("shows covid text if periodOfHealthEmergency2024 flag is enabled", async () => { + mockLDFlags.setDefault({ periodOfHealthEmergency2024: true }); + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByText( + "Describe any COVID-related difficulties encountered while collecting this data:" + ) + ).toBeInTheDocument(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SSHH/index.tsx b/services/ui-src/src/measures/2024/SSHH/index.tsx new file mode 100644 index 0000000000..37f7f68d2d --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/index.tsx @@ -0,0 +1,37 @@ +import { useEffect } from "react"; +import * as CUI from "@chakra-ui/react"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as QMR from "components"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { PerformanceMeasure } from "./questions/PerformanceMeasure"; + +export const SSHH = ({ + detailedDescription, + setValidationFunctions, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + <CUI.Text + fontSize="xl" + my="6" + fontWeight={400} + data-cy="detailed-description" + > + {detailedDescription} + </CUI.Text> + <CMQ.StatusOfData /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="health" /> + <CMQ.DefinitionOfPopulation healthHomeMeasure hybridMeasure /> + <PerformanceMeasure hybridMeasure /> + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..cd95c7ae06 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx @@ -0,0 +1,153 @@ +import { useFormContext, useFieldArray } from "react-hook-form"; +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; +import * as DC from "dataConstants"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { useEffect } from "react"; +import { useFlags } from "launchdarkly-react-client-sdk"; + +interface Props { + hybridMeasure?: boolean; + rateAlwaysEditable?: boolean; +} + +const stringIsReadOnly = (dataSource: string) => { + return dataSource === "AdministrativeData"; +}; + +const arrayIsReadOnly = (dataSource: string[]) => { + if (dataSource.length === 0) { + return false; + } + return ( + dataSource?.every((source) => source === "AdministrativeData") ?? false + ); +}; + +export const PerformanceMeasure = ({ + hybridMeasure, + rateAlwaysEditable, +}: Props) => { + const { control, reset } = useFormContext(); + + const pheIsCurrent = useFlags()?.["periodOfHealthEmergency2024"]; + + const { fields, remove, append } = useFieldArray({ + name: DC.OPM_RATES, + control, + shouldUnregister: true, + }); + + useEffect(() => { + if (fields.length === 0) { + reset({ + name: DC.OPM_RATES, + [DC.OPM_RATES]: [ + { + [DC.DESCRIPTION]: "", + }, + ], + }); + } + }, [fields, reset]); + + const register = useCustomRegister<Types.OtherPerformanceMeasure>(); + + const { watch } = useFormContext<Types.DataSource>(); + + // Watch for dataSource data + const dataSourceWatch = watch(DC.DATA_SOURCE); + + // Conditional check to let rate be readonly when administrative data is the only option or no option is selected + let rateReadOnly = false; + if (rateAlwaysEditable !== undefined) { + rateReadOnly = false; + } else if (dataSourceWatch && Array.isArray(dataSourceWatch)) { + rateReadOnly = arrayIsReadOnly(dataSourceWatch); + } else if (dataSourceWatch) { + rateReadOnly = stringIsReadOnly(dataSourceWatch); + } + + return ( + <QMR.CoreQuestionWrapper label="Performance Measure"> + <QMR.TextArea + label="Describe the methodology used:" + formLabelProps={{ fontWeight: 700 }} + {...register(DC.OPM_EXPLAINATION)} + /> + {hybridMeasure && pheIsCurrent && ( + <CUI.Box my="5"> + <CUI.Text> + CMS recognizes that social distancing will make onsite medical chart + reviews inadvisable during the COVID-19 pandemic. As such, hybrid + measures that rely on such techniques will be particularly + challenging during this time. While reporting of the Core Sets is + voluntary, CMS encourages states that can collect information safely + to continue reporting the measures they have reported in the past. + </CUI.Text> + <QMR.TextArea + formLabelProps={{ mt: 5 }} + {...register(DC.OPM_HYBRID_EXPLANATION)} + label="Describe any COVID-related difficulties encountered while collecting this data:" + /> + </CUI.Box> + )} + + <CUI.Box marginTop={10}> + {fields.map((_item, index) => { + return ( + <QMR.DeleteWrapper + allowDeletion={index !== 0} + onDelete={() => remove(index)} + key={_item.id} + > + <CUI.Stack key={index} my={10}> + <CUI.Heading fontSize="lg" fontWeight="600"> + Describe the Rate: + </CUI.Heading> + <QMR.TextInput + label="For example, specify the age groups and whether you are reporting on a certain indicator:" + name={`${DC.OPM_RATES}.${index}.${DC.DESCRIPTION}`} + key={`${DC.OPM_RATES}.${index}.${DC.DESCRIPTION}`} + /> + <CUI.Text fontWeight="bold"> + Enter a number for the numerator and the denominator. Rate + will auto-calculate: + </CUI.Text> + {(dataSourceWatch?.[0] !== "AdministrativeData" || + dataSourceWatch?.length !== 1) && ( + <CUI.Heading pt="5" size={"sm"}> + Please review the auto-calculated rate and revise if needed. + </CUI.Heading> + )} + <QMR.Rate + rates={[ + { + id: index, + }, + ]} + name={`${DC.OPM_RATES}.${index}.${DC.RATE}`} + key={`${DC.OPM_RATES}.${index}.${DC.RATE}`} + readOnly={rateReadOnly} + /> + </CUI.Stack> + </QMR.DeleteWrapper> + ); + })} + + <QMR.ContainedButton + buttonText={"+ Add Another"} + buttonProps={{ + variant: "outline", + colorScheme: "blue", + color: "blue.500", + }} + onClick={() => { + append({}); + }} + /> + </CUI.Box> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/SSHH/validation.ts b/services/ui-src/src/measures/2024/SSHH/validation.ts new file mode 100644 index 0000000000..4510465e8a --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/validation.ts @@ -0,0 +1,137 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export interface FormRateField { + denominator?: string; + numerator?: string; + label?: string; + rate?: string; +} + +const validateAtLeastOneRateComplete = (data: any) => { + const errorArray: FormError[] = []; + const PMData = data[DC.OPM_RATES]; + + let rateCompletionError = true; + + PMData && + PMData.forEach((measure: any) => { + if (measure.rate && measure.rate[0] && measure.rate[0].rate) { + rateCompletionError = false; + } + }); + + if (rateCompletionError) { + errorArray.push({ + errorLocation: `Performance Measure`, + errorMessage: `At least one Performance Measure Numerator, Denominator, and Rate must be completed`, + }); + } + return errorArray; +}; + +// If a user manually over-rides a rate it must not violate two rules: +// It must be zero if the numerator is zero or +// It Must be greater than zero if the Num and Denom are greater than zero +const validateNoNonZeroNumOrDenomPM = (OPM: any, data: any) => { + const errorArray: FormError[] = []; + const hybridData = data?.[DC.DATA_SOURCE]?.includes( + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA + ); + const location = `Performance Measure`; + const rateDataOPM = GV.getOtherPerformanceMeasureRateArray(OPM); + + const nonZeroErrors = [ + ...GV.validationRateNotZero({ location, rateData: rateDataOPM }), + ]; + const zeroErrors = [ + ...GV.validationRateZero({ location, rateData: rateDataOPM, hybridData }), + ]; + + if (!!nonZeroErrors.length) errorArray.push(nonZeroErrors[0]); + if (!!zeroErrors.length) errorArray.push(zeroErrors[0]); + return errorArray; +}; + +/** + * Checks user-created performance measures for numerator greater than denominator errors + */ +const validateNumeratorsLessThanDenominatorsPM = (OPM: any) => { + const location = `Performance Measure`; + const errorMessage = `Numerators must be less than Denominators for all applicable performance measures`; + const rateDataOPM = GV.getOtherPerformanceMeasureRateArray(OPM); + + const errorArray: FormError[] = []; + + for (const fieldSet of rateDataOPM) { + for (const [, rate] of fieldSet.entries()) { + if ( + rate.numerator && + rate.denominator && + parseFloat(rate.denominator) < parseFloat(rate.numerator) + ) { + errorArray.push({ + errorLocation: location, + errorMessage: errorMessage, + }); + } + } + } + + return !!errorArray.length ? [errorArray[0]] : []; +}; + +/** + * Checks for NDR field sets that have been partially filled out and reports them. + * + * @param OPM opm data + */ +export const validatePartialRateCompletion = (OPM: any) => { + const errors: FormError[] = []; + const rateDataOPM = GV.getOtherPerformanceMeasureRateArray(OPM); + + const location = `Performance Measure`; + + for (const [, rateSet] of rateDataOPM.entries()) { + for (const [, rate] of rateSet.entries()) { + if ( + rate && + ((rate.numerator && !rate.denominator) || + (rate.denominator && !rate.numerator)) + ) { + errors.push({ + errorLocation: location, + errorMessage: `Should not have partially filled NDR sets`, + }); + } + } + } + + return errors; +}; + +const SSHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + + errorArray = [ + ...validateAtLeastOneRateComplete(data), + ...validateNoNonZeroNumOrDenomPM(OPM, data), + ...validateNumeratorsLessThanDenominatorsPM(OPM), + ...validatePartialRateCompletion(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [SSHHValidation]; diff --git a/services/ui-src/src/measures/2024/TFLCH/data.ts b/services/ui-src/src/measures/2024/TFLCH/data.ts new file mode 100644 index 0000000000..a0a3ec50ee --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("TFL-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of enrolled children ages 1 through 20 who received at least two topical fluoride applications as: (1) dental or oral health services, (2) dental services, and (3) oral health services within the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/TFLCH/index.test.tsx b/services/ui-src/src/measures/2024/TFLCH/index.test.tsx new file mode 100644 index 0000000000..9a2747027d --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/index.test.tsx @@ -0,0 +1,349 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "TFL-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Dentalservices: [ + { + label: "Ages 1 to 2", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + isTotal: true, + label: "Total Ages 1 through 20", + }, + ], + Dentalororalhealthservices: [ + { + label: "Ages 1 to 2", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + label: "Total Ages 1 through 20", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + Oralhealthservices: [ + { + label: "Ages 1 to 2", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + isTotal: true, + label: "Total Ages 1 through 20", + }, + ], + }, + }, + MeasurementSpecification: "ADA-DQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/TFLCH/index.tsx b/services/ui-src/src/measures/2024/TFLCH/index.tsx new file mode 100644 index 0000000000..33bca33439 --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const TFLCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="ADA-DQA" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/TFLCH/validation.ts b/services/ui-src/src/measures/2024/TFLCH/validation.ts new file mode 100644 index 0000000000..1be7569e96 --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/validation.ts @@ -0,0 +1,93 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const TFLCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + + errorArray = [ + ...errorArray, + // Dental Services rate cannot be larger than the Dental or Oral Health Services rate + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + // Oral Health Services rate cannot be larger than the Dental or Oral Health Services rate + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data, 0, 2), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...sameDenominatorError, + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + // Dental Services rate cannot be larger than the Dental or Oral Health Services rate + GV.validateOneCatRateHigherThanOtherCatOMS(), + // Oral Health Services rate cannot be larger than the Dental or Oral Health Services rate + GV.validateOneCatRateHigherThanOtherCatOMS(0, 2), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [TFLCHValidation]; diff --git a/services/ui-src/src/measures/2024/W30CH/data.ts b/services/ui-src/src/measures/2024/W30CH/data.ts new file mode 100644 index 0000000000..a8b64e118c --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/data.ts @@ -0,0 +1,20 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("W30-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children who had the following number of well-child visits with a primary care practitioner (PCP) during the last 15 months. The following rates are reported:", + ], + questionListItems: [ + " Children who turned age 15 months during the measurement year: Six or more well-child visits.", + " Children who turned age 30 months during the measurement year: Two or more well-child visits.", + ], + questionListTitles: [ + "Well-Child Visits in the First 15 Months.", + "Well-Child Visits for Age 15 Months-30 Months.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/W30CH/index.test.tsx b/services/ui-src/src/measures/2024/W30CH/index.test.tsx new file mode 100644 index 0000000000..513081ce87 --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/index.test.tsx @@ -0,0 +1,262 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "W30-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: + "Rate 1 - Six or more well-child visits in the first 15 months ", + rate: "100.0", + numerator: "50", + denominator: "50", + }, + { + label: + "Rate 2 - Two or more well-child visits for ages 15 months to 30 months", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/W30CH/index.tsx b/services/ui-src/src/measures/2024/W30CH/index.tsx new file mode 100644 index 0000000000..9cf89fde09 --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const W30CH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} showtextbox={true} /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/W30CH/validation.ts b/services/ui-src/src/measures/2024/W30CH/validation.ts new file mode 100644 index 0000000000..6e9e236fc1 --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/validation.ts @@ -0,0 +1,94 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const W30CHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [W30CHValidation]; diff --git a/services/ui-src/src/measures/2024/WCCCH/data.ts b/services/ui-src/src/measures/2024/WCCCH/data.ts new file mode 100644 index 0000000000..692b655418 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("WCC-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of children ages 3 to 17 who had an outpatient visit with a primary care practitioner (PCP) or obstetrician/gynecologist (OB/GYN) and who had evidence of the following during the measurement year:", + ], + questionListItems: categories.map((item) => item.label), + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: "Medicaid Management Information System (MMIS)", + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/WCCCH/index.test.tsx b/services/ui-src/src/measures/2024/WCCCH/index.test.tsx new file mode 100644 index 0000000000..a2575ea6f8 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/index.test.tsx @@ -0,0 +1,295 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "WCC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + CounselingforPhysicalActivity: [ + { + label: "Ages 3 to 11", + }, + { + label: "Ages 12 to 17", + }, + { + isTotal: true, + label: "Total (Ages 3 to 17)", + }, + ], + BodymassindexBMIpercentiledocumentation: [ + { + label: "Ages 3 to 11", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 12 to 17", + }, + { + label: "Total (Ages 3 to 17)", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + CounselingforNutrition: [ + { + label: "Ages 3 to 11", + }, + { + label: "Ages 12 to 17", + }, + { + isTotal: true, + label: "Total (Ages 3 to 17)", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/WCCCH/index.tsx b/services/ui-src/src/measures/2024/WCCCH/index.tsx new file mode 100644 index 0000000000..86e9a7cdd5 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const WCCCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource data={PMD.dataSourceData} /> + <CMQ.DateRange type="adult" /> + <CMQ.DefinitionOfPopulation childMeasure hybridMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal hybridMeasure /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + categories={PMD.categories} + qualifiers={PMD.qualifiers} + performanceMeasureArray={performanceMeasureArray} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/WCCCH/validation.ts b/services/ui-src/src/measures/2024/WCCCH/validation.ts new file mode 100644 index 0000000000..e15ad7fbab --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/validation.ts @@ -0,0 +1,110 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const WCCHValidation = (data: FormData) => { + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateEqualQualifierDenominatorsErrorMessage = ( + qualifier: string + ) => { + const isTotal = qualifier.split(" ")[0] === "Total"; + return `${ + isTotal ? "" : "The " + }${qualifier} denominator must be the same for each indicator.`; + }; + + const validateTotalNDRErrorMessage = ( + qualifier: string, + fieldType: string + ) => { + return `${fieldType} for the ${qualifier} Total rate is not equal to the sum of the ${qualifier} age-specific ${fieldType.toLowerCase()}s.`; + }; + + errorArray = [ + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateHybridMeasurePopulation(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + PMD.qualifiers, + undefined, + validateEqualQualifierDenominatorsErrorMessage + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR( + performanceMeasureArray, + undefined, + PMD.categories, + validateTotalNDRErrorMessage + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [WCCHValidation]; diff --git a/services/ui-src/src/measures/2024/WCVCH/data.ts b/services/ui-src/src/measures/2024/WCVCH/data.ts new file mode 100644 index 0000000000..6c4fb8e424 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("WCV-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children ages 3 to 21 who had at least one comprehensive well-care visit with a primary care practitioner (PCP) or an obstetrician/gynecologist (OB/GYN) during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/WCVCH/index.test.tsx b/services/ui-src/src/measures/2024/WCVCH/index.test.tsx new file mode 100644 index 0000000000..aced36f6c2 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/index.test.tsx @@ -0,0 +1,270 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "WCV-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + <Suspense fallback={MeasuresLoading()}> + <RouterWrappedComp> + <MeasureWrapper + measure={measure} + name={description} + year={`${year}`} + measureId={measureAbbr} + /> + </RouterWrappedComp> + </Suspense> + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 3 to 11", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 12 to 17", + }, + { + label: "Ages 18 to 21", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/WCVCH/index.tsx b/services/ui-src/src/measures/2024/WCVCH/index.tsx new file mode 100644 index 0000000000..c47693f6fb --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +export const WCVCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext<FormData>(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + <CMQ.Reporting + reportingYear={year} + measureName={name} + measureAbbreviation={measureId} + /> + + {!isNotReportingData && ( + <> + <CMQ.StatusOfData /> + <CMQ.MeasurementSpecification type="HEDIS" /> + <CMQ.DataSource /> + <CMQ.DateRange type="child" /> + <CMQ.DefinitionOfPopulation childMeasure /> + {isPrimaryMeasureSpecSelected && ( + <> + <CMQ.PerformanceMeasure data={PMD.data} calcTotal /> + <CMQ.DeviationFromMeasureSpec /> + </> + )} + {isOtherMeasureSpecSelected && <CMQ.OtherPerformanceMeasure />} + <CMQ.CombinedRates /> + {showOptionalMeasureStrat && ( + <CMQ.OptionalMeasureStrat + performanceMeasureArray={performanceMeasureArray} + qualifiers={PMD.qualifiers} + categories={PMD.categories} + adultMeasure={false} + calcTotal + /> + )} + </> + )} + <CMQ.AdditionalNotes /> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/WCVCH/validation.ts b/services/ui-src/src/measures/2024/WCVCH/validation.ts new file mode 100644 index 0000000000..a3139dac7a --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +//form type +import { DefaultFormData as FormData } from "measures/2024/shared/CommonQuestions/types"; + +const WCVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateTotalNDR(performanceMeasureArray), + ]; + + return errorArray; +}; + +export const validationFunctions = [WCVCHValidation]; diff --git a/services/ui-src/src/measures/2024/index.tsx b/services/ui-src/src/measures/2024/index.tsx new file mode 100644 index 0000000000..77848752be --- /dev/null +++ b/services/ui-src/src/measures/2024/index.tsx @@ -0,0 +1,318 @@ +import { lazy } from "react"; +import { Data, Qualifier } from "./shared/Qualifiers"; + +/* +When importing a measure it should be a named import and added to the measures object below so that it routes correctly +the key should be the measure id as a string (with '-XX' included) +*/ +const AABAD = lazy(() => + import("./AABAD").then((module) => ({ default: module.AABAD })) +); +const AABCH = lazy(() => + import("./AABCH").then((module) => ({ default: module.AABCH })) +); +const ADDCH = lazy(() => + import("./ADDCH").then((module) => ({ default: module.ADDCH })) +); +const AIFHH = lazy(() => + import("./AIFHH").then((module) => ({ default: module.AIFHH })) +); +const AMBCH = lazy(() => + import("./AMBCH").then((module) => ({ default: module.AMBCH })) +); +const AMBHH = lazy(() => + import("./AMBHH").then((module) => ({ default: module.AMBHH })) +); +const AMMAD = lazy(() => + import("./AMMAD").then((module) => ({ default: module.AMMAD })) +); +const AMRAD = lazy(() => + import("./AMRAD").then((module) => ({ default: module.AMRAD })) +); +const AMRCH = lazy(() => + import("./AMRCH").then((module) => ({ default: module.AMRCH })) +); +const APMCH = lazy(() => + import("./APMCH").then((module) => ({ default: module.APMCH })) +); +const APPCH = lazy(() => + import("./APPCH").then((module) => ({ default: module.APPCH })) +); +const BCSAD = lazy(() => + import("./BCSAD").then((module) => ({ default: module.BCSAD })) +); +const CBPAD = lazy(() => + import("./CBPAD").then((module) => ({ default: module.CBPAD })) +); +const CBPHH = lazy(() => + import("./CBPHH").then((module) => ({ default: module.CBPHH })) +); +const CCPAD = lazy(() => + import("./CCPAD").then((module) => ({ default: module.CCPAD })) +); +const CCPCH = lazy(() => + import("./CCPCH").then((module) => ({ default: module.CCPCH })) +); +const CCSAD = lazy(() => + import("./CCSAD").then((module) => ({ default: module.CCSAD })) +); +const CCWAD = lazy(() => + import("./CCWAD").then((module) => ({ default: module.CCWAD })) +); +const CCWCH = lazy(() => + import("./CCWCH").then((module) => ({ default: module.CCWCH })) +); +const CDFAD = lazy(() => + import("./CDFAD").then((module) => ({ default: module.CDFAD })) +); +const CDFCH = lazy(() => + import("./CDFCH").then((module) => ({ default: module.CDFCH })) +); +const CDFHH = lazy(() => + import("./CDFHH").then((module) => ({ default: module.CDFHH })) +); +const CHLAD = lazy(() => + import("./CHLAD").then((module) => ({ default: module.CHLAD })) +); +const CHLCH = lazy(() => + import("./CHLCH").then((module) => ({ default: module.CHLCH })) +); +const CISCH = lazy(() => + import("./CISCH").then((module) => ({ default: module.CISCH })) +); +const COBAD = lazy(() => + import("./COBAD").then((module) => ({ default: module.COBAD })) +); +const COLAD = lazy(() => + import("./COLAD").then((module) => ({ default: module.COLAD })) +); +const COLHH = lazy(() => + import("./COLHH").then((module) => ({ default: module.COLHH })) +); +const CPAAD = lazy(() => + import("./CPAAD").then((module) => ({ default: module.CPAAD })) +); +const CPCCH = lazy(() => + import("./CPCCH").then((module) => ({ default: module.CPCCH })) +); +const CPUAD = lazy(() => + import("./CPUAD").then((module) => ({ default: module.CPUAD })) +); +const DEVCH = lazy(() => + import("./DEVCH").then((module) => ({ default: module.DEVCH })) +); +const FUAAD = lazy(() => + import("./FUAAD").then((module) => ({ default: module.FUAAD })) +); +const FUACH = lazy(() => + import("./FUACH").then((module) => ({ default: module.FUACH })) +); +const FUAHH = lazy(() => + import("./FUAHH").then((module) => ({ default: module.FUAHH })) +); +const FUHAD = lazy(() => + import("./FUHAD").then((module) => ({ default: module.FUHAD })) +); +const FUHCH = lazy(() => + import("./FUHCH").then((module) => ({ default: module.FUHCH })) +); +const FUHHH = lazy(() => + import("./FUHHH").then((module) => ({ default: module.FUHHH })) +); +const FUMAD = lazy(() => + import("./FUMAD").then((module) => ({ default: module.FUMAD })) +); +const FUMCH = lazy(() => + import("./FUMCH").then((module) => ({ default: module.FUMCH })) +); +const FUMHH = lazy(() => + import("./FUMHH").then((module) => ({ default: module.FUMHH })) +); +const FVAAD = lazy(() => + import("./FVAAD").then((module) => ({ default: module.FVAAD })) +); +const HVLAD = lazy(() => + import("./HVLAD").then((module) => ({ default: module.HVLAD })) +); +const HBDAD = lazy(() => + import("./HBDAD").then((module) => ({ default: module.HBDAD })) +); +const HPCMIAD = lazy(() => + import("./HPCMIAD").then((module) => ({ default: module.HPCMIAD })) +); +const IETAD = lazy(() => + import("./IETAD").then((module) => ({ default: module.IETAD })) +); +const IETHH = lazy(() => + import("./IETHH").then((module) => ({ default: module.IETHH })) +); +const IMACH = lazy(() => + import("./IMACH").then((module) => ({ default: module.IMACH })) +); +const IUHH = lazy(() => + import("./IUHH").then((module) => ({ default: module.IUHH })) +); +const LBWCH = lazy(() => + import("./LBWCH").then((module) => ({ default: module.LBWCH })) +); +const LRCDCH = lazy(() => + import("./LRCDCH").then((module) => ({ default: module.LRCDCH })) +); +const MSCAD = lazy(() => + import("./MSCAD").then((module) => ({ default: module.MSCAD })) +); +const LSCCH = lazy(() => + import("./LSCCH").then((module) => ({ default: module.LSCCH })) +); +const NCIDDSAD = lazy(() => + import("./NCIDDSAD").then((module) => ({ default: module.NCIDDSAD })) +); +const OEVCH = lazy(() => + import("./OEVCH").then((module) => ({ default: module.OEVCH })) +); +const OHDAD = lazy(() => + import("./OHDAD").then((module) => ({ default: module.OHDAD })) +); +const OUDAD = lazy(() => + import("./OUDAD").then((module) => ({ default: module.OUDAD })) +); +const OUDHH = lazy(() => + import("./OUDHH").then((module) => ({ default: module.OUDHH })) +); +const PCRAD = lazy(() => + import("./PCRAD").then((module) => ({ default: module.PCRAD })) +); +const PCRHH = lazy(() => + import("./PCRHH").then((module) => ({ default: module.PCRHH })) +); +const PPCAD = lazy(() => + import("./PPCAD").then((module) => ({ default: module.PPCAD })) +); +const PPC2CH = lazy(() => + import("./PPC2CH").then((module) => ({ default: module.PPC2CH })) +); +const PQI01AD = lazy(() => + import("./PQI01AD").then((module) => ({ default: module.PQI01AD })) +); +const PQI05AD = lazy(() => + import("./PQI05AD").then((module) => ({ default: module.PQI05AD })) +); +const PQI92HH = lazy(() => + import("./PQI92HH").then((module) => ({ default: module.PQI92HH })) +); +const PQI08AD = lazy(() => + import("./PQI08AD").then((module) => ({ default: module.PQI08AD })) +); +const PQI15AD = lazy(() => + import("./PQI15AD").then((module) => ({ default: module.PQI15AD })) +); +const SAAAD = lazy(() => + import("./SAAAD").then((module) => ({ default: module.SAAAD })) +); +const SFMCH = lazy(() => + import("./SFMCH").then((module) => ({ default: module.SFMCH })) +); +const SSDAD = lazy(() => + import("./SSDAD").then((module) => ({ default: module.SSDAD })) +); +const SSHH = lazy(() => + import("./SSHH").then((module) => ({ default: module.SSHH })) +); +const TFLCH = lazy(() => + import("./TFLCH").then((module) => ({ default: module.TFLCH })) +); +const W30CH = lazy(() => + import("./W30CH").then((module) => ({ default: module.W30CH })) +); +const WCCCH = lazy(() => + import("./WCCCH").then((module) => ({ default: module.WCCCH })) +); +const WCVCH = lazy(() => + import("./WCVCH").then((module) => ({ default: module.WCVCH })) +); + +const twentyTwentyFourMeasures = { + "AAB-AD": AABAD, + "AAB-CH": AABCH, + "ADD-CH": ADDCH, + "AIF-HH": AIFHH, + "AMB-CH": AMBCH, + "AMB-HH": AMBHH, + "AMM-AD": AMMAD, + "AMR-AD": AMRAD, + "AMR-CH": AMRCH, + "APM-CH": APMCH, + "APP-CH": APPCH, + "BCS-AD": BCSAD, + "CBP-AD": CBPAD, + "CBP-HH": CBPHH, + "CCP-AD": CCPAD, + "CCP-CH": CCPCH, + "CCS-AD": CCSAD, + "CCW-AD": CCWAD, + "CCW-CH": CCWCH, + "CDF-AD": CDFAD, + "CDF-CH": CDFCH, + "CDF-HH": CDFHH, + "CHL-AD": CHLAD, + "CHL-CH": CHLCH, + "CIS-CH": CISCH, + "COB-AD": COBAD, + "COL-AD": COLAD, + "COL-HH": COLHH, + "CPA-AD": CPAAD, + "CPC-CH": CPCCH, + "CPU-AD": CPUAD, + "DEV-CH": DEVCH, + "FUA-AD": FUAAD, + "FUA-CH": FUACH, + "FUA-HH": FUAHH, + "FUH-AD": FUHAD, + "FUH-CH": FUHCH, + "FUH-HH": FUHHH, + "FUM-AD": FUMAD, + "FUM-CH": FUMCH, + "FUM-HH": FUMHH, + "FVA-AD": FVAAD, + "HBD-AD": HBDAD, + "HPCMI-AD": HPCMIAD, + "HVL-AD": HVLAD, + "IET-AD": IETAD, + "IET-HH": IETHH, + "IMA-CH": IMACH, + "IU-HH": IUHH, + "LBW-CH": LBWCH, + "LRCD-CH": LRCDCH, + "MSC-AD": MSCAD, + "LSC-CH": LSCCH, + "NCIDDS-AD": NCIDDSAD, + "OEV-CH": OEVCH, + "OHD-AD": OHDAD, + "OUD-AD": OUDAD, + "OUD-HH": OUDHH, + "PCR-AD": PCRAD, + "PCR-HH": PCRHH, + "PPC-AD": PPCAD, + "PPC2-CH": PPC2CH, + "PQI01-AD": PQI01AD, + "PQI05-AD": PQI05AD, + "PQI08-AD": PQI08AD, + "PQI15-AD": PQI15AD, + "PQI92-HH": PQI92HH, + "SAA-AD": SAAAD, + "SFM-CH": SFMCH, + "SSD-AD": SSDAD, + "SS-1-HH": SSHH, + "SS-2-HH": SSHH, + "SS-3-HH": SSHH, + "SS-4-HH": SSHH, + "SS-5-HH": SSHH, + "TFL-CH": TFLCH, + "W30-CH": W30CH, + "WCC-CH": WCCCH, + "WCV-CH": WCVCH, + Qualifier, +}; + +export const QualifierData = Data; +export default twentyTwentyFourMeasures; diff --git a/services/ui-src/src/measures/2024/rateLabelText.test.ts b/services/ui-src/src/measures/2024/rateLabelText.test.ts new file mode 100644 index 0000000000..06ddf95985 --- /dev/null +++ b/services/ui-src/src/measures/2024/rateLabelText.test.ts @@ -0,0 +1,40 @@ +import { data } from "./rateLabelText"; + +describe("Rate Label Data", () => { + test("Categories should have globally-unique IDs", () => { + const categoryIdUsage = new Map< + string, + { measureName: string; categoryIndex: number } + >(); + for (const [measureName, measure] of Object.entries(data)) { + for (let i = 0; i < measure.categories.length; i += 1) { + const category = measure.categories[i]; + if (categoryIdUsage.has(category.id)) { + const existingIdUse = categoryIdUsage.get(category.id); + throw new Error( + `Measure ${existingIdUse?.measureName}, category #${existingIdUse?.categoryIndex} has the same ID as ${measureName}, category #${i}.` + ); + } else { + categoryIdUsage.set(category.id, { + measureName, + categoryIndex: i, + }); + } + } + } + }); + + test("Qualifiers should have unique IDs within each measure", () => { + for (const [measureName, measure] of Object.entries(data)) { + if ( + measure.qualifiers.some( + (qual, i, arr) => i !== arr.findIndex((q) => q.id === qual.id) + ) + ) { + throw new Error( + `Measure ${measureName} has multiple qualifiers with the same ID.` + ); + } + } + }); +}); diff --git a/services/ui-src/src/measures/2024/rateLabelText.ts b/services/ui-src/src/measures/2024/rateLabelText.ts new file mode 100644 index 0000000000..162152d8f7 --- /dev/null +++ b/services/ui-src/src/measures/2024/rateLabelText.ts @@ -0,0 +1,1825 @@ +/** + * Attention + * Changing the labels will change how the measure data is shaped and should not be done unless that is the desired result. + * Changing the text property of these objects will change the text that is displayed to the user. + * Changing the id's will affect the save data in the database and also how the client pulls the data. NEVER CHANGE THEM. + * id's are randomly generated from https://shortunique.id/. If there's a qualifiers that is called Total, use the word "Total" as the id + */ + +export const data = { + "AAB-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "kINeRZ" + }, + { + "label": "Age 65 and Older", + "text": "Age 65 and Older", + "id": "EreZBY" + } + ], + "categories": [{"id":"SU6HXz", "label": "", "text":""}] + }, + "AAB-CH": { + "qualifiers": [ + { + "label": "Ages 3 months to 17 years", + "text": "Ages 3 months to 17 years", + "id": "xS5HMm" + }, + ], + "categories": [{"id":"ZCy3XP", "label": "", "text":""}] + }, + "ADD-CH": { + "qualifiers": [ + { + "label": "Initiation Phase", + "text": "Initiation Phase", + "id": "UYSXR5" + }, + { + "label": "Continuation and Maintenance (C&M) Phase", + "text": "Continuation and Maintenance (C&M) Phase", + "id": "jfj0f8" + } + ], + "categories": [{"id":"ugoYfe", "label": "", "text":""}] + }, + "AIF-HH": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "Quotyg", + "excludeFromOMS": true + }, + { + "label": "Ages 65 to 74", + "text": "Ages 65 to 74", + "id": "R5nGsO", + "excludeFromOMS": true + }, + { + "label": "Ages 75 to 84", + "text": "Ages 75 to 84", + "id": "hrrJSA", + "excludeFromOMS": true + }, + { + "label": "Age 85 and older", + "text": "Age 85 and older", + "id": "QqtlDW", + "excludeFromOMS": true + }, + { + "label": "Total (Age 18 and older)", + "text": "Total (Age 18 and older)", + "id": "Total" + } + ], + "categories": [{"id":"ZyxRR5", "label": "", "text":""}] + }, + "AMB-CH": { + "qualifiers": [ + { + "label": "< Age 1", + "text": "< Age 1", + "id": "AElhke", + "excludeFromOMS": true + }, + { + "label": "Ages 1 to 9", + "text": "Ages 1 to 9", + "id": "lvn4pe", + "excludeFromOMS": true + }, + { + "label": "Ages 10 to 19", + "text": "Ages 10 to 19", + "id": "9IODyt", + "excludeFromOMS": true + }, + { + "label": "Ages unknown", + "text": "Ages unknown", + "id": "mIA2wL", + "excludeFromOMS": true + }, + { + "label": "Total (Ages <1 to 19)", + "text": "Total (Ages <1 to 19)", + "id": "Total" + } + ], + "categories": [{"id":"8KgPDM", "label": "", "text":""}] + }, + "AMB-HH": { + "qualifiers": [ + { + "label": "Ages 0 to 17", + "text": "Ages 0 to 17", + "id": "dmwUgG", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "JmMRlc", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "ISVyAj", + "excludeFromOMS": true + }, + { + "label": "Ages unknown", + "text": "Ages unknown", + "id": "f2dfi8", + "excludeFromOMS": true + }, + { + "label": "Total (All Ages)", + "text": "Total (All Ages)", + "id": "Total" + } + ], + "categories": [{"id":"9pQZSL", "label": "", "text":""}] + }, + "AMM-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "g91VU9" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "Gjknmo" + } + ], + "categories": [ + { + "label": "Effective Acute Phase Treatment", + "text": "Effective Acute Phase Treatment", + "id": "DFukSh" + }, + { + "label": "Effective Continuation Phase Treatment", + "text": "Effective Continuation Phase Treatment", + "id": "JjQeAC" + } + ] + }, + "AMR-AD": { + "qualifiers": [ + { + "label": "Ages 19 to 50", + "text": "Ages 19 to 50", + "id": "uT3Ybg", + "excludeFromOMS": true + }, + { + "label": "Ages 51 to 64", + "text": "Ages 51 to 64", + "id": "pvkFnD", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 19 to 64)", + "text": "Total (Ages 19 to 64)", + "id": "Total" + } + ], + "categories": [{"id":"HRsQ7F", "label": "", "text":""}] + }, + "AMR-CH": { + "qualifiers": [ + { + "label": "Ages 5 to 11", + "text": "Ages 5 to 11", + "id": "kijL2T", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 18", + "text": "Ages 12 to 18", + "id": "cteSKS", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 5 to 18)", + "text": "Total (Ages 5 to 18)", + "id": "Total" + } + ], + "categories": [{"id":"tMt8gW", "label": "", "text":""}] + }, + "APM-CH": { + "qualifiers": [ + { + "label": "Ages 1 to 11", + "text": "Ages 1 to 11", + "id": "rJQSKZ", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "FS1yOb", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 1 to 17)", + "text": "Total (Ages 1 to 17)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Blood Glucose", + "text": "Blood Glucose", + "id": "rcmfbq" + }, + { + "label": "Cholesterol", + "text": "Cholesterol", + "id": "0oa3fh" + }, + { + "label": "Blood Glucose and Cholesterol", + "text": "Blood Glucose and Cholesterol", + "id": "s60uoF" + } + ] + }, + "APP-CH": { + "qualifiers": [ + { + "label": "Ages 1 to 11", + "text": "Ages 1 to 11", + "id": "8KS1is", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "fNNDrp", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 1 to 17)", + "text": "Total (Ages 1 to 17)", + "id": "Total" + } + ], + "categories": [{"id":"JTlCwr", "label": "", "text":""}] + }, + "BCS-AD": { + "qualifiers": [ + { + "label": "Ages 50 to 64", + "text": "Ages 50 to 64", + "id": "bfvp78" + }, + { + "label": "Ages 65 to 74", + "text": "Ages 65 to 74", + "id": "vRqegW" + } + ], + "categories": [{"id":"xacc8a", "label": "", "text":""}] + }, + "CBP-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "lL2f0N" + }, + { + "label": "Ages 65 to 85", + "text": "Ages 65 to 85", + "id": "SynTm5" + } + ], + "categories": [{"id":"qyic1D", "label": "", "text":""}] + }, + "CBP-HH": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "KvPMQt", + "excludeFromOMS": true + }, + { + "label": "Ages 65 to 85", + "text": "Ages 65 to 85", + "id": "BgLk3U", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 18 to 85)", + "text": "Total (Ages 18 to 85)", + "id": "Total" + } + ], + "categories": [{"id":"f2iZFk", "label": "", "text":""}] + }, + "CCP-AD": { + "qualifiers": [ + { + "label": "Three Days Postpartum Rate", + "text": "Three Days Postpartum Rate", + "id": "gDSWIR" + }, + { + "label": "Ninety Days Postpartum Rate", + "text": "Ninety Days Postpartum Rate", + "id": "iHteIp" + } + ], + "categories": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "JmD71i" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "xS3fQk" + } + ] + }, + "CCP-CH": { + "qualifiers": [ + { + "label": "Three Days Postpartum Rate", + "text": "Three Days Postpartum Rate", + "id": "CeTZzq" + }, + { + "label": "Ninety Days Postpartum Rate", + "text": "Ninety Days Postpartum Rate", + "id": "XiBACI" + } + ], + "categories": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "041gNX" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "r7b2vH" + } + ] + }, + "CCS-AD": { + "qualifiers": [ + { + "label": "Percentage of women ages 21 to 64 screened", + "text": "Percentage of women ages 21 to 64 screened", + "id": "VZUrlc" + } + ], + "categories": [{"id":"Ma0WNl", "label": "", "text":""}] + }, + "CCW-AD": { + "qualifiers": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "FLFmHi" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "qken13" + } + ], + "categories": [{"id":"ntJIVl", "label": "", "text":""}] + }, + "CCW-CH": { + "qualifiers": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "XmMjXU" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "tnsawX" + } + ], + "categories": [{"id":"p2e6nu", "label": "", "text":""}] + }, + "CDF-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "wjMBXk" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "tJ0j8E" + } + ], + "categories": [{"id":"fUqwlh", "label": "", "text":""}] + }, + "CDF-CH": { + "qualifiers": [ + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "HgCHDt" + } + ], + "categories": [{"id":"hsjQhs", "label": "", "text":""}] + }, + "CDF-HH": { + "qualifiers": [ + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "UXIY6B", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "z3Sc1P", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "AcmtW5", + "excludeFromOMS": true + }, + { + "label": "Total (Age 12 and older)", + "text": "Total (Age 12 and older)", + "id": "Total" + } + ], + "categories": [{"id":"yhQjRm", "label": "", "text":""}] + }, + "CHL-AD": { + "qualifiers": [ + { + "label": "Ages 21 to 24", + "text": "Ages 21 to 24", + "id": "eV26mN" + } + ], + "categories": [{"id":"cvc5jQ", "label": "", "text":""}] + }, + "CHL-CH": { + "qualifiers": [ + { + "label": "Ages 16 to 20", + "text": "Ages 16 to 20", + "id": "PbJazd" + } + ], + "categories": [{"id":"xMHGQi", "label": "", "text":""}] + }, + "CIS-CH": { + "qualifiers": [ + { + "label": "DTaP", + "text": "DTaP", + "id": "315LFo", + "excludeFromOMS": true + }, + { + "label": "IPV", + "text": "IPV", + "id": "g11sKr", + "excludeFromOMS": true + }, + { + "label": "MMR", + "text": "MMR", + "id": "RRDiUD" + }, + { + "label": "HiB", + "text": "HiB", + "id": "MEtwzK", + "excludeFromOMS": true + }, + { + "label": "Hep B", + "text": "Hep B", + "id": "legTtc", + "excludeFromOMS": true + }, + { + "label": "VZV", + "text": "VZV", + "id": "vo0QQI", + "excludeFromOMS": true + }, + { + "label": "PCV", + "text": "PCV", + "id": "KH78dm", + "excludeFromOMS": true + }, + { + "label": "Hep A", + "text": "Hep A", + "id": "QQdRrJ", + "excludeFromOMS": true + }, + { + "label": "RV", + "text": "RV", + "id": "UACFyG", + "excludeFromOMS": true + }, + { + "label": "Flu", + "text": "Flu", + "id": "VxUjMm", + "excludeFromOMS": true + }, + { + "label": "Combo 3", + "text": "Combo 3", + "id": "aI8KQ7" + }, + { + "label": "Combo 7", + "text": "Combo 7", + "id": "xOvucQ", + "excludeFromOMS": true + }, + { + "label": "Combo 10", + "text": "Combo 10", + "id": "UwzvFc" + } + ], + "categories": [{"id":"u7wDB2", "label": "", "text":""}] + }, + "COB-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "PuPnUR" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "e37PqQ" + } + ], + "categories": [{"id":"3jFQwQ", "label": "", "text":""}] + }, + "COL-AD": { + "qualifiers": [ + { + "label": "Ages 46 to 49", + "text": "Ages 46 to 49", + "id": "jaap4W" + }, + { + "label": "Ages 50 to 64", + "text": "Ages 50 to 64", + "id": "cvwOYG" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "YHLMTu" + } + ], + "categories": [{"id":"jVTfQB", "label": "", "text":""}] + }, + "COL-HH": { + "qualifiers": [ + { + "label": "Ages 46 to 49", + "text": "Ages 46 to 49", + "id": "RHlNkr" + }, + { + "label": "Ages 50 to 64", + "text": "Ages 50 to 64", + "id": "u1n5QA" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "2ATODe" + } + ], + "categories": [{"id":"sYNAsL", "label": "", "text":""}] + }, + "CPU-AD": { + "qualifiers": [ + { + "label": "Care Plan with Core Elements Documented", + "text": "Care Plan with Core Elements Documented", + "id": "T6dADz", + }, + { + "label": "Care Plan with Supplemental Elements Documented", + "text": "Care Plan with Supplemental Elements Documented", + "id": "NBz553", + }, + ], + "categories": [{"id":"HLXNLW", "label": "", "text":""}] + }, + "DEV-CH": { + "qualifiers": [ + { + "label": "Children screened by 12 months of age", + "text": "Children screened by 12 months of age", + "id": "V9moUD", + "excludeFromOMS": true + }, + { + "label": "Children screened by 24 months of age", + "text": "Children screened by 24 months of age", + "id": "8syeJa", + "excludeFromOMS": true + }, + { + "label": "Children screened by 36 months of age", + "text": "Children screened by 36 months of age", + "id": "UjlL0h", + "excludeFromOMS": true + }, + { + "label": "Children Total", + "text": "Children Total", + "id": "Total" + } + ], + "categories": [{"id":"rnFOY6", "label": "", "text":""}] + }, + "FUA-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "YXDxKr" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "DV7M5h" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days of ED visit", + "text": "Follow-up within 30 days of ED visit", + "id": "LfkdK3" + }, + { + "label": "Follow-up within 7 days of ED visit", + "text": "Follow-up within 7 days of ED visit", + "id": "Ipp7sc" + } + ] + }, + "FUA-CH": { + "qualifiers": [ + { + "label": "Ages 13 to 17", + "text": "Ages 13 to 17", + "id": "ACAVuF" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days of ED visit", + "text": "Follow-up within 30 days of ED visit", + "id": "EMKZhW" + }, + { + "label": "Follow-up within 7 days of ED visit", + "text": "Follow-up within 7 days of ED visit", + "id": "Nz1u3I" + } + ] + }, + "FUA-HH": { + "qualifiers": [ + { + "label": "Ages 13 to 17", + "text": "Ages 13 to 17", + "id": "V4pnKr", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "Njn9ob", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "GM7MbC", + "excludeFromOMS": true + }, + { + "label": "Total (Age 13 and older)", + "text": "Total (Age 13 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days of ED visit", + "text": "Follow-up within 30 days of ED visit", + "id": "eHvs7S" + }, + { + "label": "Follow-up within 7 days of ED visit", + "text": "Follow-up within 7 days of ED visit", + "id": "PyKyIU" + } + ] + }, + "FUH-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "FbBLHo" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "MqFK9L" + } + ], + "categories": [ + { + "label": "Follow-Up within 30 days after discharge", + "text": "Follow-Up within 30 days after discharge", + "id": "8w4t99" + }, + { + "label": "Follow-Up within 7 days after discharge", + "text": "Follow-Up within 7 days after discharge", + "id": "c2CNBL" + } + ] + }, + "FUH-CH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "x1i20X" + } + ], + "categories": [ + { + "label": "Follow-Up within 30 days after discharge", + "text": "Follow-Up within 30 days after discharge", + "id": "YI7qPh" + }, + { + "label": "Follow-Up within 7 days after discharge", + "text": "Follow-Up within 7 days after discharge", + "id": "bCOfkY" + } + ] + }, + "FUH-HH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "SSE84V", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "dqF5XO", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "OLGEc6", + "excludeFromOMS": true + }, + { + "label": "Total (Age 6 and older)", + "text": "Total (Age 6 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days after discharge", + "text": "Follow-up within 30 days after discharge", + "id": "BDDUqy" + }, + { + "label": "Follow-up within 7 days after discharge", + "text": "Follow-up within 7 days after discharge", + "id": "5qOI4g" + } + ] + }, + "FUM-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "xO0lQK" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "6p8dL9" + } + ], + "categories": [ + { + "label": "30-day follow-up after ED visit for mental illness", + "text": "30-day follow-up after ED visit for mental illness", + "id": "biXkZF" + }, + { + "label": "7-day follow-up after ED visit for mental illness", + "text": "7-day follow-up after ED visit for mental illness", + "id": "jQ2y5i" + } + ] + }, + "FUM-CH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "v1LHRu" + } + ], + "categories": [ + { + "label": "30-day follow-up after ED visit for mental illness", + "text": "30-day follow-up after ED visit for mental illness", + "id": "mpaDiA" + }, + { + "label": "7-day follow-up after ED visit for mental illness", + "text": "7-day follow-up after ED visit for mental illness", + "id": "MFDMmO" + } + ] + }, + "FUM-HH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "wZkCIH", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "HOpeUo", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "CBkwgBC", + "excludeFromOMS": true + }, + { + "label": "Total (Age 6 and older)", + "text": "Total (Age 6 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "30-day follow-up after ED visit for mental illness", + "text": "30-day follow-up after ED visit for mental illness", + "id": "bLYJVL" + }, + { + "label": "7-day follow-up after ED visit for mental illness", + "text": "7-day follow-up after ED visit for mental illness", + "id": "0PNnN6" + } + ] + }, + "FVA-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "BKbFhQ" + } + ], + "categories": [{"id":"5bDNK0", "label": "", "text":""}] + }, + "HBD-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "clYQr0" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "chkl7n" + } + ], + "categories": [ + { + "label": "HbA1c control (<8.0%)", + "text": "HbA1c control (<8.0%)", + "id":"F9V8xD", + "excludeFromOMS": true + }, + { + "label": "HbA1c poor control (>9.0%)", + "text": "HbA1c poor control (>9.0%)", + "id":"MELFVb" + } + ] + }, + "HPCMI-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "T93ccP" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "LmBzTX" + } + ], + "categories": [{"id":"rCN1NM", "label": "", "text":""}] + }, + "HVL-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "NipqyO" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "0SLTQO" + } + ], + "categories": [{"id":"iAT7Xc", "label": "", "text":""}] + }, + "IET-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "Mj6NGv" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "IrauwZ" + } + ], + "categories": [ + { + "label": "Initiation of SUD Treatment: Alcohol Use Disorder", + "text": "Initiation of SUD Treatment: Alcohol Use Disorder", + "id": "pgGQFr", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Alcohol Use Disorder", + "text": "Engagement of SUD Treatment: Alcohol Use Disorder", + "id": "eWGRSg", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Opioid Use Disorder", + "text": "Initiation of SUD Treatment: Opioid Use Disorder", + "id": "yrQOQf", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Opioid Use Disorder", + "text": "Engagement of SUD Treatment: Opioid Use Disorder", + "id": "zkA4Vm", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Other Substance Use Disorder", + "text": "Initiation of SUD Treatment: Other Substance Use Disorder", + "id": "ptnmQC", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Other Substance Use Disorder", + "text": "Engagement of SUD Treatment: Other Substance Use Disorder", + "id": "leK847", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Total", + "text": "Initiation of SUD Treatment: Total", + "id": "NoGIsL" + }, + { + "label": "Engagement of SUD Treatment: Total", + "text": "Engagement of SUD Treatment: Total", + "id": "nHTL1Y" + } + ] + }, + "IET-HH": { + "qualifiers": [ + { + "label": "Ages 13 to 17", + "text": "Ages 13 to 17", + "id": "pO7AO4", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "VnqwIP", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "HJa7Gq", + "excludeFromOMS": true + }, + { + "label": "Total (age 13 and older)", + "text": "Total (age 13 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Initiation of SUD Treatment: Alcohol Use Disorder", + "text": "Initiation of SUD Treatment: Alcohol Use Disorder", + "id": "SzkEgT", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Alcohol Use Disorder", + "text": "Engagement of SUD Treatment: Alcohol Use Disorder", + "id": "fBjzVK", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Opioid Use Disorder", + "text": "Initiation of SUD Treatment: Opioid Use Disorder", + "id": "XcV5kx", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Opioid Use Disorder", + "text": "Engagement of SUD Treatment: Opioid Use Disorder", + "id": "TbEkhJ", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Other Substance Use Disorder", + "text": "Initiation of SUD Treatment: Other Substance Use Disorder", + "id": "DNIllr", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Other Substance Use Disorder", + "text": "Engagement of SUD Treatment: Other Substance Use Disorder", + "id": "J2Kfn5", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Total", + "text": "Initiation of SUD Treatment: Total", + "id": "QpzHSf" + }, + { + "label": "Engagement of SUD Treatment: Total", + "text": "Engagement of SUD Treatment: Total", + "id": "AVY2yg" + } + ] + }, + "IMA-CH": { + "qualifiers": [ + { + "label": "Meningococcal", + "text": "Meningococcal", + "id": "25diKI" + }, + { + "label": "Tdap", + "text": "Tdap", + "id": "gTSKzD" + }, + { + "label": "Human Papillomavirus (HPV)", + "text": "Human Papillomavirus (HPV)", + "id": "3r9tKr" + }, + { + "label": "Combination 1 (Meningococcal, Tdap)", + "text": "Combination 1 (Meningococcal, Tdap)", + "id": "61I5BC" + }, + { + "label": "Combination 2 (Meningococcal, Tdap, HPV)", + "text": "Combination 2 (Meningococcal, Tdap, HPV)", + "id": "agvITR" + } + ], + "categories": [{"id":"6Wts84", "label": "", "text":""}] + }, + "IU-HH": { + "qualifiers": [ + { + "label": "Ages 0 to 17", + "text": "Ages 0 to 17", + "id": "RTKRUB", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "HN9CBC", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "OdxHfY", + "excludeFromOMS": true + }, + { + "label": "Ages unknown", + "text": "Ages unknown", + "id": "At7duO", + "excludeFromOMS": true + }, + { + "label": "Total", + "text": "Total", + "id": "Total" + } + ], + "categories": [ + { + "label": "Inpatient", + "text": "Inpatient", + "id": "895Rzk" + }, + { + "label": "Mental and Behavioral Disorders", + "text": "Mental and Behavioral Disorders", + "id": "VX9uaf", + "excludeFromOMS": true + }, + { + "label": "Surgery", + "text": "Surgery", + "id": "QzuJDR", + "excludeFromOMS": true + }, + { + "label": "Medicine", + "text": "Medicine", + "id": "ioz7ed", + "excludeFromOMS": true + } + ] + }, + + "LSC-CH": { + "qualifiers": [ + { + "label": "At least one lead capillary or venous blood test on or before the child's second birthday", + "text": "At least one lead capillary or venous blood test on or before the child's second birthday", + "id": "AtXKXx" + } + ], + "categories": [{"id":"HOiTVZ", "label": "", "text":""}] + }, + "MSC-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "wNVojQ" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "SYlSVC" + } + ], + "categories": [ + { + "label": "Percentage of Current Smokers and Tobacco Users", + "text": "Percentage of Current Smokers and Tobacco Users", + "id": "fF0COe" + }, + { + "label": "Advising Smokers and Tobacco Users to Quit", + "text": "Advising Smokers and Tobacco Users to Quit", + "id": "S2ngC7" + }, + { + "label": "Discussing Cessation Medications", + "text": "Discussing Cessation Medications", + "id": "Hk5dWm" + }, + { + "label": "Discussing Cessation Strategies", + "text": "Discussing Cessation Strategies", + "id": "xiXN7R" + } + ] + }, + "OEV-CH": { + "qualifiers": [ + { + "label": "Age <1", + "text": "Age <1", + "id": "cJpLzk", + "excludeFromOMS": true + }, + { + "label": "Ages 1 to 2", + "text": "Ages 1 to 2", + "id": "lUpXnj", + "excludeFromOMS": true + }, + { + "label": "Ages 3 to 5", + "text": "Ages 3 to 5", + "id": "tfIdZe", + "excludeFromOMS": true + }, + { + "label": "Ages 6 to 7", + "text": "Ages 6 to 7", + "id": "HQtz8Q", + "excludeFromOMS": true + }, + { + "label": "Ages 8 to 9", + "text": "Ages 8 to 9", + "id": "lw6tF8", + "excludeFromOMS": true + }, + { + "label": "Ages 10 to 11", + "text": "Ages 10 to 11", + "id": "iS0hVY", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 14", + "text": "Ages 12 to 14", + "id": "OAxju0", + "excludeFromOMS": true + }, + { + "label": "Ages 15 to 18", + "text": "Ages 15 to 18", + "id": "0B13E5", + "excludeFromOMS": true + }, + { + "label": "Ages 19 to 20", + "text": "Ages 19 to 20", + "id": "2v1LvP", + "excludeFromOMS": true + }, + { + "label": "Total ages <1 to 20", + "text": "Total ages <1 to 20 (required rate)", + "id": "Total" + } + ], + "categories": [{"id":"oe5lKf", "label": "", "text":""}] + }, + "OHD-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "NyGIus" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "23IWY1" + } + ], + "categories": [{"id":"aeOiMF", "label": "", "text":""}] + }, + "OUD-AD": { + "qualifiers": [ + { + "label": "Total Rate", + "text": "Total Rate", + "id": "R74AGC" + }, + { + "label": "Buprenorphine", + "text": "Buprenorphine", + "id": "ayxxN6", + "excludeFromOMS": true + }, + { + "label": "Oral naltrexone", + "text": "Oral naltrexone", + "id": "xaFLEL", + "excludeFromOMS": true + }, + { + "label": "Long-acting, injectable naltrexone", + "text": "Long-acting, injectable naltrexone", + "id": "Pumtn4", + "excludeFromOMS": true + }, + { + "label": "Methadone", + "text": "Methadone", + "id": "CgQFr3", + "excludeFromOMS": true + } + ], + "categories": [{"id":"t75kZB", "label": "", "text":""}] + }, + "OUD-HH": { + "qualifiers": [ + { + "label": "Total Rate", + "text": "Total Rate", + "id": "yiqyWE" + }, + { + "label": "Buprenorphine", + "text": "Buprenorphine", + "id": "QczL9f", + "excludeFromOMS": true + }, + { + "label": "Oral naltrexone", + "text": "Oral naltrexone", + "id": "bNSOWQ", + "excludeFromOMS": true + }, + { + "label": "Long-acting, injectable naltrexone", + "text": "Long-acting, injectable naltrexone", + "id": "LNXU8H", + "excludeFromOMS": true + }, + { + "label": "Methadone", + "text": "Methadone", + "id": "vsv8L7", + "excludeFromOMS": true + } + ], + "categories": [{"id":"lS0z8M", "label": "", "text":""}] + }, + "PCR-AD": { + "qualifiers": [ + { + "label": "Count of Index Hospital Stays", + "text": "Count of Index Hospital Stays", + "id": "Z31BMw" + }, + { + "label": "Count of Observed 30-Day Readmissions", + "text": "Count of Observed 30-Day Readmissions", + "id": "KdVD0I" + }, + { + "label": "Observed Readmission Rate", + "text": "Observed Readmission Rate", + "id": "GWePur" + }, + { + "label": "Count of Expected 30-Day Readmissions", + "text": "Count of Expected 30-Day Readmissions", + "id": "ciVWdY" + }, + { + "label": "Expected Readmission Rate", + "text": "Expected Readmission Rate", + "id": "qi3Vd7" + }, + { + "label": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "text": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "id": "SczxqV" + }, + { + "label": "Count of Beneficiaries in Medicaid Population", + "text": "Count of Beneficiaries in Medicaid Population", + "id": "Ei65yg" + }, + { + "label": "Number of Outliers", + "text": "Number of Outliers", + "id": "pBILL1" + }, + { + "label": "Outlier Rate (Number of Outliers/Count of Beneficiaries in Medicaid Population) x 1,000", + "text": "Outlier Rate (Number of Outliers/Count of Beneficiaries in Medicaid Population) x 1,000", + "id": "Nfe4Cn" + } + ], + "categories": [{"id":"zcwVcA", "label": "", "text":""}] + }, + "PCR-HH": { + "qualifiers": [ + { + "label": "Count of Index Hospital Stays", + "text": "Count of Index Hospital Stays", + "id": "QfSJYl" + }, + { + "label": "Count of Observed 30-Day Readmissions", + "text": "Count of Observed 30-Day Readmissions", + "id": "ObB95y" + }, + { + "label": "Observed Readmission Rate", + "text": "Observed Readmission Rate", + "id": "6GwY7k" + }, + { + "label": "Count of Expected 30-Day Readmissions", + "text": "Count of Expected 30-Day Readmissions", + "id": "xBK85r" + }, + { + "label": "Expected Readmission Rate", + "text": "Expected Readmission Rate", + "id": "f1XeFZ" + }, + { + "label": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "text": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "id": "PVXXII" + }, + { + "label": "Count of Enrollees in Health Home Population", + "text": "Count of Enrollees in Health Home Population", + "id": "BkJVO0" + }, + { + "label": "Number of Outliers", + "text": "Number of Outliers", + "id": "vf2fO0" + }, + { + "label": "Outlier Rate (Number of Outliers/Count of Enrollees in Health Home Population) x 1,000", + "text": "Outlier Rate (Number of Outliers/Count of Enrollees in Health Home Population) x 1,000", + "id": "08LeiP" + } + ], + "categories": [{"id":"YGJwmu", "label": "", "text":""}] + }, + "PPC-AD": { + "qualifiers": [ + { + "label": "Postpartum visit between 7 and 84 days", + "text": "Postpartum visit between 7 and 84 days", + "id": "LrWMAT" + } + ], + "categories": [{"id":"SyrrI1", "label": "", "text":""}] + }, + "PPC2-CH": { + "qualifiers": [ + { + "label": "Timeliness of Prenatal Care: Under Age 21", + "text": "Timeliness of Prenatal Care: Under Age 21", + "id": "kCBB0a" + }, + { + "label": "Postpartum Care: Under Age 21", + "text": "Postpartum Care: Under Age 21", + "id": "PcLgEs" + }, + ], + "categories": [{"id":"fcjCsg", "label": "", "text":""}] + }, + "PQI01-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "8N7tKQ" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "g01NRO" + } + ], + "categories": [{"id":"Gp9GRU", "label": "", "text":""}] + }, + "PQI05-AD": { + "qualifiers": [ + { + "label": "Ages 40 to 64", + "text": "Ages 40 to 64", + "id": "ocpMf5" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "usIZvV" + } + ], + "categories": [{"id":"CzBbvv", "label": "", "text":""}] + }, + "PQI08-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "bi8vv6" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "rRyKdf" + } + ], + "categories": [{"id":"eYsLWE", "label": "", "text":""}] + }, + "PQI15-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 39", + "text": "Ages 18 to 39", + "id": "i57RxI" + } + ], + "categories": [{"id":"Z4aIZZ", "label": "", "text":""}] + }, + "PQI92-HH": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "cxWfJ4", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "ttjYve", + "excludeFromOMS": true + }, + { + "label": "Total (Age 18 and older)", + "text": "Total (Age 18 and older)", + "id": "Total" + } + ], + "categories": [{"id":"mIBZOk", "label": "", "text":""}] + }, + "SAA-AD": { + "qualifiers": [ + { + "label": "Beneficiaries Age 18 and Older", + "text": "Beneficiaries Age 18 and Older", + "id": "5Zy1WJ" + } + ], + "categories": [{"id":"bdI1f1", "label": "", "text":""}] + }, + "SFM-CH": { + "qualifiers": [ + { + "label": "Rate 1 - At Least One Sealant", + "text": "Rate 1 - At Least One Sealant", + "id": "5Jb7fw" + }, + { + "label": "Rate 2 - All Four Molars Sealed", + "text": "Rate 2 - All Four Molars Sealed", + "id": "VC1O0n" + } + ], + "categories": [{"id":"zooXXr", "label": "", "text":""}] + }, + "SSD-AD": { + "qualifiers": [ + { + "label": "Percentage of Beneficiaries Ages 18 to 64", + "text": "Percentage of Beneficiaries Ages 18 to 64", + "id": "QAiTJA" + } + ], + "categories": [{"id":"IJmKJZ", "label": "", "text":""}] + }, + "TFL-CH": { + "qualifiers": [ + { + "label": "Ages 1 to 2", + "text": "Ages 1 to 2", + "id": "TGl5f1", + "excludeFromOMS": true + }, + { + "label": "Ages 3 to 5", + "text": "Ages 3 to 5", + "id": "AccBCl", + "excludeFromOMS": true + }, + { + "label": "Ages 6 to 7", + "text": "Ages 6 to 7", + "id": "zcbPe0", + "excludeFromOMS": true + }, + { + "label": "Ages 8 to 9", + "text": "Ages 8 to 9", + "id": "OdDTxr", + "excludeFromOMS": true + }, + { + "label": "Ages 10 to 11", + "text": "Ages 10 to 11", + "id": "Cjw7GS", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 14", + "text": "Ages 12 to 14", + "id": "muOeEP", + "excludeFromOMS": true + }, + { + "label": "Ages 15 to 18", + "text": "Ages 15 to 18", + "id": "fbpAPY", + "excludeFromOMS": true + }, + { + "label": "Ages 19 to 20", + "text": "Ages 19 to 20", + "id": "a6okZM", + "excludeFromOMS": true + }, + { + "label": "Total Ages 1 through 20", + "text": "Total Ages 1 through 20", + "id": "Total" + } + ], + "categories": [ + { + "label": "Dental or oral health services", + "text": "Dental or oral health services", + "id": "1LW8Lr" + }, + { + "label": "Dental services", + "text": "Dental services", + "id": "1gVqJh", + "excludeFromOMS": true + }, + { + "label": "Oral health services", + "text": "Oral health services", + "id": "0NPmzO", + "excludeFromOMS": true + } + ] + }, + "W30-CH": { + "qualifiers": [ + { + "label": "Rate 1 - Six or more well-child visits in the first 15 months ", + "text": "Rate 1 - Six or more well-child visits in the first 15 months ", + "id": "VCTS6B" + }, + { + "label": "Rate 2 - Two or more well-child visits for ages 15 months to 30 months", + "text": "Rate 2 - Two or more well-child visits for ages 15 months to 30 months", + "id": "pNdn6w" + } + ], + "categories": [{"id":"HOiTVR", "label": "", "text":""}] + }, + "WCC-CH": { + "qualifiers": [ + { + "label": "Ages 3 to 11", + "text": "Ages 3 to 11", + "id": "iWwR8Z", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "BFwD7g", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 3 to 17)", + "text": "Total (Ages 3 to 17)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Body mass index (BMI) percentile documentation", + "text": "Body mass index (BMI) percentile documentation", + "id": "4TXd3h" + }, + { + "label": "Counseling for Nutrition", + "text": "Counseling for Nutrition", + "id": "cKH5gj" + }, + { + "label": "Counseling for Physical Activity", + "text": "Counseling for Physical Activity", + "id": "1POxYx" + } + ] + }, + "WCV-CH": { + "qualifiers": [ + { + "label": "Ages 3 to 11", + "text": "Ages 3 to 11", + "id": "AtXKXf", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "iqbQvH", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 21", + "text": "Ages 18 to 21", + "id": "pJmWDB", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 3 to 21)", + "text": "Total (Ages 3 to 21)", + "id": "Total" + } + ], + "categories": [{"id":"YOFx9h", "label": "", "text":""}] + } +} + +export const getCatQualLabels = (measure: keyof typeof data) => { + const qualifiers = data[measure].qualifiers; + const categories = data[measure].categories; + + return { + qualifiers, + categories, + }; + }; \ No newline at end of file diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx new file mode 100644 index 0000000000..e3de3c1bcb --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx @@ -0,0 +1,178 @@ +import fireEvent from "@testing-library/user-event"; +import { CombinedRates } from "."; +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; + +describe("Test CombinedRates component", () => { + beforeEach(() => { + renderWithHookForm(<CombinedRates />); + }); + + it("component renders", () => { + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. health plans, delivery systems, programs) to create a State-Level rate?" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. health plans, delivery systems, programs) to create a State-Level rate?" + ) + ).toBeInTheDocument(); + }); + + it("renders suboptions when Yes is clicked", async () => { + const textArea = await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a State-Level rate." + ); + fireEvent.click(textArea); + expect( + screen.getByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a State-Level rate." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeInTheDocument(); + }); + + it("renders a text area when Yes is clicked and the last option is clicked", async () => { + fireEvent.click( + await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a State-Level rate." + ) + ); + fireEvent.click( + await screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ); + + expect( + screen.getByText("Describe the other weighting factor:") + ).toBeInTheDocument(); + + const textArea = await screen.findByLabelText( + "Describe the other weighting factor:" + ); + + fireEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + it("does not render suboptions when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, we did not combine rates from multiple reporting units to create a State-Level rate." + ); + fireEvent.click(textArea); + expect( + screen.queryByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a State-Level rate." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeNull(); + }); +}); + +describe("Test CombinedRates component for Health Homes", () => { + beforeEach(() => { + renderWithHookForm(<CombinedRates healthHomeMeasure />); + }); + + it("component renders with different text", () => { + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. Health Home Providers) to create a Health Home Program-Level rate?" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. Health Home Providers) to create a Health Home Program-Level rate?" + ) + ).toBeInTheDocument(); + }); + + it("renders suboptions when Yes is clicked", async () => { + const textArea = await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a Health Home Program-Level rate." + ); + fireEvent.click(textArea); + expect( + screen.getByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a Program-Level rate." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeInTheDocument(); + }); + + it("renders a text area when Yes is clicked and the last option is clicked for Health Homes", async () => { + fireEvent.click( + await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a Health Home Program-Level rate." + ) + ); + fireEvent.click( + await screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ); + + expect( + screen.getByText("Describe the other weighting factor:") + ).toBeInTheDocument(); + + const textArea = await screen.findByLabelText( + "Describe the other weighting factor:" + ); + + fireEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + it("does not render suboptions when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, we did not combine rates from multiple reporting units to create a Program-Level rate for Health Home measures." + ); + fireEvent.click(textArea); + expect( + screen.queryByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a Program-Level rate." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeNull(); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx new file mode 100644 index 0000000000..c99c4e6663 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx @@ -0,0 +1,96 @@ +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; + +interface Props { + healthHomeMeasure?: boolean; +} + +export const CombinedRates = ({ healthHomeMeasure }: Props) => { + const register = useCustomRegister<Types.CombinedRates>(); + + return ( + <QMR.CoreQuestionWrapper + testid="combined-rates" + label="Combined Rate(s) from Multiple Reporting Units" + > + <CUI.Text fontWeight={600}> + {!healthHomeMeasure ? ( + <> + Did you combine rates from multiple reporting units (e.g. health + plans, delivery systems, programs) to create a State-Level rate? + </> + ) : ( + <> + Did you combine rates from multiple reporting units (e.g. Health + Home Providers) to create a Health Home Program-Level rate? + </> + )} + </CUI.Text> + <CUI.Text mb={2}> + For additional information refer to the{" "} + <CUI.Link + href="https://www.medicaid.gov/medicaid/quality-of-care/downloads/state-level-rates-brief.pdf" + color="blue" + isExternal + aria-label="Additional state level brief information" + > + <u>State-Level Rate Brief</u> + </CUI.Link> + . + </CUI.Text> + <QMR.RadioButton + options={[ + { + displayValue: !healthHomeMeasure + ? "Yes, we combined rates from multiple reporting units to create a State-Level rate." + : "Yes, we combined rates from multiple reporting units to create a Health Home Program-Level rate.", + value: DC.YES, + children: [ + <QMR.RadioButton + {...register(DC.COMBINED_RATES_COMBINED_RATES)} + options={[ + { + displayValue: !healthHomeMeasure + ? "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a State-Level rate." + : "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a Program-Level rate.", + value: DC.COMBINED_NOT_WEIGHTED_RATES, + }, + { + displayValue: + "The rates are weighted based on the size of the measure-eligible population for each reporting unit.", + value: DC.COMBINED_WEIGHTED_RATES, + }, + { + displayValue: + "The rates are weighted based on another weighting factor.", + value: DC.COMBINED_WEIGHTED_RATES_OTHER, + children: [ + <QMR.TextArea + {...register( + DC.COMBINED_WEIGHTED_RATES_OTHER_EXPLAINATION + )} + label="Describe the other weighting factor:" + formLabelProps={{ fontWeight: 400 }} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: !healthHomeMeasure + ? "No, we did not combine rates from multiple reporting units to create a State-Level rate." + : "No, we did not combine rates from multiple reporting units to create a Program-Level rate for Health Home measures.", + value: DC.NO, + }, + ]} + renderHelperTextAbove + {...register(DC.COMBINED_RATES)} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..800e2d0150 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap @@ -0,0 +1,258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test the global DataSource component (Custom Structure) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Data Source" + data-testid="data-source" + > + Data Source + </h2> + <fieldset> + <div + data-cy="data-source-options" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="How do we feel about a label with this text? Does it work?" + for="field-2" + id="field-2-label" + > + How do we feel about a label with this text? Does it work? + </label> + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DataSource0" + > + <input + class="chakra-checkbox__input" + id="DataSource0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Option1" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Option 1 + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DataSource1" + > + <input + class="chakra-checkbox__input" + id="DataSource1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ABetterOption" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + A Better Option + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DataSource2" + > + <input + class="chakra-checkbox__input" + id="DataSource2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="SomethingEvenCrazierThanTheFirst2" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Something Even Crazier Than The First 2 + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; + +exports[`Test the global DataSource component (Default) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Data Source" + data-testid="data-source" + > + Data Source + </h2> + <fieldset> + <div + data-cy="data-source-options" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below." + for="field-1" + id="field-1-label" + > + If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below. + </label> + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DataSource0" + > + <input + class="chakra-checkbox__input" + id="DataSource0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="AdministrativeData" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Administrative Data + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DataSource1" + > + <input + class="chakra-checkbox__input" + id="DataSource1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="OtherDataSource" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other Data Source + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts new file mode 100644 index 0000000000..a4eefef9b4 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts @@ -0,0 +1,43 @@ +import * as DC from "dataConstants"; + +export interface OptionNode { + value: string; + subOptions?: { + label?: string; + options: OptionNode[]; + }[]; + description?: boolean; +} + +export interface DataSourceData { + options: OptionNode[]; + optionsLabel: string; +} + +export const defaultData: DataSourceData = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx new file mode 100644 index 0000000000..25fbd64d41 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx @@ -0,0 +1,47 @@ +import { DataSource } from "./index"; +import { DataSourceData as DS } from "utils/testUtils/testFormData"; +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { testSnapshot } from "utils/testUtils/testSnapshot"; + +describe("Test the global DataSource component", () => { + it("(Default) Component renders with correct content", () => { + const component = <DataSource />; + testSnapshot({ component, defaultValues: DS.default }); + }); + + it("(Custom Structure) Component renders with correct content", () => { + const component = <DataSource data={data} />; + testSnapshot({ component, defaultValues: DS.custom }); + }); +}); + +const data: DataDrivenTypes.DataSource = { + optionsLabel: "How do we feel about a label with this text? Does it work?", + options: [ + { + value: "Option 1", + subOptions: [ + { + label: "What is the Option 1 Data Source?", + options: [ + { + value: "I'm not telling", + }, + { + value: "You'll have to kill me first", + description: true, + }, + ], + }, + ], + }, + { + value: "A Better Option", + description: true, + }, + { + value: "Something Even Crazier Than The First 2", + description: false, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx new file mode 100644 index 0000000000..f33bbaf771 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx @@ -0,0 +1,148 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { DataSourceData, defaultData, OptionNode } from "./data"; +import { useFormContext, useWatch } from "react-hook-form"; +import * as DC from "dataConstants"; +import { cleanString } from "utils/cleanString"; + +interface DataSourceProps { + data?: DataSourceData; +} + +interface DataSourceCheckboxBuilderProps { + data?: OptionNode[]; + label?: string; + parentName?: string; +} + +type DSCBFunc = ({ + data, +}: DataSourceCheckboxBuilderProps) => QMR.CheckboxOption[]; + +type DSCBChildFunc = ({ + data, +}: DataSourceCheckboxBuilderProps) => React.ReactElement[]; + +/** + * Build child checkboxes for data source options + */ +const buildDataSourceCheckboxOptionChildren: DSCBChildFunc = ({ + data, + label, + parentName, +}) => { + const elements: React.ReactElement[] = []; + if (data?.length) { + elements.push( + <QMR.Checkbox + label={label} + name={`${DC.DATA_SOURCE_SELECTIONS}.${parentName}.${DC.SELECTED}`} + key={`${DC.DATA_SOURCE_SELECTIONS}.${parentName}.${DC.SELECTED}`} + options={buildDataSourceOptions({ data, parentName })} + /> + ); + } + return elements; +}; + +/** + * Build Data Source checkbox options, and possible child checkbox children + */ +const buildDataSourceOptions: DSCBFunc = ({ data = [], parentName }) => { + const checkBoxOptions: QMR.CheckboxOption[] = []; + for (const node of data) { + const cleanedNodeValue = cleanString(node.value); + const adjustedParentName = parentName + ? `${parentName}-${cleanedNodeValue}` + : cleanedNodeValue; + let children: any = []; + node.subOptions?.forEach((subOption: any, i) => { + children = [ + ...children, + ...buildDataSourceCheckboxOptionChildren({ + data: subOption.options, + label: subOption.label, + parentName: `${adjustedParentName}${i}`, + }), + ]; + }); + + if (node.description) { + let label = ( + <> + Describe the data source ( + <em> + text in this field is included in publicly-reported state-specific + comments + </em> + ): + </> + ); + children.push( + <QMR.TextArea + label={ + node.value !== DC.ELECTRONIC_HEALTH_RECORDS + ? label + : "Describe the data source:" + } + name={`${DC.DATA_SOURCE_SELECTIONS}.${adjustedParentName}.${DC.DESCRIPTION}`} + key={`${DC.DATA_SOURCE_SELECTIONS}.${adjustedParentName}.${DC.DESCRIPTION}`} + /> + ); + } + + checkBoxOptions.push({ + value: cleanedNodeValue, + displayValue: node.value, + children, + }); + } + + return checkBoxOptions; +}; + +/** + * Fully built DataSource component + */ +export const DataSource = ({ data = defaultData }: DataSourceProps) => { + const register = useCustomRegister<Types.DataSource>(); + const { getValues } = useFormContext<Types.DataSource>(); + const watchDataSource = useWatch<Types.DataSource>({ + name: DC.DATA_SOURCE, + defaultValue: getValues().DataSource, + }) as string[] | undefined; + + const showExplanation = watchDataSource && watchDataSource.length >= 2; + + return ( + <QMR.CoreQuestionWrapper testid="data-source" label="Data Source"> + <div data-cy="data-source-options"> + <QMR.Checkbox + {...register(DC.DATA_SOURCE)} + label={data.optionsLabel} + options={buildDataSourceOptions({ data: data.options })} + /> + </div> + {showExplanation && ( + <CUI.VStack key={"DataSourceExplanationWrapper"}> + <CUI.Text + fontSize="sm" + py="2" + fontWeight="bold" + key="If the data source differed across" + label="Data Source Explanation" + > + For each data source selected above, describe which reporting + entities used each data source (e.g., health plans, FFS). If the + data source differed across health plans or delivery systems, + identify the number of plans or delivery systems that used each data + source. + </CUI.Text> + <QMR.TextArea {...register(DC.DATA_SOURCE_DESCRIPTION)} /> + </CUI.VStack> + )} + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts new file mode 100644 index 0000000000..e860916874 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts @@ -0,0 +1,26 @@ +export interface OptionNode { + value: string; + subOptions?: { + label?: string; + options: OptionNode[]; + }; + description?: boolean; +} + +export interface DataSourceData { + options: OptionNode[]; + optionsLabel: string; +} + +export const defaultData: DataSourceData = { + optionsLabel: "Which version of the CAHPS survey was used for reporting?", + options: [ + { + value: "CAHPS 5.1H", + }, + { + value: "Other Data Source", + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx new file mode 100644 index 0000000000..636df5bfc6 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx @@ -0,0 +1,55 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { DataSourceData, defaultData } from "./data"; +import * as DC from "dataConstants"; + +interface DataSourceProps { + data?: DataSourceData; +} + +/** + * Fully built DataSource component + */ +export const DataSourceRadio = ({ data = defaultData }: DataSourceProps) => { + const register = useCustomRegister<Types.DataSource>(); + + return ( + <QMR.CoreQuestionWrapper testid="data-source" label="Data Source"> + <QMR.RadioButton + {...register(DC.DATA_SOURCE)} + label={data.optionsLabel} + options={[ + { + displayValue: "CAHPS 5.1H", + value: "CAHPS 5.1H", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + name="describeTheDataSource" + label={ + <> + Describe the data source ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + key="dataSourceOtherTextArea" + formLabelProps={{ + fontWeight: "normal", + fontSize: "normal", + }} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx new file mode 100644 index 0000000000..d9da961de4 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx @@ -0,0 +1,104 @@ +import { DateRange } from "."; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { screen } from "@testing-library/react"; +import fireEvent from "@testing-library/user-event"; + +describe("DateRange component, adult", () => { + beforeEach(() => { + renderWithHookForm(<DateRange type="adult" />); + }); + + it("renders properly", async () => { + expect( + screen.getByText( + "Did your state adhere to Core Set specifications in defining the measurement period for calculating this measure?" + ) + ).toBeInTheDocument(); + }); + + it("renders Adult Measurement Period Table link properly when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, our state used a different measurement period." + ); + fireEvent.click(textArea); + + expect( + screen.getByRole("link", { name: "Measurement Period Table" }) + ).toHaveAttribute( + "href", + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/adult-core-set-reporting-resources/index.html" + ); + + expect( + screen.getByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeInTheDocument(); + + expect(screen.getByText("Start Date")).toBeInTheDocument(); + expect(screen.getByText("End Date")).toBeInTheDocument(); + }); + + it("does not render suboptions when Yes is clicked", async () => { + const textArea = await screen.findByLabelText( + "Yes, our state adhered to Core Set specifications in defining the measurement period for calculating this measure." + ); + fireEvent.click(textArea); + + expect( + screen.queryByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeNull(); + }); +}); + +describe("DateRange component, child", () => { + beforeEach(() => { + renderWithHookForm(<DateRange type="child" />); + }); + + it("renders Child Measurement Period Table link properly when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, our state used a different measurement period." + ); + fireEvent.click(textArea); + + expect( + screen.getByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeInTheDocument(); + + expect( + screen.getByRole("link", { name: "Measurement Period Table" }) + ).toHaveAttribute( + "href", + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/child-core-set-reporting-resources/index.html" + ); + }); +}); + +describe("DateRange component, Health Home", () => { + it("renders Health Home Measurement Period Table link properly when No is clicked", async () => { + renderWithHookForm(<DateRange type="health" />); + + const textArea = await screen.findByLabelText( + "No, our state used a different measurement period." + ); + fireEvent.click(textArea); + + expect( + screen.getByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeInTheDocument(); + + expect( + screen.getByRole("link", { name: "Measurement Period Table" }) + ).toHaveAttribute( + "href", + "https://www.medicaid.gov/state-resource-center/medicaid-state-technical-assistance/health-home-information-resource-center/quality-reporting/index.html" + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx new file mode 100644 index 0000000000..b5939decfd --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx @@ -0,0 +1,73 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; + +const measurementPeriodTableLinks = { + adult: + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/adult-core-set-reporting-resources/index.html", + child: + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/child-core-set-reporting-resources/index.html", + health: + "https://www.medicaid.gov/state-resource-center/medicaid-state-technical-assistance/health-home-information-resource-center/quality-reporting/index.html", +}; + +interface Props { + type: "adult" | "child" | "health"; +} + +const subTextElement = (link: string) => { + return ( + <CUI.Text mt="2" mb="2"> + Information for each measure is available in the{" "} + <CUI.Link href={link} color="blue" isExternal> + <u>Measurement Period Table</u> + </CUI.Link>{" "} + resource. + </CUI.Text> + ); +}; + +export const DateRange = ({ type }: Props) => { + const register = useCustomRegister<Types.DateRange>(); + const link = measurementPeriodTableLinks[type]; + + return ( + <QMR.CoreQuestionWrapper testid="date-range" label="Date Range"> + <QMR.RadioButton + formLabelProps={{ fontWeight: "bold" }} + label="Did your state adhere to Core Set specifications in defining the measurement period for calculating this measure?" + {...register(DC.MEASUREMENT_PERIOD_CORE_SET)} + subTextElement={subTextElement(link)} + options={[ + { + displayValue: + "Yes, our state adhered to Core Set specifications in defining the measurement period for calculating this measure.", + value: DC.YES, + }, + { + displayValue: "No, our state used a different measurement period.", + value: DC.NO, + children: [ + <> + <CUI.Stack spacing={4} mb="4"> + <CUI.Text> + For all measures, states should report start and end dates + to calculate the denominator. For some measures, the + specifications require a “look-back period” before or after + the measurement period to determine eligibility or + utilization. The measurement period entered in the Start and + End Date fields should not include the “look-back period.” + </CUI.Text> + </CUI.Stack> + + <QMR.DateRange {...register(DC.DATE_RANGE)} /> + </>, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..23c0d14e1c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3452 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test DefinitionOfPopulation componnent (ACS Hybrid) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Definition of Population Included in the Measure" + data-testid="definition-of-population" + > + Definition of Population Included in the Measure + </h2> + <fieldset> + <h2 + class="chakra-heading css-0" + > + Definition of denominator + </h2> + <div + class="css-0" + > + <p + class="chakra-text css-o7rt0f" + > + Please select all populations that are included. For example, if your data include both non-dual Medicaid beneficiaries and Medicare and Medicaid Dual Eligibles, select both: + </p> + <ul + class="css-71ufky" + role="list" + > + <li + class="css-0" + > + Denominator includes Medicaid population + </li> + <li + class="css-0" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </li> + </ul> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator0" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicaidPop" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicaid population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator1" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncCHIP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes CHIP population (e.g. pregnant women) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator2" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicareMedicaidDualEligible" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator3" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncOther" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-8iwwwg" + data-cy="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + for="field-9" + id="field-9-label" + > + If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below: + </label> + <textarea + class="chakra-textarea css-0" + data-cy="ChangeInPopulationExplanation" + id="field-9" + name="ChangeInPopulationExplanation" + /> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + for="field-10" + id="field-10-label" + > + Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineTotalTechSpec_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-12" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-yes" + > + Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-13" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-no" + > + No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + If you are reporting as a hybrid measure, provide the measure eligible population and sample size. + </h2> + <div + class="chakra-form-control prince-input-bottom-spacer css-pb69ky" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="What is the size of the measure-eligible population?" + for="field-14" + id="field-14-label" + > + What is the size of the measure-eligible population? + </label> + <div + class="chakra-input__group css-4302v8" + > + <input + aria-label="HybridMeasurePopulationIncluded" + class="chakra-input css-0" + data-cy="HybridMeasurePopulationIncluded" + data-testid="test-number-input" + id="field-14" + name="HybridMeasurePopulationIncluded" + placeholder="" + type="text" + value="" + /> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="Specify the sample size:" + for="field-15" + id="field-15-label" + > + Specify the sample size: + </label> + <div + class="chakra-input__group css-4302v8" + > + <input + aria-label="HybridMeasureSampleSize" + class="chakra-input css-0" + data-cy="HybridMeasureSampleSize" + data-testid="test-number-input" + id="field-15" + name="HybridMeasureSampleSize" + placeholder="" + type="text" + value="" + /> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + Which delivery systems are represented in the denominator? + </h2> + <p + class="chakra-text css-1drbkg9" + > + Select all delivery systems that apply in your state (must select at least one); for each delivery system selected, enter the percentage of the measure-eligible population represented by that service delivery system. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator0" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="FFS" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Fee-for-Service (FFS) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator1" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="PCCM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Primary Care Case Management (PCCM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator2" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="MCO-PIHP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator3" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ICM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Integrated Care Models (ICM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator4" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator4-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Other" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; + +exports[`Test DefinitionOfPopulation componnent (ACS) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Definition of Population Included in the Measure" + data-testid="definition-of-population" + > + Definition of Population Included in the Measure + </h2> + <fieldset> + <h2 + class="chakra-heading css-0" + > + Definition of denominator + </h2> + <div + class="css-0" + > + <p + class="chakra-text css-o7rt0f" + > + Please select all populations that are included. For example, if your data include both non-dual Medicaid beneficiaries and Medicare and Medicaid Dual Eligibles, select both: + </p> + <ul + class="css-71ufky" + role="list" + > + <li + class="css-0" + > + Denominator includes Medicaid population + </li> + <li + class="css-0" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </li> + </ul> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator0" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicaidPop" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicaid population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator1" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncCHIP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes CHIP population (e.g. pregnant women) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator2" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicareMedicaidDualEligible" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator3" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncOther" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-8iwwwg" + data-cy="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + for="field-2" + id="field-2-label" + > + If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below: + </label> + <textarea + class="chakra-textarea css-0" + data-cy="ChangeInPopulationExplanation" + id="field-2" + name="ChangeInPopulationExplanation" + /> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + for="field-3" + id="field-3-label" + > + Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineTotalTechSpec_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-5" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-yes" + > + Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-6" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-no" + > + No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + Which delivery systems are represented in the denominator? + </h2> + <p + class="chakra-text css-1drbkg9" + > + Select all delivery systems that apply in your state (must select at least one); for each delivery system selected, enter the percentage of the measure-eligible population represented by that service delivery system. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator0" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="FFS" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Fee-for-Service (FFS) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator1" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="PCCM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Primary Care Case Management (PCCM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator2" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="MCO-PIHP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator3" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ICM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Integrated Care Models (ICM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator4" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator4-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Other" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; + +exports[`Test DefinitionOfPopulation componnent (CCS Hybrid) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Definition of Population Included in the Measure" + data-testid="definition-of-population" + > + Definition of Population Included in the Measure + </h2> + <fieldset> + <h2 + class="chakra-heading css-0" + > + Definition of denominator + </h2> + <div + class="css-0" + > + <p + class="chakra-text css-wlea3r" + > + Please select all populations that are included. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-radio-group css-0" + id="DefinitionOfDenominator_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-30" + name="DefinitionOfDenominator" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="DenominatorIncCHIPPop" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DefinitionOfDenominator0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DefinitionOfDenominator-DenominatorIncCHIPPop" + > + Denominator includes CHIP (Title XXI) population only + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-31" + name="DefinitionOfDenominator" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="DenominatorIncMedicaidPop" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DefinitionOfDenominator1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DefinitionOfDenominator-DenominatorIncMedicaidPop" + > + Denominator includes Medicaid (Title XIX) population only + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-32" + name="DefinitionOfDenominator" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="DenominatorIncMedicaidAndCHIPPop" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DefinitionOfDenominator2" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DefinitionOfDenominator-DenominatorIncMedicaidAndCHIPPop" + > + Denominator includes CHIP and Medicaid (Title XIX) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-8iwwwg" + data-cy="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + for="field-33" + id="field-33-label" + > + If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below: + </label> + <textarea + class="chakra-textarea css-0" + data-cy="ChangeInPopulationExplanation" + id="field-33" + name="ChangeInPopulationExplanation" + /> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + for="field-34" + id="field-34-label" + > + Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineTotalTechSpec_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-36" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-yes" + > + Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-37" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-no" + > + No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + If you are reporting as a hybrid measure, provide the measure eligible population and sample size. + </h2> + <div + class="chakra-form-control prince-input-bottom-spacer css-pb69ky" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="What is the size of the measure-eligible population?" + for="field-38" + id="field-38-label" + > + What is the size of the measure-eligible population? + </label> + <div + class="chakra-input__group css-4302v8" + > + <input + aria-label="HybridMeasurePopulationIncluded" + class="chakra-input css-0" + data-cy="HybridMeasurePopulationIncluded" + data-testid="test-number-input" + id="field-38" + name="HybridMeasurePopulationIncluded" + placeholder="" + type="text" + value="" + /> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="Specify the sample size:" + for="field-39" + id="field-39-label" + > + Specify the sample size: + </label> + <div + class="chakra-input__group css-4302v8" + > + <input + aria-label="HybridMeasureSampleSize" + class="chakra-input css-0" + data-cy="HybridMeasureSampleSize" + data-testid="test-number-input" + id="field-39" + name="HybridMeasureSampleSize" + placeholder="" + type="text" + value="" + /> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + Which delivery systems are represented in the denominator? + </h2> + <p + class="chakra-text css-1drbkg9" + > + Select all delivery systems that apply in your state (must select at least one); for each delivery system selected, enter the percentage of the measure-eligible population represented by that service delivery system. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator0" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="FFS" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Fee-for-Service (FFS) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator1" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="PCCM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Primary Care Case Management (PCCM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator2" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="MCO-PIHP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator3" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ICM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Integrated Care Models (ICM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator4" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator4-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Other" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; + +exports[`Test DefinitionOfPopulation componnent (CCS) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Definition of Population Included in the Measure" + data-testid="definition-of-population" + > + Definition of Population Included in the Measure + </h2> + <fieldset> + <h2 + class="chakra-heading css-0" + > + Definition of denominator + </h2> + <div + class="css-0" + > + <p + class="chakra-text css-wlea3r" + > + Please select all populations that are included. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-radio-group css-0" + id="DefinitionOfDenominator_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-19" + name="DefinitionOfDenominator" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="DenominatorIncCHIPPop" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DefinitionOfDenominator0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DefinitionOfDenominator-DenominatorIncCHIPPop" + > + Denominator includes CHIP (Title XXI) population only + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-20" + name="DefinitionOfDenominator" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="DenominatorIncMedicaidPop" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DefinitionOfDenominator1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DefinitionOfDenominator-DenominatorIncMedicaidPop" + > + Denominator includes Medicaid (Title XIX) population only + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-21" + name="DefinitionOfDenominator" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="DenominatorIncMedicaidAndCHIPPop" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DefinitionOfDenominator2" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DefinitionOfDenominator-DenominatorIncMedicaidAndCHIPPop" + > + Denominator includes CHIP and Medicaid (Title XIX) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-8iwwwg" + data-cy="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + for="field-22" + id="field-22-label" + > + If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below: + </label> + <textarea + class="chakra-textarea css-0" + data-cy="ChangeInPopulationExplanation" + id="field-22" + name="ChangeInPopulationExplanation" + /> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + for="field-23" + id="field-23-label" + > + Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineTotalTechSpec_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-25" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-yes" + > + Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-26" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-no" + > + No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + Which delivery systems are represented in the denominator? + </h2> + <p + class="chakra-text css-1drbkg9" + > + Select all delivery systems that apply in your state (must select at least one); for each delivery system selected, enter the percentage of the measure-eligible population represented by that service delivery system. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator0" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="FFS" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Fee-for-Service (FFS) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator1" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="PCCM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Primary Care Case Management (PCCM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator2" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="MCO-PIHP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator3" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ICM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Integrated Care Models (ICM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator4" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator4-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Other" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; + +exports[`Test DefinitionOfPopulation componnent (HHS Hybrid) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Definition of Population Included in the Measure" + data-testid="definition-of-population" + > + Definition of Population Included in the Measure + </h2> + <fieldset> + <h2 + class="chakra-heading css-0" + > + Definition of denominator + </h2> + <div + class="css-0" + > + <p + class="chakra-text css-o7rt0f" + > + Please select all populations that are included. For example, if your data include both non-dual Medicaid enrollees and Medicare and Medicaid Dual Eligibles, select both: + </p> + <ul + class="css-71ufky" + role="list" + > + <li + class="css-0" + > + Denominator includes Medicaid population + </li> + <li + class="css-0" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </li> + </ul> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator0" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicaidPop" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicaid population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator2" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicareMedicaidDualEligible" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator3" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncOther" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-8iwwwg" + data-cy="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + for="field-53" + id="field-53-label" + > + If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below: + </label> + <textarea + class="chakra-textarea css-0" + data-cy="ChangeInPopulationExplanation" + id="field-53" + name="ChangeInPopulationExplanation" + /> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + for="field-54" + id="field-54-label" + > + Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineTotalTechSpec_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-56" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-yes" + > + Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-57" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-no" + > + No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + If you are reporting as a hybrid measure, provide the measure eligible population and sample size. + </h2> + <div + class="chakra-form-control prince-input-bottom-spacer css-pb69ky" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="What is the size of the measure-eligible population?" + for="field-58" + id="field-58-label" + > + What is the size of the measure-eligible population? + </label> + <div + class="chakra-input__group css-4302v8" + > + <input + aria-label="HybridMeasurePopulationIncluded" + class="chakra-input css-0" + data-cy="HybridMeasurePopulationIncluded" + data-testid="test-number-input" + id="field-58" + name="HybridMeasurePopulationIncluded" + placeholder="" + type="text" + value="" + /> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-uvho5g" + data-cy="Specify the sample size:" + for="field-59" + id="field-59-label" + > + Specify the sample size: + </label> + <div + class="chakra-input__group css-4302v8" + > + <input + aria-label="HybridMeasureSampleSize" + class="chakra-input css-0" + data-cy="HybridMeasureSampleSize" + data-testid="test-number-input" + id="field-59" + name="HybridMeasureSampleSize" + placeholder="" + type="text" + value="" + /> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + Which delivery systems are represented in the denominator? + </h2> + <p + class="chakra-text css-1drbkg9" + > + Select all delivery systems that apply in your state (must select at least one); for each delivery system selected, enter the percentage of the measure-eligible population represented by that service delivery system. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator0" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="FFS" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Fee-for-Service (FFS) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator1" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="PCCM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Primary Care Case Management (PCCM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator2" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="MCO-PIHP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator3" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ICM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Integrated Care Models (ICM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator4" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator4-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Other" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Are all Health Home Providers represented in the denominator?" + for="field-61" + id="field-61-label" + > + Are all Health Home Providers represented in the denominator? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineHealthHome_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-63" + name="DenominatorDefineHealthHome" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineHealthHome0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineHealthHome-yes" + > + Yes, all Health Home Providers are represented in the denominator. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-64" + name="DenominatorDefineHealthHome" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineHealthHome1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineHealthHome-no" + > + No, not all Health Home Providers are represented in the denominator. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; + +exports[`Test DefinitionOfPopulation componnent (HHS) Component renders with correct content 1`] = ` +<div> + <div + class="css-pb69ky" + > + <h2 + class="chakra-form__label css-e3y4kl" + data-cy="Definition of Population Included in the Measure" + data-testid="definition-of-population" + > + Definition of Population Included in the Measure + </h2> + <fieldset> + <h2 + class="chakra-heading css-0" + > + Definition of denominator + </h2> + <div + class="css-0" + > + <p + class="chakra-text css-o7rt0f" + > + Please select all populations that are included. For example, if your data include both non-dual Medicaid enrollees and Medicare and Medicaid Dual Eligibles, select both: + </p> + <ul + class="css-71ufky" + role="list" + > + <li + class="css-0" + > + Denominator includes Medicaid population + </li> + <li + class="css-0" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </li> + </ul> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator0" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicaidPop" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicaid population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator2" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncMedicareMedicaidDualEligible" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Denominator includes Medicare and Medicaid Dually-Eligible population + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DefinitionOfDenominator3" + > + <input + class="chakra-checkbox__input" + id="DefinitionOfDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="DenominatorIncOther" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-8iwwwg" + data-cy="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + for="field-42" + id="field-42-label" + > + If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below: + </label> + <textarea + class="chakra-textarea css-0" + data-cy="ChangeInPopulationExplanation" + id="field-42" + name="ChangeInPopulationExplanation" + /> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + for="field-43" + id="field-43-label" + > + Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineTotalTechSpec_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-45" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-yes" + > + Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-46" + name="DenominatorDefineTotalTechSpec" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineTotalTechSpec1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineTotalTechSpec-no" + > + No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-odz94x" + > + <h2 + class="chakra-heading css-1j9oeic" + > + Which delivery systems are represented in the denominator? + </h2> + <p + class="chakra-text css-1drbkg9" + > + Select all delivery systems that apply in your state (must select at least one); for each delivery system selected, enter the percentage of the measure-eligible population represented by that service delivery system. + </p> + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator0" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator0-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="FFS" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Fee-for-Service (FFS) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator1" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator1-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="PCCM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Primary Care Case Management (PCCM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator2" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator2-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="MCO-PIHP" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator3" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator3-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="ICM" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Integrated Care Models (ICM) + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-checkbox css-1uiwwan" + data-cy="DeliverySysRepresentationDenominator4" + > + <input + class="chakra-checkbox__input" + id="DeliverySysRepresentationDenominator4-checkbox" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="checkbox" + value="Other" + /> + <span + aria-hidden="true" + class="chakra-checkbox__control css-dnty2r" + /> + <span + class="chakra-checkbox__label css-1oeb2oe" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + > + Other + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + <div + class="css-s98few" + > + <div + class="chakra-form-control prince-input-bottom-spacer css-0" + role="group" + > + <label + class="chakra-form__label css-9vgk4i" + data-cy="Are all Health Home Providers represented in the denominator?" + for="field-48" + id="field-48-label" + > + Are all Health Home Providers represented in the denominator? + </label> + <div + class="chakra-radio-group css-0" + id="DenominatorDefineHealthHome_radiogroup" + role="radiogroup" + > + <div + class="chakra-stack css-n21gh5" + > + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-50" + name="DenominatorDefineHealthHome" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="yes" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineHealthHome0" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineHealthHome-yes" + > + Yes, all Health Home Providers are represented in the denominator. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + <div + class="prince-option-label-wrapper css-79elbk" + > + <label + class="chakra-radio css-1pw4d56" + > + <input + class="chakra-radio__input" + id="radio-51" + name="DenominatorDefineHealthHome" + style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;" + type="radio" + value="no" + /> + <span + aria-hidden="true" + class="chakra-radio__control css-ssalds" + data-cy="DenominatorDefineHealthHome1" + /> + <span + class="chakra-radio__label css-1y8kf23" + > + <p + class="chakra-text prince-option-label-text css-vjer7o" + id="DenominatorDefineHealthHome-no" + > + No, not all Health Home Providers are represented in the denominator. + </p> + </span> + </label> + <div + class="chakra-collapse" + style="overflow: hidden; display: none; opacity: 0; height: 0px;" + /> + </div> + </div> + </div> + <div + class="css-k008qs" + > + <div + class="css-17xejub" + /> + </div> + </div> + </div> + </fieldset> + </div> +</div> +`; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.test.tsx new file mode 100644 index 0000000000..a31719f02b --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.test.tsx @@ -0,0 +1,37 @@ +import { DefinitionOfPopulation } from "."; +import { DefinitionOfPopulationData as DOP } from "utils/testUtils/testFormData"; +import { testSnapshot } from "utils/testUtils/testSnapshot"; + +describe("Test DefinitionOfPopulation componnent", () => { + it("(ACS) Component renders with correct content", () => { + const component = <DefinitionOfPopulation />; + testSnapshot({ component, defaultValues: DOP.adult }); + }); + + it("(ACS Hybrid) Component renders with correct content", () => { + const component = <DefinitionOfPopulation hybridMeasure />; + testSnapshot({ component, defaultValues: DOP.adultHybrid }); + }); + + it("(CCS) Component renders with correct content", () => { + const component = <DefinitionOfPopulation childMeasure />; + testSnapshot({ component, defaultValues: DOP.child }); + }); + + it("(CCS Hybrid) Component renders with correct content", () => { + const component = <DefinitionOfPopulation childMeasure hybridMeasure />; + testSnapshot({ component, defaultValues: DOP.childHybrid }); + }); + + it("(HHS) Component renders with correct content", () => { + const component = <DefinitionOfPopulation healthHomeMeasure />; + testSnapshot({ component, defaultValues: DOP.healthHome }); + }); + + it("(HHS Hybrid) Component renders with correct content", () => { + const component = ( + <DefinitionOfPopulation healthHomeMeasure hybridMeasure /> + ); + testSnapshot({ component, defaultValues: DOP.healthHomeHybrid }); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.tsx new file mode 100644 index 0000000000..9d035c8f12 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.tsx @@ -0,0 +1,519 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { allPositiveIntegers, percentageAllowOneDecimalMax } from "utils"; +import * as DC from "dataConstants"; + +interface Props { + childMeasure?: boolean; + hybridMeasure?: boolean; + populationSampleSize?: boolean; + healthHomeMeasure?: boolean; +} + +export const DefinitionOfPopulation = ({ + childMeasure, + populationSampleSize, + hybridMeasure, + healthHomeMeasure, +}: Props) => { + const register = useCustomRegister<Types.DefinitionOfPopulation>(); + + return ( + <QMR.CoreQuestionWrapper + testid="definition-of-population" + label="Definition of Population Included in the Measure" + > + <CUI.Heading size="sm" as="h2"> + Definition of denominator + </CUI.Heading> + {!childMeasure && ( + <CUI.Box> + <CUI.Text mt="3"> + {`Please select all populations that are included. For example, if your data include both non-dual Medicaid ${ + healthHomeMeasure ? "enrollees" : "beneficiaries" + } and Medicare and Medicaid Dual Eligibles, select both:`} + </CUI.Text> + <CUI.UnorderedList m="5" ml="10"> + <CUI.ListItem> + Denominator includes Medicaid population + </CUI.ListItem> + <CUI.ListItem> + Denominator includes Medicare and Medicaid Dually-Eligible + population + </CUI.ListItem> + </CUI.UnorderedList> + + <QMR.Checkbox + {...register(DC.DEFINITION_OF_DENOMINATOR)} + options={[ + { + displayValue: "Denominator includes Medicaid population", + value: DC.DENOMINATOR_INC_MEDICAID_POP, + }, + { + displayValue: + "Denominator includes CHIP population (e.g. pregnant women)", + value: DC.DENOMINATOR_INC_CHIP, + isHealthHome: healthHomeMeasure, + }, + { + displayValue: + "Denominator includes Medicare and Medicaid Dually-Eligible population", + value: DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE, + }, + { + displayValue: "Other", + value: DC.DENOMINATOR_INC_OTHER, + children: [ + <QMR.TextArea + formLabelProps={{ fontWeight: "400" }} + label={ + <> + Define the other denominator population ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + {...register(DC.DEFINITION_DENOMINATOR_OTHER)} + />, + ], + }, + ]} + /> + </CUI.Box> + )} + {childMeasure && ( + <CUI.Box> + <CUI.Text mb="2"> + Please select all populations that are included. + </CUI.Text> + <QMR.RadioButton + {...register(DC.DEFINITION_OF_DENOMINATOR)} + valueAsArray + options={[ + { + displayValue: + "Denominator includes CHIP (Title XXI) population only", + value: "DenominatorIncCHIPPop", + }, + { + displayValue: + "Denominator includes Medicaid (Title XIX) population only", + value: "DenominatorIncMedicaidPop", + }, + { + displayValue: + "Denominator includes CHIP and Medicaid (Title XIX)", + value: "DenominatorIncMedicaidAndCHIPPop", + }, + ]} + /> + </CUI.Box> + )} + <CUI.Box my="5"> + <QMR.TextArea + formLabelProps={{ fontWeight: "400" }} + label="If this measure has been reported by the state previously and there has been a change in the included population, please provide any available context below:" + {...register(DC.CHANGE_IN_POP_EXPLANATION)} + /> + </CUI.Box> + <CUI.Box my="5"> + <QMR.RadioButton + formLabelProps={{ fontWeight: "600" }} + label="Does this denominator represent your total measure-eligible population as defined by the Technical Specifications for this measure?" + {...register(DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC)} + options={[ + { + displayValue: + "Yes, this denominator represents the total measure-eligible population as defined by the Technical Specifications for this measure.", + value: DC.YES, + }, + { + displayValue: + "No, this denominator does not represent the total measure-eligible population as defined by the Technical Specifications for this measure.", + value: DC.NO, + children: [ + <QMR.TextArea + {...register( + DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC_NO_EXPLAIN + )} + label={ + <> + Explain which populations are excluded and why ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + />, + <CUI.Box mt="10" key="DenominatorDefineTotalTechSpec-No-Size"> + <QMR.NumberInput + mask={allPositiveIntegers} + {...register(DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC_NO_SIZE)} + label="Specify the size of the population excluded:" + /> + </CUI.Box>, + ], + }, + ]} + /> + </CUI.Box> + {hybridMeasure && ( + <CUI.Box mt="5"> + <CUI.Heading size="sm" as="h2" my="2"> + If you are reporting as a hybrid measure, provide the measure + eligible population and sample size. + </CUI.Heading> + <QMR.NumberInput + {...register(DC.HYBRID_MEASURE_POPULATION_INCLUDED)} + formControlProps={{ my: "4" }} + mask={allPositiveIntegers} + label="What is the size of the measure-eligible population?" + /> + <QMR.NumberInput + {...register(DC.HYBRID_MEASURE_SAMPLE_SIZE)} + mask={allPositiveIntegers} + label="Specify the sample size:" + /> + </CUI.Box> + )} + {populationSampleSize && ( + <CUI.Box mt="5"> + <QMR.NumberInput + {...register(DC.HYBRID_MEASURE_POPULATION_INCLUDED)} + formControlProps={{ my: "4" }} + mask={allPositiveIntegers} + label="What is the size of the measure-eligible population?" + /> + <QMR.NumberInput + {...register(DC.HYBRID_MEASURE_SAMPLE_SIZE)} + mask={allPositiveIntegers} + label="Specify the sample size:" + /> + </CUI.Box> + )} + <CUI.Box mt="5"> + <CUI.Heading size="sm" as="h2" my="2"> + {"Which delivery systems are represented in the denominator?"} + </CUI.Heading> + <CUI.Text pb="2"> + Select all delivery systems that apply in your state (must select at + least one); for each delivery system selected, enter the percentage of + the measure-eligible population represented by that service delivery + system. + </CUI.Text> + + <QMR.Checkbox + formLabelProps={{ fontWeight: "400" }} + {...register(DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR)} + options={[ + { + displayValue: "Fee-for-Service (FFS)", + value: DC.FFS, + children: [ + <QMR.RadioButton + {...register(DC.DELIVERY_SYS_FFS)} + formLabelProps={{ fontWeight: "400" }} + label="Is all of your measure-eligible Fee-for-Service (FFS) population included in this measure?" + options={[ + { + displayValue: + "Yes, all of our measure-eligible Fee-for-Service (FFS) population are included in this measure.", + value: DC.YES, + }, + { + displayValue: + "No, not all of our measure-eligible Fee-for-Service (FFS) population are included in this measure.", + value: DC.NO, + children: [ + <QMR.NumberInput + {...register(DC.DELIVERY_SYS_FFS_NO_PERCENT)} + formLabelProps={{ fontWeight: "400" }} + label="What percent of your measure-eligible Fee-for-Service (FFS) population are included in the measure?" + renderHelperTextAbove + helperText="The percentage provided here should represent the + percentage of the denominator population(s) included + in the measure (i.e., Medicaid, CHIP, etc.) that + receives items/services through the selected delivery + system. For example, if the population included in the + reported data represents all managed care enrollees + and half of your state’s fee-for-service enrollees, + select managed care, and select fee-for-service and + enter 50." + displayPercent + mask={percentageAllowOneDecimalMax} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Primary Care Case Management (PCCM)", + value: DC.PCCM, + children: [ + <QMR.RadioButton + {...register(DC.DELIVERY_SYS_PCCM)} + formLabelProps={{ fontWeight: "400" }} + label="Is all of your measure-eligible Primary Care Case Management (PCCM) population included in this measure?" + options={[ + { + displayValue: + "Yes, all of our measure-eligible Primary Care Case Management (PCCM) population are included in this measure.", + value: DC.YES, + }, + { + displayValue: + "No, not all of our measure-eligible Primary Care Case Management (PCCM) population are included in this measure.", + value: DC.NO, + children: [ + <QMR.NumberInput + {...register(DC.DELIVERY_SYS_PCCM_NO_PERCENT)} + displayPercent + renderHelperTextAbove + helperText="The percentage provided here should represent the + percentage of the denominator population(s) included + in the measure (i.e., Medicaid, CHIP, etc.) that + receives items/services through the selected + delivery system. For example, if the population + included in the reported data represents all managed + care enrollees and half of your state’s + fee-for-service enrollees, select managed care, and + select fee-for-service and enter 50." + mask={percentageAllowOneDecimalMax} + formLabelProps={{ fontWeight: "400" }} + label="What percent of your measure-eligible Primary Care Case Management (PCCM) population are included in the measure?" + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: + "Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP)", + value: DC.MCO_PIHP, + children: [ + <CUI.Box py="5" key="DeliverySys-MCO_PIHP-NumberOfPlans"> + <QMR.NumberInput + formLabelProps={{ fontWeight: "400" }} + mask={allPositiveIntegers} + label="What is the number of Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) plans that are included in the reported data?" + {...register(DC.DELIVERY_SYS_MCO_PIHP_NUM_PLANS)} + /> + </CUI.Box>, + <CUI.Box pt="5" key="DeliverySys-MCO_PIHP"> + <QMR.RadioButton + {...register(DC.DELIVERY_SYS_MCO_PIHP)} + formLabelProps={{ fontWeight: "400" }} + label="Is all of your measure-eligible Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) population included in this measure?" + options={[ + { + displayValue: + "Yes, all of our measure-eligible Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) population are included in this measure.", + value: DC.YES, + }, + { + displayValue: + "No, not all of our measure-eligible Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) population are included in this measure.", + value: DC.NO, + children: [ + <CUI.Text mb="5" key="AdditionalMCOIncludedText"> + { + "What percent of your measure-eligible Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) population are" + } + <CUI.Text as="i" fontWeight="600"> + {" included "} + </CUI.Text> + {"in the measure?"} + </CUI.Text>, + <QMR.NumberInput + displayPercent + mask={percentageAllowOneDecimalMax} + renderHelperTextAbove + helperText="The percentage provided here should represent the percentage of the denominator population(s) included in the measure (i.e., Medicaid, CHIP, etc.) that receives items/services through the selected delivery system. For example, if the population included in the reported data represents all managed care enrollees and half of your state’s fee-for-service enrollees, select managed care, and select fee-for-service and enter 50." + {...register(DC.DELIVERY_SYS_MCO_PIHP_NO_INC)} + />, + <CUI.Text my="5" key="AdditionalMCOExcludedText"> + {" "} + { + "How many of your measure-eligible Managed Care Organization/Pre-paid Inpatient Health Plan (MCO/PIHP) plans are" + } + <CUI.Text as="i" fontWeight="600"> + {" excluded "} + </CUI.Text> + { + "from the measure? If none are excluded, please enter zero." + } + </CUI.Text>, + <QMR.NumberInput + mask={allPositiveIntegers} + {...register(DC.DELIVERY_SYS_MCO_PIHP_NO_EXCL)} + />, + ], + }, + ]} + /> + </CUI.Box>, + ], + }, + { + displayValue: "Integrated Care Models (ICM)", + value: DC.ICM, + children: [ + <QMR.RadioButton + formLabelProps={{ fontWeight: "400" }} + label="Is all of your measure-eligible Integrated Care Models (ICM) population included in this measure?" + {...register(DC.DELIVERY_SYS_ICM)} + options={[ + { + displayValue: + "Yes, all of our measure-eligible Integrated Care Models (ICM) population are included in this measure.", + value: DC.YES, + }, + { + displayValue: + "No, not all of our measure-eligible Integrated Care Models (ICM) population are included in this measure.", + value: DC.NO, + children: [ + <CUI.Text mb="5" key="AdditionalICMIncludedText"> + { + "What percent of your measure-eligible Integrated Care Models (ICM) population are" + } + <CUI.Text as="i" fontWeight="600"> + {" included "} + </CUI.Text> + {"in the measure?"} + </CUI.Text>, + <QMR.NumberInput + displayPercent + renderHelperTextAbove + helperText="The percentage provided here should represent the + percentage of the denominator population(s) included + in the measure (i.e., Medicaid, CHIP, etc.) that + receives items/services through the selected + delivery system. For example, if the population + included in the reported data represents all managed + care enrollees and half of your state’s + fee-for-service enrollees, select managed care, and + select fee-for-service and enter 50." + mask={percentageAllowOneDecimalMax} + formLabelProps={{ fontWeight: "400" }} + {...register(DC.DELIVERY_SYS_ICM_NO_PERCENT)} + />, + <CUI.Box py="5" key="AdditionalICMText"> + <CUI.Text my="5" key="AdditionalMCOExcludedText"> + {" "} + { + "How many of your measure-eligible Integrated Care Models (ICM) plans are" + } + <CUI.Text as="i" fontWeight="600"> + {" excluded "} + </CUI.Text> + { + "from the measure? If none are excluded, please enter zero." + } + </CUI.Text> + <QMR.NumberInput + mask={allPositiveIntegers} + formLabelProps={{ fontWeight: "400" }} + {...register(DC.DELIVERY_SYS_ICM_NO_POP)} + /> + </CUI.Box>, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Other", + value: DC.OTHER, + children: [ + <CUI.Box pb="5" key="DeliverySys-Other"> + <QMR.TextArea + formLabelProps={{ fontWeight: "400" }} + label={ + <> + Describe the Other Delivery System represented in the + denominator ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + {...register(DC.DELIVERY_SYS_OTHER)} + /> + </CUI.Box>, + <CUI.Box py="5" key="DeliverySys-Other-Percent"> + <QMR.NumberInput + displayPercent + renderHelperTextAbove + helperText="The percentage provided here should represent the percentage + of the denominator population(s) included in the measure + (i.e., Medicaid, CHIP, etc.) that receives items/services + through the selected delivery system. For example, if the + population included in the reported data represents all + managed care enrollees and half of your state’s + fee-for-service enrollees, select managed care, and select + fee-for-service and enter 50." + mask={percentageAllowOneDecimalMax} + formLabelProps={{ fontWeight: "400" }} + label="Percentage of total other population represented in data reported:" + {...register(DC.DELIVERY_SYS_OTHER_PERCENT)} + /> + </CUI.Box>, + <CUI.Box py="5" key="DeliverySys-Other-NumberOfHealthPlans"> + <QMR.NumberInput + mask={allPositiveIntegers} + formLabelProps={{ fontWeight: "400" }} + label="If applicable, list the number of Health Plans represented:" + {...register(DC.DELIVERY_SYS_OTHER_NUM_HEALTH_PLANS)} + /> + </CUI.Box>, + ], + }, + ]} + /> + </CUI.Box> + {healthHomeMeasure && ( + <CUI.Box my="5"> + <QMR.RadioButton + formLabelProps={{ fontWeight: "600" }} + label="Are all Health Home Providers represented in the denominator?" + {...register(DC.DENOMINATOR_DEFINE_HEALTH_HOME)} + options={[ + { + displayValue: + "Yes, all Health Home Providers are represented in the denominator.", + value: DC.YES, + }, + { + displayValue: + "No, not all Health Home Providers are represented in the denominator.", + value: DC.NO, + children: [ + <QMR.TextArea + {...register(DC.DENOMINATOR_DEFINE_HEALTH_HOME_NO_EXPLAIN)} + label="Explain why all Health Home Providers are not represented in the denominator:" + />, + ], + }, + ]} + /> + </CUI.Box> + )} + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DeviationFromMeasureSpecification/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DeviationFromMeasureSpecification/index.tsx new file mode 100644 index 0000000000..233802d71f --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DeviationFromMeasureSpecification/index.tsx @@ -0,0 +1,51 @@ +import * as QMR from "components"; +import * as Types from "../types"; +import * as DC from "dataConstants"; +import { useCustomRegister } from "hooks/useCustomRegister"; + +export const DeviationFromMeasureSpec = () => { + const register = useCustomRegister<Types.DeviationFromMeasureSpecification>(); + + return ( + <QMR.CoreQuestionWrapper + label="Deviations from Measure Specifications" + testid="deviation-from-measure-specification" + > + <QMR.RadioButton + renderHelperTextAbove + {...register(DC.DID_CALCS_DEVIATE)} + formLabelProps={{ fontWeight: 600 }} + label="Did your calculation of the measure deviate from the measure specification in any way?" + helperText="For example: deviation from measure specification might include different methodology, timeframe, or reported age groups." + options={[ + { + displayValue: + "Yes, the calculation of the measure deviates from the measure specification.", + value: DC.YES, + children: [ + <QMR.TextArea + {...register(DC.DEVIATION_REASON)} + label={ + <> + Explain the deviation(s) ( + <em> + text in this field is included in publicly-reported + state-specific comments + </em> + ): + </> + } + formLabelProps={{ fontWeight: 400 }} + />, + ], + }, + { + displayValue: + "No, the calculation of the measure does not deviate from the measure specification in any way.", + value: DC.NO, + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.test.tsx new file mode 100644 index 0000000000..e46bcee3d8 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.test.tsx @@ -0,0 +1,122 @@ +import fireEvent from "@testing-library/user-event"; +import { MeasurementSpecification } from "."; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { screen } from "@testing-library/react"; + +describe("MeasurementSpecification component", () => { + it("renders the component", async () => { + renderWithHookForm(<MeasurementSpecification type="CMS" />); + + expect(screen.getByText("Measurement Specification")).toBeInTheDocument(); + + expect( + screen.getByText("Centers for Medicare & Medicaid Services (CMS)") + ).toBeInTheDocument(); + + expect(screen.getByText("Other")).toBeInTheDocument(); + + const otherRadio = await screen.getByLabelText("Other"); + fireEvent.click(otherRadio); + + const textArea = await screen.findByLabelText( + "Describe the specifications that were used to calculate the measure and explain how they deviated from Core Set specifications:" + ); + + expect(textArea).toBeInTheDocument(); + + const testText = "This is test text for TextArea"; + fireEvent.type(textArea, testText); + expect(textArea).toHaveDisplayValue(testText); + + expect( + screen.getByText( + "If you need additional space to describe your state's methodology, please attach further documentation below." + ) + ).toBeInTheDocument(); + }); +}); + +interface Props { + type: + | "ADA-DQA" + | "AHRQ" + | "AHRQ-NCQA" + | "CDC" + | "CMS" + | "HEDIS" + | "HRSA" + | "JOINT" + | "NCQA" + | "OHSU" + | "OPA" + | "PQA"; +} + +const specifications: { + [name: string]: { propType: Props; displayValue: string }; +} = { + "ADA-DQA": { + propType: { type: "ADA-DQA" }, + displayValue: + "American Dental Association/Dental Quality Alliance (ADA/DQA)", + }, + AHRQ: { + propType: { type: "AHRQ" }, + displayValue: "Agency for Healthcare Research and Quality (AHRQ)", + }, + "AHRQ-NCQA": { + propType: { type: "AHRQ-NCQA" }, + displayValue: + "Agency for Healthcare Research and Quality (AHRQ) (survey instrument) and National Committee for Quality Assurance (survey administrative protocol)", + }, + CDC: { + propType: { type: "CDC" }, + displayValue: "Centers for Disease Contol and Prevention (CDC)", + }, + CMS: { + propType: { type: "CMS" }, + displayValue: "Centers for Medicare & Medicaid Services (CMS)", + }, + HEDIS: { + propType: { type: "HEDIS" }, + displayValue: + "National Committee for Quality Assurance (NCQA)/Healthcare Effectiveness Data and Information Set (HEDIS)", + }, + HRSA: { + propType: { type: "HRSA" }, + displayValue: "Health Resources and Services Administration (HRSA)", + }, + JOINT: { + propType: { type: "JOINT" }, + displayValue: "The Joint Commission", + }, + NCQA: { + propType: { type: "NCQA" }, + displayValue: "National Committee for Quality Assurance (NCQA)", + }, + OHSU: { + propType: { type: "OHSU" }, + displayValue: "Oregon Health and Science University (OHSU)", + }, + OPA: { + propType: { type: "OPA" }, + displayValue: "HHS Office of Population Affairs (OPA)", + }, + PQA: { + propType: { type: "PQA" }, + displayValue: "Pharmacy Quality Alliance (PQA)", + }, +}; + +describe("all specification types", () => { + for (const spec in specifications) { + it(`renders ${spec} specification type correctly`, async () => { + renderWithHookForm( + <MeasurementSpecification type={specifications[spec].propType.type} /> + ); + expect( + screen.getByText(specifications[spec].displayValue) + ).toBeInTheDocument(); + }); + } +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.tsx new file mode 100644 index 0000000000..759f35e455 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.tsx @@ -0,0 +1,143 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; + +const HEDISChildren = () => { + const register = useCustomRegister<Types.MeasurementSpecification>(); + + return ( + <> + <QMR.Select + {...register(DC.MEASUREMENT_SPECIFICATION_HEDIS)} + label="Specify the version of HEDIS measurement year used:" + placeholder="Select option" + options={[ + { + displayValue: "HEDIS MY 2023 (FFY 2024 Core Set Reporting)", + value: DC.HEDIS_MY_2023, + }, + { + displayValue: "HEDIS MY 2022 (FFY 2023 Core Set Reporting)", + value: DC.HEDIS_MY_2022, + }, + { + displayValue: "HEDIS MY 2021 (FFY 2022 Core Set Reporting)", + value: DC.HEDIS_MY_2021, + }, + { + displayValue: "HEDIS MY 2020 (FFY 2021 Core Set Reporting)", + value: DC.HEDIS_MY_2020, + }, + ]} + /> + </> + ); +}; + +interface Props { + type: + | "ADA-DQA" + | "AHRQ" + | "AHRQ-NCQA" + | "CDC" + | "CMS" + | "HEDIS" + | "HRSA" + | "JOINT" + | "NCQA" + | "OHSU" + | "OPA" + | "PQA"; +} + +const specifications = { + "ADA-DQA": { + displayValue: + "American Dental Association/Dental Quality Alliance (ADA/DQA)", + value: DC.ADA_DQA, + }, + AHRQ: { + displayValue: "Agency for Healthcare Research and Quality (AHRQ)", + value: DC.AHRQ, + }, + "AHRQ-NCQA": { + displayValue: + "Agency for Healthcare Research and Quality (AHRQ) (survey instrument) and National Committee for Quality Assurance (survey administrative protocol)", + value: DC.AHRQ_NCQA, + }, + CDC: { + displayValue: "Centers for Disease Contol and Prevention (CDC)", + value: DC.CDC, + }, + CMS: { + displayValue: "Centers for Medicare & Medicaid Services (CMS)", + value: DC.CMS, + }, + HEDIS: { + displayValue: + "National Committee for Quality Assurance (NCQA)/Healthcare Effectiveness Data and Information Set (HEDIS)", + value: DC.NCQA, + children: [<HEDISChildren key="HEDIS-Child" />], + }, + HRSA: { + displayValue: "Health Resources and Services Administration (HRSA)", + value: DC.HRSA, + }, + JOINT: { + displayValue: "The Joint Commission", + value: DC.JOINT_COMMISSION, + }, + NCQA: { + displayValue: "National Committee for Quality Assurance (NCQA)", + value: DC.NCQA, + }, + OHSU: { + displayValue: "Oregon Health and Science University (OHSU)", + value: DC.OHSU, + }, + OPA: { + displayValue: "HHS Office of Population Affairs (OPA)", + value: DC.OPA, + }, + PQA: { + displayValue: "Pharmacy Quality Alliance (PQA)", + value: DC.PQA, + }, +}; + +export const MeasurementSpecification = ({ type }: Props) => { + const register = useCustomRegister<Types.MeasurementSpecification>(); + + return ( + <QMR.CoreQuestionWrapper + testid="measurement-specification" + label="Measurement Specification" + > + <div data-cy="measurement-specification-options"> + <QMR.RadioButton + {...register(DC.MEASUREMENT_SPECIFICATION)} + options={[ + specifications[type], + { + displayValue: "Other", + value: DC.OTHER, + children: [ + <QMR.TextArea + textAreaProps={{ marginBottom: "10" }} + {...register(DC.MEASUREMENT_SPEC_OMS_DESCRIPTION)} + label="Describe the specifications that were used to calculate the measure and explain how they deviated from Core Set specifications:" + key={DC.MEASUREMENT_SPEC_OMS_DESCRIPTION} + />, + <QMR.Upload + label="If you need additional space to describe your state's methodology, please attach further documentation below." + {...register(DC.MEASUREMENT_SPEC_OMS_DESCRIPTION_UPLOAD)} + />, + ], + }, + ]} + /> + </div> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.test.tsx new file mode 100644 index 0000000000..cf01b0c714 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.test.tsx @@ -0,0 +1,17 @@ +import { NotCollectingOMS } from "."; +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; + +describe("NotCollectingOMS component", () => { + beforeEach(() => { + renderWithHookForm(<NotCollectingOMS />); + }); + + it("component renders", () => { + expect( + screen.getByText( + "CMS is not collecting stratified data for this measure for FFY 2024 Core Set Reporting." + ) + ).toBeInTheDocument(); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.tsx new file mode 100644 index 0000000000..251317ff31 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.tsx @@ -0,0 +1,16 @@ +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; + +export const NotCollectingOMS = () => { + return ( + <QMR.CoreQuestionWrapper + testid="OMS" + label="Optional Measure Stratification" + > + <CUI.Text> + CMS is not collecting stratified data for this measure for FFY 2024 Core + Set Reporting. + </CUI.Text> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/additionalCategory.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/additionalCategory.tsx new file mode 100644 index 0000000000..a40347b2ed --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/additionalCategory.tsx @@ -0,0 +1,67 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useFieldArray, useFormContext } from "react-hook-form"; + +import { AddAnotherButton, SubCatSection } from "./subCatClassification"; +import { NDRSets } from "./ndrSets"; + +interface AdditonalCategoryProps { + /** name for react-hook-form registration */ + name: string; + /** name of parent category for additional text */ + parentName: string; + /** should the additional categories have a subCat option? */ + flagSubCat: boolean; +} + +/** + * Additional [Race/Sex/Language/Etc] Category Section + */ +export const AddAnotherSection = ({ + name, + parentName, + flagSubCat, +}: AdditonalCategoryProps) => { + const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + name: `${name}.additionalSelections`, + control, + shouldUnregister: true, + }); + + return ( + <CUI.Box key={`${name}.additionalCategoriesWrapper`}> + {fields.map((field: any, idx: number) => ( + <QMR.DeleteWrapper + allowDeletion + onDelete={() => remove(idx)} + key={field.id} + > + <CUI.Text size={"2xl"} my="3">{`Additional ${parentName}`}</CUI.Text> + <QMR.QuestionChild show key={field.id}> + <CUI.Stack spacing={"5"}> + <QMR.TextInput + name={`${name}.additionalSelections.${idx}.description`} + label={`Define the Additional ${parentName}`} + rules={{ required: true }} + /> + <NDRSets name={`${name}.additionalSelections.${idx}.rateData`} /> + </CUI.Stack> + {flagSubCat && ( + <SubCatSection + name={`${name}.additionalSelections.${idx}`} + key={`${name}.additionalSelections.${idx}`} + /> + )} + </QMR.QuestionChild> + </QMR.DeleteWrapper> + ))} + <AddAnotherButton + onClick={() => append({})} + additionalText={parentName} + key={`${name}.additionalCategoriesButton`} + testid={`${name}.additionalCategoriesButton`} + /> + </CUI.Box> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/context.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/context.ts new file mode 100644 index 0000000000..6244f3cff2 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/context.ts @@ -0,0 +1,56 @@ +import { createContext, useContext } from "react"; +import * as Types from "../types"; +import { ndrFormula } from "types"; +import { LabelData } from "utils"; + +export type ComponentFlagType = "DEFAULT" | "AIF" | "IU" | "PCR"; + +interface ContextProps { + OPM?: Types.OtherRatesFields[]; + performanceMeasureArray?: Types.RateFields[][]; + IUHHPerformanceMeasureArray?: Types.complexRateFields[][]; + AIFHHPerformanceMeasureArray?: Types.complexRateFields[][]; + rateReadOnly?: boolean; + calcTotal?: boolean; + categories: LabelData[]; + qualifiers: LabelData[]; + measureName?: string; + inputFieldNames?: LabelData[]; + ndrFormulas?: ndrFormula[]; + rateMultiplicationValue?: number; + customMask?: RegExp; + allowNumeratorGreaterThanDenominator?: boolean; + numberOfDecimals: number; + componentFlag?: ComponentFlagType; + customNumeratorLabel?: string; + customDenominatorLabel?: string; + customRateLabel?: string; + customPrompt?: string; + rateCalculation?: RateFormula; +} + +const PerformanceMeasureContext = createContext<ContextProps>({ + OPM: [], + performanceMeasureArray: [[]], + rateReadOnly: true, + calcTotal: false, + categories: [], + qualifiers: [], + measureName: "", + inputFieldNames: [], + ndrFormulas: [], + rateMultiplicationValue: undefined, + customMask: undefined, + allowNumeratorGreaterThanDenominator: false, + numberOfDecimals: 1, + componentFlag: "DEFAULT", + customNumeratorLabel: "Numerator", + customDenominatorLabel: "Denominator", + customRateLabel: "Rate", + customPrompt: undefined, +}); + +export const usePerformanceMeasureContext = () => + useContext(PerformanceMeasureContext); + +export const PerformanceMeasureProvider = PerformanceMeasureContext.Provider; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data.ts new file mode 100644 index 0000000000..68c325e54a --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data.ts @@ -0,0 +1,139 @@ +export interface OmsNode { + /** id value for option */ + id: string; + /** displayName value for option*/ + label: string; + /** should additional category render? */ + addMore?: boolean; + /** should this node have a subCatOption? */ + flagSubCat?: boolean; + /** additional checkbox options below this node */ + options?: OmsNode[]; + /** should additional category values have subCatOptions? */ + addMoreSubCatFlag?: boolean; + /** should the aggregate question have a diffrent title than label? */ + aggregateTitle?: string; +} + +export const OMSData = (): OmsNode[] => { + const data: OmsNode[] = [ + { + id: "3dpUZu", + label: "Race", + options: [ + { + id: "ll9YP8", + flagSubCat: false, + label: "American Indian or Alaska Native", + }, + { + id: "RKWD6S", + label: "Asian", + options: [ + { id: "e68Cj8", flagSubCat: false, label: "Asian Indian" }, + { id: "gCxXhf", flagSubCat: false, label: "Chinese" }, + { id: "i2fIgY", flagSubCat: false, label: "Filipino" }, + { id: "WxWvJ8", flagSubCat: false, label: "Japanese" }, + { id: "78IBC7", flagSubCat: false, label: "Korean" }, + { id: "GPgIYd", flagSubCat: false, label: "Vietnamese" }, + { id: "5v7GMy", flagSubCat: false, label: "Other Asian" }, + ], + flagSubCat: true, + }, + { + id: "6NrBa5", + flagSubCat: false, + label: "Black or African American", + }, + { + id: "Qu4kZK", + label: "Native Hawaiian or Other Pacific Islander", + options: [ + { + id: "GDJJx4", + flagSubCat: false, + label: "Native Hawaiian", + }, + { + id: "LgwPP1", + flagSubCat: false, + label: "Guamanian or Chamorro", + }, + { id: "LTJcrA", flagSubCat: false, label: "Samoan" }, + { + id: "Ri1PWc", + flagSubCat: false, + label: "Other Pacific Islander", + }, + ], + flagSubCat: true, + }, + { id: "szjphG", flagSubCat: false, label: "White" }, + { + id: "OmjSBa", + flagSubCat: false, + label: "Two or More Races", + }, + { id: "uZTnKi", flagSubCat: false, label: "Some Other Race" }, + { + id: "nN7fNs", + flagSubCat: false, + label: "Missing or not reported", + }, + ], + addMore: true, + addMoreSubCatFlag: false, + }, + { + id: "elakUl", + label: "Ethnicity", + options: [ + { + id: "51ZZEh", + label: "Not of Hispanic, Latino/a, or Spanish origin", + }, + { + id: "BFeF4k", + label: "Hispanic, Latino/a, or Spanish origin", + aggregateTitle: "Hispanic, Latino/a, or Spanish origin", + options: [ + { + id: "ZP5n08", + label: "Mexican, Mexican American, Chicano/a", + }, + { id: "4cq5P4", label: "Puerto Rican" }, + { id: "XCzK5D", label: "Cuban" }, + { + id: "kHsTcd", + label: "Another Hispanic, Latino/a or Spanish origin", + }, + ], + }, + { id: "WBIqgU", label: "Missing or not reported" }, + ], + addMore: true, + }, + { + id: "O8BrOa", + label: "Sex", + options: [ + { id: "KRwFRN", label: "Male" }, + { id: "8M0aAo", label: "Female" }, + { id: "BnVURC", label: "Missing or not reported" }, + ], + addMore: true, + }, + { + id: "afMbTr", + label: "Geography", + options: [ + { id: "r07WKZ", label: "Urban" }, + { id: "bbaSzG", label: "Rural" }, + { id: "i11ZUj", label: "Missing or not reported" }, + ], + addMore: true, + }, + ]; + + return data; +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/index.tsx new file mode 100644 index 0000000000..2c00179d7c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/index.tsx @@ -0,0 +1,243 @@ +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; +import * as Types from "../types"; +import { OMSData, OmsNode } from "./data"; +import { PerformanceMeasureProvider, ComponentFlagType } from "./context"; +import { TopLevelOmsChildren } from "./omsNodeBuilder"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { ndrFormula } from "types"; +import { LabelData } from "utils"; + +interface OmsCheckboxProps { + /** name for react-hook-form registration */ + name: string; + /** data object for dynamic rendering */ + data: OmsNode[]; + isSingleSex: boolean; +} + +/** + * Builds out parent level checkboxes + * ex: Race, Ethnicity, Sex, Etc. + */ +export const buildOmsCheckboxes = ({ + name, + data, + isSingleSex, +}: OmsCheckboxProps) => { + return data + .filter((d) => !isSingleSex || d.id !== "O8BrOa") // remove sex as a top level option if isSingleSex + .map((lvlOneOption) => { + const displayValue = lvlOneOption.label; + const value = lvlOneOption.id; + + const children = [ + <TopLevelOmsChildren + options={lvlOneOption.options} + addMore={!!lvlOneOption.addMore} + parentDisplayName={lvlOneOption.aggregateTitle || lvlOneOption.label} + addMoreSubCatFlag={!!lvlOneOption.addMoreSubCatFlag} + name={`${name}.selections.${value}`} + key={`${name}.selections.${value}`} + id={value} + label={displayValue} + />, + ]; + + return { value, displayValue, children }; + }); +}; + +interface BaseProps extends Types.Qualifiers, Types.Categories { + measureName?: string; + inputFieldNames?: LabelData[]; + ndrFormulas?: ndrFormula[]; + /** string array for perfromance measure descriptions */ + performanceMeasureArray?: Types.RateFields[][]; + IUHHPerformanceMeasureArray?: Types.complexRateFields[][]; + AIFHHPerformanceMeasureArray?: Types.complexRateFields[][]; + /** should the total for each portion of OMS be calculated? */ + calcTotal?: boolean; + rateMultiplicationValue?: number; + allowNumeratorGreaterThanDenominator?: boolean; + customMask?: RegExp; + isSingleSex?: boolean; + rateAlwaysEditable?: boolean; + numberOfDecimals?: number; + componentFlag?: ComponentFlagType; + customNumeratorLabel?: string; + customDenominatorLabel?: string; + customRateLabel?: string; + customPrompt?: string; + rateCalc?: RateFormula; +} + +/** data for dynamic rendering will be provided */ +interface DataDrivenProp { + /** data array for dynamic rendering */ + data: OmsNode[]; + /** cannot set adultMeasure if using custom data*/ + adultMeasure?: never; +} + +/** default data is being used for this component */ +interface DefaultDataProp { + /** is this an adult measure? Should this contain the ACA portion? */ + adultMeasure: boolean; + /** cannot set data if using default data */ + data?: never; +} + +type Props = BaseProps & (DataDrivenProp | DefaultDataProp); + +/** OMS react-hook-form typing */ +type OMSType = Types.OptionalMeasureStratification & { + DataSource: string[]; +} & { MeasurementSpecification: string } & { + "OtherPerformanceMeasure-Rates": Types.OtherRatesFields[]; +}; + +const stringIsReadOnly = (dataSource: string) => { + return dataSource === "AdministrativeData"; +}; + +const arrayIsReadOnly = (dataSource: string[]) => { + if (dataSource.length === 0) { + return false; + } + return ( + dataSource?.every((source) => source === "AdministrativeData") ?? false + ); +}; + +/** + * Final OMS built + */ +export const OptionalMeasureStrat = ({ + performanceMeasureArray, + IUHHPerformanceMeasureArray, + AIFHHPerformanceMeasureArray, + qualifiers = [], + categories = [], + measureName, + inputFieldNames, + ndrFormulas, + data, + calcTotal = false, + rateMultiplicationValue, + allowNumeratorGreaterThanDenominator = false, + customMask, + isSingleSex = false, + rateAlwaysEditable, + numberOfDecimals = 1, + componentFlag = "DEFAULT", + customNumeratorLabel = "Numerator", + customDenominatorLabel = "Denominator", + customRateLabel = "Rate", + customPrompt, + rateCalc, +}: Props) => { + const omsData = data ?? OMSData(); + const { control, watch, getValues, setValue, unregister } = + useFormContext<OMSType>(); + const values = getValues(); + + const dataSourceWatch = watch("DataSource"); + const watchDataSourceSwitch = watch("MeasurementSpecification"); + //For some reason, this component grabs OPM data when it's showing OMS data. Removing OPM data directly causes things to break + const OPM = + watchDataSourceSwitch === "Other" + ? values["OtherPerformanceMeasure-Rates"] + : undefined; + + const register = useCustomRegister<Types.OptionalMeasureStratification>(); + const checkBoxOptions = buildOmsCheckboxes({ + ...register("OptionalMeasureStratification"), + data: omsData, + isSingleSex, + }); + + let rateReadOnly = false; + if (rateAlwaysEditable !== undefined) { + rateReadOnly = false; + } else if (dataSourceWatch && Array.isArray(dataSourceWatch)) { + rateReadOnly = arrayIsReadOnly(dataSourceWatch); + } else if (dataSourceWatch) { + rateReadOnly = stringIsReadOnly(dataSourceWatch); + } + + /** + * Clear all data from OMS if the user switches from Performance Measure to Other Performance measure or vice-versa + */ + useEffect(() => { + return () => { + //unregister does not clean the data properly + //setValue only handles it on the surface but when you select a checkbox again, it repopulates with deleted data + setValue("OptionalMeasureStratification", { + options: [], + selections: {}, + }); + //this is definitely the wrong way to fix this issue but it cleans a layer deeper than setValue, we need to use both + control._defaultValues.OptionalMeasureStratification = { + options: [], + selections: {}, + }; + unregister("OptionalMeasureStratification.options"); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [watchDataSourceSwitch]); + return ( + <QMR.CoreQuestionWrapper + testid="OMS" + label="Optional Measure Stratification" + > + <PerformanceMeasureProvider + value={{ + OPM, + performanceMeasureArray, + IUHHPerformanceMeasureArray, + AIFHHPerformanceMeasureArray, + rateReadOnly, + calcTotal, + qualifiers: qualifiers.filter((qual) => !qual.excludeFromOMS), + measureName, + inputFieldNames, + ndrFormulas, + categories: categories.filter((cat) => !cat.excludeFromOMS), + rateMultiplicationValue, + customMask, + allowNumeratorGreaterThanDenominator, + numberOfDecimals, + componentFlag, + customDenominatorLabel, + customNumeratorLabel, + customRateLabel, + customPrompt, + rateCalculation: rateCalc, + }} + > + <CUI.Text py="3"> + If this measure is also reported by additional + classifications/sub-categories, e.g. racial, ethnic, sex, or + geography, complete the following as applicable. If your state + reported classifications/sub-categories other than those listed below, + or reported different rate sets, please click on “Add Another + Sub-Category” to add Additional/Alternative + Classification/Sub-categories as needed. Please note that CMS may add + in additional categories for language and disability status in future + reporting years. + </CUI.Text> + <CUI.Text py="3"> + Do not select categories and sub-classifications for which you will + not be reporting any data. + </CUI.Text> + <QMR.Checkbox + {...register("OptionalMeasureStratification.options")} + options={checkBoxOptions} + /> + </PerformanceMeasureProvider> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/ndrSets.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/ndrSets.tsx new file mode 100644 index 0000000000..baeee668eb --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/ndrSets.tsx @@ -0,0 +1,629 @@ +import * as CUI from "@chakra-ui/react"; +import * as DC from "dataConstants"; +import * as Types from "../types"; +import * as QMR from "components"; +import { LabelData, cleanString } from "utils"; +import { useFormContext } from "react-hook-form"; +import { ComponentFlagType, usePerformanceMeasureContext } from "./context"; +import { useTotalAutoCalculation } from "./omsUtil"; + +interface NdrProps { + name: string; +} + +interface TotalProps { + name: string; + componentFlag: ComponentFlagType; + qualifier?: LabelData; + category?: LabelData; +} + +type CheckBoxBuilder = (name: string) => QMR.CheckboxOption[]; +type RateArrayBuilder = (name: string) => React.ReactElement[][]; + +/** + * Total Rate NDR that calculates from filled OMS NDR sets + */ +const TotalNDR = ({ + name, + componentFlag, + category = { + id: DC.SINGLE_CATEGORY, + label: DC.SINGLE_CATEGORY, + text: DC.SINGLE_CATEGORY, + }, + qualifier, +}: TotalProps) => { + const { + qualifiers, + measureName, + inputFieldNames, + ndrFormulas, + customMask, + rateMultiplicationValue, + rateReadOnly, + allowNumeratorGreaterThanDenominator, + customDenominatorLabel, + customNumeratorLabel, + customRateLabel, + rateCalculation, + } = usePerformanceMeasureContext(); + + const lastQualifier = qualifier ?? qualifiers.slice(-1)[0]; + const cleanedQualifier = lastQualifier.id; + const cleanedCategory = category.id; + const cleanedName = `${name}.rates.${cleanedCategory}.${cleanedQualifier}`; + const label = + category.label === DC.SINGLE_CATEGORY + ? lastQualifier.label + : category.label; + + useTotalAutoCalculation({ name, cleanedCategory, componentFlag }); + + if (componentFlag === "IU" || componentFlag === "AIF") { + return ( + <QMR.ComplexRate + key={cleanedName} + name={cleanedName} + readOnly={rateReadOnly} + measureName={measureName} + inputFieldNames={inputFieldNames} + ndrFormulas={ndrFormulas} + rates={[{ label: label, id: 0 }]} + categoryName={""} + /> + ); + } else { + return ( + <QMR.Rate + key={cleanedName} + name={cleanedName} + readOnly={rateReadOnly} + customMask={customMask} + rates={[{ label: label, id: 0 }]} + rateMultiplicationValue={rateMultiplicationValue} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + rateCalc={rateCalculation} + /> + ); + } +}; + +/** OMS Total wrapper for any variation of qulaifier and category combination*/ +const TotalNDRSets = ({ + componentFlag = "DEFAULT", + name, +}: { + componentFlag?: ComponentFlagType; + name: string; +}) => { + const rateArray: React.ReactElement[] = []; + + const { qualifiers, categories } = usePerformanceMeasureContext(); + const totalQual = qualifiers.slice(-1)[0]; + + if (categories.length && categories.some((item) => item.label)) { + categories.forEach((cat, idx) => { + rateArray.push( + <CUI.Box key={`${name}.${idx}.totalWrapper`}> + <TotalNDR + name={name} + category={cat} + componentFlag={componentFlag} + qualifier={totalQual} + /> + </CUI.Box> + ); + }); + } else { + rateArray.push( + <CUI.Box key={`${name}.totalWrapper`}> + <TotalNDR + name={name} + category={categories[0]?.id ? categories[0] : undefined} + componentFlag={componentFlag} + key={`${name}.TotalWrapper`} + />{" "} + </CUI.Box> + ); + } + + return ( + <CUI.Box> + <CUI.Heading size={"sm"} key={`totalNDRHeader`}> + {totalQual.label} + </CUI.Heading> + <CUI.Box>{rateArray}</CUI.Box> + </CUI.Box> + ); +}; + +/** Creates Rate Component Arrays for every category with a filled qualifier */ +const useStandardRateArray: RateArrayBuilder = (name) => { + const { + categories, + qualifiers, + measureName, + inputFieldNames, + ndrFormulas, + calcTotal, + allowNumeratorGreaterThanDenominator, + customMask, + performanceMeasureArray, + IUHHPerformanceMeasureArray, + rateMultiplicationValue, + rateReadOnly, + customDenominatorLabel, + customNumeratorLabel, + customRateLabel, + rateCalculation, + } = usePerformanceMeasureContext(); + const rateArrays: React.ReactElement[][] = []; + + //categories at this point has been filtered by excludeFromOMS + categories?.forEach((cat) => { + const ndrSets: React.ReactElement[] = []; + + if (IUHHPerformanceMeasureArray) { + const quals = IUHHPerformanceMeasureArray.flatMap((arr) => + arr.filter( + (rate) => + rate.uid?.includes(cat.id) && + (calcTotal ? !rate.uid?.includes("Total") : true) + ) + ); + + quals?.forEach((qual) => { + const cleanedName = `${name}.rates.${qual.uid}`; + + // Confirm that there is at least 1 rate complete + const rate1 = qual.fields?.[2]?.value ? true : false; + const rate2 = qual.fields?.[4]?.value ? true : false; + const rate3 = qual.fields?.[5]?.value ? true : false; + if (rate1 || rate2 || rate3) { + ndrSets.push( + <QMR.ComplexRate + readOnly={rateReadOnly} + name={cleanedName} + key={cleanedName} + measureName={measureName} + inputFieldNames={inputFieldNames} + ndrFormulas={ndrFormulas} + rates={[ + { + id: 0, + label: qual.label, + }, + ]} + categoryName={""} + /> + ); + } + }); + } else if (performanceMeasureArray) { + //Used performanceMeasureArray over qualifiers because in OMS, we want to capture OMS n/d/r from performance measure qualifiers that had values added + const rateQuals = performanceMeasureArray.flatMap((arr) => + arr.filter( + (rate) => + rate.uid?.includes(cat.id) && + (calcTotal ? !rate.uid?.includes("Total") : true) + ) + ); + + //performanceMeasureArray does not do a filter for excludedFromOMS so we need a second filter to remove excluded qualifiers from oms. + const unexcludedQuals = rateQuals.filter((rateQual) => + qualifiers.find((qual) => rateQual.uid?.includes(qual.id)) + ); + + unexcludedQuals?.forEach((qual) => { + if (qual.rate) { + const adjustedName = `${name}.rates.${qual.uid}`; //uid is both category id appended to qualifier id + + ndrSets.push( + <QMR.Rate + readOnly={rateReadOnly} + name={adjustedName} + key={adjustedName} + rateMultiplicationValue={rateMultiplicationValue} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + customMask={customMask} + rateCalc={rateCalculation} + rates={[ + { + id: 0, + label: qual.label, + }, + ]} + /> + ); + } + }); + } + rateArrays.push(ndrSets); + }); + return rateArrays; +}; + +/** Creates Rate Components for each Qualifier if filled in PM */ +const useRatesForCompletedPmQualifiers: RateArrayBuilder = (name) => { + const { + categories, + qualifiers, + measureName, + inputFieldNames, + ndrFormulas, + calcTotal, + allowNumeratorGreaterThanDenominator, + customMask, + performanceMeasureArray, + rateMultiplicationValue, + AIFHHPerformanceMeasureArray, + rateReadOnly, + customDenominatorLabel, + customNumeratorLabel, + customRateLabel, + rateCalculation, + } = usePerformanceMeasureContext(); + const quals = calcTotal ? qualifiers.slice(0, -1) : qualifiers; + const rateArrays: React.ReactElement[][] = []; + + /* + * Each qualifier should only show in OMS if the rate for that qualifier + * has been filled out in the Performance Measure. + * This is determined by pulling the qualifier ID out of the rate UID. + */ + const completedQualifierIds = performanceMeasureArray?.[0] + ?.filter((qualRateFields) => qualRateFields?.rate) + .map((qualRateFields) => qualRateFields.uid?.split(".")[1]); + + quals?.forEach((singleQual, qualIndex) => { + const categoryID = categories[0]?.id + ? categories[0].id + : DC.SINGLE_CATEGORY; + const cleanedName = `${name}.rates.${categoryID}.${singleQual.id}`; + + if (completedQualifierIds?.includes(singleQual.id)) { + rateArrays.push([ + <QMR.Rate + readOnly={rateReadOnly} + name={cleanedName} + key={cleanedName} + rateMultiplicationValue={rateMultiplicationValue} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + customMask={customMask} + rateCalc={rateCalculation} + rates={[{ id: 0 }]} + />, + ]); + } else if (AIFHHPerformanceMeasureArray) { + AIFHHPerformanceMeasureArray?.forEach((measure) => { + //Confirm that there is at least 1 rate complete + const rate1 = measure?.[qualIndex]?.fields?.[2]?.value ? true : false; + const rate2 = measure?.[qualIndex]?.fields?.[4]?.value ? true : false; + const rate3 = measure?.[qualIndex]?.fields?.[6]?.value ? true : false; + if (rate1 || rate2 || rate3) { + rateArrays.push([ + <QMR.ComplexRate + readOnly={rateReadOnly} + name={cleanedName} + key={cleanedName} + measureName={measureName} + inputFieldNames={inputFieldNames} + ndrFormulas={ndrFormulas} + rates={[ + { + id: 0, + }, + ]} + />, + ]); + } else { + rateArrays.push([]); + } + }); + } else { + rateArrays.push([]); + } + }); + + return rateArrays; +}; + +/** + * Builds Performance Measure AgeGroup Checkboxes + */ +const useAgeGroupsCheckboxes: CheckBoxBuilder = (name) => { + const options: QMR.CheckboxOption[] = []; + const { categories, qualifiers, calcTotal, customPrompt } = + usePerformanceMeasureContext(); + + const qualRates = useRatesForCompletedPmQualifiers(name); + const standardRates = useStandardRateArray(name); + const rateArrays = + !categories.length || !categories.some((item) => item.label) + ? qualRates + : standardRates; + const quals = calcTotal ? qualifiers.slice(0, -1) : qualifiers; + const { watch } = useFormContext<Types.DataSource>(); + const dataSourceWatch = watch(DC.DATA_SOURCE); + + const shouldDisplay = + dataSourceWatch?.[0] !== "AdministrativeData" || + dataSourceWatch?.length !== 1; + + const checkbox = categories.some((cat) => cat.label) ? categories : quals; + checkbox?.forEach((value, idx) => { + if (rateArrays?.[idx]?.length) { + const ageGroupCheckBox = { + value: value.id, + displayValue: value.text, + children: [ + <CUI.Heading + key={`${name}.rates.${value.id}Header`} + size={"sm"} + dangerouslySetInnerHTML={{ + __html: + customPrompt ?? + `Enter a number for the numerator and the denominator. Rate will + auto-calculate:`, + }} + />, + <CUI.Heading + pt="1" + key={`${name}.rates.${value.id}HeaderHelper`} + size={"sm"} + hidden={!shouldDisplay} + > + Please review the auto-calculated rate and revise if needed. + </CUI.Heading>, + ...rateArrays[idx], + ], + }; + options.push(ageGroupCheckBox); + } + }); + + return options; +}; + +/** + * Builds NDRs for Performance Measure AgeGroups + */ +const AgeGroupNDRSets = ({ name }: NdrProps) => { + const { calcTotal } = usePerformanceMeasureContext(); + + const ageGroupsOptions = useAgeGroupsCheckboxes(name); + + return ( + <> + <QMR.Checkbox + name={`${name}.options`} + key={`${name}.options`} + options={ageGroupsOptions} + /> + {calcTotal && <TotalNDRSets name={name} key={`${name}.totalWrapper`} />} + </> + ); +}; + +const IUHHNDRSets = ({ name }: NdrProps) => { + const { calcTotal } = usePerformanceMeasureContext(); + + return ( + <> + {calcTotal && ( + <TotalNDRSets + componentFlag={"IU"} + name={`${name}.iuhh-rate`} + key={`${name}.iuhh-rate.totalWrapper`} + /> + )} + </> + ); +}; + +const AIFHHNDRSets = ({ name }: NdrProps) => { + const { calcTotal } = usePerformanceMeasureContext(); + + const ageGroupsOptions = useAgeGroupsCheckboxes(`${name}.aifhh-rate`); + return ( + <> + <QMR.Checkbox + name={`${name}.aifhh-rate.options`} + key={`${name}.aifhh-rate.options`} + options={ageGroupsOptions} + /> + {calcTotal && ( + <TotalNDRSets + componentFlag={"AIF"} + name={`${name}.aifhh-rate`} + key={`${name}.aifhh-rate.totalWrapper`} + /> + )} + </> + ); +}; + +const PCRNDRSets = ({ name }: NdrProps) => { + const { rateReadOnly, qualifiers, customMask } = + usePerformanceMeasureContext(); + const rates = qualifiers.map((qual, i) => { + return { label: qual.label, id: i }; + }); + // ! Waiting for data source refactor to type data source here + const { watch } = useFormContext<Types.DataSource>(); + + // Watch for dataSource data + const dataSourceWatch = watch(DC.DATA_SOURCE); + + return ( + <> + <CUI.Heading key={`${name}.rates.Header`} size={"sm"}> + Enter a number for the numerator and the denominator. Rate will + auto-calculate + </CUI.Heading> + {dataSourceWatch?.[0] !== "AdministrativeData" || + (dataSourceWatch?.length !== 1 && ( + <CUI.Heading pt="1" key={`${name}.rates.HeaderHelper`} size={"sm"}> + Please review the auto-calculated rate and revise if needed. + </CUI.Heading> + ))} + <QMR.PCRRate + rates={rates} + name={`${name}.pcr-rate`} + readOnly={rateReadOnly} + customMask={customMask} + /> + </> + ); +}; + +/** + * Builds OPM Checkboxes + */ +const useRenderOPMCheckboxOptions = (name: string) => { + const checkBoxOptions: QMR.CheckboxOption[] = []; + + const { + OPM, + rateReadOnly, + rateMultiplicationValue, + customMask, + allowNumeratorGreaterThanDenominator, + customDenominatorLabel, + customNumeratorLabel, + customRateLabel, + rateCalculation, + customPrompt, + } = usePerformanceMeasureContext(); + + const { watch } = useFormContext<Types.DataSource>(); + const dataSourceWatch = watch(DC.DATA_SOURCE); + + const shouldDisplay = + dataSourceWatch?.[0] !== "AdministrativeData" || + dataSourceWatch?.length !== 1; + + OPM?.forEach(({ description }, idx) => { + if (description) { + const cleanedFieldName = `${DC.OPM_KEY}${cleanString(description)}`; + + const RateComponent = ( + <QMR.Rate + rates={[ + { + id: 0, + }, + ]} + name={`${name}.rates.OPM.${cleanedFieldName}`} + key={`${name}.rates.OPM.${cleanedFieldName}`} + readOnly={rateReadOnly} + rateMultiplicationValue={rateMultiplicationValue} + customMask={customMask} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + rateCalc={rateCalculation} + /> + ); + + checkBoxOptions.push({ + value: cleanedFieldName, + displayValue: description ?? `UNSET_OPM_FIELD_NAME_${idx}`, + children: [ + <CUI.Heading + key={`${name}.rates.${cleanedFieldName}Header`} + size={"sm"} + dangerouslySetInnerHTML={{ + __html: + customPrompt ?? + `Enter a number for the numerator and the denominator. Rate will + auto-calculate:`, + }} + />, + <CUI.Heading + pt="1" + size={"sm"} + key={`${name}.rates.${cleanedFieldName}HeaderHelper`} + hidden={!shouldDisplay} + > + Please review the auto-calculated rate and revise if needed. + </CUI.Heading>, + RateComponent, + ], + }); + } + }); + + return checkBoxOptions; +}; + +/** + * Builds NDRs for Other Performance Measure sets + */ +const OPMNDRSets = ({ name }: NdrProps) => { + const options = useRenderOPMCheckboxOptions(name); + return ( + <QMR.Checkbox + name={`${name}.options`} + key={`${name}.options`} + options={options} + /> + ); +}; + +/** + * Builds Base level NDR Sets + */ +export const NDRSets = ({ name }: NdrProps) => { + const { OPM, componentFlag } = usePerformanceMeasureContext(); + const children: JSX.Element[] = []; + + if (OPM) children.push(<OPMNDRSets name={name} key={name} />); + switch (componentFlag) { + case "DEFAULT": + if (!OPM) { + children.push(<AgeGroupNDRSets name={name} key={name} />); + } + break; + case "IU": + if (!OPM) { + children.push(<IUHHNDRSets name={name} key={name} />); + } + break; + case "AIF": + if (!OPM) { + children.push(<AIFHHNDRSets name={name} key={name} />); + } + break; + case "PCR": + if (!OPM) { + children.push(<PCRNDRSets name={name} key={name} />); + } + break; + } + + return ( + <CUI.VStack key={`${name}.NDRwrapper`} alignItems={"flex-start"}> + {children} + </CUI.VStack> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsNodeBuilder.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsNodeBuilder.tsx new file mode 100644 index 0000000000..9e0de36d4b --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsNodeBuilder.tsx @@ -0,0 +1,148 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; + +import { OmsNode } from "./data"; + +import { AddAnotherSection } from "./additionalCategory"; +import { SubCatSection } from "./subCatClassification"; +import { NDRSets } from "./ndrSets"; + +interface CheckboxChildrenProps extends OmsNode { + /** name for react-hook-form registration */ + name: string; + /** name of parent category for additionalCategory rendering */ + parentDisplayName: string; +} + +interface ChildCheckBoxOptionProps { + omsNode?: OmsNode; + name: string; +} + +interface NdrNodeProps { + name: string; + flagSubCat: boolean; +} + +const NdrNode = ({ name }: NdrNodeProps) => { + return ( + <CUI.Box key={`${name}.ndrWrapper`}> + <NDRSets name={`${name}.rateData`} key={`${name}.rateData`} /> + </CUI.Box> + ); +}; + +/** + * Build Sub-Category checkbox options + * ex: Asian -> Korean, Chinese, Japanese, etc. + */ +const renderRadioButtonOptions = ({ + omsNode, + name, +}: ChildCheckBoxOptionProps) => { + return [ + { + displayValue: `Yes, we are reporting aggregate data for the ${ + omsNode?.aggregateTitle || omsNode?.label + } categories.`, + value: "YesAggregateData", + children: [ + <NdrNode flagSubCat={!!omsNode?.flagSubCat} name={name} key={name} />, + ], + }, + { + displayValue: `No, we are reporting disaggregated data for ${ + omsNode?.aggregateTitle || omsNode?.label + } sub-categories`, + value: "NoIndependentData", + children: [ + <QMR.Checkbox + name={`${name}.options`} + key={`${name}.options`} + options={ + omsNode?.options!.map((node) => { + return buildChildCheckboxOption({ + omsNode: node, + name: `${name}.selections.${node.id ?? "ID_NOT_SET"}`, + }); + }) || [] + } + />, + <SubCatSection name={name} />, + ], + }, + ]; +}; + +/** + * Builds child level checkbox options + * ex: Race -> White, African American, Asian, etc. + */ +const buildChildCheckboxOption = ({ + omsNode, + name, +}: ChildCheckBoxOptionProps) => { + let children = []; + const id = omsNode?.id ? omsNode.id : "ID_NOT_SET"; + + if (!omsNode?.options) { + children = [ + <NdrNode flagSubCat={!!omsNode?.flagSubCat} name={name} key={name} />, + ]; + } + // catch condition for subCategory ex: Asian -> Korean + else { + children = [ + <QMR.RadioButton + name={`${name}.aggregate`} + key={`${name}.aggregate`} + options={renderRadioButtonOptions({ omsNode, name })} + label={`Are you reporting aggregate data for the ${ + omsNode.aggregateTitle || omsNode.label + } category?`} + />, + ]; + } + return { + value: id, + displayValue: omsNode?.label ?? "DISPLAY_ID_NOT_SET", + children, + }; +}; + +/** + * Renders Parent Level Children + * ex: checkbox options, additional category, or NDR for ACA + */ +export const TopLevelOmsChildren = (props: CheckboxChildrenProps) => { + if (!props.options) { + return <NDRSets name={`${props.name}.rateData`} />; + } + + return ( + <CUI.Box key={`${props.name}.topLevelCheckbox`}> + <QMR.Checkbox + name={`${props.name}.options`} + key={`${props.name}.options`} + options={[ + ...props.options.map((lvlTwoOption) => { + const cleanedId = lvlTwoOption?.id ?? "LVL_TWO_ID_NOT_SET"; + + return buildChildCheckboxOption({ + omsNode: lvlTwoOption, + name: `${props.name}.selections.${cleanedId}`, + }); + }), + ]} + /> + {props.addMore && ( + <AddAnotherSection + name={props.name} + flagSubCat + parentName={props.parentDisplayName} + key={`${props.name}.AdditionalCategorySection`} + /> + )} + </CUI.Box> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsUtil.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsUtil.tsx new file mode 100644 index 0000000000..08a95a9ce5 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsUtil.tsx @@ -0,0 +1,382 @@ +import objectPath from "object-path"; +import { complexRateFields, RateFields } from "../types"; +import { useEffect, useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { ComponentFlagType, usePerformanceMeasureContext } from "./context"; +import { LabelData } from "utils"; + +interface TempRate { + numerator?: number; + denominator?: number; + rate?: string; +} + +interface TotalCalcHookProps { + // OMS name of current rate section + name: string; + // current cleaned category + cleanedCategory?: string; + // Rate component type identifier + componentFlag: ComponentFlagType; +} + +interface CalcOmsTotalProp { + watchOMS: any; + cleanedCategory: string; + qualifiers: LabelData[]; + rateMultiplicationValue?: number; + numberOfDecimals: number; + componentFlag?: any; +} + +/** Process all OMS rate values pertaining to set category and calculate new rate object */ +/** Note: this currently isn't in use with 2024 because we updated the OMS to show only the total qualifier if there is one in the list of qualifiers. */ +const calculateOMSTotal = ({ + cleanedCategory, + numberOfDecimals, + qualifiers, + rateMultiplicationValue = 100, + watchOMS, +}: CalcOmsTotalProp): RateFields => { + const tempRate: TempRate = { + numerator: undefined, + denominator: undefined, + rate: undefined, + }; + + for (const qual of qualifiers.slice(0, -1)) { + if ( + watchOMS?.[cleanedCategory]?.[qual.id]?.[0]?.numerator && + watchOMS?.[cleanedCategory]?.[qual.id]?.[0]?.denominator && + watchOMS?.[cleanedCategory]?.[qual.id]?.[0]?.rate + ) { + tempRate.numerator ??= 0; + tempRate.denominator ??= 0; + tempRate.numerator += parseFloat( + watchOMS[cleanedCategory][qual.id][0].numerator + ); + tempRate.denominator += parseFloat( + watchOMS[cleanedCategory][qual.id][0].denominator + ); + } + } + + if (tempRate.numerator !== undefined && tempRate.denominator !== undefined) { + tempRate.rate = ( + Math.round( + (tempRate.numerator / tempRate.denominator) * + rateMultiplicationValue * + Math.pow(10, numberOfDecimals) + ) / Math.pow(10, numberOfDecimals) + ).toFixed(1); + } + + return { + numerator: + tempRate.numerator !== undefined ? `${tempRate.numerator}` : undefined, + denominator: + tempRate.denominator !== undefined + ? `${tempRate.denominator}` + : undefined, + rate: tempRate.rate, + }; +}; + +/** Checks if previous non-undefined OMS values have changed */ +const checkNewOmsValuesChanged = ( + next: RateFields[], + prev?: RateFields[] +): boolean => { + if (!prev) return false; + return !next.every( + (v, i) => + v.numerator === prev?.[i]?.numerator && + v.denominator === prev?.[i]?.denominator && + v.rate === prev?.[i]?.rate + ); +}; + +interface complexTempRate { + label: string; + fields: { label: string; value: any }[]; + isTotal: true; +} + +const IUHHndrForumlas = [ + // Discharges per 1,000 Enrollee Months + { + num: 1, + denom: 0, + rate: 2, + mult: 1000, + }, + // Days per 1,000 Enrollee Months + { + num: 3, + denom: 0, + rate: 4, + mult: 1000, + }, + // Average Length of Stay + { + num: 3, + denom: 1, + rate: 5, + mult: 1, + }, +]; + +const AIFHHndrFormulas = [ + // short term + { + num: 1, + denom: 0, + rate: 2, + mult: 1000, + }, + // medium term + { + num: 3, + denom: 0, + rate: 4, + mult: 1000, + }, + // long term + { + num: 5, + denom: 0, + rate: 6, + mult: 1000, + }, +]; + +/** (IU-HH Specific) Process all OMS rate values pertaining to set category and calculate new rate object */ +const calculateComplexOMSTotal = ({ + cleanedCategory, + qualifiers, + watchOMS, + componentFlag, +}: CalcOmsTotalProp): complexRateFields => { + const cleanedQualifiers = qualifiers.slice(0, -1); + const fieldNames = watchOMS?.["Total"]?.[cleanedCategory]?.[0]?.fields.map( + (field: any) => field.label + ); + + // Create empty temp obj + const tempRate: complexTempRate = { + label: cleanedCategory, + fields: fieldNames?.map((f: string) => { + return { + label: f, + value: undefined, + }; + }), + isTotal: true, + }; + + // Store sums in temp + for (const qual of cleanedQualifiers) { + const fields = watchOMS?.[cleanedCategory]?.[qual.id]?.[0]?.fields; + if (fields?.every((f: { value?: string }) => !!f?.value)) { + fields?.forEach((field: { value: string }, i: number) => { + if (field?.value && tempRate?.fields?.[i]) { + tempRate.fields[i].value ??= 0; + tempRate.fields[i].value += parseFloat(field.value); + } + }); + } + } + let formulaSet: any; + switch (componentFlag) { + case "AIF": + formulaSet = AIFHHndrFormulas; + break; + case "IU": + formulaSet = IUHHndrForumlas; + break; + } + // Calculate rates for totals + for (const f of formulaSet!) { + const numerator = tempRate.fields?.[f.num]?.value; + const denominator = tempRate.fields?.[f.denom]?.value; + if (numerator && denominator) { + tempRate.fields[f.rate].value = ( + Math.round((numerator / denominator) * f.mult * 10) / 10 + ).toFixed(1); + } + } + + // Convert numbers to strings + for (const field of tempRate?.fields ?? []) { + field.value = field.value !== undefined ? `${field.value}` : undefined; + } + + return tempRate; +}; + +/** (IU-HH Specific) Checks if previous non-undefined OMS values have changed */ +const checkNewIUHHOmsValuesChanged = ( + next: complexRateFields[], + prev?: complexRateFields[] +): boolean => { + if (!prev) return false; + return !next.every((v, i) => { + return ( + v.fields?.[0]?.value === prev?.[i]?.fields?.[0]?.value && + v.fields?.[1]?.value === prev?.[i]?.fields?.[1]?.value && + v.fields?.[2]?.value === prev?.[i]?.fields?.[2]?.value && + v.fields?.[3]?.value === prev?.[i]?.fields?.[3]?.value && + v.fields?.[4]?.value === prev?.[i]?.fields?.[4]?.value && + v.fields?.[5]?.value === prev?.[i]?.fields?.[5]?.value + ); + }); +}; + +/** (AIF-HH Specific) Checks if previous non-undefined OMS values have changed */ +const checkNewAIFHHOmsValuesChanged = ( + next: complexRateFields[], + prev?: complexRateFields[] +): boolean => { + if (!prev) return false; + return !next.every((v, i) => { + return ( + v.fields?.[0]?.value === prev?.[i]?.fields?.[0]?.value && + v.fields?.[1]?.value === prev?.[i]?.fields?.[1]?.value && + v.fields?.[2]?.value === prev?.[i]?.fields?.[2]?.value && + v.fields?.[3]?.value === prev?.[i]?.fields?.[3]?.value && + v.fields?.[4]?.value === prev?.[i]?.fields?.[4]?.value && + v.fields?.[5]?.value === prev?.[i]?.fields?.[5]?.value && + v.fields?.[6]?.value === prev?.[i]?.fields?.[6]?.value + ); + }); +}; + +/** + * Hook to handle OMS total calculation only on field changes + * + * - type === undefined: is a field reset or form load + * - type === change: NDR rate calculation or field change + * + * NOTE: we track previous fields in all states, but only fire recalculation if type === change and + * fields have changed since last triggered event + */ +export const useTotalAutoCalculation = ({ + name, + cleanedCategory = "singleCategory", + componentFlag, +}: TotalCalcHookProps) => { + const { watch, setValue } = useFormContext(); + const { qualifiers, numberOfDecimals, rateMultiplicationValue } = + usePerformanceMeasureContext(); + const [previousOMS, setPreviousOMS] = useState< + complexRateFields[] | undefined + >(); + + useEffect(() => { + const totalFieldName = `${name}.rates.${ + qualifiers.slice(-1)[0].id + }.${cleanedCategory}`; + const nonTotalQualifiers = qualifiers.slice(0, -1); + const includedNames = nonTotalQualifiers.map( + (s) => `${name}.rates.${s.id}.${cleanedCategory}` + ); + + const subscription = watch((values, { name: fieldName, type }) => { + if (fieldName && values) { + let omsFields; + switch (componentFlag) { + case "IU": + omsFields = [] as complexRateFields[]; + break; + case "AIF": + omsFields = [] as complexRateFields[]; + break; + default: + omsFields = [] as RateFields[]; + break; + } + const watchOMS = objectPath.get(values, `${name}.rates`); + for (const q of nonTotalQualifiers) { + omsFields.push(watchOMS?.[q.id]?.[cleanedCategory]?.[0] ?? {}); + } + + let OMSValuesChanged: boolean; + switch (componentFlag) { + case "IU": + OMSValuesChanged = checkNewIUHHOmsValuesChanged( + omsFields, + previousOMS + ); + break; + case "AIF": + OMSValuesChanged = checkNewAIFHHOmsValuesChanged( + omsFields, + previousOMS + ); + break; + default: + OMSValuesChanged = checkNewOmsValuesChanged(omsFields, previousOMS); + break; + } + if ( + type === "change" && + includedNames.includes(fieldName) && + OMSValuesChanged + ) { + let newFieldValue; + switch (componentFlag) { + case "IU": + newFieldValue = calculateComplexOMSTotal({ + cleanedCategory, + qualifiers, + numberOfDecimals, + rateMultiplicationValue, + watchOMS, + componentFlag, + }); + break; + case "AIF": + newFieldValue = calculateComplexOMSTotal({ + cleanedCategory, + qualifiers, + numberOfDecimals, + rateMultiplicationValue, + watchOMS, + componentFlag, + }); + break; + default: + newFieldValue = calculateOMSTotal({ + cleanedCategory, + qualifiers, + numberOfDecimals, + rateMultiplicationValue, + watchOMS, + }); + break; + } + setValue(totalFieldName, [newFieldValue]); + } + if (values) { + const currentOMSFields = JSON.parse(JSON.stringify(omsFields)); + setPreviousOMS(currentOMSFields); + } + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [ + watch, + setValue, + cleanedCategory, + componentFlag, + name, + numberOfDecimals, + qualifiers, + rateMultiplicationValue, + previousOMS, + setPreviousOMS, + ]); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/subCatClassification.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/subCatClassification.tsx new file mode 100644 index 0000000000..2e2a112355 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/subCatClassification.tsx @@ -0,0 +1,93 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { NDRSets } from "./ndrSets"; + +interface AddAnotherButtonProps { + /** onClick state updating function for dynamic rendering */ + onClick: React.MouseEventHandler<HTMLButtonElement>; + /** additional text to display after "+ Add Another" on the button */ + additionalText?: string; + isDisabled?: boolean; + /** name for location for dynamic testing */ + testid: string; +} + +/** + * Button for handling additional values in dynamic rendering + */ +export const AddAnotherButton = ({ + onClick, + additionalText, + isDisabled, + testid, +}: AddAnotherButtonProps) => { + return ( + <QMR.ContainedButton + buttonText={"+ Add Another " + additionalText} + buttonProps={{ + variant: "outline", + colorScheme: "blue", + color: "blue.500", + mt: "4", + }} + key={"AddAnotherButton"} + onClick={onClick} + disabledStatus={isDisabled} + testId={testid} + /> + ); +}; + +interface AdditonalCategoryProps { + /** name for react-hook-form registration */ + name: string; +} + +/** + * Additional [Race/Sex/Language/Etc] Category Section + */ +export const SubCatSection = ({ name }: AdditonalCategoryProps) => { + const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + name: `${name}.additionalSubCategories`, + control, + shouldUnregister: true, + }); + + return ( + <CUI.Box key={`${name}.additionalSubCategoriesWrapper`}> + {fields.map((field: any, idx: number) => ( + <QMR.DeleteWrapper + allowDeletion + key={field.id} + onDelete={() => remove(idx)} + > + <CUI.Text size={"xl"} my="3"> + {"Additional/Alternative Classification/Sub-category"} + </CUI.Text> + <QMR.QuestionChild show key={field.id}> + <CUI.Stack spacing={"5"}> + <QMR.TextInput + name={`${name}.additionalSubCategories.${idx}.description`} + key={`${name}.additionalSubCategories.${idx}.description`} + label={"Define the Alternative Classification/Sub-category"} + rules={{ required: true }} + /> + <NDRSets + name={`${name}.additionalSubCategories.${idx}.rateData`} + key={`${name}.additionalSubCategories.${idx}.rateData`} + /> + </CUI.Stack> + </QMR.QuestionChild> + </QMR.DeleteWrapper> + ))} + <AddAnotherButton + onClick={() => append({})} + additionalText={"Sub-Category"} + key={`${name}.additionalSubCategoriesButton`} + testid={`${name}.additionalSubCategoriesButton`} + /> + </CUI.Box> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.test.tsx new file mode 100644 index 0000000000..8510c338a9 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.test.tsx @@ -0,0 +1,76 @@ +import fireEvent from "@testing-library/user-event"; +import { OtherPerformanceMeasure } from "."; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { screen } from "@testing-library/react"; +import { DataDrivenTypes } from "../types"; +import * as DC from "dataConstants"; + +interface Props { + rateAlwaysEditable?: boolean; + rateMultiplicationValue?: number; + allowNumeratorGreaterThanDenominator?: boolean; + data?: DataDrivenTypes.PerformanceMeasure; + RateComponent?: RateComp | undefined; +} + +const renderComponent = ({ RateComponent, data, rateAlwaysEditable }: Props) => + renderWithHookForm( + <OtherPerformanceMeasure + rateAlwaysEditable={rateAlwaysEditable} + data={data} + RateComponent={RateComponent} + />, + { + defaultValues: { + [DC.OPM_RATES]: [ + { + rate: [{ denominator: "", numerator: "", rate: "" }], + description: "", + }, + ], + }, + } + ); + +describe("Test the OtherPerformanceMeasure RateComponent prop", () => { + let props: Props; + beforeEach(() => { + props = { + RateComponent: undefined, // QMR.Rate is default + data: undefined, + rateAlwaysEditable: undefined, + }; + }); + + test("Component renders", () => { + renderComponent(props); + // should render QMR.Rate layout using example data + expect(screen.getByText(/Other Performance Measure/i)).toBeVisible(); + expect(screen.getByText("Describe the Rate:")).toBeVisible(); + + const numeratorTextBox = screen.getByLabelText("Numerator"); + const denominatorTextBox = screen.getByLabelText("Denominator"); + const rateTextBox = screen.getByLabelText("Rate"); + fireEvent.type(numeratorTextBox, "123"); + fireEvent.type(denominatorTextBox, "123"); + expect(rateTextBox).toHaveDisplayValue("100.0"); + + // rates should be editable by default + fireEvent.type(rateTextBox, "99.9"); + expect(rateTextBox).toHaveDisplayValue("99.9"); + }); + + test("Added Rates can be deleted", () => { + const labelText = + "For example, specify the age groups and whether you are reporting on a certain indicator:"; + renderComponent(props); + const addRate = screen.getByText("+ Add Another"); + fireEvent.click(addRate); + const deleteAddedRate = screen.getByTestId("delete-wrapper"); + const rateTextBox = screen.getAllByLabelText("Rate")[0]; + fireEvent.type(rateTextBox, "123"); + fireEvent.click(deleteAddedRate); + const rateHeaders = screen.getAllByLabelText(labelText); + expect(rateHeaders).toHaveLength(1); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.tsx new file mode 100644 index 0000000000..a2d9557175 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.tsx @@ -0,0 +1,153 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import * as DC from "dataConstants"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { useEffect } from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { DataDrivenTypes } from "../types"; + +interface Props { + rateAlwaysEditable?: boolean; + rateMultiplicationValue?: number; + customMask?: RegExp; + allowNumeratorGreaterThanDenominator?: boolean; + data?: DataDrivenTypes.PerformanceMeasure; + RateComponent?: RateComp; + rateCalc?: RateFormula; +} + +const stringIsReadOnly = (dataSource: string) => { + return dataSource === "AdministrativeData"; +}; + +const arrayIsReadOnly = (dataSource: string[]) => { + if (dataSource.length === 0) { + return false; + } + return ( + dataSource?.every((source) => source === "AdministrativeData") ?? false + ); +}; + +export const OtherPerformanceMeasure = ({ + rateAlwaysEditable, + rateMultiplicationValue, + customMask, + allowNumeratorGreaterThanDenominator, + data = {}, + RateComponent = QMR.Rate, + rateCalc, +}: Props) => { + const register = useCustomRegister<Types.OtherPerformanceMeasure>(); + const { control } = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + name: DC.OPM_RATES, + control, + shouldUnregister: true, + }); + + useEffect(() => { + register(DC.OPM_RATES); + }, [register]); + + // ! Waiting for data source refactor to type data source here + const { watch } = useFormContext<Types.DataSource>(); + + // Watch for dataSource data + const dataSourceWatch = watch(DC.DATA_SOURCE); + + // Conditional check to let rate be readonly when administrative data is the only option or no option is selected + + let rateReadOnly = false; + if (rateAlwaysEditable !== undefined) { + rateReadOnly = false; + } else if (dataSourceWatch && Array.isArray(dataSourceWatch)) { + rateReadOnly = arrayIsReadOnly(dataSourceWatch); + } else if (dataSourceWatch) { + rateReadOnly = stringIsReadOnly(dataSourceWatch); + } + + return ( + <QMR.CoreQuestionWrapper testid="OPM" label="Other Performance Measure"> + <QMR.TextArea + label="Describe the other methodology used:" + formLabelProps={{ fontWeight: 700 }} + {...register(DC.OPM_EXPLAINATION)} + /> + <CUI.Box marginTop={10}> + {fields.map((_item, index) => { + return ( + <QMR.DeleteWrapper + allowDeletion={index !== 0} + onDelete={() => remove(index)} + key={_item.id} + > + <CUI.Stack key={index} my={10}> + <CUI.Heading fontSize="lg" fontWeight="600"> + Describe the Rate: + </CUI.Heading> + <QMR.TextInput + label="For example, specify the age groups and whether you are reporting on a certain indicator:" + name={`${DC.OPM_RATES}.${index}.${DC.DESCRIPTION}`} + /> + <CUI.Text + fontWeight="bold" + mt={5} + data-cy="Enter a number for the numerator and the denominator" + > + {data.customPrompt ?? + `Enter a number for the numerator and the denominator. Rate will + auto-calculate:`} + </CUI.Text> + {(dataSourceWatch?.[0] !== "AdministrativeData" || + dataSourceWatch?.length !== 1) && ( + <CUI.Heading pt="5" size={"sm"}> + Please review the auto-calculated rate and revise if needed. + </CUI.Heading> + )} + <RateComponent + rates={[ + { + id: index, + }, + ]} + name={`${DC.OPM_RATES}.${index}.${DC.RATE}`} + rateMultiplicationValue={rateMultiplicationValue} + customMask={customMask} + readOnly={rateReadOnly} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + rateCalc={rateCalc} + /> + </CUI.Stack> + </QMR.DeleteWrapper> + ); + })} + + <QMR.ContainedButton + buttonText={"+ Add Another"} + buttonProps={{ + variant: "outline", + colorScheme: "blue", + color: "blue.500", + }} + onClick={() => + append({ + description: "", + rate: [ + { + denominator: "", + numerator: "", + rate: "", + }, + ], + }) + } + /> + </CUI.Box> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/data.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/data.ts new file mode 100644 index 0000000000..a4c5c2b251 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/data.ts @@ -0,0 +1,82 @@ +import { ndrFormula } from "types"; +import { LabelData } from "utils"; + +export interface PerformanceMeasureData { + qualifiers?: LabelData[]; // age ranges, etc + categories?: LabelData[]; //performance measure descriptions + measureName?: string; + inputFieldNames?: LabelData[]; + ndrFormulas?: ndrFormula[]; + customPrompt?: string; // Default: "Enter a number for the numerator and the denominator. Rate will auto-calculate:" + questionText?: string[]; + questionListItems?: string[]; + questionListOrderedItems?: string[]; + questionListTitles?: string[]; + questionSubtext?: string[]; + questionSubtextTitles?: string[]; +} + +/** Example data built from IETAD */ +export const exampleData: PerformanceMeasureData = { + questionText: [ + "The percentage of beneficiaries age 18 and Older with a new episode of alcohol or other drug (AOD) abuse or dependence who received the following", + ], + questionListItems: [ + "Initiation of AOD Treatment: Percentage of beneficiaries who initiate treatment through an inpatient AOD admission, outpatient visit, intensive outpatient encounter, or partial hospitalization, telehealth, or medication assisted treatment within 14 days of the diagnosis.", + "Engagement of AOD Treatment: Percentage of beneficiaries who initiated treatment and who were engaged in ongoing AOD treatment within 34 days of the initiation visit.", + ], + qualifiers: [ + { + label: "Ages 18 to 64", + text: "Ages 18 to 64", + id: "Ages18to64", + }, + { + label: "Age 65 and older", + text: "Age 65 and older", + id: "Age65andolder", + }, + ], + categories: [ + { + label: "Initiation of AOD Treatment: Alcohol Abuse or Dependence", + text: "Initiation of AOD Treatment: Alcohol Abuse or Dependence", + id: "InitiationofAODTreatmentAlcoholAbuseorDependence", + }, + { + label: "Engagement of AOD Treatment: Alcohol Abuse or Dependence", + text: "Engagement of AOD Treatment: Alcohol Abuse or Dependence", + id: "EngagementofAODTreatmentAlcoholAbuseorDependence", + }, + { + label: "Initiation of AOD Treatment: Opioid Abuse or Dependence", + text: "Initiation of AOD Treatment: Opioid Abuse or Dependence", + id: "InitiationofAODTreatmentOpioidAbuseorDependence", + }, + { + label: "Engagement of AOD Treatment: Opioid Abuse or Dependence", + text: "Engagement of AOD Treatment: Opioid Abuse or Dependence", + id: "EngagementofAODTreatmentOpioidAbuseorDependence", + }, + { + label: "Initiation of AOD Treatment: Other Drug Abuse or Dependence", + text: "Initiation of AOD Treatment: Other Drug Abuse or Dependence", + id: "InitiationofAODTreatmentOtherDrugAbuseorDependence", + }, + { + label: "Engagement of AOD Treatment: Other Drug Abuse or Dependence", + text: "Engagement of AOD Treatment: Other Drug Abuse or Dependence", + id: "EngagementofAODTreatmentOtherDrugAbuseorDependence", + }, + { + label: "Initiation of AOD Treatment: Total AOD Abuse or Dependence", + text: "Initiation of AOD Treatment: Total AOD Abuse or Dependence", + id: "InitiationofAODTreatmentTotalAODAbuseorDependence", + }, + { + label: "Engagement of AOD Treatment: Total AOD Abuse or Dependence", + text: "Engagement of AOD Treatment: Total AOD Abuse or Dependence", + id: "EngagementofAODTreatmentTotalAODAbuseorDependence", + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.test.tsx new file mode 100644 index 0000000000..936a81b4cc --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.test.tsx @@ -0,0 +1,206 @@ +import { + exampleData, + PerformanceMeasureData, +} from "measures/2024/shared/CommonQuestions/PerformanceMeasure/data"; +import { data as PCRData } from "measures/2024/PCRAD/data"; +import { data as CBPdata } from "measures/2024/CBPAD/data"; +import fireEvent from "@testing-library/user-event"; +import { PerformanceMeasure } from "."; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { screen } from "@testing-library/react"; +import { PCRRate } from "components"; +import { mockLDFlags } from "../../../../../../setupJest"; + +interface Props { + component?: RateComp | undefined; + calcTotal: boolean; + data: PerformanceMeasureData; + rateReadOnly: undefined | boolean; + hybridMeasure: undefined | boolean; +} + +mockLDFlags.setDefault({ periodOfHealthEmergency2024: false }); + +const renderComponet = ({ + component, + calcTotal, + data, + rateReadOnly, + hybridMeasure, +}: Props) => + renderWithHookForm( + <PerformanceMeasure + data={data} + calcTotal={calcTotal} + RateComponent={component} + rateReadOnly={rateReadOnly} + hybridMeasure={hybridMeasure} + /> + ); + +// TODO: Mock the datasource change to trigger rate editability +describe("Test the PerformanceMeasure RateComponent prop", () => { + let props: Props; + beforeEach(() => { + props = { + component: undefined, // QMR.Rate is default + calcTotal: false, + data: exampleData, + rateReadOnly: undefined, + hybridMeasure: undefined, + }; + }); + + test("(QMR.Rate) Ensure component renders", () => { + renderComponet(props); + // should render QMR.Rate layout using example data + expect(screen.getByText(/Performance Measure/i)).toBeVisible(); + expect(screen.getByText(exampleData.questionText![0])).toBeVisible(); + expect(screen.getByText(exampleData.questionListItems![0])).toBeVisible(); + expect(screen.getByText(exampleData.questionListItems![1])).toBeVisible(); + for (const label of exampleData.qualifiers!) + expect(screen.queryAllByText(label.label).length).toBe( + exampleData.categories!.length + ); + for (const label of exampleData.categories!) + expect(screen.getByText(label.label)).toBeVisible(); + + const numeratorTextBox = screen.queryAllByLabelText("Numerator")[0]; + const denominatorTextBox = screen.queryAllByLabelText("Denominator")[0]; + const rateTextBox = screen.queryAllByLabelText("Rate")[0]; + fireEvent.type(numeratorTextBox, "123"); + fireEvent.type(denominatorTextBox, "123"); + expect(rateTextBox).toHaveDisplayValue("100.0"); + + // rates should be editable by default + fireEvent.type(rateTextBox, "99.9"); + expect(rateTextBox).toHaveDisplayValue("99.9"); + + // last NDR in categroy should not total + const lastNumeratorTextBox = screen.queryAllByLabelText("Numerator")[1]; + const lastDenominatorTextBox = screen.queryAllByLabelText("Denominator")[1]; + const lastRateTextBox = screen.queryAllByLabelText("Rate")[1]; + expect(lastNumeratorTextBox).toHaveDisplayValue(""); + expect(lastDenominatorTextBox).toHaveDisplayValue(""); + expect(lastRateTextBox).toHaveDisplayValue(""); + }); + + test("(QMR.Rate) Rates should not be editable", () => { + props.rateReadOnly = true; + renderComponet(props); + + const numeratorTextBox = screen.queryAllByLabelText("Numerator")[0]; + const denominatorTextBox = screen.queryAllByLabelText("Denominator")[0]; + const rateTextBox = screen.queryAllByLabelText("Rate")[0]; + fireEvent.type(numeratorTextBox, "123"); + fireEvent.type(denominatorTextBox, "123"); + expect(rateTextBox).toHaveDisplayValue("100.0"); + + fireEvent.type(rateTextBox, "99.9"); + expect(rateTextBox).toHaveDisplayValue("100.0"); + }); + + test("(QMR.Rate) Should total in last NDR", () => { + props.calcTotal = true; + renderComponet(props); + + const numeratorTextBox = screen.queryAllByLabelText("Numerator")[0]; + const denominatorTextBox = screen.queryAllByLabelText("Denominator")[0]; + const rateTextBox = screen.queryAllByLabelText("Rate")[0]; + fireEvent.type(numeratorTextBox, "123"); + fireEvent.type(denominatorTextBox, "123"); + expect(numeratorTextBox).toHaveDisplayValue("123"); + expect(denominatorTextBox).toHaveDisplayValue("123"); + expect(rateTextBox).toHaveDisplayValue("100.0"); + + // last NDR set should not total + const lastNumeratorTextBox = screen.queryAllByLabelText("Numerator")[1]; + const lastDenominatorTextBox = screen.queryAllByLabelText("Denominator")[1]; + const lastRateTextBox = screen.queryAllByLabelText("Rate")[1]; + expect(lastNumeratorTextBox).toHaveDisplayValue("123"); + expect(lastDenominatorTextBox).toHaveDisplayValue("123"); + expect(lastRateTextBox).toHaveDisplayValue("100.0"); + }); + + test("(PCR-XX) Ensure component renders", () => { + // modifying data to be easier to check + PCRData.qualifiers = PCRData.qualifiers!.map((qual) => ({ + id: qual.id, + text: `qual ${qual.label}`, + label: `qual ${qual.label}`, + })); //CHANGED + + props.component = PCRRate; + props.data = PCRData; + renderComponet(props); + + // should render match PCRRate layout using PCR-XX data + expect(screen.getByText(/Performance Measure/i)).toBeVisible(); + expect(screen.getByText(PCRData.questionText![0])).toBeVisible(); + expect(screen.getByText(PCRData.questionListItems![0])).toBeVisible(); + expect(screen.getByText(PCRData.questionListItems![1])).toBeVisible(); + for (const label of PCRData.qualifiers!) { + expect(screen.getByText(label.label)).toBeVisible(); + } + + // rates should be editable by default + const numeratorTextBox = screen.getByLabelText(PCRData.qualifiers[1].label); + const denominatorTextBox = screen.getByLabelText( + PCRData.qualifiers[0].label + ); + const rateTextBox = screen.getByLabelText(PCRData.qualifiers[2].label); + fireEvent.type(numeratorTextBox, "123"); + fireEvent.type(denominatorTextBox, "123"); + expect(numeratorTextBox).toHaveDisplayValue("123"); + expect(denominatorTextBox).toHaveDisplayValue("123"); + expect(rateTextBox).toHaveDisplayValue("100.0000"); + + fireEvent.type(rateTextBox, "123"); + expect(rateTextBox).toHaveDisplayValue("123"); + }); + + test("(PCR-XX) Rates should not be editable", () => { + props.component = PCRRate; + props.data = PCRData; + props.rateReadOnly = true; + renderComponet(props); + + // rates should not be editable + const numeratorTextBox = screen.queryAllByLabelText( + PCRData.qualifiers![1].label + )[0]; + const denominatorTextBox = screen.queryAllByLabelText( + PCRData.qualifiers![0].label + )[0]; + const rateTextBox = screen.getByText( + PCRData.qualifiers![2].label + ).nextSibling; + fireEvent.type(numeratorTextBox, "123"); + fireEvent.type(denominatorTextBox, "123"); + expect(numeratorTextBox).toHaveDisplayValue("123"); + expect(denominatorTextBox).toHaveDisplayValue("123"); + expect(rateTextBox?.textContent).toEqual("100.0000"); + expect(rateTextBox?.nodeName).toBe("P"); + }); + + test("periodOfHealthEmergency2024 flag is set to false, covid text and textbox should not render", () => { + props.data = CBPdata; + props.hybridMeasure = true; + renderComponet(props); + const covidText = screen.queryByLabelText( + "Describe any COVID-related difficulties encountered while collecting this data:" + ); + expect(covidText).toBeNull(); + }); + + test("periodOfHealthEmergency2024 flag is set to true, covid text and textbox should render", () => { + mockLDFlags.set({ periodOfHealthEmergency2024: true }); + props.data = CBPdata; + props.hybridMeasure = true; + renderComponet(props); + const covidText = screen.getByLabelText( + "Describe any COVID-related difficulties encountered while collecting this data:" + ); + expect(covidText).toBeInTheDocument(); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.tsx new file mode 100644 index 0000000000..e8b1285ec7 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.tsx @@ -0,0 +1,343 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; +import { PerformanceMeasureData } from "./data"; +import { useWatch } from "react-hook-form"; +import { LabelData, getLabelText } from "utils"; +import { ndrFormula } from "types"; +import { useFlags } from "launchdarkly-react-client-sdk"; + +interface Props { + data: PerformanceMeasureData; + rateReadOnly?: boolean; + calcTotal?: boolean; + rateScale?: number; + customMask?: RegExp; + hybridMeasure?: boolean; + showtextbox?: boolean; + allowNumeratorGreaterThanDenominator?: boolean; + RateComponent?: RateComp; + customNumeratorLabel?: string; + customDenominatorLabel?: string; + customRateLabel?: string; + rateCalc?: RateFormula; +} + +interface NdrSetProps { + categories?: LabelData[]; + qualifiers?: LabelData[]; + measureName?: string; + inputFieldNames?: LabelData[]; + ndrFormulas?: ndrFormula[]; + rateReadOnly: boolean; + calcTotal: boolean; + rateScale?: number; + customMask?: RegExp; + allowNumeratorGreaterThanDenominator?: boolean; + RateComponent: RateComp; + customNumeratorLabel?: string; + customDenominatorLabel?: string; + customRateLabel?: string; + rateCalc?: RateFormula; +} + +/** Maps over the categories given and creates rate sets based on the qualifiers, with a default of one rate */ +const CategoryNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers, + measureName, + inputFieldNames, + ndrFormulas, + rateScale, + customMask, + allowNumeratorGreaterThanDenominator, + calcTotal, + RateComponent, + customNumeratorLabel, + customDenominatorLabel, + customRateLabel, + rateCalc, +}: NdrSetProps) => { + const register = useCustomRegister(); + const labelText = getLabelText(); + + return ( + <> + {categories.map((cat) => { + let rates: QMR.IRate[] | undefined = qualifiers?.map((qual, idx) => ({ + label: qual.label, + uid: cat.id + "." + qual.id, + id: idx, + })); + + rates = rates?.length ? rates : [{ id: 0 }]; + + return ( + <CUI.Box key={cat.id}> + <CUI.Text fontWeight="bold" my="5"> + {labelText[cat.label] ?? cat.label} + </CUI.Text> + <RateComponent + readOnly={rateReadOnly} + rates={rates} + measureName={measureName} + inputFieldNames={inputFieldNames} + ndrFormulas={ndrFormulas} + rateMultiplicationValue={rateScale} + calcTotal={calcTotal} + categoryName={cat.label} + categories={categories} + customMask={customMask} + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + rateCalc={rateCalc} + {...register(`${DC.PERFORMANCE_MEASURE}.${DC.RATES}.${cat.id}`)} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + /> + </CUI.Box> + ); + })} + </> + ); +}; + +/** If no categories, we still need a rate for the PM + * 2023 and onward, categories are expected to have at least object filled for creating uid in database + */ +const QualifierNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers = [], + measureName, + inputFieldNames, + ndrFormulas, + rateScale, + customMask, + calcTotal, + allowNumeratorGreaterThanDenominator, + RateComponent, + customNumeratorLabel, + customDenominatorLabel, + customRateLabel, + rateCalc, +}: NdrSetProps) => { + const register = useCustomRegister(); + const categoryID = categories[0]?.id ? categories[0].id : DC.SINGLE_CATEGORY; + + const rates: QMR.IRate[] = qualifiers.map((item, idx) => ({ + label: item.label, + uid: `${categoryID}.${item.id}`, //this uid is used to map to the N/D/R data's id key in the database + id: idx, + })); + return ( + <> + <RateComponent + rates={rates} + readOnly={rateReadOnly} + measureName={measureName} + inputFieldNames={inputFieldNames} + ndrFormulas={ndrFormulas} + rateMultiplicationValue={rateScale} + customMask={customMask} + calcTotal={calcTotal} + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + rateCalc={rateCalc} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + {...register(`${DC.PERFORMANCE_MEASURE}.${DC.RATES}.${categoryID}`)} + /> + </> + ); +}; + +/** Creates the NDR sets based on given categories and qualifiers */ +const PerformanceMeasureNdrs = (props: NdrSetProps) => { + let ndrSets; + + if (props.categories?.length && props.categories.some((item) => item.label)) { + ndrSets = <CategoryNdrSets {...props} />; + } else if (props.qualifiers?.length) { + ndrSets = <QualifierNdrSets {...props} />; + } + + return <CUI.Box key="PerformanceMeasureNdrSets">{ndrSets}</CUI.Box>; +}; + +const stringIsReadOnly = (dataSource: string) => { + return dataSource === "AdministrativeData"; +}; + +const arrayIsReadOnly = (dataSource: string[]) => { + if (dataSource.length === 0) { + return false; + } + return ( + dataSource?.every((source) => source === "AdministrativeData") ?? false + ); +}; +/** Data Driven Performance Measure Comp */ +export const PerformanceMeasure = ({ + data, + calcTotal = false, + rateReadOnly, + rateScale, + customMask, + hybridMeasure, + allowNumeratorGreaterThanDenominator, + customNumeratorLabel, + customDenominatorLabel, + customRateLabel, + showtextbox = true, + rateCalc, + RateComponent = QMR.Rate, // Default to QMR.Rate +}: Props) => { + const register = useCustomRegister<Types.PerformanceMeasure>(); + const pheIsCurrent = useFlags()?.["periodOfHealthEmergency2024"]; + const dataSourceWatch = useWatch<Types.DataSource>({ + name: DC.DATA_SOURCE, + }) as string[] | string | undefined; + let readOnly = false; + if (rateReadOnly !== undefined) { + readOnly = rateReadOnly; + } else if (dataSourceWatch && Array.isArray(dataSourceWatch)) { + readOnly = arrayIsReadOnly(dataSourceWatch); + } else if (dataSourceWatch) { + readOnly = stringIsReadOnly(dataSourceWatch); + } + + data.questionText = data.questionText ?? []; + + return ( + <QMR.CoreQuestionWrapper + testid="performance-measure" + label="Performance Measure" + > + <CUI.Stack> + {data.questionText.map((item, idx) => { + return ( + <CUI.Text key={`questionText.${idx}`} mb={5}> + {item} + </CUI.Text> + ); + })} + </CUI.Stack> + {data.questionSubtext && ( + <CUI.Stack my="5" spacing={5}> + {data.questionSubtext.map((item, idx) => { + return ( + <CUI.Text key={`performanceMeasureListItem.${idx}`}> + {data.questionSubtextTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionSubtextTitles?.[idx]} + </CUI.Text> + )} + <CUI.Text>{item}</CUI.Text> + </CUI.Text> + ); + })} + </CUI.Stack> + )} + {data.questionListItems && ( + <CUI.UnorderedList m="5" ml="10" spacing={5}> + {data.questionListItems.map((item, idx) => { + return ( + <CUI.ListItem key={`performanceMeasureListItem.${idx}`}> + {data.questionListTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionListTitles?.[idx]} + </CUI.Text> + )} + {item} + </CUI.ListItem> + ); + })} + </CUI.UnorderedList> + )} + {data.questionListOrderedItems && ( + <CUI.OrderedList m="5" ml="10" spacing={5}> + {data.questionListOrderedItems?.map((item, idx) => { + return ( + <CUI.ListItem key={`performanceMeasureListItem.${idx}`}> + {data.questionListTitles?.[idx] && ( + <CUI.Text display="inline" fontWeight="600"> + {data.questionListTitles?.[idx]} + <br /> + </CUI.Text> + )} + {item} + </CUI.ListItem> + ); + })} + </CUI.OrderedList> + )} + {showtextbox && ( + <QMR.TextArea + label="If this measure has been reported by the state previously and there has been a substantial change in the rate or measure-eligible population, please provide any available context below:" + {...register(`${DC.PERFORMANCE_MEASURE}.${DC.EXPLAINATION}`)} + /> + )} + {hybridMeasure && pheIsCurrent && ( + <CUI.Box my="5"> + <CUI.Text> + CMS recognizes that social distancing will make onsite medical chart + reviews inadvisable during the COVID-19 pandemic. As such, hybrid + measures that rely on such techniques will be particularly + challenging during this time. While reporting of the Core Sets is + voluntary, CMS encourages states that can collect information safely + to continue reporting the measures they have reported in the past. + </CUI.Text> + <QMR.TextArea + formLabelProps={{ mt: 5 }} + {...register("PerformanceMeasure.hybridExplanation")} + label="Describe any COVID-related difficulties encountered while collecting this data:" + /> + </CUI.Box> + )} + <CUI.Text + fontWeight="bold" + mt={5} + dangerouslySetInnerHTML={{ + __html: + data.customPrompt ?? + `Enter a number for the numerator and the denominator. Rate will + auto-calculate:`, + }} + data-cy="Enter a number for the numerator and the denominator" + /> + {(dataSourceWatch?.[0] !== "AdministrativeData" || + dataSourceWatch?.length !== 1) && ( + <CUI.Heading pt="5" size={"sm"}> + Please review the auto-calculated rate and revise if needed. + </CUI.Heading> + )} + <PerformanceMeasureNdrs + RateComponent={RateComponent} + categories={data.categories} + qualifiers={data.qualifiers} + measureName={data.measureName} + inputFieldNames={data.inputFieldNames} + ndrFormulas={data.ndrFormulas} + rateReadOnly={readOnly} + calcTotal={calcTotal} + rateScale={rateScale} + customMask={customMask} + customNumeratorLabel={customNumeratorLabel} + customDenominatorLabel={customDenominatorLabel} + customRateLabel={customRateLabel} + allowNumeratorGreaterThanDenominator={ + allowNumeratorGreaterThanDenominator + } + rateCalc={rateCalc} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.test.tsx new file mode 100644 index 0000000000..75fcfbec0c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.test.tsx @@ -0,0 +1,86 @@ +import fireEvent from "@testing-library/user-event"; +import { Reporting } from "."; +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; + +describe("Test Reporting component", () => { + beforeEach(() => { + renderWithHookForm( + <Reporting + measureName="My Test Measure" + reportingYear="2021" + measureAbbreviation="MTM" + /> + ); + }); + + it("component renders", () => { + expect( + screen.getByText("Are you reporting on this measure?") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Yes, I am reporting My Test Measure (MTM) for FFY 2021 quality measure reporting." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "No, I am not reporting My Test Measure (MTM) for FFY 2021 quality measure reporting." + ) + ).toBeInTheDocument(); + }); + + it("Why are you not reporting shows when no is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, I am not reporting My Test Measure (MTM) for FFY 2021 quality measure reporting." + ); + fireEvent.click(textArea); + expect( + screen.getByText("Why are you not reporting on this measure?") + ).toBeInTheDocument(); + }); +}); + +describe("Test Reporting component on a Health Home Measure", () => { + beforeEach(() => { + renderWithHookForm( + <Reporting + measureName="My Test Measure" + reportingYear="2021" + measureAbbreviation="MTM" + healthHomeMeasure + /> + ); + }); + + it("component renders", () => { + expect( + screen.getByText("Are you reporting on this measure?") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Yes, I am reporting My Test Measure (MTM) for FFY 2021 quality measure reporting." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "No, I am not reporting My Test Measure (MTM) for FFY 2021 quality measure reporting." + ) + ).toBeInTheDocument(); + }); + + it("Why are you not reporting shows when no is clicked with Helath Home options", async () => { + const textArea = await screen.findByLabelText( + "No, I am not reporting My Test Measure (MTM) for FFY 2021 quality measure reporting." + ); + fireEvent.click(textArea); + expect( + screen.getByText("Why are you not reporting on this measure?") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Continuous enrollment requirement not met due to start date of SPA" + ) + ).toBeInTheDocument(); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.tsx new file mode 100644 index 0000000000..4c7a248fb0 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.tsx @@ -0,0 +1,55 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; +import { useFormContext } from "react-hook-form"; +import { WhyAreYouNotReporting } from "../WhyAreYouNotReporting"; + +interface Props { + measureName: string; + measureAbbreviation: string; + reportingYear: string; + healthHomeMeasure?: boolean; + removeLessThan30?: boolean; +} + +export const Reporting = ({ + measureName, + reportingYear, + measureAbbreviation, + healthHomeMeasure, + removeLessThan30, +}: Props) => { + const register = useCustomRegister<Types.DidReport>(); + const { watch } = useFormContext<Types.DidReport>(); + const watchRadioStatus = watch(DC.DID_REPORT); + + return ( + <> + <QMR.CoreQuestionWrapper + testid="reporting" + label="Are you reporting on this measure?" + > + <QMR.RadioButton + {...register(DC.DID_REPORT)} + options={[ + { + displayValue: `Yes, I am reporting ${measureName} (${measureAbbreviation}) for FFY ${reportingYear} quality measure reporting.`, + value: DC.YES, + }, + { + displayValue: `No, I am not reporting ${measureName} (${measureAbbreviation}) for FFY ${reportingYear} quality measure reporting.`, + value: DC.NO, + }, + ]} + /> + </QMR.CoreQuestionWrapper> + {watchRadioStatus === DC.NO && ( + <WhyAreYouNotReporting + healthHomeMeasure={healthHomeMeasure} + removeLessThan30={removeLessThan30} + /> + )} + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.test.tsx new file mode 100644 index 0000000000..5144271c9d --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.test.tsx @@ -0,0 +1,42 @@ +import fireEvent from "@testing-library/user-event"; +import { StatusOfData } from "."; +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; + +describe("Test StatusOfData component", () => { + beforeEach(() => { + renderWithHookForm(<StatusOfData />); + }); + + it("component renders", () => { + expect( + screen.getByText("What is the status of the data being reported?") + ).toBeInTheDocument(); + expect( + screen.getByText("I am reporting provisional data.") + ).toBeInTheDocument(); + expect(screen.getByText("I am reporting final data.")).toBeInTheDocument(); + }); + + it("Additional information text box shows when 1st option clicked", async () => { + const textArea = await screen.findByLabelText( + "I am reporting provisional data." + ); + fireEvent.click(textArea); + expect( + screen.getByText( + "Please provide additional information such as when the data will be final and if you plan to modify the data reported here:" + ) + ).toBeInTheDocument(); + }); + + it("Additional information text box does not show when 2st option clicked", async () => { + const textArea = await screen.findByLabelText("I am reporting final data."); + fireEvent.click(textArea); + expect( + screen.queryByText( + "Please provide additional information such as when the data will be final and if you plan to modify the data reported here:" + ) + ).toBeNull(); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.tsx new file mode 100644 index 0000000000..98f52f2511 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.tsx @@ -0,0 +1,40 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; + +export const StatusOfData = () => { + const register = useCustomRegister<Types.StatusOfData>(); + return ( + <QMR.CoreQuestionWrapper + testid="status-of-data" + label="Status of Data Reported" + > + <QMR.RadioButton + {...register(DC.DATA_STATUS)} + options={[ + { + displayValue: "I am reporting provisional data.", + value: DC.REPORTING_PROVISIONAL_DATA, + children: [ + <QMR.TextArea + {...register(DC.DATA_STATUS_PROVISIONAL_EXPLAINATION)} + label="Please provide additional information such as when the data will be final and if you plan to modify the data reported here:" + formLabelProps={{ + fontWeight: "normal", + fontSize: "normal", + }} + />, + ], + }, + { + displayValue: "I am reporting final data.", + value: DC.REPORTING_FINAL_DATA, + }, + ]} + label="What is the status of the data being reported?" + formLabelProps={{ fontWeight: "bold" }} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.test.tsx new file mode 100644 index 0000000000..926c897804 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.test.tsx @@ -0,0 +1,236 @@ +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { WhyAreYouNotReporting } from "."; +import { mockLDFlags } from "../../../../../../setupJest"; +import userEvent from "@testing-library/user-event"; + +mockLDFlags.setDefault({ periodOfHealthEmergency2024: false }); + +describe("WhyAreYouNotReporting component initial appearance", () => { + beforeEach(() => { + renderWithHookForm(<WhyAreYouNotReporting />); + }); + + it("displays description text properly", () => { + expect( + screen.getByText("Why are you not reporting on this measure?") + ).toBeInTheDocument(); + expect(screen.getByText("Select all that apply:")).toBeInTheDocument(); + }); + + it("displays label text properly", () => { + verifyOptions(); + }); + + it("does not display Health Homes option by default", () => { + expect( + screen.queryByLabelText( + "Continuous enrollment requirement not met due to start date of SPA" + ) + ).not.toBeInTheDocument(); + }); +}); + +describe(`Options`, () => { + beforeEach(() => { + renderWithHookForm(<WhyAreYouNotReporting />); + }); + + describe("Population not covered", () => { + beforeEach(() => { + userEvent.click(screen.getByLabelText("Population not covered")); + }); + + it("displays sub-options", () => { + expect( + screen.getByLabelText("Entire population not covered") + ).toBeInTheDocument(); + expect( + screen.getByLabelText("Partial population not covered") + ).toBeInTheDocument(); + expect( + screen.queryByLabelText("Explain the partial population not covered:") + ).not.toBeInTheDocument(); + }); + + describe("sub-options", () => { + it("Partial population not covered", () => { + userEvent.click( + screen.getByLabelText("Partial population not covered") + ); + + expect( + screen.getByLabelText("Explain the partial population not covered:") + ).toBeInTheDocument(); + }); + }); + }); + + describe("Data not available", () => { + beforeEach(() => { + userEvent.click(screen.getByLabelText("Data not available")); + }); + + it("displays sub-options", () => { + expect( + screen.getByText("Why is data not available?") + ).toBeInTheDocument(); + + // Verify expected subselections + expect(screen.getByLabelText("Budget constraints")).toBeInTheDocument(); + expect(screen.getByLabelText("Staff Constraints")).toBeInTheDocument(); + expect( + screen.getByLabelText("Data inconsistencies/Accuracy") + ).toBeInTheDocument(); + expect( + screen.getByLabelText("Data source not easily accessible") + ).toBeInTheDocument(); + expect( + screen.getByLabelText("Information not collected") + ).toBeInTheDocument(); + + // There should now be 2 "Other" selections (one parent, one child) + // in the component + expect(screen.queryAllByLabelText("Other")).toHaveLength(2); + }); + + describe("sub-options", () => { + test("Data inconsistencies/Accuracy", () => { + // Open option + userEvent.click(screen.getByLabelText("Data inconsistencies/Accuracy")); + + const textArea = screen.getByLabelText( + "Explain the Data inconsistencies/Accuracy issues:" + ); + expect(textArea).toBeInTheDocument(); + userEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + test("Data source not easily accessible", () => { + // Open option + userEvent.click( + screen.getByLabelText("Data source not easily accessible") + ); + + // Verify sub-options + expect( + screen.getByLabelText("Requires medical record review") + ).toBeInTheDocument(); + expect( + screen.getByLabelText( + "Requires data linkage which does not currently exist" + ) + ).toBeInTheDocument(); + expect(screen.queryAllByLabelText("Other")).toHaveLength(3); + + // "Other" + userEvent.click(screen.queryAllByLabelText("Other")[0]); + const textArea = screen.getByLabelText("Explain:"); + expect(textArea).toBeInTheDocument(); + userEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + test("Information not collected", () => { + // Open Option + userEvent.click(screen.getByLabelText("Information not collected")); + }); + + test("Other", () => { + // Open Option + userEvent.click(screen.queryAllByLabelText("Other")[0]); + const textArea = screen.getByLabelText("Explain:"); + expect(textArea).toBeInTheDocument(); + userEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + }); + }); + + describe("Small sample size (less than 30)", () => { + it("renders textBox correctly with max value 29", () => { + userEvent.click( + screen.getByLabelText("Small sample size (less than 30)") + ); + + const numberInput = screen.getByTestId("test-number-input"); + expect(numberInput).toBeInTheDocument(); + userEvent.type(numberInput, "29"); + expect(numberInput).toHaveDisplayValue("29"); + userEvent.type(numberInput, "30"); + expect(numberInput).toHaveDisplayValue("3"); + }); + }); + + describe("Other", () => { + it("renders textBox correctly", () => { + userEvent.click(screen.getByLabelText("Other")); + + const textArea = screen.getByLabelText("Explain:"); + expect(textArea).toBeInTheDocument(); + userEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + }); +}); + +describe("WhyAreYouNotReporting component, Health Homes", () => { + beforeEach(() => { + renderWithHookForm(<WhyAreYouNotReporting healthHomeMeasure />); + }); + + it("renders the Health Homes version of the component", () => { + verifyOptions(); + expect( + screen.getByLabelText( + "Continuous enrollment requirement not met due to start date of SPA" + ) + ).toBeInTheDocument(); + }); + + it("displays the correct Health Homes sub-options", () => { + userEvent.click(screen.getByLabelText("Data not available")); + expect( + screen.getByLabelText("Data not submitted by Providers to State") + ).toBeInTheDocument(); + }); +}); + +describe("Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic (PHE active)", () => { + it("renders textBox correctly", () => { + mockLDFlags.set({ periodOfHealthEmergency2024: true }); + renderWithHookForm(<WhyAreYouNotReporting />); + userEvent.click( + screen.getByLabelText( + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic" + ) + ); + const textArea = screen.getByLabelText( + "Describe your state's limitations with regard to collection, reporting, or accuracy of data for this measure:" + ); + expect(textArea).toBeInTheDocument(); + userEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); +}); + +function verifyOptions() { + expect(screen.getByLabelText("Service not covered")).toBeInTheDocument(); + expect(screen.getByLabelText("Population not covered")).toBeInTheDocument(); + expect(screen.getByLabelText("Data not available")).toBeInTheDocument(); + expect( + screen.queryByText( + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic" + ) + ).not.toBeInTheDocument(); + expect( + screen.getByLabelText("Small sample size (less than 30)") + ).toBeInTheDocument(); + expect(screen.getByLabelText("Other")).toBeInTheDocument(); + + // Expect suboptions to not be open by default + expect( + screen.queryByText("Why is data not available?") + ).not.toBeInTheDocument(); +} diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.tsx new file mode 100644 index 0000000000..6eb64e1f08 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.tsx @@ -0,0 +1,220 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; +import { useFlags } from "launchdarkly-react-client-sdk"; + +interface Props { + healthHomeMeasure?: boolean; + removeLessThan30?: boolean; +} + +export const WhyAreYouNotReporting = ({ + healthHomeMeasure, + removeLessThan30, +}: Props) => { + const pheIsCurrent = useFlags()?.["periodOfHealthEmergency2024"]; + const register = useCustomRegister<Types.WhyAreYouNotReporting>(); + return ( + <QMR.CoreQuestionWrapper + testid="why-are-you-not-reporting" + label="Why are you not reporting on this measure?" + > + <QMR.Checkbox + {...register(DC.WHY_ARE_YOU_NOT_REPORTING)} + helperText="Select all that apply:" + renderHelperTextAbove + options={[ + { + displayValue: `Service not covered`, + value: DC.SERVICE_NOT_COVERED, + }, + { + displayValue: `Population not covered`, + value: DC.POP_NOT_COVERED, + children: [ + <QMR.RadioButton + {...register(DC.AMOUNT_OF_POP_NOT_COVERED)} + options={[ + { + displayValue: "Entire population not covered", + value: DC.ENTIRE_POP_NOT_COVERED, + }, + { + displayValue: "Partial population not covered", + value: DC.PARTIAL_POP_NOT_COVERED, + children: [ + <QMR.TextArea + label="Explain the partial population not covered:" + {...register(DC.PARTIAL_POP_NOT_COVERED_EXPLAINATION)} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: `Data not available`, + value: DC.DATA_NOT_AVAILABLE, + children: [ + <QMR.Checkbox + {...register(DC.WHY_IS_DATA_NOT_AVAILABLE)} + label="Why is data not available?" + renderHelperTextAbove + helperText="Select all that apply:" + options={[ + { + displayValue: "Budget constraints", + value: DC.BUDGET_CONSTRAINTS, + }, + { + displayValue: "Staff Constraints", + value: DC.STAFF_CONSTRAINTS, + }, + { + displayValue: "Data inconsistencies/Accuracy", + value: DC.DATA_INCONSISTENCIES_ACCURACY_ISSUES, + children: [ + <QMR.TextArea + label="Explain the Data inconsistencies/Accuracy issues:" + {...register(DC.DATA_INCONSISTENCIES_ACCURACY_ISSUES)} + />, + ], + }, + ...(healthHomeMeasure + ? [ + { + displayValue: + "Data not submitted by Providers to State", + value: DC.DATA_NOT_SUBMITTED_BY_PROVIDERS_TO_STATE, + }, + ] + : []), + { + displayValue: "Data source not easily accessible", + value: DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE, + children: [ + <QMR.Checkbox + label="Select all that apply:" + {...register(DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE)} + options={[ + { + displayValue: "Requires medical record review", + value: DC.REQUIRES_MEDICAL_RECORD_REVIEW, + }, + { + displayValue: + "Requires data linkage which does not currently exist", + value: DC.REQUIRES_DATA_LINKAGE, + }, + { + displayValue: "Other", + value: DC.OTHER, + children: [ + <QMR.TextArea + label="Explain:" + {...register( + DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE_OTHER + )} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Information not collected", + value: DC.INFO_NOT_COLLECTED, + children: [ + <QMR.Checkbox + label="Select all that apply:" + {...register(DC.INFO_NOT_COLLECTED)} + options={[ + { + displayValue: + "Not Collected by Provider (Hospital/Health Plan)", + value: DC.NOT_COLLECTED_BY_PROVIDER, + }, + { + displayValue: "Other", + value: DC.OTHER, + children: [ + <QMR.TextArea + label="Explain:" + {...register(DC.INFO_NOT_COLLECTED_OTHER)} + />, + ], + }, + ]} + />, + ], + }, + { + displayValue: "Other", + value: DC.OTHER, + children: [ + <QMR.TextArea + label="Explain:" + {...register(DC.WHY_IS_DATA_NOT_AVAILABLE_OTHER)} + />, + ], + }, + ]} + />, + ], + }, + ...(pheIsCurrent + ? [ + { + displayValue: + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic", + value: DC.LIMITATION_WITH_DATA_COLLECTION, + children: [ + <QMR.TextArea + label="Describe your state's limitations with regard to collection, reporting, or accuracy of data for this measure:" + {...register(DC.LIMITATION_WITH_DATA_COLLECTION)} + />, + ], + }, + ] + : []), + { + displayValue: `Small sample size ${ + removeLessThan30 ? "" : "(less than 30)" + }`, + value: DC.SMALL_SAMPLE_SIZE, + children: [ + <QMR.NumberInput + {...register(DC.SMALL_SAMPLE_SIZE)} + label="Enter specific sample size:" + mask={removeLessThan30 ? /^[0-9]*$/i : /^([1-2]?\d)?$/i} + />, + ], + }, + ...(healthHomeMeasure + ? [ + { + displayValue: + "Continuous enrollment requirement not met due to start date of SPA", + value: + DC.CONTINUOUS_ENROLLMENT_REQUIREMENT_NOT_MET_DUE_TO_START_DATE_OF_SPA, + }, + ] + : []), + { + displayValue: "Other", + value: DC.OTHER, + children: [ + <QMR.TextArea + label="Explain:" + {...register(DC.WHY_ARE_YOU_NOT_REPORTING_OTHER)} + />, + ], + }, + ]} + /> + </QMR.CoreQuestionWrapper> + ); +}; diff --git a/services/ui-src/src/measures/2023/shared/CommonQuestions/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/index.ts similarity index 90% rename from services/ui-src/src/measures/2023/shared/CommonQuestions/index.tsx rename to services/ui-src/src/measures/2024/shared/CommonQuestions/index.ts index b678ceaeee..a776e83af0 100644 --- a/services/ui-src/src/measures/2023/shared/CommonQuestions/index.tsx +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/index.ts @@ -3,7 +3,7 @@ export * from "./DateRange"; export * from "./DefinitionsOfPopulation"; export * from "./DataSource"; export * from "./DataSourceCahps"; -export * from "./AdditionalNotes"; +export * from "shared/commonQuestions/AdditionalNotes"; export * from "./OtherPerformanceMeasure"; export * from "./Reporting"; export * from "./StatusOfData"; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/types.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/types.ts new file mode 100644 index 0000000000..a9d8e1c676 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/types.ts @@ -0,0 +1,313 @@ +import { LabelData } from "utils"; +import { DataSourceData } from "./DataSource/data"; +import { OmsNode } from "./OptionalMeasureStrat/data"; +import { PerformanceMeasureData } from "./PerformanceMeasure/data"; +import * as DC from "dataConstants"; +import * as Types from "shared/types"; + +type YesNo = typeof DC.YES | typeof DC.NO; + +export interface MeasurementSpecification { + [DC.MEASUREMENT_SPECIFICATION]: // Selected Measurement Specification + | typeof DC.NCQA + | typeof DC.OPA + | typeof DC.AHRQ + | typeof DC.CMS + | typeof DC.OTHER + | typeof DC.HRSA + | typeof DC.PQA; + [DC.MEASUREMENT_SPECIFICATION_HEDIS]: // if Measure Spec is NCQA/HEDIS -> which version are they using + typeof DC.HEDIS_MY_2022 | typeof DC.HEDIS_MY_2021 | typeof DC.HEDIS_MY_2020; + [DC.MEASUREMENT_SPEC_OMS_DESCRIPTION]: string; // If user selects OTHER in MEASUREMENT_SPECIFICATION -> this is the description + [DC.MEASUREMENT_SPEC_OMS_DESCRIPTION_UPLOAD]: File; // If user selects OTHER in MEASUREMENT_SPECIFICATION -> this is optional file upload +} + +export interface DefinitionOfPopulation { + [DC.DEFINITION_OF_DENOMINATOR]: Array< + | typeof DC.DENOMINATOR_INC_MEDICAID_POP + | typeof DC.DENOMINATOR_INC_CHIP + | typeof DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE + | typeof DC.DENOMINATOR_INC_OTHER + >; + [DC.DEFINITION_DENOMINATOR_OTHER]: string; // if DENOMINATOR_INC_OTHER selected in DEFINITION_OF_DENOMINATOR -> an explaination + [DC.CHANGE_IN_POP_EXPLANATION]: string; // text explaination of change in polulation + [DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC]: YesNo; // Does this denominator represent your total measure-eligible population? + [DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC_NO_EXPLAIN]: string; // if NO selected in DENOMINATOR_DEFINE_TOTAL_TECH_SPEC - > explaination which populations are excluded + [DC.DENOMINATOR_DEFINE_HEALTH_HOME]: YesNo; // Does this denominator represent your total measure-eligible population? + [DC.DENOMINATOR_DEFINE_HEALTH_HOME_NO_EXPLAIN]: string; // if NO selected in DENOMINATOR_DEFINE_TOTAL_TECH_SPEC - > explaination which populations are excluded + [DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC_NO_SIZE]: string; // if NO selected in DENOMINATOR_DEFINE_TOTAL_TECH_SPEC - > explaination of the size of population excluded + [DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR]: Array< + | typeof DC.FFS + | typeof DC.PCCM + | typeof DC.MCO_PIHP + | typeof DC.ICM + | typeof DC.OTHER // which delivery systems are represented in the denominator? + >; + + [DC.HYBRID_MEASURE_POPULATION_INCLUDED]: string; // Section rendered in hybird data source measures + [DC.HYBRID_MEASURE_SAMPLE_SIZE]: string; // Section rendered in hybird data source measures + [DC.DEFINITION_OF_DENOMINATOR_SUBSET_EXPLAIN]: string; //Section rendered in child data source measures + [DC.DELIVERY_SYS_FFS]: YesNo; // If FFS selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> Is all of your FFS population included in this measure?" + [DC.DELIVERY_SYS_FFS_NO_PERCENT]: string; // If NO in DELIVERY_SYS_FFS -> what percent included in measure + [DC.DELIVERY_SYS_FFS_NO_POP]: string; // If NO in DELIVERY_SYS_FFS -> what number of your FFS population are included in the measure? + [DC.DELIVERY_SYS_PCCM]: YesNo; // If PCCM selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> Is all of your PCCM population included in this measure?" + [DC.DELIVERY_SYS_PCCM_NO_PERCENT]: string; // If NO in DELIVERY_SYS_PCCM -> what percent included in measure + [DC.DELIVERY_SYS_PCCM_NO_POP]: string; // if NO in DELIVERY_SYS_PCCM -> what number of your PCCM population are included in the measure? + [DC.DELIVERY_SYS_MCO_PIHP]: YesNo; // If MCO-PIHP selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR and Is all of your MCO-PIHP population included in this measure? + [DC.DELIVERY_SYS_MCO_PIHP_PERCENT]: string; // If MCO-PIHP selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> what percent + [DC.DELIVERY_SYS_MCO_PIHP_NUM_PLANS]: string; // If MCO-PIHP selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> what number + [DC.DELIVERY_SYS_MCO_PIHP_NO_INC]: string; // If NO in DELIVERY_SYS_MCO_PIHP -> percentage included + [DC.DELIVERY_SYS_MCO_PIHP_NO_EXCL]: string; // If NO in DELIVERY_SYS_MCO_PIHP -> number excluded + [DC.DELIVERY_SYS_ICM]: YesNo; // If ICM selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> Is all of your ICM population included in this measure?" + [DC.DELIVERY_SYS_ICM_NO_PERCENT]: string; // If NO in DELIVERY_SYS_ICM -> what percent included in measure + [DC.DELIVERY_SYS_ICM_NO_POP]: string; // If NO in DELIVERY_SYS_ICM -> what number of your ICM population are included in the measure? + [DC.DELIVERY_SYS_OTHER]: string; // If OTHER selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> describe the denominator + [DC.DELIVERY_SYS_OTHER_PERCENT]: string; // If OTHER selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> percentage represented + [DC.DELIVERY_SYS_OTHER_NUM_HEALTH_PLANS]: string; // If OTHER selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> number of health plans represented + [DC.DELIVERY_SYS_OTHER_POP]: string; // If OTHER selected in DELIVERY_SYS_REPRESENTATION_DENOMINATOR -> number of population represented +} + +export interface CombinedRates { + [DC.COMBINED_RATES]?: YesNo; // if the user combined rates from multiple reporting units + [DC.COMBINED_RATES_COMBINED_RATES]?: // if YES in COMBINED_RATES-> the reporting units they combined + | typeof DC.COMBINED_NOT_WEIGHTED_RATES + | typeof DC.COMBINED_WEIGHTED_RATES + | typeof DC.COMBINED_WEIGHTED_RATES_OTHER; + [DC.COMBINED_WEIGHTED_RATES_OTHER_EXPLAINATION]?: string; // if the user selected COMBINED_WEIGHTED_RATES_OTHER -> the explaination of the other weighing factor +} + +export interface OtherPerformanceMeasure { + [DC.OPM_EXPLAINATION]: string; + [DC.OPM_RATES]: OtherRatesFields[]; + [DC.OPM_NOTES]: string; + [DC.OPM_NOTES_TEXT_INPUT]: string; + [DC.OPM_HYBRID_EXPLANATION]?: string; +} + +type MonthYear = { + [DC.SELECTED_MONTH]: number; + [DC.SELECTED_YEAR]: number; +}; + +export interface DateRange { + [DC.DATE_RANGE]: { + [DC.END_DATE]: MonthYear; + [DC.START_DATE]: MonthYear; + }; + + [DC.MEASUREMENT_PERIOD_CORE_SET]: YesNo; // if state adhered to Core Set specifications in defining the measurement period for calculating measure +} + +export interface WhyAreYouNotReporting { + // if a user is not reporting -> the reason(s) they are not reporting + [DC.WHY_ARE_YOU_NOT_REPORTING]: Array< + | typeof DC.SERVICE_NOT_COVERED + | typeof DC.POP_NOT_COVERED + | typeof DC.DATA_NOT_AVAILABLE + | typeof DC.LIMITATION_WITH_DATA_COLLECTION + | typeof DC.SMALL_SAMPLE_SIZE + | typeof DC.OTHER + >; + + [DC.AMOUNT_OF_POP_NOT_COVERED]: // if POP_NOT_COVERED selected in WHY_ARE_YOU_NOT_REPORTING + typeof DC.ENTIRE_POP_NOT_COVERED | typeof DC.PARTIAL_POP_NOT_COVERED; + + [DC.PARTIAL_POP_NOT_COVERED_EXPLAINATION]: string; // if PARTIAL_POP_NOT_COVERED in AMOUNT_OF_POP_NOT_COVERED selected -> explaination of the population not covered + + // if DATA_NOT_AVAILABLE selected in WHY_ARE_YOU_NOT_REPORTING + [DC.WHY_IS_DATA_NOT_AVAILABLE]: Array< + | typeof DC.BUDGET_CONSTRAINTS + | typeof DC.STAFF_CONSTRAINTS + | typeof DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE + | typeof DC.DATA_INCONSISTENCIES_ACCURACY_ISSUES + | typeof DC.INFO_NOT_COLLECTED + | typeof DC.OTHER + >; + [DC.WHY_IS_DATA_NOT_AVAILABLE_OTHER]: string; // if OTHER selected in WHY_IS_DATA_NOT_AVAILABLE -> an explaination + [DC.DATA_INCONSISTENCIES_ACCURACY_ISSUES]: string; // if DATA_INCONSISTENCIES_ACCURACY_ISSUES selected in WHY_IS_DATA_NOT_AVAILABLE -> an explaination + [DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE]: Array< + | typeof DC.REQUIRES_MEDICAL_RECORD_REVIEW + | typeof DC.REQUIRES_DATA_LINKAGE + | typeof DC.OTHER // if DATA_SOURCE_NOT_EASILY_ACCESSIBLE selected in WHY_IS_DATA_NOT_AVAILABLE + >; + [DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE_OTHER]: string; // if OTHER selected in DATA_SOURCE_NOT_EASILY_ACCESSIBLE -> an explaination + [DC.INFO_NOT_COLLECTED]: Array< + typeof DC.NOT_COLLECTED_BY_PROVIDER | typeof DC.OTHER + >; + [DC.INFO_NOT_COLLECTED_OTHER]: string; // if OTHER selected in INFO_NOT_COLLECTED -> an explaination + [DC.LIMITATION_WITH_DATA_COLLECTION]: string; // if LIMITATION_WITH_DATA_COLLECTION selected in WHY_ARE_YOU_NOT_REPORTING -> an explaination + [DC.SMALL_SAMPLE_SIZE]: string; // if SMALL_SAMPLE_SIZE in WHY_ARE_YOU_NOT_REPORTING -> an explaination of sample size + [DC.WHY_ARE_YOU_NOT_REPORTING_OTHER]: string; // if OTHER selected in WHY_ARE_YOU_NOT_REPORTING -> an explaination +} + +export interface DidReport { + [DC.DID_REPORT]: YesNo; +} + +export interface DidCollect { + [DC.DID_COLLECT]: YesNo; +} + +export interface StatusOfData { + [DC.DATA_STATUS]: + | typeof DC.REPORTING_FINAL_DATA + | typeof DC.REPORTING_PROVISIONAL_DATA; + [DC.DATA_STATUS_PROVISIONAL_EXPLAINATION]: string; +} + +export interface DataSource { + [DC.DATA_SOURCE]: string[]; + [DC.DATA_SOURCE_SELECTIONS]: { + [label: string]: { + [DC.DESCRIPTION]: string; + [DC.SELECTED]: string[]; + }; + }; + [DC.DATA_SOURCE_DESCRIPTION]: string; + [DC.DATA_SOURCE_CAHPS_VERSION]?: string; + [DC.DATA_SOURCE_CAHPS_VERSION_OTHER]?: string; +} +export interface RateFields { + [DC.LABEL]?: string; + [DC.NUMERATOR]?: string; + [DC.DENOMINATOR]?: string; + [DC.RATE]?: string; + [DC.UID]?: string; +} + +export interface complexRateFields { + [DC.LABEL]?: string; + [DC.UID]?: string; + fields?: { [DC.LABEL]?: string; value: string | undefined }[]; +} + +export interface DeviationField { + [DC.DEVIATION_REASON]: string; +} +export interface DeviationFields { + [DC.OPTIONS]: string[]; + [DC.REASON]: string; +} +export interface OtherRatesFields { + [DC.DESCRIPTION]?: string; + [DC.RATE]?: RateFields[]; +} + +export type PerformanceMeasureRate = { + [label: string]: RateFields[] | undefined; +}; + +export interface PerformanceMeasure { + [DC.PERFORMANCE_MEASURE]?: { + [DC.EXPLAINATION]?: string; + [DC.RATES]?: PerformanceMeasureRate; + [DC.PMHYBRIDEXPLANATION]?: string; + }; + [DC.PERFORMANCE_MEASURE_APPLY_ALL_AGES]?: string; // Applicable to State Specific Measures +} +export namespace OmsNodes { + export interface OmsRateFields { + [DC.OPTIONS]?: string[]; + [DC.RATES]?: { + [ + category: string /** rate label will be some combination of ageRange_perfDesc or opmFieldLabel */ + ]: { + [qualifier: string]: RateFields[]; + }; + }; + [DC.TOTAL]?: RateFields[]; + } + + export interface LowLevelOmsNode { + [DC.RATE_DATA]?: OmsRateFields; // if just ndr sets + [DC.SUB_CAT_OPTIONS]?: string[]; // for additional subCats/add anothers + [DC.SUB_CATS]?: { + [DC.DESCRIPTION]?: string; + [DC.RATE_DATA]?: OmsRateFields; + }[]; + } + export interface MidLevelOMSNode extends LowLevelOmsNode { + // if sub-options + [DC.AGGREGATE]?: string; + [DC.LABEL]?: string; + [DC.OPTIONS]?: string[]; + [DC.SELECTIONS]?: { + [option: string]: LowLevelOmsNode; + }; + } + + export interface TopLevelOmsNode { + // top level child, ex: Race, Sex, Ethnicity + [DC.LABEL]?: string; + [DC.OPTIONS]?: string[]; // checkbox + [DC.ADDITIONAL_CATS]?: string[]; // add another section + [DC.SELECTIONS]?: { + [option: string]: MidLevelOMSNode; + }; + [DC.ADDITIONAL_SELECTIONS]?: AddtnlOmsNode[]; + + // catch case for ACA + [DC.RATE_DATA]?: OmsRateFields; + } + + export interface AddtnlOmsNode extends LowLevelOmsNode { + [DC.DESCRIPTION]?: string; + } +} + +export interface QualifierLabelData extends LabelData { + [DC.EXCLUDE_FROM_OMS]?: boolean; +} + +export interface Qualifiers { + [DC.QUALIFIERS]?: QualifierLabelData[]; +} + +export interface CategoryLabelData extends LabelData { + [DC.EXCLUDE_FROM_OMS]?: boolean; +} + +export interface Categories { + [DC.CATEGORIES]?: CategoryLabelData[]; +} + +export interface OptionalMeasureStratification { + [DC.OMS]: { + [DC.OPTIONS]: string[]; //checkbox + [DC.SELECTIONS]: { + [option: string]: OmsNodes.TopLevelOmsNode; + }; + }; +} +export interface DeviationFromMeasureSpecification { + [DC.DID_CALCS_DEVIATE]: YesNo; // does the calculation of the measure deviate from the measure specification + [DC.DEVIATION_OPTIONS]: string[]; // if YES selected from DID_CALCS_DEVIATE -> which deviations options selected + [DC.DEVIATION_REASON]: string; +} + +export namespace DataDrivenTypes { + export type OptionalMeasureStrat = OmsNode[]; + export type SingleOmsNode = OmsNode; + export type PerformanceMeasure = PerformanceMeasureData; + export type DataSource = DataSourceData; +} +export type DeviationKeys = + | "numerator" + | "denominator" + | "Other" + | "RateDeviationsSelected"; + +export type DefaultFormData = Types.AdditionalNotes & + DidCollect & + StatusOfData & + WhyAreYouNotReporting & + DidReport & + CombinedRates & + DateRange & + DefinitionOfPopulation & + MeasurementSpecification & + OtherPerformanceMeasure & + OptionalMeasureStratification & + PerformanceMeasure & + DeviationFromMeasureSpecification & + DataSource; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.test.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.test.tsx new file mode 100644 index 0000000000..34f3b36b53 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.test.tsx @@ -0,0 +1,84 @@ +import { AdministrativeQuestions } from "."; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import * as CUI from "@chakra-ui/react"; +import { fireEvent, screen } from "@testing-library/react"; + +const fieldKeys = [ + "numberOfAdults", + "minAgeOfAdults", + "numberOfChildren", + "maxAgeChildren", + "numberOfIndividuals", + "numberOfProviders", +]; + +const AdministrativeList = () => { + return ( + <CUI.OrderedList> + <AdministrativeQuestions /> + </CUI.OrderedList> + ); +}; + +describe("Test AdministrativeQuestions Component", () => { + beforeEach(() => { + renderWithHookForm(<AdministrativeList />); + }); + + it("Check that the input fields are rendered", () => { + fieldKeys.forEach((key) => { + expect(screen.getByLabelText(`AdministrativeData.${key}`)); + }); + }); + + it("Check entering a number into the input fields", () => { + fieldKeys.forEach((key) => { + const inputField = screen.getByLabelText(`AdministrativeData.${key}`); + fireEvent.change(inputField, { target: { value: "5" } }); + expect(inputField).toHaveDisplayValue("5"); + }); + }); +}); + +describe("Test Summation Of Total Number Of Individuals", () => { + beforeEach(() => { + renderWithHookForm(<AdministrativeList />); + }); + + it("Check auto sum of number of individuals", () => { + const numOfAdults = screen.getByLabelText( + `AdministrativeData.numberOfAdults` + ); + fireEvent.change(numOfAdults, { target: { value: "3" } }); + + const numOfChildren = screen.getByLabelText( + `AdministrativeData.numberOfChildren` + ); + fireEvent.change(numOfChildren, { target: { value: "5" } }); + + const numberOfIndividuals = screen.getByLabelText( + `AdministrativeData.numberOfIndividuals` + ); + expect(numberOfIndividuals).toHaveDisplayValue("8"); + }); + + it("Check number of individuals is changable after summation", () => { + const numOfAdults = screen.getByLabelText( + `AdministrativeData.numberOfAdults` + ); + fireEvent.change(numOfAdults, { target: { value: "3" } }); + + const numOfChildren = screen.getByLabelText( + `AdministrativeData.numberOfChildren` + ); + fireEvent.change(numOfChildren, { target: { value: "5" } }); + + const numberOfIndividuals = screen.getByLabelText( + `AdministrativeData.numberOfIndividuals` + ); + expect(numberOfIndividuals).toHaveDisplayValue("8"); + + fireEvent.change(numberOfIndividuals, { target: { value: "15" } }); + expect(numberOfIndividuals).toHaveDisplayValue("15"); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.tsx new file mode 100644 index 0000000000..ad8ac58e71 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.tsx @@ -0,0 +1,134 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import * as Common from "."; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { + allPositiveIntegersWith10Digits, + allPositiveIntegersWith3Digits, +} from "utils"; +import { useFormContext } from "react-hook-form"; +import { HHCSQualifierForm } from "../types"; + +export const AdministrativeQuestions = () => { + const register = useCustomRegister<Types.HHCSQualifierForm>(); + const padding = "10px"; + + const { setValue, watch } = useFormContext<HHCSQualifierForm>(); + const data = watch(); + + //function to only invoke when the value has changed for number of adult or number of children + //only want the function to run when the value of numberOfAdults or numberOfChildren change + //the numberOfIndividuals needs to allow overwrite from states; is NOT always the sum of children + adult + const sumOnChange = (v: any) => { + if (data.AdministrativeData) { + let name: string = v.target.name; + let numOfAdults = name.includes("numberOfAdults") + ? v.target.value + : data.AdministrativeData.numberOfAdults; + let numOfChildren = name.includes("numberOfChildren") + ? v.target.value + : data.AdministrativeData.numberOfChildren; + + let sum = parseInt(numOfAdults) + parseInt(numOfChildren); + data.AdministrativeData.numberOfIndividuals = sum ? sum.toString() : ""; + + setValue("AdministrativeData", data.AdministrativeData); + } + }; + + return ( + <CUI.ListItem mr="4"> + <Common.QualifierHeader + header="Administrative Questions" + description="" + /> + <QMR.NumberInput + {...register("AdministrativeData.numberOfAdults")} + mask={allPositiveIntegersWith10Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + onChange={sumOnChange} + label={ + <> + What is the total annual number of{" "} + <b> + <i>adults</i> + </b>{" "} + in the Health Home program? + </> + } + /> + <QMR.NumberInput + {...register("AdministrativeData.minAgeOfAdults")} + mask={allPositiveIntegersWith3Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + label={ + <> + The minimum age of an{" "} + <b> + <i>adult</i> + </b>{" "} + in the program is: + </> + } + /> + <QMR.NumberInput + {...register("AdministrativeData.numberOfChildren")} + onChange={sumOnChange} + mask={allPositiveIntegersWith10Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + label={ + <> + What is the total annual number of{" "} + <b> + <i>children</i> + </b>{" "} + in the Health Home program? + </> + } + /> + <QMR.NumberInput + {...register("AdministrativeData.maxAgeChildren")} + mask={allPositiveIntegersWith3Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + label={ + <> + The maximum age of a{" "} + <b> + <i>child</i> + </b>{" "} + in the program is: + </> + } + /> + <QMR.NumberInput + {...register("AdministrativeData.numberOfIndividuals")} + mask={allPositiveIntegersWith10Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + label={ + <> + What is the total annual number of{" "} + <b> + <i>individuals</i> + </b>{" "} + in the Health Home program? + </> + } + /> + <QMR.NumberInput + {...register("AdministrativeData.numberOfProviders")} + mask={allPositiveIntegersWith10Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + label={ + <> + What is the number of{" "} + <b> + <i>providers</i> + </b>{" "} + operating under the Health Home program? + </> + } + /> + </CUI.ListItem> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/audit.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/audit.tsx new file mode 100644 index 0000000000..4c1975e887 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/audit.tsx @@ -0,0 +1,168 @@ +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; + +import { HiX } from "react-icons/hi"; +import { useFieldArray } from "react-hook-form"; + +import { useGetMeasures } from "hooks/api"; +import { ICheckbox } from "components/MultiSelect"; +import { QualifierHeader } from "./qualifierHeader"; +import { measureDescriptions } from "measures/measureDescriptions"; + +export const initialAuditValues = { + MeasuresAuditedOrValidated: [], + WhoConductedAuditOrValidation: "", +}; + +export const CloseButton = ({ onClick }: { onClick: () => void }) => ( + <CUI.IconButton + fontSize="1.5em" + variant="ghost" + icon={<HiX />} + aria-label="Remove Audit Item" + onClick={onClick} + className="disabled-print-preview-items hidden-print-items" + /> +); + +interface Props { + type: "CH" | "AD" | "HH"; + year: string; +} + +export const Audit = ({ type, year }: Props) => { + const { fields, append, remove, replace } = useFieldArray({ + name: "CoreSetMeasuresAuditedOrValidatedDetails", + }); + const { data, isLoading } = useGetMeasures(); + + const multiSelectList: ICheckbox[] = + data?.Items + // filter out the autocompleted measures. + ?.filter((item: any) => { + return !item.autoCompleted; + }) + // filter out the qualifier measures + ?.filter((item: any) => { + return !item?.measure?.includes("CSQ"); + }) + // filter out HH user-created state specific measures + ?.filter((item: any) => { + return !item?.userCreated; + }) + // filter out placeholder HH user-created state specific measures + ?.filter((item: any) => { + return !item?.placeholder; + }) + ?.map((obj: any) => { + const desc = measureDescriptions?.[year]?.[obj.measure]; + return { + label: `${obj.measure}${desc ? ` - ${desc}` : ""}`, + value: obj.measure, + isVisible: true, + }; + }) ?? []; + + if (isLoading || !data.Items) { + return <QMR.LoadingWave />; + } + + return ( + <CUI.ListItem> + <QualifierHeader + header="Audit or Validation of Measures" + description={ + "Were any of the Core Set measures audited or validated" + + (type === "HH" ? " (optional)?" : "?") + } + /> + <CUI.Spacer /> + <CUI.Stack> + <CUI.Box pt="4"> + <QMR.RadioButton + formLabelProps={{ fontWeight: "600" }} + name="CoreSetMeasuresAuditedOrValidated" + options={[ + { + displayValue: + "Yes, some of the Core Set measures have been audited or validated", + value: + "Yes, some of the Core Set measures have been audited or validated", + children: [ + <CUI.Stack mb="5" spacing="6" key={"AuditSelectorStack"}> + {fields?.map((field, index: number) => { + return ( + <CUI.Box + borderWidth="1px" + borderColor="gray.200" + borderRadius="md" + key={field.id} + className="prince-gray-border" + > + <CUI.Flex className="prince-audit-padding"> + <QMR.TextInput + rules={{ required: true }} + formLabelProps={{ fontWeight: "400" }} + label="Who conducted the audit or validation?" + name={`CoreSetMeasuresAuditedOrValidatedDetails.${index}.WhoConductedAuditOrValidation`} + formControlProps={{ + p: "5", + pb: "0", + }} + /> + <CUI.Spacer /> + {index !== 0 && ( + <CloseButton onClick={() => remove(index)} /> + )} + </CUI.Flex> + <CUI.Box p="5"> + <CUI.Text + mb="4" + data-cy={`which-measures-did-they-audit-${index}`} + > + Which measures did they audit or validate? + </CUI.Text> + + <QMR.MultiSelect + isRequired + multiSelectList={multiSelectList} + name={`CoreSetMeasuresAuditedOrValidatedDetails.${index}.MeasuresAuditedOrValidated`} + /> + </CUI.Box> + </CUI.Box> + ); + })} + </CUI.Stack>, + <QMR.ContainedButton + buttonText={"+ Add Another"} + buttonProps={{ + variant: "outline", + colorScheme: "blue", + color: "blue.500", + }} + onClick={() => append(initialAuditValues)} + key={"AddAnotherAuditSelectorButton"} + />, + ], + onClick: () => { + if (fields.length === 0) { + replace(initialAuditValues); + } + }, + }, + { + displayValue: + "No, none of the Core Set measures have been audited or validated", + value: + "No, none of the Core Set measures have been audited or validated", + onClick: () => { + remove(); + }, + }, + ]} + /> + </CUI.Box> + </CUI.Stack> + </CUI.ListItem> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/costSavingsData.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/costSavingsData.tsx new file mode 100644 index 0000000000..6e1a848f2e --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/costSavingsData.tsx @@ -0,0 +1,39 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import * as DC from "dataConstants"; +import * as Common from "."; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { allPositiveIntegersWith10Digits } from "utils"; + +interface Props { + year: string; +} + +export const CostSavingsData = ({ year }: Props) => { + const register = useCustomRegister<Types.CostSavingsData>(); + const padding = "10px"; + + return ( + <CUI.ListItem mr="4"> + <Common.QualifierHeader header="Cost Savings Data" description="" /> + <QMR.NumberInput + {...register("yearlyCostSavings")} + mask={allPositiveIntegersWith10Digits} + formLabelProps={{ fontWeight: "400", padding: padding }} + label={`Amount of cost savings for FFY ${parseInt(year) - 1}`} + /> + <QMR.TextArea + {...register("costSavingsMethodology")} + label="Please describe your cost savings methodology:" + formLabelProps={{ fontWeight: "400", padding: padding }} + /> + <CUI.Box marginTop={10}> + <QMR.Upload + label="If you need additional space to provide information regarding cost savings data, please attach further documentation below." + {...register(DC.HEALTH_HOME_QUALIFIER_FILE_UPLOAD)} + /> + </CUI.Box> + </CUI.ListItem> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/externalContractor.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/externalContractor.tsx new file mode 100644 index 0000000000..b40be7979c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/externalContractor.tsx @@ -0,0 +1,70 @@ +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; +import * as Common from "."; + +export const ExternalContractor = () => { + return ( + <CUI.ListItem> + <Common.QualifierHeader + header="External Contractor" + description="Please indicate whether your state obtained assistance from one or + more external contractors in collecting, calculating, and/or reporting + Core Set data (optional)." + /> + <CUI.Spacer /> + <CUI.Stack> + <CUI.Box pl="5" my="5"> + <QMR.RadioButton + formLabelProps={{ fontWeight: "600" }} + label="" + name="WasExternalContractorUsed" + options={[ + { + displayValue: + "Yes, we did obtain assistance from one or more external contractors in collecting, calculating, and/or reporting Core Set data.", + value: "yes", + children: [ + <> + <CUI.Text>Select all that apply:</CUI.Text> + <QMR.Checkbox + name="ExternalContractorsUsed" + options={[ + { + displayValue: + "External Quality Review Organization (EQRO)", + value: "EQRO", + }, + { + displayValue: "MMIS Contractor", + value: "MMIS", + }, + { + displayValue: "Data Analytics Contractor", + value: "dataAnalytics", + }, + { + displayValue: "Other", + value: "Other", + children: [ + <QMR.TextArea + label="Please explain:" + name="OtherContractorDetails" + />, + ], + }, + ]} + /> + </>, + ], + }, + { + displayValue: "No, we calculated all the measures internally.", + value: "no", + }, + ]} + /> + </CUI.Box> + </CUI.Stack> + </CUI.ListItem> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/index.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/index.tsx new file mode 100644 index 0000000000..a0d43be84a --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/index.tsx @@ -0,0 +1,5 @@ +export * from "./qualifierHeader"; +export * from "./audit"; +export * from "./externalContractor"; +export * from "./costSavingsData"; +export * from "./administrativeQuestions"; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/Common/qualifierHeader.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/qualifierHeader.tsx new file mode 100644 index 0000000000..8cc6a2aa88 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/Common/qualifierHeader.tsx @@ -0,0 +1,18 @@ +import * as CUI from "@chakra-ui/react"; + +interface Props { + header: string; + description: string; +} +export const QualifierHeader = ({ header, description }: Props) => { + return ( + <CUI.Stack spacing="4" mt="10"> + <CUI.Text as="h2" fontWeight="bold"> + {header} + </CUI.Text> + <CUI.Text data-cy={"qualifier-header-description"} as="h3"> + {description} + </CUI.Text> + </CUI.Stack> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/data.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/data.ts new file mode 100644 index 0000000000..bb8ad809c8 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/data.ts @@ -0,0 +1,191 @@ +import { initialAuditValues } from "./Common"; + +export interface DataDriven { + title: string; + formData: any; + questionTitle: string; + qualifierHeader: (year: string) => string; + textTable: string[][]; + fieldValues: string[]; +} + +const AdultData: DataDriven = { + title: "Adult Core Set Qualifiers", + questionTitle: "Adult Core Set Questions", + qualifierHeader: (year) => + `As of September 30, ${year} what percentage of your Medicaid/CHIP enrollees (above age 21) were enrolled in each delivery system?`, + textTable: [["Ages 21 to 64"], ["Age 65 and older"]], + fieldValues: ["TwentyOneToSixtyFour", "GreaterThanSixtyFour"], + formData: { + CoreSetMeasuresAuditedOrValidatedDetails: [initialAuditValues], + PercentageEnrolledInEachDeliverySystem: [ + { + label: "Fee-for-Service", + TwentyOneToSixtyFour: "", + GreaterThanSixtyFour: "", + }, + { + label: "PCCM", + TwentyOneToSixtyFour: "", + GreaterThanSixtyFour: "", + }, + { + label: "Managed Care", + TwentyOneToSixtyFour: "", + GreaterThanSixtyFour: "", + }, + { + label: "Integrated Care Model (ICM)", + TwentyOneToSixtyFour: "", + GreaterThanSixtyFour: "", + }, + ], + }, +}; + +const ChildData: DataDriven = { + title: "Child Core Set Qualifiers: Medicaid & CHIP", + questionTitle: "Child Core Set Questions: Medicaid & CHIP", + qualifierHeader: (year) => + `As of September 30, ${year} what percentage of your Medicaid/CHIP enrollees (under age 21) were enrolled in each delivery system?`, + textTable: [ + ["Medicaid", "Under Age 21"], + ["CHIP", "Under Age 21"], + ], + fieldValues: ["UnderTwentyOneMedicaid", "UnderTwentyOneCHIP"], + formData: { + PercentageEnrolledInEachDeliverySystem: [ + { + label: "Fee-for-Service", + UnderTwentyOneMedicaid: "", + UnderTwentyOneCHIP: "", + }, + { + label: "PCCM", + UnderTwentyOneMedicaid: "", + UnderTwentyOneCHIP: "", + }, + { + label: "Managed Care", + UnderTwentyOneMedicaid: "", + UnderTwentyOneCHIP: "", + }, + { + label: "Integrated Care Model (ICM)", + UnderTwentyOneMedicaid: "", + UnderTwentyOneCHIP: "", + }, + ], + CoreSetMeasuresAuditedOrValidatedDetails: [initialAuditValues], + }, +}; + +const ChildChipData: DataDriven = { + title: "Child Core Set Qualifiers: CHIP", + questionTitle: "Child Core Set Questions: CHIP", + qualifierHeader: (year) => + `As of September 30, ${year} what percentage of your CHIP enrollees (under age 21) were enrolled in each delivery system?`, + textTable: [["CHIP", "Under Age 21"]], + fieldValues: ["UnderTwentyOne"], + formData: { + PercentageEnrolledInEachDeliverySystem: [ + { + label: "Fee-for-Service", + UnderTwentyOne: "", + }, + { + label: "PCCM", + UnderTwentyOne: "", + }, + { + label: "Managed Care", + UnderTwentyOne: "", + }, + { + label: "Integrated Care Model (ICM)", + UnderTwentyOne: "", + }, + ], + CoreSetMeasuresAuditedOrValidatedDetails: [initialAuditValues], + }, +}; + +const ChildMedicaidData: DataDriven = { + title: "Child Core Set Qualifiers: Medicaid", + questionTitle: "Child Core Set Questions: Medicaid", + qualifierHeader: (year) => + `As of September 30, ${year} what percentage of your Medicaid enrollees (under age 21) were enrolled in each delivery system (optional)?`, + textTable: [["Medicaid", "Under Age 21"]], + fieldValues: ["UnderTwentyOne"], + formData: { + PercentageEnrolledInEachDeliverySystem: [ + { + label: "Fee-for-Service", + UnderTwentyOne: "", + }, + { + label: "PCCM", + UnderTwentyOne: "", + }, + { + label: "Managed Care", + UnderTwentyOne: "", + }, + { + label: "Integrated Care Model (ICM)", + UnderTwentyOne: "", + }, + ], + CoreSetMeasuresAuditedOrValidatedDetails: [initialAuditValues], + }, +}; + +const HomeData: DataDriven = { + title: "Health Home Core Set Qualifiers", + questionTitle: `Health Home Core Set Questions: SPA ID:`, + qualifierHeader: (year) => + `As of September 30, ${year} what percentage of your Medicaid Health Home enrollees were enrolled in each delivery system?`, + textTable: [["Ages 0 to 17"], ["Ages 18 to 64"], ["Age 65 and older"]], + fieldValues: [ + "ZeroToSeventeen", + "EighteenToSixtyFour", + "GreaterThanSixtyFive", + ], + formData: { + PercentageEnrolledInEachDeliverySystem: [ + { + label: "Fee-for-Service", + ZeroToSeventeen: "", + EighteenToSixtyFour: "", + GreaterThanSixtyFive: "", + }, + { + label: "PCCM", + ZeroToSeventeen: "", + EighteenToSixtyFour: "", + GreaterThanSixtyFive: "", + }, + { + label: "Managed Care", + ZeroToSeventeen: "", + EighteenToSixtyFour: "", + GreaterThanSixtyFive: "", + }, + { + label: "Integrated Care Model (ICM)", + ZeroToSeventeen: "", + EighteenToSixtyFour: "", + GreaterThanSixtyFive: "", + }, + ], + CoreSetMeasuresAuditedOrValidatedDetails: [initialAuditValues], + }, +}; + +export const Data = { + HHCS: HomeData, + ACS: AdultData, + CCS: ChildData, + CCSC: ChildChipData, + CCSM: ChildMedicaidData, +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/deliverySystems.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/deliverySystems.tsx new file mode 100644 index 0000000000..88c56b97eb --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/deliverySystems.tsx @@ -0,0 +1,186 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import * as Common from "./Common"; +import { useFieldArray, useWatch } from "react-hook-form"; +import { DeliverySystem } from "./types"; +import { BsPercent } from "react-icons/bs"; +import { percentageAllowOneDecimalMax } from "utils"; +import { useUser } from "hooks/authHooks"; +import { UserRoles } from "types"; +import { DataDriven } from "./data"; + +const initialDeliverySystemValue = { + label: "", + TwentyOneToSixtyFour: "", + GreaterThanSixtyFour: "", +}; + +interface Props { + data: DataDriven; + year: string; +} + +export const DeliverySystems = ({ data, year }: Props) => { + const { userRole } = useUser(); + const { fields, append, remove } = useFieldArray({ + name: "PercentageEnrolledInEachDeliverySystem", + }); + + const values = useWatch({ name: "PercentageEnrolledInEachDeliverySystem" }); + + return ( + <CUI.ListItem mr="4"> + <Common.QualifierHeader + header="Delivery System" + description={data.qualifierHeader( + year ? `${parseInt(year) - 1}` : "2020" + )} + /> + <CUI.Table variant="simple" mt="4" size="md" verticalAlign="top"> + <CUI.Thead> + <CUI.Tr> + <CUI.Th key="labelRow" minWidth={"xs"}></CUI.Th> + {data.textTable.map((textTableArr, index) => { + return ( + <CUI.Th + key={`tabelHeaderContainer-${index}`} + textAlign={"center"} + > + {textTableArr.map((str, idx) => { + return ( + <CUI.Text + key={`labelRow.${index}.${idx}`} + data-cy={`labelRow.${index}.${idx}`} + fontSize={idx === 0 ? "md" : "sm"} + > + {str} + </CUI.Text> + ); + })} + </CUI.Th> + ); + })} + </CUI.Tr> + </CUI.Thead> + <CUI.Tbody> + {fields?.map((field, index: number) => ( + <CUI.Tr verticalAlign="top" key={field.id}> + <CUI.Td px="none"> + {index >= 4 ? ( + <QMR.TextInput + rules={{ required: true }} + name={`PercentageEnrolledInEachDeliverySystem.${index}.label`} + ariaLabel={`Percentage enrolled in each delivery system - ${ + values?.[index]?.label + ? `${values?.[index]?.label}` + : "Enter Custom Label" + }`} + /> + ) : ( + <QMR.TextInput + rules={{ required: true }} + textInputProps={{ + isReadOnly: true, + border: "none", + pl: "0", + tabIndex: -1, + }} + name={`PercentageEnrolledInEachDeliverySystem.${index}.label`} + ariaLabel={`Percentage enrolled in each delivery system - ${values?.[index]?.label}`} + /> + )} + </CUI.Td> + + {data.fieldValues.map((fieldValue, idx, arr) => { + return ( + <CUI.Td key={`dataField.${idx}`}> + {userRole === UserRoles.STATE_USER && ( + <QMR.DeleteWrapper + allowDeletion={index >= 4 && !!(idx === arr.length - 1)} + onDelete={() => remove(index)} + showText={false} + > + <QMR.NumberInput + displayPercent + name={`PercentageEnrolledInEachDeliverySystem.${index}.${fieldValue}`} + numberInputProps={{ textAlign: "right" }} + mask={percentageAllowOneDecimalMax} + ariaLabel={`Percentage enrolled in each delivery system - ${ + values?.[index]?.label + ? `${values?.[index]?.label} - ` + : "" + }${data.textTable[idx]}`} + /> + </QMR.DeleteWrapper> + )} + {/* only display to admin-type users (admin, approver, help desk, internal) */} + {userRole !== UserRoles.STATE_USER && ( + <QMR.NumberInput + displayPercent + name={`PercentageEnrolledInEachDeliverySystem.${index}.${fieldValue}`} + numberInputProps={{ textAlign: "right" }} + mask={percentageAllowOneDecimalMax} + /> + )} + </CUI.Td> + ); + })} + </CUI.Tr> + ))} + <CUI.Tr> + <CUI.Td px={"0"}> + <QMR.ContainedButton + buttonText={"+ Add Another"} + buttonProps={{ + variant: "outline", + colorScheme: "blue", + color: "blue.500", + isFullWidth: true, + }} + onClick={() => append(initialDeliverySystemValue)} + /> + </CUI.Td> + </CUI.Tr> + </CUI.Tbody> + <CUI.Tfoot borderTop="2px"> + <CUI.Tr> + <CUI.Th px="none" textTransform="none"> + <CUI.Text fontSize="medium" color="gray.900"> + Total + </CUI.Text> + </CUI.Th> + {data.fieldValues.map((field, idx) => { + const total = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr?.[field] || "0"); + }, + 0 + ); + + return ( + <CUI.Td key={`totalValField.${idx}`}> + <CUI.InputGroup> + <CUI.Input + isReadOnly + type="text" + value={total?.toFixed(1) ?? ""} + border="none" + textAlign="right" + fontWeight="bold" + tabIndex={-1} + /> + + <CUI.InputRightElement + pointerEvents="none" + children={<BsPercent />} + /> + </CUI.InputGroup> + </CUI.Td> + ); + })} + </CUI.Tr> + </CUI.Tfoot> + </CUI.Table> + </CUI.ListItem> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/index.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/index.tsx new file mode 100644 index 0000000000..e5204962c7 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/index.tsx @@ -0,0 +1,69 @@ +import * as QMR from "components"; +import * as Common from "./Common"; +import * as CUI from "@chakra-ui/react"; + +import { Data } from "./data"; +import { useEffect, useState } from "react"; +import { validationFunctions } from "./validationFunctions"; +import { DeliverySystems } from "./deliverySystems"; +import { useParams } from "react-router-dom"; +import * as Types from "types"; + +export * from "./data"; + +export const Qualifier = ({ + setValidationFunctions, + year, +}: QMR.MeasureWrapperProps) => { + const { coreSetId } = useParams(); + const [type, setType] = useState<"CH" | "AD" | "HH">("AD"); + const coreSet = (coreSetId?.split("_")?.[0] ?? + coreSetId) as Types.CoreSetAbbr; + + useEffect(() => { + if (setValidationFunctions && coreSetId) { + setValidationFunctions( + validationFunctions?.[ + (coreSetId?.split("_")?.[0] ?? coreSetId) as Types.CoreSetAbbr + ] ?? [] + ); + } + if ( + coreSetId === Types.CoreSetAbbr.CCS || + coreSetId === Types.CoreSetAbbr.CCSC || + coreSetId === Types.CoreSetAbbr.CCSM + ) { + setType("CH"); + } else if (coreSet === Types.CoreSetAbbr.HHCS) { + setType("HH"); + } + }, [setValidationFunctions, coreSetId, setType, coreSet]); + + const data = Data[coreSet]; + + return ( + <> + <CUI.VStack maxW="5xl" as="section" justifyContent={"space-between"}> + <CUI.Box mb="7" mt="3"> + <CUI.Text as="h1" fontSize="xl" mb="3" fontWeight="bold"> + {data?.title} + </CUI.Text> + <QMR.SupportLinks /> + {type === "HH" && <QMR.HealthHomeInfo />} + </CUI.Box> + <CUI.OrderedList> + {type === "HH" && ( + <> + <Common.AdministrativeQuestions /> + <Common.CostSavingsData year={year} /> + </> + )} + <DeliverySystems data={data} year={year} /> + <CUI.Spacer flex={2} /> + <Common.Audit type={type} year={year} /> + {type !== "HH" && <Common.ExternalContractor />} + </CUI.OrderedList> + </CUI.VStack> + </> + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/types.tsx b/services/ui-src/src/measures/2024/shared/Qualifiers/types.tsx new file mode 100644 index 0000000000..fe8b87420d --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/types.tsx @@ -0,0 +1,44 @@ +interface BaseQualifierForm { + AdministrativeData: AdministrativeQuestions; + CostSavingsData: CostSavingsData; + PercentageEnrolledInEachDeliverySystem: DeliverySystem[]; + CoreSetMeasuresAuditedOrValidated: string; + CoreSetMeasuresAuditedOrValidatedDetails: AuditDetails[]; + WasExternalContractorUsed: string; + ExternalContractorsUsed: string[]; + OtherContractorDetails: string; +} + +export interface HHCSQualifierForm extends BaseQualifierForm {} + +export interface ACSQualifierForm extends BaseQualifierForm {} + +export interface CCSQualifierForm extends BaseQualifierForm {} + +export interface CCSCQualifierForm extends BaseQualifierForm {} + +export interface CCSMQualifierForm extends BaseQualifierForm {} + +export interface DeliverySystem { + [type: string]: any; +} + +export interface AdministrativeQuestions { + numberOfAdults: string; + minAgeOfAdults: string; + numberOfChildren: string; + maxAgeChildren: string; + numberOfIndividuals: string; + numberOfProviders: string; +} + +export interface CostSavingsData { + yearlyCostSavings: number; + costSavingsMethodology: string; + costSavingsFile: File[]; +} + +export interface AuditDetails { + WhoConductedAuditOrValidation: string; + MeasuresAuditedOrValidated: string[]; +} diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validationFunctions.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validationFunctions.ts new file mode 100644 index 0000000000..0fba52e41c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validationFunctions.ts @@ -0,0 +1,9 @@ +import { ACS, CCS, CCSC, CCSM, HHCS } from "./validations"; + +export const validationFunctions = { + ACS, + CCS, + CCSC, + CCSM, + HHCS, +}; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validations/adult.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/adult.ts new file mode 100644 index 0000000000..83264d096c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/adult.ts @@ -0,0 +1,48 @@ +import { ACSQualifierForm } from "../types"; +import { DeliverySystem } from "../types"; + +const validate21To64EqualsToOneHundredPercent = (data: ACSQualifierForm) => { + const values = data["PercentageEnrolledInEachDeliverySystem"]; + const errorArray: any[] = []; + const total21To64Percent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.TwentyOneToSixtyFour || "0"); + }, + 0 + ); + + const total64PlusPercent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.GreaterThanSixtyFour || "0"); + }, + 0 + ); + + if (total21To64Percent === 0) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Ages 21 to 64 column must have values", + }); + } + if ( + (total21To64Percent < 99 || total21To64Percent > 101) && + total21To64Percent !== 0 + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Ages 21 to 64 column must total 100", + }); + } + if ( + (total64PlusPercent < 99 || total64PlusPercent > 101) && + total64PlusPercent !== 0 + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Age 65 and Older column must total 100", + }); + } + return errorArray.length ? errorArray : []; +}; + +export const ACS = [validate21To64EqualsToOneHundredPercent]; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCHIP.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCHIP.ts new file mode 100644 index 0000000000..5be9bc8e5d --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCHIP.ts @@ -0,0 +1,33 @@ +import { CCSCQualifierForm } from "../types"; +import { DeliverySystem } from "../types"; + +const validate21To64EqualsToOneHundredPercent = (data: CCSCQualifierForm) => { + const values = data["PercentageEnrolledInEachDeliverySystem"]; + const errorArray: any[] = []; + + const totalUnder21CHIPPercent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.UnderTwentyOne || "0"); + }, + 0 + ); + if (totalUnder21CHIPPercent === 0) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Under Age 21 CHIP are required.", + }); + } + + if ( + totalUnder21CHIPPercent > 0 && + (totalUnder21CHIPPercent < 99 || totalUnder21CHIPPercent > 101) + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Under Age 21 CHIP column must total 100", + }); + } + return errorArray.length ? errorArray : []; +}; + +export const CCSC = [validate21To64EqualsToOneHundredPercent]; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCombined.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCombined.ts new file mode 100644 index 0000000000..0172bf657e --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCombined.ts @@ -0,0 +1,51 @@ +import { CCSQualifierForm } from "../types"; +import { DeliverySystem } from "../types"; + +const validate21To64EqualsToOneHundredPercent = (data: CCSQualifierForm) => { + const values = data["PercentageEnrolledInEachDeliverySystem"]; + const errorArray: any[] = []; + const totalUnder21MedicaidPercent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.UnderTwentyOneMedicaid || "0"); + }, + 0 + ); + + const totalUnder21CHIPPercent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.UnderTwentyOneCHIP || "0"); + }, + 0 + ); + + if (totalUnder21MedicaidPercent === 0 && totalUnder21CHIPPercent === 0) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: + "Entries are required in at least one column. Entries are permitted in the second column but are not required", + }); + } + + if ( + (totalUnder21MedicaidPercent > 0 && totalUnder21MedicaidPercent < 99) || + totalUnder21MedicaidPercent > 101 + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Medicaid Under Age 21 column must total 100", + }); + } + + if ( + totalUnder21CHIPPercent > 0 && + (totalUnder21CHIPPercent < 99 || totalUnder21CHIPPercent > 101) + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for CHIP Under Age 21 column must total 100", + }); + } + return errorArray.length ? errorArray : []; +}; + +export const CCS = [validate21To64EqualsToOneHundredPercent]; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childMedicaid.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childMedicaid.ts new file mode 100644 index 0000000000..cd7847d69d --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/childMedicaid.ts @@ -0,0 +1,33 @@ +import { CCSMQualifierForm, DeliverySystem } from "../types"; + +const validate21To64EqualsToOneHundredPercent = (data: CCSMQualifierForm) => { + const values = data["PercentageEnrolledInEachDeliverySystem"]; + const errorArray: any[] = []; + + const totalUnder21MedicaidPercent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.UnderTwentyOne || "0"); + }, + 0 + ); + + if (totalUnder21MedicaidPercent === 0) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Under Age 21 Medicaid are required.", + }); + } + + if ( + totalUnder21MedicaidPercent > 0 && + (totalUnder21MedicaidPercent < 99 || totalUnder21MedicaidPercent > 101) + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Under Age 21 Medicaid column must total 100", + }); + } + return errorArray.length ? errorArray : []; +}; + +export const CCSM = [validate21To64EqualsToOneHundredPercent]; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validations/healthHome.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/healthHome.ts new file mode 100644 index 0000000000..ddf729170c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/healthHome.ts @@ -0,0 +1,79 @@ +import { HHCSQualifierForm, DeliverySystem } from "../types"; + +const validateEqualsToOneHundredPercent = (data: HHCSQualifierForm) => { + const values = data["PercentageEnrolledInEachDeliverySystem"]; + const errorArray: any[] = []; + const total0To17Percent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.ZeroToSeventeen || "0"); + }, + 0 + ); + + const total18To64Percent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.EighteenToSixtyFour || "0"); + }, + 0 + ); + + const total65PlusPercent = values?.reduce( + (acc: number, curr: DeliverySystem) => { + return acc + parseFloat(curr.GreaterThanSixtyFive || "0"); + }, + 0 + ); + if ( + (total0To17Percent < 99 || total0To17Percent > 101) && + total0To17Percent !== 0 + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Ages 0 to 17 column must total 100", + }); + } + if ( + (total18To64Percent < 99 || total18To64Percent > 101) && + total18To64Percent !== 0 + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Ages 18 to 64 column must total 100", + }); + } + + if ( + (total65PlusPercent < 99 || total65PlusPercent > 101) && + total65PlusPercent !== 0 + ) { + errorArray.push({ + errorLocation: "Delivery System", + errorMessage: "Entries for Age 65 and Older column must total 100", + }); + } + + return errorArray.length ? errorArray : []; +}; + +const validateTotalNumberOfIndividuals = (data: HHCSQualifierForm) => { + const adults = parseInt(data.AdministrativeData.numberOfAdults) || 0; + const children = parseInt(data.AdministrativeData.numberOfChildren) || 0; + const totalIndividuals = + parseInt(data.AdministrativeData.numberOfIndividuals) || 0; + const errorArray: any[] = []; + + if (adults + children !== totalIndividuals) { + errorArray.push({ + errorLocation: "Administrative Questions", + errorMessage: + "The sum of adults and children did not equal total individuals", + }); + } + + return errorArray.length ? errorArray : []; +}; + +export const HHCS = [ + validateEqualsToOneHundredPercent, + validateTotalNumberOfIndividuals, +]; diff --git a/services/ui-src/src/measures/2024/shared/Qualifiers/validations/index.ts b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/index.ts new file mode 100644 index 0000000000..fd0c7694b7 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/Qualifiers/validations/index.ts @@ -0,0 +1,5 @@ +export * from "./adult"; +export * from "./childCombined"; +export * from "./childCHIP"; +export * from "./childMedicaid"; +export * from "./healthHome"; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexAtLeastOneRateComplete/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexAtLeastOneRateComplete/index.tsx new file mode 100644 index 0000000000..eae77b30a7 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexAtLeastOneRateComplete/index.tsx @@ -0,0 +1,66 @@ +import { validatePartialRateCompletionPM } from "../../validatePartialRateCompletion"; + +/* At least one NDR set must be complete (OPM or PM) */ +export const ComplexAtLeastOneRateComplete = ( + performanceMeasureArray: any, + OPM: any, + errorLocation: string = "Performance Measure/Other Performance Measure" +) => { + let error = true; + let partialError = false; + let errorArray: FormError[] = []; + + // Check OPM first + OPM && + OPM.forEach((measure: any) => { + if (measure?.rate && measure?.rate?.[0]?.rate) { + error = false; + } + }); + + // Check regular Performance Measures if cannot validate OPM + // For each Performance Measure + // Check that the performance measure has a field representation for each age groups + // Check that each field has a "value" and it is not an empty string + // For a complete measure the sum of the booleans will equal the length of the age groups + for (const category of performanceMeasureArray) { + for (const qualifier of category) { + const qualComplete = qualifier.fields.every( + (field: { value: string; label: string }) => { + return field.value !== undefined && field.value !== ""; + } + ); + if ( + !qualComplete && + qualifier.fields.some((field: { value?: string; label?: string }) => { + return !!(field?.value !== undefined && field?.value !== ""); + }) + ) { + partialError = true; + } + if (qualComplete) { + error = false; + } + } + } + + if (partialError) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: `Should not have partially filled data sets.`, + }); + } + + if (error) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: "At least one set of fields must be complete.", + }); + } + + if (OPM) { + errorArray.push(...validatePartialRateCompletionPM([], OPM, [])); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexNoNonZeroNumOrDenom/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexNoNonZeroNumOrDenom/index.tsx new file mode 100644 index 0000000000..eeb57efa9c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexNoNonZeroNumOrDenom/index.tsx @@ -0,0 +1,105 @@ +interface NDRforumla { + numerator: number; + denominator: number; + rateIndex: number; +} + +export const ComplexNoNonZeroNumOrDenomOMS = ( + rateData: any, + OPM: any, + ndrFormulas: NDRforumla[], + errorLocation: string +) => { + let errorArray: any[] = []; + for (const key in rateData) { + if (OPM && OPM.length > 0) { + errorArray.push( + ...ComplexNoNonZeroNumOrDenom( + [], + [{ description: key, rate: [...rateData[key]["OPM"]] }], + ndrFormulas, + `${errorLocation} - ${key}` + ) + ); + } else { + for (const category in rateData[key]) { + errorArray.push( + ...ComplexNoNonZeroNumOrDenom( + [rateData[key][category]], + false, + ndrFormulas, + `${errorLocation} - ${key} - ${category}` + ) + ); + } + } + } + + return errorArray; +}; + +/* Validation for manually entered rates */ +export const ComplexNoNonZeroNumOrDenom = ( + performanceMeasureArray: any, + OPM: any, + ndrFormulas: NDRforumla[], + errorLocation: string = "Performance Measure/Other Performance Measure" +) => { + let nonZeroRateError = false; + let zeroRateError = false; + let errorArray: any[] = []; + + if (!OPM) { + for (const category of performanceMeasureArray) { + if (category && category.length > 0) { + for (const qualifier of category) { + for (const formula of ndrFormulas) { + const numerator = qualifier.fields[formula.numerator]?.value; + const denominator = qualifier.fields[formula.denominator]?.value; + const rate = qualifier.fields[formula.rateIndex]?.value; + + if (numerator && denominator && rate) { + if (parseFloat(numerator) === 0 && parseFloat(rate) !== 0) + nonZeroRateError = true; + if ( + parseFloat(rate) === 0 && + parseFloat(numerator) !== 0 && + parseFloat(denominator) !== 0 + ) + zeroRateError = true; + } + } + } + } + } + } + OPM && + OPM.forEach((performanceMeasure: any) => { + performanceMeasure.rate?.forEach((rate: any) => { + if (parseFloat(rate.numerator) === 0 && parseFloat(rate.rate) !== 0) { + nonZeroRateError = true; + } + + if ( + parseFloat(rate.numerator) !== 0 && + parseFloat(rate.denominator) !== 0 && + parseFloat(rate.rate) === 0 + ) { + zeroRateError = true; + } + }); + }); + if (nonZeroRateError) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: `Manually entered rate should be 0 if numerator is 0`, + }); + } + if (zeroRateError) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: `Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation.`, + }); + } + return zeroRateError || nonZeroRateError ? errorArray : []; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateDualPopInformation/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateDualPopInformation/index.tsx new file mode 100644 index 0000000000..bb68513969 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateDualPopInformation/index.tsx @@ -0,0 +1,60 @@ +import * as DC from "dataConstants"; +import { FormRateField } from "measures/2024/shared/globalValidations/types"; + +export const ComplexValidateDualPopInformation = ( + performanceMeasureArray: any, + OPM: any, + DefinitionOfDenominator: string[] | undefined, + errorReplacementText: string = "Age 65 and Older" +) => { + if (OPM) { + return []; + } + + const dualEligible = DefinitionOfDenominator + ? DefinitionOfDenominator.indexOf( + DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE + ) !== -1 + : false; + + const errorArray: FormError[] = []; + const filledInData: FormRateField[] = []; + const age65Data: FormRateField[] = []; + + performanceMeasureArray.forEach((cat: any) => { + cat?.forEach((qual: any) => { + if (qual?.label === "Age 65 and older") age65Data.push(qual); + if (qual?.label === "Ages 65 to 74") age65Data.push(qual); + if (qual?.label === "Ages 75 to 84") age65Data.push(qual); + if (qual?.label === "Age 85 and older") age65Data.push(qual); + }); + }); + + const allFieldsComplete = (qual: { + fields: { label: string; value: string }[]; + }) => { + return qual.fields.every( + (field) => field.value !== undefined && field.value !== "" + ); + }; + + age65Data.forEach((qual: any) => { + if (qual && allFieldsComplete(qual)) filledInData.push(qual); + }); + + if (!dualEligible && filledInData.length > 0) { + errorArray.push({ + errorLocation: "Performance Measure", + errorMessage: `Information has been included in the ${errorReplacementText} Performance Measure but the checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is missing`, + errorType: "Warning", + }); + } + if (dualEligible && filledInData.length === 0) { + errorArray.push({ + errorLocation: "Performance Measure", + errorMessage: `The checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is checked but you are missing performance measure data for ${errorReplacementText}`, + errorType: "Warning", + }); + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateNDRTotals/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateNDRTotals/index.tsx new file mode 100644 index 0000000000..de6f1de5cb --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateNDRTotals/index.tsx @@ -0,0 +1,140 @@ +import { LabelData } from "utils"; +import * as DC from "dataConstants"; + +interface NDRforumla { + numerator: number; + denominator: number; + rateIndex: number; +} + +interface Qualifier { + fields: Field[]; + label: string; +} + +interface Field { + label: string; + value?: string; +} + +/* At least one NDR set must be complete (OMS) */ +export const ComplexValidateNDRTotalsOMS = ( + rateData: any, + categories: LabelData[], + ndrFormulas: NDRforumla[], + errorLocation: string +) => { + // Using a subset of rateData as iterator to be sure that Total + // is always at the end of the category array. + const qualifierObj = { ...rateData }; + delete qualifierObj["Total"]; + const totalData = rateData["Total"]; + const categoryID = categories[0]?.id ? categories[0].id : DC.SINGLE_CATEGORY; + + // build performanceMeasureArray + let performanceMeasureArray = []; + const cleanedCategories = categories; + if (cleanedCategories.length > 0) { + for (const cat of cleanedCategories) { + let row = []; + for (const q in qualifierObj) { + const qual = qualifierObj[q]?.[cat.id]?.[0] ?? {}; + if (qual) { + row.push(qual); + } + } + if (row) { + row.push(totalData[cat.id][0]); + performanceMeasureArray.push(row); + } + } + } else { + let row = []; + for (const q in qualifierObj) { + const qual = qualifierObj[q]?.[categoryID]?.[0] ?? {}; + if (qual) { + row.push(qual); + } + } + if (row) { + row.push(totalData[categoryID][0]); + performanceMeasureArray.push(row); + } + } + + let errorArray: any[] = ComplexValidateNDRTotals( + performanceMeasureArray, + categories, + ndrFormulas, + `${errorLocation}` + ); + + return errorArray; +}; + +/* Validate Totals have data if qualifiers in section have data + * and validate Total is equal to the sum of other qualifiers in section + */ +export const ComplexValidateNDRTotals = ( + performanceMeasureArray: any, + categories: LabelData[], + ndrFormulas: NDRforumla[], + errorLocation: string = "Performance Measure Total" +) => { + let errorArray: any[] = []; + const rateLocations = ndrFormulas.map((ndr: NDRforumla) => ndr.rateIndex); + + performanceMeasureArray.forEach((category: Qualifier[], i: number) => { + // Sum all fields for each qualifier + let categorySums: any[] = []; + for (const qualifier of category.slice(0, -1)) { + if (qualifier?.fields?.every((f: Field) => !!f?.value)) { + qualifier?.fields?.forEach((field: Field, x: number) => { + if (field?.value) { + categorySums[x] ??= 0; + categorySums[x] += parseFloat(field.value); + } + }); + } + } + + // Compare calculated sums to values in Total qualifier + const categoryTotal = category.slice(-1)[0]; + if ( + categorySums.length > 0 && + !categoryTotal?.fields.every( + (field) => field.value !== undefined && field.value !== "" + ) + ) { + errorArray.push({ + errorLocation: `${errorLocation} - ${ + categories[i].label ? categories[i].label : "" + }`, + errorMessage: `Total ${ + categories[i].label ? categories[i].label : "" + } must contain values if other fields are filled.`, + }); + } else { + categoryTotal?.fields?.forEach((field: Field, x: number) => { + if ( + !rateLocations.includes(x) && + field?.value && + categorySums[x] !== parseFloat(field.value) + ) { + errorArray.push({ + errorLocation: `${errorLocation} - ${ + categories[i].label ? categories[i].label : "" + }`, + errorMessage: `Total ${ + field.label + } is not equal to the sum of other "${field.label}" fields in ${ + categories[i].label ? categories[i].label : "" + } section.`, + }); + } + }); + } + }); + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValueSameCrossCategory/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValueSameCrossCategory/index.tsx new file mode 100644 index 0000000000..a7c536c968 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValueSameCrossCategory/index.tsx @@ -0,0 +1,110 @@ +import { LabelData } from "utils"; + +export const ComplexValueSameCrossCategoryOMS = ( + rateData: any, + categories: LabelData[], + qualifiers: LabelData[], + errorLocation: string +) => { + // Using a subset of rateData as iterator to be sure that Total + // is always at the end of the category array. + const qualifierObj = { ...rateData }; + delete qualifierObj["Total"]; + const totalData = rateData["Total"]; // quick reference variable + + const qualifierLabels: any = {}; + for (const q of qualifiers) { + qualifierLabels[q.id] = q.label; + } + + // build performanceMeasureArray + let performanceMeasureArray = []; + for (const cat of categories) { + let row = []; + for (const q in qualifierObj) { + const qual = qualifierObj[q]?.[cat.id]?.[0]; + if (qual) { + qual.label = qualifierLabels[q]; + row.push(qual); + } + } + // only need to add total data if other data exists + if (row.length > 0) { + const catTotal = { ...totalData[cat.id][0] }; + catTotal.label = "Total"; + row.push(catTotal); + performanceMeasureArray.push(row); + } + } + + let errorArray: any[] = ComplexValueSameCrossCategory({ + rateData: performanceMeasureArray, + OPM: undefined, + errorLocation, + }); + return errorArray; +}; + +interface Props { + rateData: any; + OPM: any; + fieldIndex?: number; + fieldLabel?: string; + errorLocation?: string; +} + +/* + * Validate that the value of a given field with a given qualifier is consistent + * across all measurement categories. + * + * Ex - "Number of Enrollee Months" in Inpatient ages 0-17 === Medicine ages 0-17 + */ +export const ComplexValueSameCrossCategory = ({ + rateData, + OPM, + fieldIndex = 0, + fieldLabel = "Number of Enrollee Months", + errorLocation = "Performance Measure/Other Performance Measure", +}: Props) => { + let errorArray: any[] = []; + if (!OPM) { + const tempValues: { + [cleanedQualifier: string]: { + value: string; + label: string; + error?: boolean; + }; + } = {}; + for (const category of rateData) { + for (const qualifier of category.slice(0, -1)) { + const cleanQual = qualifier.uid.split(".")[1]; + if (tempValues[cleanQual]?.value) { + if ( + qualifier.fields[fieldIndex]?.value && + tempValues[cleanQual].value !== qualifier.fields[fieldIndex]?.value + ) { + // Set an error if the qualifier does not match tempValues + tempValues[cleanQual].error = true; + } + } else { + // Set tempValues[cleanQual] to be used in future checks against other qualifiers + tempValues[cleanQual] = { + value: qualifier.fields[fieldIndex]?.value, + label: qualifier.label, + }; + } + } + } + + // Using tempValues as a reference prevents multiple error messages per qualifier + for (const tempValue in tempValues) { + if (tempValues[tempValue]?.error) { + errorArray.push({ + errorLocation, + errorMessage: `Value of "${fieldLabel}" in ${tempValues[tempValue].label} must be the same across all measurement categories.`, + }); + } + } + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRatLeastOneRateComplete/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRatLeastOneRateComplete/index.tsx new file mode 100644 index 0000000000..a6fed647b7 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRatLeastOneRateComplete/index.tsx @@ -0,0 +1,52 @@ +import { LabelData } from "utils"; +import { validatePartialRateCompletionPM } from "../../validatePartialRateCompletion"; + +/* At least one NDR set must be complete (OPM or PM) */ +export const PCRatLeastOneRateComplete = ( + performanceMeasureArray: any, + OPM: any, + ageGroups: LabelData[], + errorLocation: string = "Performance Measure/Other Performance Measure", + omsFlag = false +) => { + let error = true; + let errorArray: FormError[] = []; + + // Check OPM first + if (OPM) { + error = !OPM.some( + (measure: any) => !!(measure?.rate && measure?.rate?.[0]?.rate) + ); + } + + // Check regular Performance Measures if cannot validate OPM + // For each Performance Measure + // Check that the performance measure has a field representation for each age groups + // Check that each field has a "value" and it is not an empty string + // For a complete measure the sum of the booleans will equal the length of the age groups + if (error) { + performanceMeasureArray?.forEach((_performanceObj: any) => { + if (_performanceObj.length === ageGroups.length) { + const values = _performanceObj.map((obj: any) => { + if (obj?.value && obj.value) return true; + return false; + }); + const sum = values.reduce((x: any, y: any) => x + y); + if (sum === ageGroups.length) error = false; + } + }); + } + + if (error) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: "All data fields must be completed.", + }); + } + + if (OPM && !omsFlag) { + errorArray.push(...validatePartialRateCompletionPM([], OPM, ageGroups)); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRnoNonZeroNumOrDenom/index.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRnoNonZeroNumOrDenom/index.tsx new file mode 100644 index 0000000000..4df62757d8 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRnoNonZeroNumOrDenom/index.tsx @@ -0,0 +1,71 @@ +interface NDRforumla { + numerator: number; + denominator: number; + rateIndex: number; +} + +/* Validation for manually entered rates */ +export const PCRnoNonZeroNumOrDenom = ( + performanceMeasureArray: any, + OPM: any, + ndrFormulas: NDRforumla[], + errorLocation: string = "Performance Measure/Other Performance Measure" +) => { + let nonZeroRateError = false; + let zeroRateError = false; + let errorArray: any[] = []; + performanceMeasureArray?.forEach((performanceMeasure: any) => { + if (performanceMeasure && performanceMeasure.length > 0) { + ndrFormulas.forEach((ndr: NDRforumla) => { + if ( + performanceMeasure[ndr.numerator].value && + performanceMeasure[ndr.denominator].value && + performanceMeasure[ndr.rateIndex].value + ) { + if ( + parseFloat(performanceMeasure[ndr.rateIndex].value!) !== 0 && + parseFloat(performanceMeasure[ndr.numerator].value!) === 0 + ) { + nonZeroRateError = true; + } + if ( + parseFloat(performanceMeasure[ndr.rateIndex].value!) === 0 && + parseFloat(performanceMeasure[ndr.numerator].value!) !== 0 && + parseFloat(performanceMeasure[ndr.denominator].value!) !== 0 + ) { + zeroRateError = true; + } + } + }); + } + }); + + OPM && + OPM.forEach((performanceMeasure: any) => { + performanceMeasure.rate?.forEach((rate: any) => { + if (parseFloat(rate.numerator) === 0 && parseFloat(rate.rate) !== 0) { + nonZeroRateError = true; + } + if ( + parseFloat(rate.numerator) !== 0 && + parseFloat(rate.denominator) !== 0 && + parseFloat(rate.rate) === 0 + ) { + zeroRateError = true; + } + }); + }); + if (nonZeroRateError) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: `Manually entered rate should be 0 if numerator is 0`, + }); + } + if (zeroRateError) { + errorArray.push({ + errorLocation: errorLocation, + errorMessage: `Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation.`, + }); + } + return zeroRateError || nonZeroRateError ? errorArray : []; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.test.ts new file mode 100644 index 0000000000..fb7daecb8d --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.test.ts @@ -0,0 +1,82 @@ +import { SINGLE_CATEGORY, PERFORMANCE_MEASURE } from "dataConstants"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +import { + generateOmsQualifierRateData, + generatePmQualifierRateData, + simpleRate, +} from "utils/testUtils/2024/validationHelpers"; +import { + convertOmsDataToRateArray, + getPerfMeasureRateArray, + omsLocationDictionary, + performanceMeasureErrorLocationDicitonary, +} from "./dataDrivenTools"; + +describe("Test Data Driven Tools", () => { + const categories = [ + { id: "TestCat1", text: "TestCat1", label: "TestCat1" }, + { id: "TestCat2", text: "TestCat2", label: "TestCat2" }, + ]; + const qualifiers = [ + { id: "TestQual1", text: "TestQual1", label: "TestQual1" }, + { id: "TestQual2", text: "TestQual2", label: "TestQual2" }, + ]; + + describe("convertOmsDataToRateArray", () => { + it("should take an oms structure and return an array or rate arrays", () => { + const rateData = generateOmsQualifierRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const arr = convertOmsDataToRateArray(categories, qualifiers, rateData); + expect(arr.length).toBe(2); + expect(arr[0].length).toBe(2); + expect(arr[0][0]).toBe(simpleRate); + }); + + it("should return an empty array if no data", () => { + const arr = convertOmsDataToRateArray(categories, qualifiers, {}); + expect(arr.length).toBe(2); + expect(arr[0].length).toBe(2); + expect(JSON.stringify(arr[0][0])).toBe("{}"); + }); + }); + + describe("omsLocationDictionary", () => { + it("should make a dictionary function", () => { + const func = omsLocationDictionary(OMSData(), qualifiers, categories); + expect(func(qualifiers.map((item) => item.label))).toBe( + `${qualifiers[0].label} - ${qualifiers[1].label}` + ); + expect(func(categories.map((item) => item.label))).toBe( + `${categories[0].label} - ${categories[1].label}` + ); + expect(func([qualifiers[0].label])).toBe(qualifiers[0].label); + }); + }); + + describe("getPerfMeasureRateArray", () => { + it("should return a rate field double array", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + simpleRate, + simpleRate, + ]); + const arr = getPerfMeasureRateArray(data, { categories, qualifiers }); + expect(arr.length).toBe(2); + expect(arr[0].length).toBe(2); + expect(arr[0][0]).toBe(simpleRate); + }); + }); + + describe("performanceMeasureErrorLocationDicitonary", () => { + it("should generate a location dictionary", () => { + const dictionary = performanceMeasureErrorLocationDicitonary({ + categories, + qualifiers, + }); + expect(dictionary?.[categories[0].id]).toBe(categories[0].label); + expect(dictionary?.[categories[1].id]).toBe(categories[1].label); + expect(dictionary?.[SINGLE_CATEGORY]).toBe(PERFORMANCE_MEASURE); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.ts b/services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.ts new file mode 100644 index 0000000000..d4d58bd4dd --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.ts @@ -0,0 +1,135 @@ +import * as DC from "dataConstants"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { DataDrivenTypes as DDT } from "measures/2024/shared/CommonQuestions/types"; +import { LabelData } from "utils"; +import { FormRateField as PM, RateData } from "./types"; + +/** + * Extracts Performance Measure Rates into double array for validation. + * Should be in order of category string array. + * If no categories, grabs singleCat backup from data. + */ +export const getPerfMeasureRateArray = ( + formData: Types.PerformanceMeasure, + renderData: DDT.PerformanceMeasure +) => { + const performanceMeasureData: PM[][] = []; + + if (renderData.categories?.length) { + for (const cat of renderData.categories) { + performanceMeasureData.push( + formData.PerformanceMeasure?.rates?.[cat.id] ?? [] + ); + } + } else if (renderData.qualifiers?.length) { + performanceMeasureData.push( + formData.PerformanceMeasure?.rates?.[DC.SINGLE_CATEGORY] ?? [] + ); + } + + return performanceMeasureData; +}; + +/** + * Extracts OPM into double array for validation. + */ +export const getOtherPerformanceMeasureRateArray = ( + opmRates: Types.OtherRatesFields[] +) => { + const otherPmData: PM[][] = []; + if (opmRates && opmRates?.length) { + for (const rates of opmRates) { + if (rates.rate) { + otherPmData.push(rates.rate); + } + } + } + return otherPmData; +}; + +/** Utility function for converting oms data to be the same as returned performance measure. Encourages shared validations. */ +export const convertOmsDataToRateArray = ( + categories: LabelData[], + qualifiers: LabelData[], + rateData: RateData +) => { + const rateArray: PM[][] = []; + + for (const cat of categories) { + const tempArr: PM[] = []; + for (const qual of qualifiers) { + tempArr.push(rateData.rates?.[cat.id]?.[qual.id]?.[0] ?? {}); + } + rateArray.push(tempArr); + } + + return rateArray; +}; + +interface PMErrorDictionary { + [cleanedLabel: string]: string; +} + +/** Map the user readable location category to the cleaned category used for data storage. */ +export const performanceMeasureErrorLocationDicitonary = ( + renderData: DDT.PerformanceMeasure +) => { + const errorDict: PMErrorDictionary = {}; + + for (const cat of renderData?.categories ?? []) { + errorDict[cat.id] = cat.label; + } + + errorDict[DC.SINGLE_CATEGORY] = DC.PERFORMANCE_MEASURE; + + return errorDict; +}; + +/** + * Takes render data for OMS and creates a cleaned dictionary of node locations for error generation. + */ +export const omsLocationDictionary = ( + renderData: DDT.OptionalMeasureStrat, + qualifiers?: LabelData[], + categories?: LabelData[] +) => { + const dictionary: { [cleanedLabel: string]: string } = {}; + const checkNode = (node: DDT.SingleOmsNode) => { + // dive a layer + for (const option of node.options ?? []) { + checkNode(option); + } + dictionary[node.id] = node.label; + }; + + for (const node of renderData) { + checkNode(node); + } + + for (const qual of qualifiers ?? []) { + dictionary[qual.id] = qual.label; + } + + for (const cat of categories ?? []) { + dictionary[cat.id] = cat.label; + } + + return (labels: string[]) => + labels.reduce((prevValue, currentValue, index) => { + if (index === 0) { + return dictionary[currentValue] ?? currentValue; + } + return `${prevValue} - ${dictionary[currentValue] ?? currentValue}`; + }, ""); +}; + +export const getDeviationReason = ( + deviationOptions: Types.DeviationFromMeasureSpecification[typeof DC.DEVIATION_OPTIONS], + data: Types.DeviationFromMeasureSpecification[typeof DC.DEVIATION_REASON] +) => { + let deviationReason: string = ""; + if (deviationOptions) { + deviationReason = data; + } + return deviationReason; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/index.ts new file mode 100644 index 0000000000..f6d8501952 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/index.ts @@ -0,0 +1,50 @@ +export * from "./dataDrivenTools"; +export * from "./omsValidator"; +export * as Types from "./types"; + +export * from "./validateAtLeastOneDataSource"; +export * from "./validateAtLeastOneDefinitionOfPopulation"; +export * from "./validateHybridMeasurePopulation"; +export * from "./validateAtLeastOneDataSourceType"; +export * from "./validateAtLeastOneDeliverySystem"; +export * from "./validateAtLeastOneDeviationFieldFilled"; +export * from "./validateAtLeastOneRateComplete"; +export * from "./validateBothDatesInRange"; +export * from "./validateDateRangeRadioButtonCompletion"; +export * from "./validateDualPopInformation"; +export * from "./validateEqualCategoryDenominators"; +export * from "./validateEqualQualifierDenominators"; +export * from "./validateFfsRadioButtonCompletion"; +export * from "./validateRateNotZero"; +export * from "./validateRateZero"; +export * from "./validateNumeratorsLessThanDenominators"; +export * from "./validateOneCatRateHigherThanOtherCat"; +export * from "./validateOneQualDenomHigherThanOtherDenomOMS"; +export * from "./validateOneQualRateHigherThanOtherQual"; +export * from "./validateReasonForNotReporting"; +export * from "./validateRequiredRadioButtonForCombinedRates"; +export * from "./validateTotals"; +export * from "./validateYearFormat"; +export * from "./validateOPMRates"; +export * from "./validateHedisYear"; +export * from "./validateSameDenominatorSets"; + +// PCR-XX Specific Validations +export { PCRatLeastOneRateComplete } from "./PCRValidations/PCRatLeastOneRateComplete"; +export { PCRnoNonZeroNumOrDenom } from "./PCRValidations/PCRnoNonZeroNumOrDenom"; + +//Complex Measure Specific Validations +export { ComplexAtLeastOneRateComplete } from "./ComplexValidations/ComplexAtLeastOneRateComplete"; +export { + ComplexNoNonZeroNumOrDenom, + ComplexNoNonZeroNumOrDenomOMS, +} from "./ComplexValidations/ComplexNoNonZeroNumOrDenom"; +export { + ComplexValidateNDRTotals, + ComplexValidateNDRTotalsOMS, +} from "./ComplexValidations/ComplexValidateNDRTotals"; +export { ComplexValidateDualPopInformation } from "./ComplexValidations/ComplexValidateDualPopInformation"; +export { + ComplexValueSameCrossCategory, + ComplexValueSameCrossCategoryOMS, +} from "./ComplexValidations/ComplexValueSameCrossCategory"; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.test.ts new file mode 100644 index 0000000000..82cf055068 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.test.ts @@ -0,0 +1,156 @@ +import { omsValidations } from "."; +import { + locationDictionary, + generateOmsQualifierRateData, + simpleRate, + generateOmsFormData, +} from "utils/testUtils/2024/validationHelpers"; +import { DefaultFormData } from "measures/2024/shared/CommonQuestions/types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +describe("Testing OMS validation processor", () => { + const categories = [ + { id: "TestCat1", text: "TestCat1", label: "TestCat1" }, + { id: "TestCat2", text: "TestCat2", label: "TestCat2" }, + ]; + const qualifiers = [ + { id: "TestQual1", text: "TestQual1", label: "TestQual1" }, + { id: "TestQual2", text: "TestQual2", label: "TestQual2" }, + ]; + + it("should have no errors for basic data", () => { + const errors = omsValidations({ + categories, + qualifiers, + locationDictionary, + dataSource: [], + data: generateOmsFormData( + generateOmsQualifierRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]) + ) as DefaultFormData, + validationCallbacks: [], + }); + + expect(errors.length).toBe(0); + }); + + it("should have no errors for basic data - no ACA", () => { + const errors = omsValidations({ + categories, + qualifiers, + locationDictionary, + dataSource: [], + data: generateOmsFormData( + generateOmsQualifierRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]), + true, + OMSData() + ) as DefaultFormData, + validationCallbacks: [], + }); + + expect(errors.length).toBe(0); + }); + + it("should have no errors for no data", () => { + const errors = omsValidations({ + categories, + qualifiers, + locationDictionary, + dataSource: [], + data: generateOmsFormData( + generateOmsQualifierRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]), + false + ) as DefaultFormData, + validationCallbacks: [], + }); + + expect(errors.length).toBe(0); + }); + + it("should have errors for not filling data into selected checkboxes", () => { + const errors = omsValidations({ + categories, + qualifiers, + locationDictionary, + dataSource: [], + checkIsFilled: true, + data: generateOmsFormData( + generateOmsQualifierRateData(categories, qualifiers, [{}, {}]), + true + ) as DefaultFormData, + validationCallbacks: [], + }); + + expect(errors.length).toBe(136); + expect( + errors.some((e) => + e.errorMessage.includes("Must fill out at least one NDR set.") + ) + ); + expect( + errors.some((e) => + e.errorMessage.includes( + "For any category selected, all NDR sets must be filled." + ) + ) + ); + }); + + it("should have errors from callbacks for every node", () => { + const errors = omsValidations({ + categories, + qualifiers, + locationDictionary, + dataSource: [], + data: generateOmsFormData( + generateOmsQualifierRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]) + ) as DefaultFormData, + validationCallbacks: [ + () => { + return [ + { errorLocation: "TestLocation", errorMessage: "TestMessage" }, + ]; + }, + ], + }); + expect(errors.length).toBe(74); + }); + + it("should have errors from empty rate description in OPM", () => { + const errors = omsValidations({ + categories, + qualifiers, + locationDictionary, + dataSource: [], + data: { + MeasurementSpecification: "Other", + "OtherPerformanceMeasure-Rates": [ + { + rate: [ + { + denominator: "", + numerator: "", + rate: "", + }, + ], + description: "", + }, + ], + } as DefaultFormData, + validationCallbacks: [], + }); + + expect(errors.length).toBe(1); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.ts new file mode 100644 index 0000000000..53c65b8f90 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.ts @@ -0,0 +1,362 @@ +import * as DC from "dataConstants"; +import { + OmsValidationCallback, + locationDictionaryFunction, + RateData, +} from "../types"; +import { + OmsNodes as OMS, + DefaultFormData, +} from "measures/2024/shared/CommonQuestions/types"; +import { validatePartialRateCompletionOMS } from "../validatePartialRateCompletion"; +import { LabelData, cleanString } from "utils"; + +interface OmsValidationProps { + data: DefaultFormData; + qualifiers: LabelData[]; + categories: LabelData[]; + locationDictionary: locationDictionaryFunction; + checkIsFilled?: boolean; + validationCallbacks: OmsValidationCallback[]; + customTotalLabel?: string; + dataSource?: string[]; +} +export const omsValidations = ({ + categories, + checkIsFilled = true, + data, + locationDictionary, + qualifiers, + validationCallbacks, + customTotalLabel, + dataSource, +}: OmsValidationProps) => { + const opmCats: LabelData[] = [{ id: "OPM", text: "OPM", label: "OPM" }]; + const opmQuals: LabelData[] = []; + let isOPM = false; + if ( + data.MeasurementSpecification === "Other" && + data["OtherPerformanceMeasure-Rates"] + ) { + isOPM = true; + opmQuals.push( + ...data["OtherPerformanceMeasure-Rates"].map((rate) => ({ + id: rate.description + ? `${DC.OPM_KEY}${cleanString(rate.description)}` + : "Fill out description", + label: rate.description ?? "Fill out description", + text: "", + })) + ); + } + const cats = + categories.length === 0 + ? [ + { + id: "singleCategory", + text: "singleCategory", + label: "singleCategory", + }, + ] + : categories; + return validateNDRs( + data, + validationCallbacks, + opmQuals.length ? opmQuals : qualifiers, + opmQuals.length ? opmCats : cats, + locationDictionary, + checkIsFilled, + isOPM, + customTotalLabel, + dataSource + ); +}; + +const validateNDRs = ( + data: DefaultFormData, + callbackArr: OmsValidationCallback[], + qualifiers: LabelData[], + categories: LabelData[], + locationDictionary: locationDictionaryFunction, + checkIsFilled: boolean, + isOPM: boolean, + customTotalLabel?: string, + dataSource?: string[] +) => { + const isFilled: { [key: string]: boolean } = {}; + const isDeepFilled: { [key: string]: boolean } = {}; + const isClassificationFilled: { [key: string]: boolean } = {}; + const isDisaggregateFilled: { [key: string]: boolean } = {}; + const errorArray: FormError[] = []; + // validates top levels, ex: Race, Geography, Sex + const validateTopLevelNode = (node: OMS.TopLevelOmsNode, label: string[]) => { + //add label for db data + if (!node.label) { + const cleanString = locationDictionary(label); + node.label = cleanString + .substring(cleanString.lastIndexOf("-") + 1) + .trim(); + } + // validate children if exist + if (node.options?.length) { + for (const option of node.options) { + validateChildNodes(node.selections?.[option] ?? {}, [...label, option]); + } + } + // validate for additionals category + for (const addtnl of node.additionalSelections ?? []) { + validateChildNodes(addtnl, [ + ...label, + addtnl.description ?? "Additional Category", + ]); + } + // ACA validate + if (node.rateData) { + validateNodeRates(node.rateData, label); + } + }; + // validate mid level, ex: White, African American, etc + const validateChildNodes = (node: OMS.MidLevelOMSNode, label: string[]) => { + //add label for db data + if (!node.label) { + const cleanString = locationDictionary(label); + node.label = cleanString + .substring(cleanString.lastIndexOf("-") + 1) + .trim(); + } + // validate sub categories + if (node.additionalSubCategories?.length) { + for (const subCat of node.additionalSubCategories) { + validateChildNodes(subCat, [ + ...label, + subCat.description ?? "sub-category", + ]); + } + } + // validate sub type, ex: Asian -> Korean, Chinese, etc + if (node.aggregate?.includes("NoIndependentData")) { + //if options are empty but there's a no + for (const key of node.options ?? []) { + validateChildNodes(node.selections?.[key] ?? {}, [...label, key]); + } + //check if disaggregate has sub-categories selected + checkIsDisaggregateFilled(label, node.selections); + } + //validate rates + if (node.rateData) { + validateNodeRates(node.rateData, label); + } + }; + // Rate containers to be validated + const validateNodeRates = (rateData: RateData, label: string[]) => { + for (const callback of callbackArr) { + errorArray.push( + ...callback({ + rateData, + categories, + qualifiers, + label, + locationDictionary, + isOPM, + customTotalLabel, + dataSource, + }) + ); + } + + if (checkIsFilled) { + isFilled[label[0]] = isFilled[label[0]] || checkNdrsFilled(rateData); + + // check for complex rate type and assign appropriate tag + const rateType = !!rateData?.["iuhh-rate"] + ? "iuhh-rate" + : !!rateData?.["aifhh-rate"] + ? "aifhh-rate" + : undefined; + + if (!rateData?.["pcr-rate"]) + errorArray.push( + ...validatePartialRateCompletionOMS(rateType)({ + rateData, + categories, + qualifiers, + label, + locationDictionary, + isOPM, + customTotalLabel, + dataSource, + }) + ); + } + const locationReduced = label.reduce( + (prev, curr, i) => `${prev}${i ? "-" : ""}${curr}`, + "" + ); + checkIsDeepFilled(locationReduced, rateData); + checkIsClassificationFilled(locationReduced, rateData); + }; + //checks at least one ndr filled + const checkNdrsFilled = (rateData: RateData) => { + // iu-hh check + if (rateData?.["iuhh-rate"]) { + const section = rateData["iuhh-rate"]?.rates ?? {}; + for (const category in section) { + for (const qual in section[category]) { + const fields = section[category][qual][0].fields; + if ( + fields.every( + (field: { label: string; value?: string }) => !!field?.value + ) + ) { + return true; + } + } + } + return false; + } + // aif-hh check + if (rateData?.["aifhh-rate"]) { + const section = rateData["aifhh-rate"]?.rates ?? {}; + for (const category in section) { + for (const qual in section[category]) { + const fields = section[category][qual][0].fields; + if ( + fields.every( + (field: { label: string; value?: string }) => !!field?.value + ) + ) { + return true; + } + } + } + return false; + } + // pcr-ad check + if (rateData?.["pcr-rate"]) { + return rateData["pcr-rate"].every((o) => !!o?.value); + } + for (const cat of categories) { + for (const qual of qualifiers) { + if (rateData.rates?.[cat.id]?.[qual.id]) { + const temp = rateData.rates[cat.id][qual.id][0]; + if (temp && temp.denominator && temp.numerator && temp.rate) { + return true; + } + } + } + } + return false; + }; + + // checks that if a qualifier was selected that it was filled + const checkIsDeepFilled = (location: string, rateData: RateData) => { + if (!rateData || !rateData.options?.length) return; + + // pcr-ad check + if (rateData?.["pcr-rate"]) { + isDeepFilled[`${location}`] = rateData["pcr-rate"].every( + (o) => !!o?.value + ); + } + + for (const cat of categories) { + for (const qual of qualifiers) { + //array key order is determined in component useQualRateArray, cleanedName variable + if (rateData.rates?.[cat.id]?.[qual.id]) { + const temp = rateData.rates[cat.id][qual.id][0]; + let cleanQual = isOPM ? qual.label : qual.id; + if (temp && temp.denominator && temp.numerator && temp.rate) { + isDeepFilled[`${location}-${cleanQual}`] ??= true; + } else { + isDeepFilled[`${location}-${cleanQual}`] = false; + } + } + } + } + }; + + //check if sub-classifications have rateData entered + const checkIsClassificationFilled = ( + location: string, + rateData: RateData + ) => { + isClassificationFilled[location] = rateData?.rates !== undefined; + }; + + //if selection is empty, it means that no sub classification was selected + const checkIsDisaggregateFilled = (locations: string[], selection: any) => { + isDisaggregateFilled[locations[1]] = selection !== undefined; + }; + // Loop through top level nodes for validation + for (const key of data.OptionalMeasureStratification?.options ?? []) { + isFilled[key] = false; + validateTopLevelNode( + data.OptionalMeasureStratification.selections?.[key] ?? {}, + [key] + ); + } + if (checkIsFilled) { + // check at least one ndr filled for a category + for (const topLevelKey in isFilled) { + if (!isFilled[topLevelKey]) { + errorArray.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + [topLevelKey] + )}`, + errorMessage: "Must fill out at least one NDR set.", + }); + } + } + + // check selected qualifiers were filled + for (const topLevelKey in isDeepFilled) { + if (!isDeepFilled[topLevelKey]) { + errorArray.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + topLevelKey.split("-") + )}`, + errorMessage: + "For any category selected, all NDR sets must be filled.", + }); + } + } + + //if at least one sub-classifications qualifiers is false (no rate data entered), we want to generate an error message, + //else if all is false, we will ignore it as another error message would already be there + if (!Object.values(isClassificationFilled).every((v) => v === false)) { + for (const classKey in isClassificationFilled) { + if (!isClassificationFilled[classKey]) { + errorArray.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + classKey.split("-") + )}`, + errorMessage: "Must fill out at least one NDR set.", + }); + } + } + } + + //checking if the user has selected no to aggregate data for certain classifictions (i.e. asian, native hawaiian or pacific islanders) + //keeping the error message seperate in case we want to have unique messages in the future + for (const classKey in isDisaggregateFilled) { + if (!isDisaggregateFilled[classKey]) { + errorArray.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + classKey.split("-") + )}`, + errorMessage: "Must fill out at least one NDR set.", + }); + } + } + } + + //check to see if the rate has been described for other performance measure + if (isOPM && qualifiers.find((qual) => !qual.label)) { + errorArray.push({ + errorLocation: `Other Performance Measure`, + errorMessage: "Rate name required", + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_helper.ts b/services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_helper.ts new file mode 100644 index 0000000000..60f33675a2 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_helper.ts @@ -0,0 +1,39 @@ +import * as DC from "dataConstants"; +import { + DefaultFormData, + RateFields, +} from "measures/2024/shared/CommonQuestions/types"; +import { exampleData } from "measures/2024/shared/CommonQuestions/PerformanceMeasure/data"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; + +export const test_setup = (data: DefaultFormData) => { + return { + ageGroups: exampleData.qualifiers!, + performanceMeasureArray: getPerfMeasureRateArray(data, exampleData), + OPM: data[DC.OPM_RATES], + }; +}; + +// Set empty values throughout OPM Measure while keeping the shape of the data +export const zero_OPM = (data: DefaultFormData) => { + const OPM = data[DC.OPM_RATES]; + for (const opmObj of OPM) + opmObj?.rate !== undefined ? zero_out_rate_field(opmObj.rate[0]) : false; +}; + +// Set empty values throughout Performance Measure while keeping the shape of the data +export const zero_PM = (data: DefaultFormData) => { + const PM = data[DC.PERFORMANCE_MEASURE]![DC.RATES]!; + Object.keys(PM).forEach((label: string) => { + if (label) { + PM[label]?.forEach((rate: any) => zero_out_rate_field(rate)); + } + }); +}; + +// clear the values of a RateField +const zero_out_rate_field = (rateField: RateFields) => { + rateField.rate = ""; + rateField.numerator = ""; + rateField.denominator = ""; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_testFormData.ts b/services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_testFormData.ts new file mode 100644 index 0000000000..96d0fef9b2 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_testFormData.ts @@ -0,0 +1,346 @@ +import * as DC from "dataConstants"; +import { DefaultFormData } from "measures/2024/shared/CommonQuestions/types"; + +/* Seed Data for validation tests - Uses DefaultFormData to ensure that our data matches the shape of production data.*/ +export const testFormData: DefaultFormData = { + [DC.ADDITIONAL_NOTES]: "", + [DC.ADDITIONAL_NOTES_UPLOAD]: [], + [DC.DID_COLLECT]: "yes", + [DC.DATA_STATUS]: DC.REPORTING_FINAL_DATA, + [DC.DATA_STATUS_PROVISIONAL_EXPLAINATION]: "", + + [DC.WHY_ARE_YOU_NOT_REPORTING]: [], + + [DC.AMOUNT_OF_POP_NOT_COVERED]: DC.ENTIRE_POP_NOT_COVERED, + + [DC.PARTIAL_POP_NOT_COVERED_EXPLAINATION]: "", + + [DC.WHY_IS_DATA_NOT_AVAILABLE]: [], + [DC.WHY_IS_DATA_NOT_AVAILABLE_OTHER]: "", + [DC.DATA_INCONSISTENCIES_ACCURACY_ISSUES]: "", + [DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE]: [], + [DC.DATA_SOURCE_NOT_EASILY_ACCESSIBLE_OTHER]: "", + [DC.INFO_NOT_COLLECTED]: [], + [DC.INFO_NOT_COLLECTED_OTHER]: "", + [DC.LIMITATION_WITH_DATA_COLLECTION]: "", + [DC.SMALL_SAMPLE_SIZE]: "", + [DC.WHY_ARE_YOU_NOT_REPORTING_OTHER]: "", + [DC.DID_REPORT]: "yes", + [DC.COMBINED_RATES]: "yes", + [DC.COMBINED_RATES_COMBINED_RATES]: DC.COMBINED_NOT_WEIGHTED_RATES, + [DC.COMBINED_WEIGHTED_RATES_OTHER_EXPLAINATION]: "", + [DC.DATE_RANGE]: { + [DC.END_DATE]: { [DC.SELECTED_MONTH]: 1, [DC.SELECTED_YEAR]: 2021 }, + [DC.START_DATE]: { [DC.SELECTED_MONTH]: 12, [DC.SELECTED_YEAR]: 2021 }, + }, + [DC.DEFINITION_OF_DENOMINATOR]: [ + DC.DENOMINATOR_INC_MEDICAID_POP, + DC.DENOMINATOR_INC_CHIP, + DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE, + DC.DENOMINATOR_INC_OTHER, + ], + [DC.DEFINITION_DENOMINATOR_OTHER]: "", + [DC.CHANGE_IN_POP_EXPLANATION]: "", + [DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC]: "yes", + [DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC_NO_EXPLAIN]: "", + [DC.DENOMINATOR_DEFINE_HEALTH_HOME]: "yes", + [DC.DENOMINATOR_DEFINE_HEALTH_HOME_NO_EXPLAIN]: "", + [DC.DENOMINATOR_DEFINE_TOTAL_TECH_SPEC_NO_SIZE]: "", + [DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR]: [ + DC.FFS, + DC.PCCM, + DC.MCO_PIHP, + DC.ICM, + ], + + [DC.HYBRID_MEASURE_POPULATION_INCLUDED]: "", + [DC.HYBRID_MEASURE_SAMPLE_SIZE]: "", + [DC.DEFINITION_OF_DENOMINATOR_SUBSET_EXPLAIN]: "", + [DC.DELIVERY_SYS_FFS]: "yes", + [DC.DELIVERY_SYS_FFS_NO_PERCENT]: "", + [DC.DELIVERY_SYS_FFS_NO_POP]: "", + [DC.DELIVERY_SYS_PCCM]: "yes", + [DC.DELIVERY_SYS_PCCM_NO_PERCENT]: "", + [DC.DELIVERY_SYS_PCCM_NO_POP]: "", + [DC.DELIVERY_SYS_MCO_PIHP]: "yes", + [DC.DELIVERY_SYS_MCO_PIHP_PERCENT]: "", + [DC.DELIVERY_SYS_MCO_PIHP_NUM_PLANS]: "", + [DC.DELIVERY_SYS_MCO_PIHP_NO_INC]: "", + [DC.DELIVERY_SYS_MCO_PIHP_NO_EXCL]: "", + [DC.DELIVERY_SYS_ICM]: "yes", + [DC.DELIVERY_SYS_ICM_NO_PERCENT]: "", + [DC.DELIVERY_SYS_ICM_NO_POP]: "", + [DC.DELIVERY_SYS_OTHER]: "", + [DC.DELIVERY_SYS_OTHER_PERCENT]: "", + [DC.DELIVERY_SYS_OTHER_NUM_HEALTH_PLANS]: "", + [DC.DELIVERY_SYS_OTHER_POP]: "", + + [DC.MEASUREMENT_PERIOD_CORE_SET]: "yes", + [DC.MEASUREMENT_SPECIFICATION]: DC.NCQA, + + [DC.MEASUREMENT_SPECIFICATION_HEDIS]: DC.HEDIS_MY_2020, + [DC.MEASUREMENT_SPEC_OMS_DESCRIPTION]: "", + [DC.MEASUREMENT_SPEC_OMS_DESCRIPTION_UPLOAD]: new File([], ""), + [DC.OPM_EXPLAINATION]: "", + [DC.OPM_RATES]: [ + { + [DC.DESCRIPTION]: "Label 1", + [DC.RATE]: [ + { + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "1", + [DC.DENOMINATOR]: "2", + }, + ], + }, + { + [DC.DESCRIPTION]: "Label 2", + [DC.RATE]: [ + { + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + }, + ], + [DC.OPM_NOTES]: "", + [DC.OPM_NOTES_TEXT_INPUT]: "", + [DC.OMS]: { + [DC.OPTIONS]: ["RaceNonHispanic"], + [DC.SELECTIONS]: { + RaceNonHispanic: { + [DC.OPTIONS]: ["White", "BlackorAfricanAmerican"], + [DC.SELECTIONS]: { + White: { + [DC.RATE_DATA]: { + [DC.OPTIONS]: ["Ages18to64", "Age65andolder"], + [DC.RATES]: { + Ages18to64: { + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Initiation of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "25.0", + [DC.NUMERATOR]: "1", + [DC.DENOMINATOR]: "4", + }, + ], + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Engagement of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "25.0", + [DC.NUMERATOR]: "1", + [DC.DENOMINATOR]: "4", + }, + ], + }, + Age65andolder: { + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Initiation of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "2", + [DC.DENOMINATOR]: "4", + }, + ], + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Engagement of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "2", + [DC.DENOMINATOR]: "4", + }, + ], + }, + }, + }, + [DC.SUB_CATS]: [], + }, + BlackorAfricanAmerican: { + [DC.RATE_DATA]: { + [DC.OPTIONS]: ["Ages18to64", "Age65andolder"], + [DC.RATES]: { + Ages18to64: { + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Initiation of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "33.3", + [DC.NUMERATOR]: "2", + [DC.DENOMINATOR]: "6", + }, + ], + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Engagement of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "33.3", + [DC.NUMERATOR]: "2", + [DC.DENOMINATOR]: "6", + }, + ], + }, + Age65andolder: { + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Initiation of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "3", + [DC.DENOMINATOR]: "6", + }, + ], + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: + "Engagement of AOD Treatment: Alcohol Abuse or Dependence", + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "3", + [DC.DENOMINATOR]: "6", + }, + ], + }, + }, + }, + [DC.SUB_CATS]: [], + }, + }, + [DC.ADDITIONAL_SELECTIONS]: [], + }, + }, + }, + [DC.PERFORMANCE_MEASURE]: { + [DC.EXPLAINATION]: "", + [DC.RATES]: { + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "25.0", + [DC.NUMERATOR]: "1", + [DC.DENOMINATOR]: "4", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "25.0", + [DC.NUMERATOR]: "1", + [DC.DENOMINATOR]: "4", + }, + ], + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "2", + [DC.DENOMINATOR]: "4", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "50.0", + [DC.NUMERATOR]: "2", + [DC.DENOMINATOR]: "4", + }, + ], + InitiationofAODTreatmentOpioidAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + EngagementofAODTreatmentOpioidAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + InitiationofAODTreatmentOtherDrugAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + EngagementofAODTreatmentOtherDrugAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + InitiationofAODTreatmentTotalAODAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + EngagementofAODTreatmentTotalAODAbuseorDependence: [ + { + [DC.LABEL]: "Ages 18 to 64", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + { + [DC.LABEL]: "Age 65 and older", + [DC.RATE]: "", + [DC.NUMERATOR]: "", + [DC.DENOMINATOR]: "", + }, + ], + }, + [DC.PMHYBRIDEXPLANATION]: "", + }, + [DC.DID_CALCS_DEVIATE]: "no", + [DC.DEVIATION_OPTIONS]: [], + [DC.DEVIATION_REASON]: "", + + [DC.DATA_SOURCE]: [DC.ADMINISTRATIVE_DATA], + [DC.DATA_SOURCE_SELECTIONS]: { + [DC.ADMINISTRATIVE_DATA]: { + [DC.DESCRIPTION]: "", + [DC.SELECTED]: ["MedicaidManagementInformationSystemMMIS"], + }, + }, + [DC.DATA_SOURCE_DESCRIPTION]: "", +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/types.ts b/services/ui-src/src/measures/2024/shared/globalValidations/types.ts new file mode 100644 index 0000000000..d4236811d5 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/types.ts @@ -0,0 +1,41 @@ +import { OmsNodes as OMS } from "measures/2024/shared/CommonQuestions/types"; +import { LabelData } from "utils"; + +export interface FormRateField { + denominator?: string; + numerator?: string; + label?: string; + rate?: string; + isTotal?: string; +} + +export type locationDictionaryFunction = (labels: string[]) => string; + +export interface RateData extends OMS.OmsRateFields { + "pcr-rate"?: { id?: number; value?: string; label?: string }[]; + "iuhh-rate"?: any; + "aifhh-rate"?: any; +} + +export interface UnifiedValFuncProps { + categories?: LabelData[]; + qualifiers?: LabelData[]; + rateData: FormRateField[][]; + location: string; + errorMessage?: string; +} + +export type UnifiedValidationFunction = ( + props: UnifiedValFuncProps +) => FormError[]; + +export type OmsValidationCallback = (data: { + rateData: RateData; + qualifiers: LabelData[]; + categories: LabelData[]; + label: string[]; + locationDictionary: locationDictionaryFunction; + isOPM: boolean; + customTotalLabel?: string; + dataSource?: string[]; +}) => FormError[]; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.test.ts new file mode 100644 index 0000000000..1ea5b23a9b --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.test.ts @@ -0,0 +1,51 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateAtLeastOneDataSource } from "."; + +describe("validateOneDataSource", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateAtLeastOneDataSource(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When no Data Source is Selected a validation warning shows", () => { + formData[DC.DATA_SOURCE] = []; + formData[DC.DATA_SOURCE_SELECTIONS] = {}; + _check_errors(formData, 1); + }); + + it("When a Data Source is Selected no validation warning shows", () => { + formData[DC.DATA_SOURCE_SELECTIONS] = {}; + _check_errors(formData, 0); + }); + //This scenario below is actually impossible from a ui perspective I believe + it("When no Data Source but Data Source Selections are Selected a validation warning shows", () => { + formData[DC.DATA_SOURCE] = []; + _check_errors(formData, 1); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DATA_SOURCE] = []; + errorArray = [...validateAtLeastOneDataSource(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "You must select at least one Data Source option" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DATA_SOURCE] = []; + const errorMessage = "Another one bites the dust."; + errorArray = [...validateAtLeastOneDataSource(formData, errorMessage)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.ts new file mode 100644 index 0000000000..0bbf207720 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.ts @@ -0,0 +1,20 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateAtLeastOneDataSource = ( + data: Types.DataSource, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + if ( + (!data.DataSource || data.DataSource.length === 0) && + !data?.["DataSource-CAHPS-Version"] + ) { + errorArray.push({ + errorLocation: "Data Source", + errorMessage: + errorMessage ?? "You must select at least one Data Source option", + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.test.ts new file mode 100644 index 0000000000..336cc0235c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.test.ts @@ -0,0 +1,63 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateAtLeastOneDataSourceType } from "."; + +describe("validateOneDataSourceType", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateAtLeastOneDataSourceType(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When a Data Source option is Selected no validation warning shows", () => { + formData[DC.DATA_SOURCE] = DC.ADMINISTRATIVE_DATA; + formData[DC.DATA_SOURCE_SELECTIONS] = { + AdministrativeData0: { + selected: ["MedicaidManagementInformationSystemMMIS"], + }, + }; + _check_errors(formData, 0); + }); + //This scenario below is actually impossible from a ui perspective I believe + it("When no Data Source but Data Source Selections are Selected a validation warning shows", () => { + formData[DC.DATA_SOURCE] = DC.ADMINISTRATIVE_DATA; + formData[DC.DATA_SOURCE_SELECTIONS] = { + AdministrativeData0: { + selected: undefined, + }, + }; + _check_errors(formData, 1); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DATA_SOURCE] = []; + formData[DC.DATA_SOURCE_SELECTIONS] = { + AdministrativeData0: { + selected: undefined, + }, + }; + errorArray = [...validateAtLeastOneDataSourceType(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe("You must select a data source"); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DATA_SOURCE] = []; + formData[DC.DATA_SOURCE_SELECTIONS] = { + AdministrativeData0: { + selected: undefined, + }, + }; + const errorMessage = "Another one bites the dust."; + errorArray = [...validateAtLeastOneDataSourceType(formData, errorMessage)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.ts new file mode 100644 index 0000000000..57fa4872a3 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.ts @@ -0,0 +1,26 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateAtLeastOneDataSourceType = ( + data: Types.DataSource, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + const dataSources = data.DataSourceSelections; + if (dataSources) { + Object.entries(dataSources).forEach((source) => { + const index = source + .map((s) => { + return typeof s === "object"; + }) + .indexOf(true); + if (!Object.values(source[index])[0]) { + errorArray.push({ + errorLocation: "Data Source", + errorMessage: errorMessage ?? "You must select a data source", + }); + } + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.test.ts new file mode 100644 index 0000000000..cc2eb43db5 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.test.ts @@ -0,0 +1,47 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateAtLeastOneDefinitionOfPopulation } from "."; + +describe("validateAtLeastOneDefinitionOfPopulation", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateAtLeastOneDefinitionOfPopulation(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When no Definition of Population is Selected a validation warning shows", () => { + formData[DC.DEFINITION_OF_DENOMINATOR] = []; + _check_errors(formData, 1); + }); + + it("When a Definition of Population is Selected no validation warning shows", () => { + formData[DC.DEFINITION_OF_DENOMINATOR] = [DC.DENOMINATOR_INC_MEDICAID_POP]; + _check_errors(formData, 0); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DEFINITION_OF_DENOMINATOR] = []; + errorArray = [...validateAtLeastOneDefinitionOfPopulation(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "You must select at least one definition of population option" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DEFINITION_OF_DENOMINATOR] = []; + const errorMessage = "Another one bites the dust."; + errorArray = [ + ...validateAtLeastOneDefinitionOfPopulation(formData, errorMessage), + ]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.ts new file mode 100644 index 0000000000..36f2d985ea --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.ts @@ -0,0 +1,21 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateAtLeastOneDefinitionOfPopulation = ( + data: Types.DefinitionOfPopulation, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + if ( + !data.DefinitionOfDenominator || + data.DefinitionOfDenominator.length === 0 + ) { + errorArray.push({ + errorLocation: "Definition of Population", + errorMessage: + errorMessage ?? + "You must select at least one definition of population option", + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.test.ts new file mode 100644 index 0000000000..52f188ae26 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.test.ts @@ -0,0 +1,47 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateAtLeastOneDeliverySystem } from "."; + +describe("validateAtLeastOneDeliverySystem", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateAtLeastOneDeliverySystem(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When no Delivery System is Selected a validation warning shows", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = []; + _check_errors(formData, 1); + }); + + it("When a Delivery System is Selected no validation warning shows", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = [ + DC.DELIVERY_SYS_FFS, + ]; + _check_errors(formData, 0); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = []; + errorArray = [...validateAtLeastOneDeliverySystem(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "You must select at least one delivery system option" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = []; + const errorMessage = "Another one bites the dust."; + errorArray = [...validateAtLeastOneDeliverySystem(formData, errorMessage)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.ts new file mode 100644 index 0000000000..10198b9f29 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.ts @@ -0,0 +1,20 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateAtLeastOneDeliverySystem = ( + data: Types.DefinitionOfPopulation, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + if ( + !data.DeliverySysRepresentationDenominator || + data.DeliverySysRepresentationDenominator.length === 0 + ) { + errorArray.push({ + errorLocation: "Delivery Systems", + errorMessage: + errorMessage ?? "You must select at least one delivery system option", + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.test.ts new file mode 100644 index 0000000000..d45e607aee --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.test.ts @@ -0,0 +1,61 @@ +import * as DC from "dataConstants"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateAtLeastOneDeviationFieldFilled } from "."; + +describe("validateAtLeastOneNDRInDeviationOfMeasureSpec", () => { + let formData: any = {}; + let errorArray: FormError[]; + + const _run_validation = (data: any, errorMessage?: string): FormError[] => { + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + return [ + ...validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason, + errorMessage + ), + ]; + }; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = _run_validation(data); + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("Default Form Data", () => { + _check_errors(formData, 0); + }); + + it("Calculations deviated, but no answer given", () => { + formData[DC.DID_CALCS_DEVIATE] = DC.YES; + _check_errors(formData, 1); + }); + + it("Calculations deviated, reason given", () => { + formData[DC.DID_CALCS_DEVIATE] = DC.YES; + formData[DC.DEVIATION_OPTIONS] = ["Test"]; + formData[DC.DEVIATION_REASON] = "Deviation Reason Here"; + _check_errors(formData, 0); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DID_CALCS_DEVIATE] = DC.YES; + errorArray = _run_validation(formData); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe("Deviation(s) must be explained"); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DID_CALCS_DEVIATE] = DC.YES; + const errorMessage = "Another one bites the dust."; + errorArray = _run_validation(formData, errorMessage); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.ts new file mode 100644 index 0000000000..45e75228e1 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.ts @@ -0,0 +1,23 @@ +// When a user indicates that there is a deviation, they must add an explanation in the textarea. +export const validateAtLeastOneDeviationFieldFilled = ( + didCalculationsDeviate: boolean, + deviationReason: string, + errorMessage?: string +) => { + let errorArray: FormError[] = []; + let reasonGiven: boolean = false; + + if (didCalculationsDeviate) { + if (!!deviationReason) { + reasonGiven = true; + } + + if (!reasonGiven) { + errorArray.push({ + errorLocation: "Deviations from Measure Specifications", + errorMessage: errorMessage ?? "Deviation(s) must be explained", + }); + } + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.test.ts new file mode 100644 index 0000000000..1fbe1e029a --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.test.ts @@ -0,0 +1,94 @@ +import * as DC from "dataConstants"; +import * as HELP from "../testHelpers/_helper"; +import { testFormData } from "../testHelpers/_testFormData"; +import { DefaultFormData } from "measures/2024/shared/CommonQuestions/types"; +import { validateAtLeastOneRateComplete } from "."; + +/* Ensure that at least 1 NDR in a set is complete for either the Performance Measure or Other Performance Measure */ +describe("atLeastOneRateComplete", () => { + let formData: DefaultFormData; + + const _run_validation = ( + data: DefaultFormData, + errorMessage?: string + ): FormError[] => { + const { ageGroups, performanceMeasureArray, OPM } = HELP.test_setup(data); + return [ + ...validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + undefined, + errorMessage + ), + ]; + }; + + // Check that the provided Form Data returns a certain number of validation errors. + const check_errors = (data: DefaultFormData, numErrors: number) => { + const errorArray = _run_validation(data); + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + }); + + it("when Peformance Measure is partially complete and OPM is partially complete", () => { + check_errors(formData, 0); + }); + + it("when Performance Measure is undefined and OPM is undefined", () => { + formData[DC.PERFORMANCE_MEASURE] = {}; + formData[DC.OPM_RATES] = []; + check_errors(formData, 1); + }); + + it("when Peformance Measure is partially complete and OPM is undefined", () => { + formData[DC.OPM_RATES] = []; + check_errors(formData, 0); + }); + + it("when Performance Measure is undefined and OPM is partially complete", () => { + delete formData[DC.PERFORMANCE_MEASURE]; + check_errors(formData, 0); + }); + + it("when Performance Measure is incomplete and OPM is incomplete", () => { + HELP.zero_PM(formData); + HELP.zero_OPM(formData); + check_errors(formData, 1); + }); + + it("when Performance Measure is incomplete and OPM is undefined", () => { + HELP.zero_PM(formData); + formData[DC.OPM_RATES] = []; + check_errors(formData, 1); + }); + + it("when Performance Measure is undefined and OPM is incomplete", () => { + formData[DC.PERFORMANCE_MEASURE] = {}; + HELP.zero_OPM(formData); + + check_errors(formData, 1); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.PERFORMANCE_MEASURE] = {}; + HELP.zero_OPM(formData); + const errorArray = _run_validation(formData); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "At least one Performance Measure Numerator, Denominator, and Rate must be completed" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.PERFORMANCE_MEASURE] = {}; + HELP.zero_OPM(formData); + const errorMessage = "Another one bites the dust."; + const errorArray = _run_validation(formData, errorMessage); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.ts new file mode 100644 index 0000000000..01e5bba678 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.ts @@ -0,0 +1,53 @@ +import { LabelData } from "utils"; +import { FormRateField } from "../types"; +import { validatePartialRateCompletionPM } from "../validatePartialRateCompletion"; + +export const validateAtLeastOneRateComplete = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + qualifiers: LabelData[], + categories?: LabelData[], + errorMessage?: string +) => { + const errorArray: FormError[] = []; + let rateCompletionError = true; + // Check OPM first + OPM && + OPM.forEach((measure: any) => { + if (measure.rate && measure.rate[0] && measure.rate[0].rate) { + rateCompletionError = false; + } + }); + + // Then Check regular Performance Measures + qualifiers.forEach((_ageGroup, i) => { + performanceMeasureArray?.forEach((_performanceObj, index) => { + if ( + performanceMeasureArray[index] && + performanceMeasureArray[index][i] && + performanceMeasureArray[index][i].denominator && + performanceMeasureArray[index][i].rate && + performanceMeasureArray[index][i].numerator + ) { + rateCompletionError = false; + } + }); + }); + if (rateCompletionError) { + errorArray.push({ + errorLocation: `Performance Measure/Other Performance Measure`, + errorMessage: + errorMessage ?? + `At least one Performance Measure Numerator, Denominator, and Rate must be completed`, + }); + } + return [ + ...errorArray, + ...validatePartialRateCompletionPM( + performanceMeasureArray, + OPM, + qualifiers, + categories + ), + ]; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.test.ts new file mode 100644 index 0000000000..10e7de4406 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.test.ts @@ -0,0 +1,121 @@ +import * as DC from "dataConstants"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateBothDatesCompleted } from "."; + +/* This validation checks that both date fields have been completed. */ +describe("ensureBothDatesCompletedInRange", () => { + let formData: any; + + const run_validation = (data: any, errorMessage?: string): FormError[] => { + const dateRange = data[DC.DATE_RANGE]; + return [...validateBothDatesCompleted(dateRange, errorMessage)]; + }; + + const check_errors = (data: any, numErrors: number) => { + const errorArray: FormError[] = run_validation(data); + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + }); + + it("when DATE_RANGE is undefined", () => { + delete formData[DC.DATE_RANGE]; + check_errors(formData, 0); + }); + + it("when START_DATE is complete and END_DATE is complete", () => { + check_errors(formData, 0); + }); + + it("when START_DATE is undefined and END_DATE is undefined", () => { + delete formData[DC.DATE_RANGE][DC.START_DATE]; + delete formData[DC.DATE_RANGE][DC.END_DATE]; + check_errors(formData, 1); + }); + + it("when START_DATE is empty and END_DATE is empty", () => { + formData[DC.DATE_RANGE][DC.START_DATE] = {}; + formData[DC.DATE_RANGE][DC.END_DATE] = {}; + check_errors(formData, 1); + }); + + it("when START_DATE is empty and END_DATE is undefined", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_MONTH] = undefined; + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = undefined; + + // End Date + delete formData[DC.DATE_RANGE][DC.END_DATE]; + check_errors(formData, 1); + }); + + it("when START_DATE is undefined and END_DATE is empty", () => { + // Start Date + delete formData[DC.DATE_RANGE][DC.START_DATE]; + + // End Date + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_MONTH] = undefined; + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = undefined; + check_errors(formData, 1); + }); + + it("when START_DATE and END_DATE have '0' values", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_MONTH] = 0; + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 0; + + // End Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_MONTH] = 0; + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 0; + check_errors(formData, 1); + }); + + it("when START_DATE is complete and END_DATE is empty", () => { + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_MONTH] = undefined; + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = undefined; + check_errors(formData, 1); + }); + + it("when START_DATE is empty and END_DATE is complete", () => { + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_MONTH] = undefined; + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = undefined; + check_errors(formData, 1); + }); + + it("when START_DATE has a month and END_DATE has a year", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = undefined; + + // End Date + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_MONTH] = undefined; + check_errors(formData, 1); + }); + + it("when START_DATE has a year and END_DATE has a month", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_MONTH] = undefined; + + // End Date + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = undefined; + check_errors(formData, 1); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DATE_RANGE][DC.START_DATE] = {}; + formData[DC.DATE_RANGE][DC.END_DATE] = {}; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe("Date Range must be completed"); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DATE_RANGE][DC.START_DATE] = {}; + formData[DC.DATE_RANGE][DC.END_DATE] = {}; + const errorMessage = "Another one bites the dust."; + const errorArray = run_validation(formData, errorMessage); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.ts new file mode 100644 index 0000000000..bf12ed0cfd --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.ts @@ -0,0 +1,32 @@ +import { DateRange } from "measures/2024/shared/CommonQuestions/types"; + +// Ensure the user populates the data range +export const validateBothDatesCompleted = ( + dateRange: DateRange["DateRange"], + errorMessage?: string +) => { + let errorArray: any[] = []; + let error; + + if (dateRange) { + const startDateCompleted = + !!dateRange.startDate?.selectedMonth && + !!dateRange.startDate?.selectedYear; + + const endDateCompleted = + !!dateRange.endDate?.selectedMonth && !!dateRange.endDate?.selectedYear; + + if (!startDateCompleted || !endDateCompleted) { + error = true; + } + + if (error) { + errorArray.push({ + errorLocation: `Date Range`, + errorMessage: errorMessage ?? `Date Range must be completed`, + }); + } + } + + return error ? errorArray : []; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.test.ts new file mode 100644 index 0000000000..13463b7860 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.test.ts @@ -0,0 +1,42 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateDateRangeRadioButtonCompletion } from "."; + +describe("validateDateRangeRadioButtonCompletion", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateDateRangeRadioButtonCompletion(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When no date range radio button is selected a validation warning shows", () => { + delete formData[DC.MEASUREMENT_PERIOD_CORE_SET]; + _check_errors(formData, 1); + }); + + it("Error message text should match default errorMessage", () => { + delete formData[DC.MEASUREMENT_PERIOD_CORE_SET]; + errorArray = [...validateDateRangeRadioButtonCompletion(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "Date Range answer must be selected" + ); + }); + + it("Error message text should match provided errorMessage", () => { + delete formData[DC.MEASUREMENT_PERIOD_CORE_SET]; + const errorMessage = "Another one bites the dust."; + errorArray = [ + ...validateDateRangeRadioButtonCompletion(formData, errorMessage), + ]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.ts new file mode 100644 index 0000000000..5e51bc0618 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.ts @@ -0,0 +1,16 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateDateRangeRadioButtonCompletion = ( + data: Types.DateRange, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + if (!data["MeasurementPeriodAdhereToCoreSetSpecification"]) { + errorArray.push({ + errorLocation: "Date Range", + errorMessage: errorMessage ?? "Date Range answer must be selected", + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.test.ts new file mode 100644 index 0000000000..a7a289a2c9 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.test.ts @@ -0,0 +1,137 @@ +import * as DC from "dataConstants"; +import { simpleRate, partialRate } from "utils/testUtils/validationHelpers"; +import { validateDualPopInformationPM } from "."; + +describe("Testing Dual Population Selection Validation", () => { + it("should be no errors", () => { + const errors = validateDualPopInformationPM([], undefined, 0, []); + + expect(errors.length).toBe(0); + }); + + it("should be no errors - partial data", () => { + const errors = validateDualPopInformationPM( + [[partialRate, partialRate]], + undefined, + 0, + [] + ); + + expect(errors.length).toBe(0); + }); + + it("should be no errors - OPM", () => { + const errors = validateDualPopInformationPM([], [], 0, []); + + expect(errors.length).toBe(0); + }); + + it("should be errors for no checkbox selections", () => { + const errors = validateDualPopInformationPM( + [[simpleRate, simpleRate]], + undefined, + 0, + undefined + ); + + expect(errors.length).toBe(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `Information has been included in the Age 65 and Older Performance Measure but the checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is missing` + ); + }); + + it("should be errors for no matching checkbox selection", () => { + const errors = validateDualPopInformationPM( + [[simpleRate, simpleRate]], + undefined, + 0, + [] + ); + + expect(errors.length).toBe(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `Information has been included in the Age 65 and Older Performance Measure but the checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is missing` + ); + }); + + it("should be errors for no matching checkbox selection - specified string", () => { + const errors = validateDualPopInformationPM( + [[simpleRate, simpleRate]], + undefined, + 0, + [], + "TestLabel" + ); + + expect(errors.length).toBe(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `Information has been included in the TestLabel Performance Measure but the checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is missing` + ); + }); + + it("should be errors for no data", () => { + const errors = validateDualPopInformationPM([], undefined, 0, [ + DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE, + ]); + + expect(errors.length).toBe(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `The checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is checked but you are missing performance measure data for Age 65 and Older` + ); + }); + + it("should be errors for no data - specified string", () => { + const errors = validateDualPopInformationPM( + [], + undefined, + 0, + [DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE], + "TestLabel" + ); + + expect(errors.length).toBe(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `The checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is checked but you are missing performance measure data for TestLabel` + ); + }); + + it("Error message text should match default errorMessage", () => { + const errorArray = validateDualPopInformationPM( + [[simpleRate, simpleRate]], + undefined, + 0, + undefined + ); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "Information has been included in the Age 65 and Older Performance Measure but the checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is missing" + ); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessageFunc = ( + _dualEligible: boolean, + errorReplacementText: string + ) => { + return `Another ${errorReplacementText} bites the dust`; + }; + + const errorArray = validateDualPopInformationPM( + [[simpleRate, simpleRate]], + undefined, + 0, + undefined, + undefined, + errorMessageFunc + ); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + errorMessageFunc(true, "Age 65 and Older") + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.ts new file mode 100644 index 0000000000..18a57160e9 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.ts @@ -0,0 +1,62 @@ +import * as DC from "dataConstants"; +import { FormRateField } from "../types"; + +const validateDualPopInformationPMErrorMessage = ( + dualEligible: boolean, + errorReplacementText: string +) => { + if (!dualEligible) { + return `Information has been included in the ${errorReplacementText} Performance Measure but the checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is missing`; + } else { + return `The checkmark for (Denominator Includes Medicare and Medicaid Dually-Eligible population) is checked but you are missing performance measure data for ${errorReplacementText}`; + } +}; + +export const validateDualPopInformationPM = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + age65PlusIndex: number, + DefinitionOfDenominator: string[] | undefined, + errorReplacementText: string = "Age 65 and Older", + errorMessageFunc = validateDualPopInformationPMErrorMessage +) => { + if (OPM) { + return []; + } + + const dualEligible = DefinitionOfDenominator + ? DefinitionOfDenominator.indexOf( + DC.DENOMINATOR_INC_MEDICAID_DUAL_ELIGIBLE + ) !== -1 + : false; + + const errorArray: FormError[] = []; + const filledInData: FormRateField[] = []; + performanceMeasureArray?.forEach((performanceMeasure) => { + if ( + performanceMeasure && + performanceMeasure[age65PlusIndex] && + performanceMeasure[age65PlusIndex].denominator && + performanceMeasure[age65PlusIndex].numerator && + performanceMeasure[age65PlusIndex].rate + ) { + filledInData.push(performanceMeasure[age65PlusIndex]); + } + }); + + if (!dualEligible && filledInData.length > 0) { + errorArray.push({ + errorLocation: "Performance Measure", + errorMessage: errorMessageFunc(dualEligible, errorReplacementText), + errorType: "Warning", + }); + } + if (dualEligible && filledInData.length === 0) { + errorArray.push({ + errorLocation: "Performance Measure", + errorMessage: errorMessageFunc(dualEligible, errorReplacementText), + errorType: "Warning", + }); + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.test.ts new file mode 100644 index 0000000000..53227bcb21 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.test.ts @@ -0,0 +1,184 @@ +import { LabelData } from "utils"; +import { + validateEqualCategoryDenominatorsOMS, + validateEqualCategoryDenominatorsPM, +} from "."; + +import { + generateOmsCategoryRateData, + locationDictionary, + doubleRate, + simpleRate, + partialRate, + generatePmQualifierRateData, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Equal Denominators For All Qualifiers Validation", () => { + const noCat: LabelData[] = []; + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + const pmd = { categories, qualifiers }; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM/OPM Validation", () => { + it("should return NO errors", () => { + const errors = validateEqualCategoryDenominatorsPM( + generatePmQualifierRateData(pmd, [simpleRate, simpleRate]), + categories, + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error", () => { + const errors = validateEqualCategoryDenominatorsPM( + generatePmQualifierRateData(pmd, [simpleRate, doubleRate]), + categories, + qualifiers + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorList).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `The following categories must have the same denominator:` + ); + expect(errors[0].errorList).toContain(categories[0].label); + expect(errors[0].errorList).toContain(categories[1].label); + }); + + it("should have error, with qualifiers listed", () => { + const errors = validateEqualCategoryDenominatorsPM( + generatePmQualifierRateData({ qualifiers, categories: noCat }, [ + simpleRate, + doubleRate, + ]), + noCat, + qualifiers + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorList).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `The following categories must have the same denominator:` + ); + expect(errors[0].errorList).toContain(qualifiers[0].label); + expect(errors[0].errorList).toContain(qualifiers[1].label); + }); + + it("should NOT have error from empty rate value", () => { + const errors = validateEqualCategoryDenominatorsPM( + generatePmQualifierRateData(pmd, [partialRate, partialRate]), + categories, + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const errorArray = validateEqualCategoryDenominatorsPM( + generatePmQualifierRateData({ qualifiers, categories: noCat }, [ + simpleRate, + doubleRate, + ]), + noCat, + qualifiers, + errorMessage + ); + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return NO errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validateEqualCategoryDenominatorsOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should return no errors if OPM", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validateEqualCategoryDenominatorsOMS()({ + ...baseOMSInfo, + rateData: data, + isOPM: true, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have errors, list qualifiers", () => { + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + doubleRate, + ]); + const errors = validateEqualCategoryDenominatorsOMS()({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(errors[0].errorMessage).toBe( + `The following categories must have the same denominator:` + ); + expect(errors[0].errorList).toContain(categories[0].label); + expect(errors[0].errorList).toContain(categories[1].label); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith(["TestLabel"]); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + doubleRate, + ]); + const errors = validateEqualCategoryDenominatorsOMS(errorMessage)({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(errors[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.ts new file mode 100644 index 0000000000..66775e8ad1 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.ts @@ -0,0 +1,80 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { + OmsValidationCallback, + UnifiedValidationFunction as UVF, +} from "../types"; +import { + convertOmsDataToRateArray, + getPerfMeasureRateArray, +} from "../dataDrivenTools"; +import { SINGLE_CATEGORY } from "dataConstants"; +import { LabelData } from "utils"; + +const _validation: UVF = ({ + rateData, + qualifiers, + categories, + location, + errorMessage, +}) => { + const errorArray: FormError[] = []; + const locationArray: string[] = []; + const denominatorArray: string[] = []; + + for (const [i, rateSet] of rateData.entries()) { + for (const [j, rate] of rateSet.entries()) { + if (rate && rate.denominator) { + denominatorArray.push(rate.denominator); + locationArray.push( + !!categories?.length && + categories[0].label !== SINGLE_CATEGORY && + categories.some((item) => item.label) + ? categories![i].label + : qualifiers![j].label + ); + } + } + } + + if (!denominatorArray.every((v) => denominatorArray[0] === v)) { + errorArray.push({ + errorLocation: location, + errorMessage: + errorMessage ?? + `The following categories must have the same denominator:`, + errorList: locationArray.filter((v, i, a) => a.indexOf(v) === i), + }); + } + + return errorArray; +}; + +/** Checks all rates have the same denominator for both categories and qualifiers. NOTE: only use qualifiers if category is empty */ +export const validateEqualCategoryDenominatorsOMS = + (errorMessage?: string): OmsValidationCallback => + ({ rateData, categories, qualifiers, label, locationDictionary, isOPM }) => { + if (isOPM) return []; + return _validation({ + categories, + qualifiers, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + errorMessage, + }); + }; + +/** Checks all rates have the same denominator for both categories and qualifiers. NOTE: only pass qualifiers if category is empty */ +export const validateEqualCategoryDenominatorsPM = ( + data: Types.PerformanceMeasure, + categories: LabelData[], + qualifiers?: LabelData[], + errorMessage?: string +) => { + return _validation({ + categories, + qualifiers, + location: "Performance Measure", + rateData: getPerfMeasureRateArray(data, { categories, qualifiers }), + errorMessage, + }); +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.test.ts new file mode 100644 index 0000000000..e88fac4809 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.test.ts @@ -0,0 +1,182 @@ +import { + validateEqualQualifierDenominatorsOMS, + validateEqualQualifierDenominatorsPM, +} from "."; + +import { + generateOmsCategoryRateData, + locationDictionary, + doubleRate, + simpleRate, + partialRate, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Equal Qualifier Denominators Across Category Validation", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM/OPM Validation", () => { + it("should return NO errors", () => { + const errors = validateEqualQualifierDenominatorsPM( + [[simpleRate, simpleRate]], + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error", () => { + const errors = validateEqualQualifierDenominatorsPM( + [ + [simpleRate, simpleRate], + [doubleRate, doubleRate], + ], + qualifiers + ); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `Denominators must be the same for each category of performance measures for ${qualifiers[0].label}` + ); + }); + + it("should NOT have error from empty rate value", () => { + const errors = validateEqualQualifierDenominatorsPM( + [ + [partialRate, partialRate], + [partialRate, partialRate], + ], + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust"; + const errorArray = validateEqualQualifierDenominatorsPM( + [ + [simpleRate, simpleRate], + [doubleRate, doubleRate], + ], + qualifiers, + errorMessage + ); + + expect(errorArray).toHaveLength(2); + expect(errorArray[0].errorMessage).toBe(errorMessage); + expect(errorArray[1].errorMessage).toBe(errorMessage); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = (qualifier: string) => { + return `Another ${qualifier} bites the dust.`; + }; + const errorArray = validateEqualQualifierDenominatorsPM( + [ + [simpleRate, simpleRate], + [doubleRate, doubleRate], + ], + qualifiers, + undefined, + errorMessageFunc + ); + + expect(errorArray).toHaveLength(2); + expect(errorArray[0].errorMessage).toBe( + errorMessageFunc(qualifiers[0].label) + ); + expect(errorArray[1].errorMessage).toBe( + errorMessageFunc(qualifiers[1].label) + ); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return NO errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validateEqualQualifierDenominatorsOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should return no errors if OPM", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validateEqualQualifierDenominatorsOMS()({ + ...baseOMSInfo, + rateData: data, + isOPM: true, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have errors", () => { + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + doubleRate, + ]); + const errors = validateEqualQualifierDenominatorsOMS()({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith([ + "TestLabel", + qualifiers[0].label, + ]); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const locationDictionaryJestFunc = jest.fn(); + const errorMessage = "Another one bites the dust"; + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + doubleRate, + ]); + const errors = validateEqualQualifierDenominatorsOMS(errorMessage)({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(errors[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.ts new file mode 100644 index 0000000000..bc2672f471 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.ts @@ -0,0 +1,85 @@ +import { + OmsValidationCallback, + FormRateField, + UnifiedValFuncProps as UVFP, +} from "../types"; +import { convertOmsDataToRateArray } from "../dataDrivenTools"; +import { LabelData } from "utils"; + +interface ValProps extends UVFP { + locationFunc?: (qualifier: string) => string; + errorMessageFunc?: (qualifier: string) => string; +} + +const validateEqualQualifierDenominatorsErrorMessage = (qualifier: string) => { + return `Denominators must be the same for each category of performance measures for ${qualifier}`; +}; + +const _validation = ({ + rateData, + qualifiers, + location, + errorMessage, + locationFunc, + errorMessageFunc = validateEqualQualifierDenominatorsErrorMessage, +}: ValProps): FormError[] => { + const errorArray: FormError[] = []; + + for (const [i, qual] of qualifiers!.entries()) { + const denominators: (string | undefined)[] = []; + for (const rateSet of rateData) { + if (rateSet && rateSet[i] && rateSet[i].denominator) { + denominators.push(rateSet[i].denominator); + } + } + + const error = !denominators.every((v) => !v || v === denominators[0]); + if (error) { + errorArray.push({ + errorLocation: locationFunc ? locationFunc(qual.label) : location, + errorMessage: errorMessage ?? errorMessageFunc(qual.label), + }); + } + } + + return errorArray; +}; + +/** + * All qualifiers need to have the same denominator + */ +export const validateEqualQualifierDenominatorsOMS = + (errorMessage?: string): OmsValidationCallback => + ({ rateData, categories, qualifiers, label, locationDictionary, isOPM }) => { + if (isOPM) return []; + return _validation({ + qualifiers, + location: "Optional Measure Stratification", + locationFunc: (qual) => + `Optional Measure Stratification: ${locationDictionary([ + ...label, + qual, + ])}`, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + errorMessage: + errorMessage ?? "Denominators must be the same for each category.", + }); + }; + +/** + * All qualifiers need to have the same denominator + */ +export const validateEqualQualifierDenominatorsPM = ( + performanceMeasureArray: FormRateField[][], + qualifiers: LabelData[], + errorMessage?: string, + errorMessageFunc?: (qualifier: string) => string +) => { + return _validation({ + location: "Performance Measure", + errorMessage, + qualifiers, + rateData: performanceMeasureArray, + errorMessageFunc, + }); +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.test.ts new file mode 100644 index 0000000000..bbad461994 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.test.ts @@ -0,0 +1,49 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateFfsRadioButtonCompletion } from "."; + +describe("validateFfsRadioButtonCompletion", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateFfsRadioButtonCompletion(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When no Delivery System nested question is checked a validation warning shows", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = [DC.FFS]; + formData[DC.DELIVERY_SYS_FFS] = undefined; + _check_errors(formData, 1); + }); + + it("When a Delivery System nested question is checked no validation warning shows", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = [DC.FFS]; + formData[DC.DELIVERY_SYS_FFS] = "yes"; + _check_errors(formData, 0); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = [DC.FFS]; + formData[DC.DELIVERY_SYS_FFS] = undefined; + errorArray = [...validateFfsRadioButtonCompletion(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "You must indicate if the measure-eligible population is included" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DELIVERY_SYS_REPRESENTATION_DENOMINATOR] = [DC.FFS]; + formData[DC.DELIVERY_SYS_FFS] = undefined; + const errorMessage = "Another one bites the dust."; + errorArray = [...validateFfsRadioButtonCompletion(formData, errorMessage)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.ts new file mode 100644 index 0000000000..5001931e5a --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.ts @@ -0,0 +1,37 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; + +export const validateFfsRadioButtonCompletion = ( + data: Types.DefinitionOfPopulation, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + + // map the delivery systems to their respective nested selections + const deliverySystemsMap = { + [DC.FFS]: DC.DELIVERY_SYS_FFS, + [DC.ICM]: DC.DELIVERY_SYS_ICM, + [DC.PCCM]: DC.DELIVERY_SYS_PCCM, + [DC.MCO_PIHP]: DC.DELIVERY_SYS_MCO_PIHP, + [DC.OTHER]: DC.DELIVERY_SYS_OTHER, + }; + + const selectedDeliverySystems = data.DeliverySysRepresentationDenominator; + if (selectedDeliverySystems) { + selectedDeliverySystems.forEach((system) => { + Object.entries(data).forEach((selection) => { + // check if user has actually checked the nested radio button selection + if (selection[0] === deliverySystemsMap[system] && !selection[1]) { + errorArray.push({ + errorLocation: "Delivery Systems", + errorMessage: + errorMessage ?? + "You must indicate if the measure-eligible population is included", + }); + } + }); + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.test.ts new file mode 100644 index 0000000000..a4b7814d11 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.test.ts @@ -0,0 +1,41 @@ +import * as DC from "dataConstants"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateHedisYear } from "."; + +describe("Hedis Year Validation - FY 2024", () => { + let formData: any; + + const run_validation = (data: any): FormError[] => { + return [...validateHedisYear(data)]; + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + }); + + it("Should not show hedis validation error", () => { + const hedisOptions = [ + "DC.HEDIS_MY_2020", + "DC.HEDIS_MY_2021", + "DC.HEDIS_MY_2022", + "DC.HEDIS_MY_2023", + ]; + for (let i = 0; i < hedisOptions.length - 1; i++) { + formData[DC.MEASUREMENT_SPECIFICATION_HEDIS] = hedisOptions[i]; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(0); + } + }); + + it("Should show hedis validation error", () => { + formData[DC.MEASUREMENT_SPECIFICATION_HEDIS] = ""; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(1); + }); + + it("Should show no error when measure specification is other", () => { + formData[DC.MEASUREMENT_SPECIFICATION] = "Other"; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(0); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.ts new file mode 100644 index 0000000000..31a8578e06 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.ts @@ -0,0 +1,27 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; + +export const validateHedisYear = ( + data: Types.MeasurementSpecification, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + const measurementSpecification: string = data[DC.MEASUREMENT_SPECIFICATION]; + const measurementSpecificationHedis: string = + data[DC.MEASUREMENT_SPECIFICATION_HEDIS]; + + if ( + measurementSpecificationHedis === "" || + (measurementSpecificationHedis === undefined && + measurementSpecification !== "Other") + ) { + errorArray.push({ + errorLocation: "Measure Specification", + errorMessage: + errorMessage ?? + "Version of HEDIS measurement year used must be specified", + }); + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateHybridMeasurePopulation/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateHybridMeasurePopulation/index.test.ts new file mode 100644 index 0000000000..d8680fe85b --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateHybridMeasurePopulation/index.test.ts @@ -0,0 +1,56 @@ +import { testFormData } from "../testHelpers/_testFormData"; +import * as DC from "dataConstants"; +import { validateHybridMeasurePopulation } from "."; + +describe("validateHybridMeasurePopulation", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + errorArray = [...validateHybridMeasurePopulation(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + errorArray = []; + }); + + it("When no Data Source is selected, no validation warning shows", () => { + formData[DC.DEFINITION_OF_DENOMINATOR] = []; + _check_errors(formData, 0); + }); + + it("When Data Source - Hybrid is selected, a validation warning will show", () => { + formData[DC.DATA_SOURCE] = [ + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA, + ]; + _check_errors(formData, 1); + }); + + it("When Data Source - Case management record review is selected, a validation warning will show", () => { + formData[DC.DATA_SOURCE] = [DC.CASE_MANAGEMENT_RECORD_REVIEW_DATA]; + _check_errors(formData, 1); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DATA_SOURCE] = [ + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA, + ]; + errorArray = [...validateHybridMeasurePopulation(formData)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe( + "Size of the measure-eligible population is required" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DATA_SOURCE] = [ + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA, + ]; + const errorMessage = "Another one bites the dust."; + errorArray = [...validateHybridMeasurePopulation(formData, errorMessage)]; + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateHybridMeasurePopulation/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateHybridMeasurePopulation/index.ts new file mode 100644 index 0000000000..e955560895 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateHybridMeasurePopulation/index.ts @@ -0,0 +1,25 @@ +import { + CASE_MANAGEMENT_RECORD_REVIEW_DATA, + HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA, +} from "dataConstants"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateHybridMeasurePopulation = ( + data: Types.DefaultFormData, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + if ( + data.DataSource?.includes(HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA) || + data.DataSource?.includes(CASE_MANAGEMENT_RECORD_REVIEW_DATA) + ) { + if (!data.HybridMeasurePopulationIncluded) { + errorArray.push({ + errorLocation: "Definition of Population", + errorMessage: + errorMessage ?? "Size of the measure-eligible population is required", + }); + } + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateNoNonZeroNumOrDenom/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateNoNonZeroNumOrDenom/index.ts new file mode 100644 index 0000000000..29ffb4b6ef --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateNoNonZeroNumOrDenom/index.ts @@ -0,0 +1,137 @@ +import * as DC from "dataConstants"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { + OmsValidationCallback, + FormRateField, + UnifiedValFuncProps as UVFP, +} from "../types"; +import { + convertOmsDataToRateArray, + getOtherPerformanceMeasureRateArray, +} from "../dataDrivenTools"; +import { LabelData } from "utils"; + +interface ValProps extends UVFP { + hybridData?: boolean; +} + +export const _validationRateNotZero = ({ location, rateData }: UVFP) => { + const errorArray: FormError[] = []; + + for (const ratefields of rateData) { + for (const rate of ratefields) { + if (rate && rate.denominator && rate.numerator && rate.rate) { + if ( + parseFloat(rate.numerator) > 0 && + parseFloat(rate.denominator) > 0 && + parseFloat(rate.rate) === 0 + ) { + errorArray.push({ + errorLocation: location, + errorMessage: + "Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation.", + }); + } + } + } + } + + return errorArray; +}; + +export const _validationRateZero = ({ + location, + rateData, + hybridData, +}: ValProps) => { + const errorArray: FormError[] = []; + + for (const ratefields of rateData) { + for (const rate of ratefields) { + if (rate && rate.denominator && rate.numerator && rate.rate) { + if ( + parseFloat(rate.numerator) === 0 && + parseFloat(rate.denominator) > 0 && + parseFloat(rate.rate) !== 0 && + !hybridData + ) { + errorArray.push({ + errorLocation: location, + errorMessage: "Manually entered rate should be 0 if numerator is 0", + }); + } + } + } + } + + return errorArray; +}; + +export const validateRateZeroOMS: OmsValidationCallback = ({ + categories, + qualifiers, + rateData, + label, + locationDictionary, + dataSource, +}) => { + const hybridData = dataSource?.includes( + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA + ); + return _validationRateZero({ + categories, + qualifiers, + hybridData, + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + }).filter((v, i, a) => i === 0 || a[0].errorLocation !== v.errorLocation); +}; + +export const validateRateNotZeroOMS: OmsValidationCallback = ({ + categories, + qualifiers, + rateData, + label, + locationDictionary, +}) => { + return _validationRateNotZero({ + categories, + qualifiers, + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + }).filter((v, i, a) => i === 0 || a[0].errorLocation !== v.errorLocation); +}; + +// If a user manually over-rides a rate it must not violate two rules: +// It must be zero if the numerator is zero or +// It Must be greater than zero if the Num and Denom are greater than zero +export const validateNoNonZeroNumOrDenomPM = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + _qualifiers: LabelData[], + data: Types.DefaultFormData +) => { + const errorArray: FormError[] = []; + const hybridData = data?.[DC.DATA_SOURCE]?.includes( + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA + ); + const location = `Performance Measure/Other Performance Measure`; + const rateDataOPM = getOtherPerformanceMeasureRateArray(OPM); + + const nonZeroErrors = [ + ..._validationRateNotZero({ location, rateData: performanceMeasureArray }), + ..._validationRateNotZero({ location, rateData: rateDataOPM }), + ]; + const zeroErrors = [ + ..._validationRateZero({ + location, + rateData: performanceMeasureArray, + hybridData, + }), + ..._validationRateZero({ location, rateData: rateDataOPM, hybridData }), + ]; + + if (!!nonZeroErrors.length) errorArray.push(nonZeroErrors[0]); + if (!!zeroErrors.length) errorArray.push(zeroErrors[0]); + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.test.ts new file mode 100644 index 0000000000..ca8ef9e478 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.test.ts @@ -0,0 +1,175 @@ +import { + validateNumeratorLessThanDenominatorOMS, + validateNumeratorsLessThanDenominatorsPM, +} from "."; + +import { + generateOmsCategoryRateData, + locationDictionary, + badNumeratorRate, + simpleRate, + partialRate, + generateOtherPerformanceMeasureData, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Numerator Less Than Denominator", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM/OPM Validation", () => { + it("should return NO errors - PM", () => { + const errors = validateNumeratorsLessThanDenominatorsPM( + [[simpleRate, simpleRate]], + undefined, + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error - PM", () => { + const errors = validateNumeratorsLessThanDenominatorsPM( + [[badNumeratorRate, badNumeratorRate]], + undefined, + qualifiers + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + "Performance Measure/Other Performance Measure" + ); + expect(errors[0].errorMessage).toBe( + `Numerators must be less than Denominators for all applicable performance measures` + ); + }); + + it("should NOT have error from empty rate value", () => { + const errors = validateNumeratorsLessThanDenominatorsPM( + [[partialRate, partialRate]], + generateOtherPerformanceMeasureData([partialRate, partialRate]), + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should return NO errors - OPM", () => { + const errors = validateNumeratorsLessThanDenominatorsPM( + [], + generateOtherPerformanceMeasureData([simpleRate, simpleRate]), + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error - OPM", () => { + const errors = validateNumeratorsLessThanDenominatorsPM( + [], + generateOtherPerformanceMeasureData([ + badNumeratorRate, + badNumeratorRate, + ]), + qualifiers + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + "Performance Measure/Other Performance Measure" + ); + expect(errors[0].errorMessage).toBe( + `Numerators must be less than Denominators for all applicable performance measures` + ); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const errorArray = validateNumeratorsLessThanDenominatorsPM( + [[badNumeratorRate, badNumeratorRate]], + undefined, + qualifiers, + errorMessage + ); + + expect(errorArray.length).toBe(1); + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return NO errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validateNumeratorLessThanDenominatorOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have errors with qualifier in message", () => { + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + badNumeratorRate, + badNumeratorRate, + ]); + const errors = validateNumeratorLessThanDenominatorOMS()({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith([ + "TestLabel", + qualifiers[0].label, + ]); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + badNumeratorRate, + badNumeratorRate, + ]); + const errors = validateNumeratorLessThanDenominatorOMS(errorMessage)({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith([ + "TestLabel", + qualifiers[0].label, + ]); + expect(errors[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.ts new file mode 100644 index 0000000000..dd11928616 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.ts @@ -0,0 +1,94 @@ +import { + OmsValidationCallback, + FormRateField, + UnifiedValFuncProps as UVFP, +} from "../types"; +import { + convertOmsDataToRateArray, + getOtherPerformanceMeasureRateArray, +} from "../dataDrivenTools"; +import { LabelData } from "utils"; + +interface ValProps extends UVFP { + locationFunc?: (qualifier: string) => string; +} + +const _validation = ({ + location, + rateData, + errorMessage, + locationFunc, + qualifiers, +}: ValProps) => { + const errorArray: FormError[] = []; + + for (const fieldSet of rateData) { + for (const [i, rate] of fieldSet.entries()) { + if ( + rate.numerator && + rate.denominator && + parseFloat(rate.denominator) < parseFloat(rate.numerator) + ) { + errorArray.push({ + errorLocation: locationFunc + ? locationFunc(qualifiers![i].label) + : location, + errorMessage: errorMessage!, + }); + } + } + } + + return errorArray; +}; + +/** + * Validated OMS sections for numerator being less than denominator + */ +export const validateNumeratorLessThanDenominatorOMS = + (errorMessage?: string): OmsValidationCallback => + ({ categories, qualifiers, rateData, label, locationDictionary }) => { + return _validation({ + location: "Optional Measure Stratification", + categories, + qualifiers, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + locationFunc: (q) => + `Optional Measure Stratification: ${locationDictionary([...label, q])}`, + errorMessage: + errorMessage ?? + "Numerator cannot be greater than the Denominator for NDR sets.", + }); + }; + +/** + * Checks both performance measure and other performance measure for numerator greater than denominator errors + */ +export const validateNumeratorsLessThanDenominatorsPM = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + qualifiers: LabelData[], + errorMessage?: string +) => { + const location = `Performance Measure/Other Performance Measure`; + const rateDataOPM = getOtherPerformanceMeasureRateArray(OPM); + errorMessage = + errorMessage ?? + `Numerators must be less than Denominators for all applicable performance measures`; + const errorArray: FormError[] = [ + ..._validation({ + location, + qualifiers, + rateData: performanceMeasureArray, + errorMessage, + }), + ..._validation({ + location, + qualifiers, + rateData: rateDataOPM, + errorMessage, + }), + ]; + + return !!errorArray.length ? [errorArray[0]] : []; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.test.ts new file mode 100644 index 0000000000..5856f33002 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.test.ts @@ -0,0 +1,30 @@ +import * as DC from "dataConstants"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateOPMRates } from "."; + +describe("OPM Validation", () => { + let formData: any; + + const run_validation = (data: any): FormError[] => { + const OPM = data[DC.OPM_RATES]; + return [...validateOPMRates(OPM)]; + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + }); + + it("Should not throw error if there are no duplicates", () => { + formData[DC.OPM_RATES][0][DC.DESCRIPTION] = "18-30"; + formData[DC.OPM_RATES][1][DC.DESCRIPTION] = "31-64"; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(0); + }); + + it("Should not treat multiple empty descriptions as duplicates", () => { + formData[DC.OPM_RATES][0][DC.DESCRIPTION] = ""; + formData[DC.OPM_RATES][1][DC.DESCRIPTION] = ""; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(0); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.ts new file mode 100644 index 0000000000..7c121a00e4 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.ts @@ -0,0 +1,34 @@ +import { OtherPerformanceMeasure } from "measures/2024/shared/CommonQuestions/types"; + +/* Other Performance Measure Rate Description. Check all rate descriptions +to make sure there are no identical descriptions */ + +export const validateOPMRates = ( + otherPerformanceMeasure: OtherPerformanceMeasure["OtherPerformanceMeasure-Rates"], + errorMessage?: string +) => { + const errorArray: FormError[] = []; + + if (otherPerformanceMeasure) { + const opm_descriptions = otherPerformanceMeasure.filter( + (item: any) => !!item.description + ); + + const formattedDescriptions = opm_descriptions.map((item: any) => + item.description.trim() + ); + + const hasDuplicates = formattedDescriptions.some((desc: any, index) => { + return formattedDescriptions.indexOf(desc) !== index; + }); + + if (hasDuplicates) { + errorArray.push({ + errorLocation: "Other Performance Measure", + errorMessage: errorMessage ?? "Measure description must be unique.", + }); + } + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.test.ts new file mode 100644 index 0000000000..e5e8fe7f60 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.test.ts @@ -0,0 +1,231 @@ +import { + validateOneCatRateHigherThanOtherCatOMS, + validateOneCatRateHigherThanOtherCatPM, +} from "."; + +import { + generatePmCategoryRateData, + generateOmsCategoryRateData, + locationDictionary, + higherRate, + lowerRate, + partialRate, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Category Rate Higher Than Other Validation", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const expandedCategories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + { label: "TestCat3", text: "TestCat3", id: "TestCat3" }, + { label: "TestCat4", text: "TestCat4", id: "TestCat4" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM Validation", () => { + it("should return no errors", () => { + const data = generatePmCategoryRateData({ categories, qualifiers }, [ + higherRate, + lowerRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have error", () => { + const data = generatePmCategoryRateData({ categories, qualifiers }, [ + lowerRate, + higherRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `${categories[1].label} Rate should not be higher than ${categories[0].label} Rate for ${qualifiers[0].label} Rates.` + ); + }); + + it("should NOT have error from empty rate value ", () => { + const data = generatePmCategoryRateData({ categories, qualifiers }, [ + partialRate, + partialRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(0); + }); + + it("should generate multiple error sets for increment props", () => { + const data = generatePmCategoryRateData( + { categories: expandedCategories, qualifiers }, + [lowerRate, higherRate, lowerRate, higherRate] + ); + const errors = validateOneCatRateHigherThanOtherCatPM( + data, + { + categories: expandedCategories, + qualifiers, + }, + 0, + 1, + 2 + ); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `${categories[1].label} Rate should not be higher than ${categories[0].label} Rate for ${qualifiers[0].label} Rates.` + ); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessageFunc = ( + highCat: string, + lowCat: string, + qualifier: string + ) => { + return `Another ${lowCat} bites the ${highCat} and the ${qualifier}.`; + }; + + const data = generatePmCategoryRateData( + { categories: expandedCategories, qualifiers }, + [lowerRate, higherRate, lowerRate, higherRate] + ); + const errors = validateOneCatRateHigherThanOtherCatPM( + data, + { + categories: expandedCategories, + qualifiers, + }, + 0, + 1, + 2, + errorMessageFunc + ); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + errorMessageFunc( + categories[0].label, + categories[1].label, + qualifiers[0].label + ) + ); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return no errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + higherRate, + lowerRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should return with no error - isOPM", () => { + const errors = validateOneCatRateHigherThanOtherCatOMS()({ + ...baseOMSInfo, + rateData: {}, + isOPM: true, + }); + expect(errors).toHaveLength(0); + }); + + it("should not show last string portion when OMS has no categories", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + lowerRate, + higherRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorMessage).toBe( + `${categories[1].label} Rate should not be higher than ${categories[0].label} Rates.` + ); + }); + + it("should generate multi-error sets for increment prop", () => { + const data = generateOmsCategoryRateData(expandedCategories, qualifiers, [ + lowerRate, + higherRate, + lowerRate, + higherRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatOMS( + 0, + 1, + 2 + )({ + ...baseOMSInfo, + categories: expandedCategories, + rateData: data, + }); + + expect(errors).toHaveLength(4); + expect(errors[0].errorMessage).toBe( + `${categories[1].label} Rate should not be higher than ${categories[0].label} Rates.` + ); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessageFunc = (highCat: string, lowCat: string) => { + return `Another ${lowCat} bites the ${highCat}.`; + }; + const data = generateOmsCategoryRateData(categories, qualifiers, [ + lowerRate, + higherRate, + ]); + const errors = validateOneCatRateHigherThanOtherCatOMS( + undefined, + undefined, + undefined, + errorMessageFunc + )({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorMessage).toBe( + errorMessageFunc(categories[0].label, categories[1].label) + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.ts new file mode 100644 index 0000000000..a9ff1d5308 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.ts @@ -0,0 +1,191 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +import { OmsValidationCallback, UnifiedValFuncProps as UVFP } from "../types"; +import { + getPerfMeasureRateArray, + convertOmsDataToRateArray, +} from "../dataDrivenTools"; + +type ErrorMessageFunc = ( + highCat: string, + lowCat: string, + qualifier: string +) => string; + +interface ValProps extends UVFP { + lowerIndex: number; + higherIndex: number; + errorMessageFunc: ErrorMessageFunc; + locationFunc?: (qualifier: string) => string; +} + +const _validation = ({ + categories, + location, + qualifiers, + rateData, + higherIndex, + lowerIndex, + locationFunc, + errorMessageFunc, +}: ValProps) => { + const errorArray: FormError[] = []; + const lowerRate = rateData[lowerIndex]; + const higherRate = rateData[higherIndex]; + for (let i = 0; i < lowerRate?.length; i++) { + const lrate = lowerRate?.[i]?.rate; + const hrate = higherRate?.[i]?.rate; + + if (lrate && hrate) { + if (parseFloat(lrate) > parseFloat(hrate)) { + errorArray.push({ + errorLocation: locationFunc + ? locationFunc(qualifiers![i].label) + : location, + errorMessage: errorMessageFunc( + categories![higherIndex].label, + categories![lowerIndex].label, + qualifiers![i].label + ), + }); + } + } + } + return errorArray; +}; + +const validateOneCatRateHigherThanOtherCatOMSErrorMessage = ( + highCat: string, + lowCat: string +) => { + return `${lowCat} Rate should not be higher than ${highCat} Rates.`; +}; + +/** + * Validates that one category's rate is higher than the other specified category's rate + * @note this function returns the oms validation function + * + * + * @param higherIndex which category index should have the higher rate + * @param lowerIndex which category index should have the lower rate + */ +export const validateOneCatRateHigherThanOtherCatOMS = ( + higherIndex = 0, + lowerIndex = 1, + increment?: number, + errorMessageFunc = validateOneCatRateHigherThanOtherCatOMSErrorMessage +): OmsValidationCallback => { + return ({ + rateData, + categories, + qualifiers, + label, + locationDictionary, + isOPM, + }) => { + if (isOPM) return []; + const errorArray: FormError[] = []; + + if (increment) { + for ( + let i = higherIndex, j = lowerIndex; + i <= categories.length && j <= categories.length; + i = i + increment, j = j + increment + ) { + errorArray.push( + ..._validation({ + categories, + qualifiers, + rateData: convertOmsDataToRateArray( + categories, + qualifiers, + rateData + ), + higherIndex: i, + lowerIndex: j, + locationFunc: (q) => + `Optional Measure Stratification: ${locationDictionary( + label + )} - ${q}`, + location: "Optional Measure Stratification", + errorMessageFunc, + }) + ); + } + return errorArray; + } else { + return _validation({ + categories, + qualifiers, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + higherIndex, + lowerIndex, + locationFunc: (q) => + `Optional Measure Stratification: ${locationDictionary( + label + )} - ${q}`, + location: "Optional Measure Stratification", + errorMessageFunc, + }); + } + }; +}; + +const validateOneCatRateHigherThanOtherCatPMErrorMessage = ( + highCat: string, + lowCat: string, + qualifier: string +) => { + return `${lowCat} Rate should not be higher than ${highCat} Rate for ${qualifier} Rates.`; +}; + +/** + * Validates that one categoyr's rate is higher than the other specified categoyr's rate + * + * @param data form data + * @param performanceMeasureData data driven information + * @param higherIndex which category index should have the higher rate + * @param lowerIndex which category index should have the lower rate + */ +export const validateOneCatRateHigherThanOtherCatPM = ( + data: Types.PerformanceMeasure, + performanceMeasureData: Types.DataDrivenTypes.PerformanceMeasure, + higherIndex = 0, + lowerIndex = 1, + increment?: number, + errorMessageFunc = validateOneCatRateHigherThanOtherCatPMErrorMessage +) => { + const errorArray: FormError[] = []; + + if (increment) { + const catLength = performanceMeasureData!.categories!.length; + for ( + let i = higherIndex, j = lowerIndex; + i <= catLength && j <= catLength; + i = i + increment, j = j + increment + ) { + errorArray.push( + ..._validation({ + categories: performanceMeasureData.categories, + qualifiers: performanceMeasureData.qualifiers, + rateData: getPerfMeasureRateArray(data, performanceMeasureData), + higherIndex: i, + location: "Performance Measure", + lowerIndex: j, + errorMessageFunc, + }) + ); + } + return errorArray; + } else { + return _validation({ + categories: performanceMeasureData.categories, + qualifiers: performanceMeasureData.qualifiers, + rateData: getPerfMeasureRateArray(data, performanceMeasureData), + higherIndex, + location: "Performance Measure", + lowerIndex, + errorMessageFunc, + }); + } +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.test.ts new file mode 100644 index 0000000000..3dbb454d45 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.test.ts @@ -0,0 +1,194 @@ +import { SINGLE_CATEGORY } from "dataConstants"; +import { + validateOneQualDenomHigherThanOtherDenomOMS, + validateOneQualDenomHigherThanOtherDenomPM, +} from "."; + +import { + generatePmQualifierRateData, + generateOmsQualifierRateData, + locationDictionary, + doubleRate, + simpleRate, + partialRate, +} from "utils/testUtils/2024/validationHelpers"; +import { LabelData } from "utils"; + +describe("Testing Qualifier Denominator Higher Than Other Validation", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + const singleCat = [ + { label: SINGLE_CATEGORY, text: SINGLE_CATEGORY, id: SINGLE_CATEGORY }, + ]; + const noCat: LabelData[] = []; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM Validation", () => { + it("should return no errors", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + doubleRate, + simpleRate, + ]); + const errors = validateOneQualDenomHigherThanOtherDenomPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have error", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + simpleRate, + doubleRate, + ]); + const errors = validateOneQualDenomHigherThanOtherDenomPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `${qualifiers[1].label} denominator must be less than or equal to ${qualifiers[0].label} denominator.` + ); + }); + + it("should have error - single category", () => { + const data = generatePmQualifierRateData( + { categories: noCat, qualifiers }, + [simpleRate, doubleRate] + ); + const errors = validateOneQualDenomHigherThanOtherDenomPM(data, { + categories: noCat, + qualifiers, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `${qualifiers[1].label} denominator must be less than or equal to ${qualifiers[0].label} denominator.` + ); + }); + + it("should NOT have error from empty rate value ", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + partialRate, + partialRate, + ]); + const errors = validateOneQualDenomHigherThanOtherDenomPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(0); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = (lowerQual: string, higherQual: string) => { + return `Another ${lowerQual} bites the ${higherQual}.`; + }; + + const data = generatePmQualifierRateData( + { categories: noCat, qualifiers }, + [simpleRate, doubleRate] + ); + const errors = validateOneQualDenomHigherThanOtherDenomPM( + data, + { + categories: noCat, + qualifiers, + }, + undefined, + undefined, + errorMessageFunc + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + errorMessageFunc(qualifiers[1].label, qualifiers[0].label) + ); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return no errors", () => { + const data = generateOmsQualifierRateData(categories, qualifiers, [ + doubleRate, + simpleRate, + ]); + const errors = validateOneQualDenomHigherThanOtherDenomOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should return with no error - isOPM", () => { + const errors = validateOneQualDenomHigherThanOtherDenomOMS()({ + ...baseOMSInfo, + rateData: {}, + isOPM: true, + }); + expect(errors).toHaveLength(0); + }); + + it("should not show last string portion when OMS has no categories", () => { + const data = generateOmsQualifierRateData(singleCat, qualifiers, [ + simpleRate, + doubleRate, + ]); + const errors = validateOneQualDenomHigherThanOtherDenomOMS()({ + ...baseOMSInfo, + categories: singleCat, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorMessage).toBe( + `${qualifiers?.[1].label} denominator must be less than or equal to ${qualifiers?.[0].label} denominator.` + ); + }); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = (lowerQual: string, higherQual: string) => { + return `Another ${lowerQual} bites the ${higherQual}.`; + }; + const data = generateOmsQualifierRateData(singleCat, qualifiers, [ + simpleRate, + doubleRate, + ]); + const errors = validateOneQualDenomHigherThanOtherDenomOMS( + undefined, + undefined, + errorMessageFunc + )({ + ...baseOMSInfo, + categories: singleCat, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorMessage).toBe( + errorMessageFunc(qualifiers?.[1].label, qualifiers?.[0].label) + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.ts new file mode 100644 index 0000000000..92ee56af0c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.ts @@ -0,0 +1,119 @@ +import * as Types from "../../CommonQuestions/types"; + +import { OmsValidationCallback, UnifiedValFuncProps as UVFP } from "../types"; +import { + convertOmsDataToRateArray, + getPerfMeasureRateArray, +} from "../dataDrivenTools"; + +type ErrorMessageFunc = (lowerQual: string, higherQual: string) => string; + +interface ValProps extends UVFP { + lowerIndex: number; + higherIndex: number; + errorMessageFunc?: ErrorMessageFunc; +} + +const validateOneQualDenomHigherThanOtherDenomErrorMessage = ( + lowerQual: string, + higherQual: string +) => { + return `${lowerQual} denominator must be less than or equal to ${higherQual} denominator.`; +}; + +const _validation = ({ + location, + qualifiers, + rateData, + higherIndex, + lowerIndex, + errorMessageFunc = validateOneQualDenomHigherThanOtherDenomErrorMessage, +}: ValProps) => { + const errorArray: FormError[] = []; + + for (const ratefields of rateData) { + const highDenom = ratefields[higherIndex]; + const lowerDenom = ratefields[lowerIndex]; + + if ( + highDenom && + lowerDenom && + highDenom.denominator && + lowerDenom.denominator + ) { + if ( + parseFloat(lowerDenom.denominator) > parseFloat(highDenom.denominator) + ) { + errorArray.push({ + errorLocation: location, + errorMessage: errorMessageFunc( + qualifiers?.[lowerIndex]?.label!, + qualifiers?.[higherIndex]?.label! + ), + }); + } + } + } + + return errorArray; +}; + +/** + * Validates that one qualifier's denominator is higher than the other specified qualifier's denominator + * @note this function returns the oms validation function + * + * + * @param higherIndex which qualifier index should have the higher denominator + * @param lowerIndex which qualifier index should have the lower denominator + */ +export const validateOneQualDenomHigherThanOtherDenomOMS = ( + higherIndex = 0, + lowerIndex = 1, + errorMessageFunc?: ErrorMessageFunc +): OmsValidationCallback => { + return ({ + rateData, + categories, + qualifiers, + label, + locationDictionary, + isOPM, + }) => { + if (isOPM) return []; + return _validation({ + categories, + qualifiers, + higherIndex, + lowerIndex, + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + errorMessageFunc, + }); + }; +}; + +/** + * Validates that one qualifier's denominator is higher than the other specified qualifier's denominator + * + * @param data form data + * @param pmData data driven information + * @param higherIndex which qualifier index should have the higher denominator + * @param lowerIndex which qualifier index should have the lower denominator + */ +export const validateOneQualDenomHigherThanOtherDenomPM = ( + data: Types.PerformanceMeasure, + pmData: Types.DataDrivenTypes.PerformanceMeasure, + higherIndex = 0, + lowerIndex = 1, + errorMessageFunc?: ErrorMessageFunc +) => { + return _validation({ + higherIndex, + location: "Performance Measure", + lowerIndex, + rateData: getPerfMeasureRateArray(data, pmData), + categories: pmData.categories, + qualifiers: pmData.qualifiers, + errorMessageFunc, + }); +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.test.ts new file mode 100644 index 0000000000..c00402945e --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.test.ts @@ -0,0 +1,215 @@ +import { SINGLE_CATEGORY } from "dataConstants"; +import { + validateOneQualRateHigherThanOtherQualOMS, + validateOneQualRateHigherThanOtherQualPM, +} from "."; + +import { + generatePmQualifierRateData, + generateOmsQualifierRateData, + locationDictionary, + higherRate, + lowerRate, + partialRate, +} from "utils/testUtils/2024/validationHelpers"; +import { LabelData } from "utils"; + +describe("Testing Qualifier Rate Higher Than Other Validation", () => { + const categories = [ + { label: "Test Cat 1", text: "Test Cat 1", id: "Test Cat 1" }, + { label: "Test Cat 2", text: "Test Cat 2", id: "Test Cat 2" }, + ]; + const qualifiers = [ + { label: "Test Qual 1", text: "Test Qual 1", id: "Test Qual 1" }, + { label: "Test Qual 2", text: "Test Qual 2", id: "Test Qual 2" }, + ]; + const singleCat = [ + { label: SINGLE_CATEGORY, text: SINGLE_CATEGORY, id: SINGLE_CATEGORY }, + ]; + const noCat: LabelData[] = []; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM Validation", () => { + it("should return no errors", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + higherRate, + lowerRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have error", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + lowerRate, + higherRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `${qualifiers[1].label} rate must be less than or equal to ${qualifiers[0].label} rate within ${categories[0].label}.` + ); + }); + + it("should have error - single category", () => { + const data = generatePmQualifierRateData( + { categories: noCat, qualifiers }, + [lowerRate, higherRate] + ); + const errors = validateOneQualRateHigherThanOtherQualPM(data, { + categories: noCat, + qualifiers, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `${qualifiers[1].label} rate must be less than or equal to ${qualifiers[0].label} rate.` + ); + }); + + it("should NOT have error from empty rate value ", () => { + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + partialRate, + partialRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualPM(data, { + categories, + qualifiers, + }); + + expect(errors).toHaveLength(0); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = ( + lowQual: string, + highQual: string, + _notSingleCategory: boolean, + category: string + ) => { + return `Another ${lowQual} bites the ${highQual} and the ${category}.`; + }; + + const data = generatePmQualifierRateData({ categories, qualifiers }, [ + lowerRate, + higherRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualPM( + data, + { + categories, + qualifiers, + }, + undefined, + undefined, + errorMessageFunc + ); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + errorMessageFunc( + qualifiers[1].label, + qualifiers[0].label, + false, + categories[0].label + ) + ); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return no errors", () => { + const data = generateOmsQualifierRateData(categories, qualifiers, [ + higherRate, + lowerRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should return with no error - isOPM", () => { + const errors = validateOneQualRateHigherThanOtherQualOMS()({ + ...baseOMSInfo, + rateData: {}, + isOPM: true, + }); + expect(errors).toHaveLength(0); + }); + + it("should not show last string portion when OMS has no categories", () => { + const data = generateOmsQualifierRateData(singleCat, qualifiers, [ + lowerRate, + higherRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualOMS()({ + ...baseOMSInfo, + categories: singleCat, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorMessage).toBe( + `${qualifiers?.[1].label} rate must be less than or equal to ${qualifiers?.[0].label} rate.` + ); + }); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = ( + lowQual: string, + highQual: string, + _notSingleCategory: boolean, + category: string + ) => { + return `Another ${lowQual} bites the ${highQual} and the ${category}.`; + }; + + const data = generateOmsQualifierRateData(singleCat, qualifiers, [ + lowerRate, + higherRate, + ]); + const errors = validateOneQualRateHigherThanOtherQualOMS( + undefined, + undefined, + errorMessageFunc + )({ + ...baseOMSInfo, + categories: singleCat, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorMessage).toBe( + errorMessageFunc( + qualifiers[1].label, + qualifiers[0].label, + false, + singleCat[0].label + ) + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.ts new file mode 100644 index 0000000000..c11e363d0f --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.ts @@ -0,0 +1,129 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { OmsValidationCallback, UnifiedValFuncProps as UVFP } from "../types"; +import { + getPerfMeasureRateArray, + convertOmsDataToRateArray, +} from "../dataDrivenTools"; + +type ErrorMessageFunc = ( + lowQual: string, + highQual: string, + singleCategoryCheck: boolean, + category: string +) => string; + +interface ValProps extends UVFP { + lowerIndex: number; + higherIndex: number; + errorMessageFunc?: ErrorMessageFunc; +} + +const validateOneQualRateHigherThanOtherQualErrorMessage: ErrorMessageFunc = ( + lowQual: string, + highQual: string, + notSingleCategory: boolean, + category: string +) => { + return `${lowQual} rate must be less than or equal to ${highQual} rate${ + notSingleCategory ? ` within ${category}` : "" + }.`; +}; + +const _validation = ({ + categories, + location, + qualifiers, + rateData, + higherIndex, + lowerIndex, + errorMessageFunc = validateOneQualRateHigherThanOtherQualErrorMessage, +}: ValProps) => { + const errorArray: FormError[] = []; + + for (const [i, ratefields] of rateData.entries()) { + if ( + ratefields?.length >= 2 && + parseFloat(ratefields[lowerIndex]?.rate ?? "") > + parseFloat(ratefields[higherIndex]?.rate ?? "") + ) { + const notSingleCategory: boolean = + categories?.length && + categories[0].label !== DC.SINGLE_CATEGORY && + categories[0].label + ? true + : false; + errorArray.push({ + errorLocation: location, + errorMessage: errorMessageFunc( + qualifiers?.[lowerIndex]?.label!, + qualifiers?.[higherIndex]?.label!, + notSingleCategory, + categories?.[i]?.label! + ), + }); + } + } + return errorArray; +}; + +/** + * Validates that one qualifier's rate is higher than the other specified qualifier's rate + * @note this function returns the oms validation function + * + * + * @param higherIndex which qualifier index should have the higher rate + * @param lowerIndex which qualifier index should have the lower rate + */ +export const validateOneQualRateHigherThanOtherQualOMS = ( + higherIndex = 0, + lowerIndex = 1, + errorMessageFunc?: ErrorMessageFunc +): OmsValidationCallback => { + return ({ + rateData, + categories, + qualifiers, + label, + locationDictionary, + isOPM, + }) => { + if (isOPM) return []; + return _validation({ + categories, + qualifiers, + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + higherIndex, + lowerIndex, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + errorMessageFunc, + }); + }; +}; + +/** + * Validates that one qualifier's rate is higher than the other specified qualifier's rate + * + * @param data form data + * @param performanceMeasureData data driven information + * @param higherIndex which qualifier index should have the higher rate + * @param lowerIndex which qualifier index should have the lower rate + */ +export const validateOneQualRateHigherThanOtherQualPM = ( + data: Types.PerformanceMeasure, + performanceMeasureData: Types.DataDrivenTypes.PerformanceMeasure, + higherIndex = 0, + lowerIndex = 1, + errorMessageFunc?: ErrorMessageFunc +) => { + const perfMeasure = getPerfMeasureRateArray(data, performanceMeasureData); + return _validation({ + categories: performanceMeasureData.categories, + qualifiers: performanceMeasureData.qualifiers, + higherIndex, + lowerIndex, + rateData: perfMeasure, + location: "Performance Measure", + errorMessageFunc, + }); +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.test.ts new file mode 100644 index 0000000000..97c93e7ec6 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.test.ts @@ -0,0 +1,262 @@ +import { SINGLE_CATEGORY } from "dataConstants"; +import { + validatePartialRateCompletionOMS, + validatePartialRateCompletionPM, +} from "."; +import { + generateOmsCategoryRateData, + generateOtherPerformanceMeasureData, + locationDictionary, + simpleRate, + partialRate, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Partial Rate Validation", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM/OPM Validation", () => { + it("should return NO errors", () => { + const errors = validatePartialRateCompletionPM( + [[simpleRate, simpleRate]], + undefined, + qualifiers, + categories + ); + + expect(errors).toHaveLength(0); + }); + + it("should return NO errors - OPM", () => { + const errors = validatePartialRateCompletionPM( + [], + generateOtherPerformanceMeasureData([simpleRate, simpleRate]), + qualifiers, + categories + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error", () => { + const errors = validatePartialRateCompletionPM( + [ + [partialRate, partialRate], + [partialRate, partialRate], + ], + undefined, + qualifiers, + categories + ); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + `Should not have partially filled NDR sets${` for ${qualifiers[0].label}`}${`, ${categories[0].label}`}.` + ); + }); + + it("should have error - OPM", () => { + const errors = validatePartialRateCompletionPM( + [], + generateOtherPerformanceMeasureData([partialRate, partialRate]), + qualifiers, + categories + ); + + expect(errors).toHaveLength(6); + expect(errors[0].errorLocation).toBe("Other Performance Measure"); + expect(errors[0].errorMessage).toBe( + `Should not have partially filled NDR sets.` + ); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = ( + multipleQuals: boolean, + qualifier: string, + multipleCats: boolean, + category: string + ) => { + return `Another${multipleQuals} bites the ${qualifier}... dun dun dun... Another ${multipleCats} bites the ${category}`; + }; + + const errors = validatePartialRateCompletionPM( + [ + [partialRate, partialRate], + [partialRate, partialRate], + ], + undefined, + qualifiers, + categories, + errorMessageFunc + ); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toBe("Performance Measure"); + expect(errors[0].errorMessage).toBe( + errorMessageFunc(true, qualifiers[0].label, true, categories[0].label) + ); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return NO errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validatePartialRateCompletionOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("should return NO errors - OPM", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = validatePartialRateCompletionOMS()({ + ...baseOMSInfo, + rateData: data, + isOPM: true, + }); + + expect(errors).toHaveLength(0); + }); + + it("should have errors", () => { + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + partialRate, + partialRate, + ]); + const errors = validatePartialRateCompletionOMS()({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(errors[0].errorMessage).toBe( + `Should not have partially filled NDR sets${` for ${qualifiers[0].label}`}${`, ${categories[0].label}`}.` + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith(["TestLabel"]); + }); + + it("should have errors - singleCategory", () => { + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData( + [ + { + label: SINGLE_CATEGORY, + text: SINGLE_CATEGORY, + id: SINGLE_CATEGORY, + }, + ], + qualifiers, + [partialRate] + ); + const errors = validatePartialRateCompletionOMS()({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + categories: [ + { + label: SINGLE_CATEGORY, + text: SINGLE_CATEGORY, + id: SINGLE_CATEGORY, + }, + ], + }); + + expect(errors).toHaveLength(2); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(errors[0].errorMessage).toBe( + `Should not have partially filled NDR sets${` for ${qualifiers[0].label}`}.` + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith(["TestLabel"]); + }); + + it("should have errors - OPM", () => { + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + partialRate, + partialRate, + ]); + const errors = validatePartialRateCompletionOMS()({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + isOPM: true, + }); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(errors[0].errorMessage).toBe( + `Should not have partially filled NDR sets.` + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith(["TestLabel"]); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessageFunc = ( + multipleQuals: boolean, + qualifier: string, + multipleCats: boolean, + category: string + ) => { + return `Another${multipleQuals} bites the ${qualifier}... dun dun dun... Another ${multipleCats} bites the ${category}`; + }; + + const locationDictionaryJestFunc = jest.fn(); + const data = generateOmsCategoryRateData(categories, qualifiers, [ + partialRate, + partialRate, + ]); + const errors = validatePartialRateCompletionOMS( + undefined, + errorMessageFunc + )({ + ...baseOMSInfo, + locationDictionary: locationDictionaryJestFunc, + rateData: data, + }); + + expect(errors).toHaveLength(4); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification:" + ); + expect(locationDictionaryJestFunc).toHaveBeenCalledWith(["TestLabel"]); + expect(errors[0].errorMessage).toBe( + errorMessageFunc(true, qualifiers[0].label, true, categories[0].label) + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.ts new file mode 100644 index 0000000000..60271879ce --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.ts @@ -0,0 +1,194 @@ +import { SINGLE_CATEGORY } from "dataConstants"; +import { + convertOmsDataToRateArray, + getOtherPerformanceMeasureRateArray, +} from "../dataDrivenTools"; +import { + UnifiedValFuncProps as UVFP, + FormRateField, + OmsValidationCallback, +} from "../types"; +import { LabelData } from "utils"; + +type ErrorMessageFunc = ( + multipleQuals: boolean, + qualifier: string, + multipleCats: boolean, + category: string +) => string; + +interface ValProps extends UVFP { + errorMessageFunc?: ErrorMessageFunc; +} + +const validatePartialRateCompletionErrorMessage: ErrorMessageFunc = ( + multipleQuals, + qualifier, + multipleCats, + category +) => { + return `Should not have partially filled NDR sets${ + multipleQuals ? ` for ${qualifier}` : "" + }${multipleCats ? `, ${category}` : ""}.`; +}; + +const _validation = ({ + location, + rateData, + categories, + qualifiers, + errorMessageFunc = validatePartialRateCompletionErrorMessage, +}: ValProps) => { + const errors: FormError[] = []; + + for (const [i, rateSet] of rateData.entries()) { + for (const [j, rate] of rateSet.entries()) { + if ( + rate && + (rate.numerator || rate.denominator || rate.rate) && + (!rate.denominator || !rate.numerator || !rate.rate) + ) { + const multipleQuals: boolean = !!qualifiers?.length; + const multipleCats: boolean = !!categories?.some((item) => item.label); + errors.push({ + errorLocation: location, + errorMessage: errorMessageFunc( + multipleQuals, + qualifiers?.[j].label!, + multipleCats, + categories?.[i].label! + ), + }); + } + } + } + + return errors; +}; + +interface SVVProps { + location: string; + rateData: any; + categories?: LabelData[]; + qualifiers?: LabelData[]; + locationDictionary: (s: string[]) => string; + errorMessageFunc?: ErrorMessageFunc; +} + +const _singleValueValidation = ({ + location, + rateData, + categories, + qualifiers, + locationDictionary, + errorMessageFunc = validatePartialRateCompletionErrorMessage, +}: SVVProps): FormError[] => { + const errors: FormError[] = []; + + for (const qualKey of Object.keys(rateData?.rates ?? {})) { + for (const catKey of Object.keys(rateData?.rates?.[qualKey] ?? {})) { + if ( + !!rateData?.rates?.[qualKey]?.[catKey]?.[0]?.fields && + // check some fields are empty + rateData.rates[qualKey][catKey][0].fields.some( + (field: any) => !field.value + ) && + // check not all fields are empty + !rateData.rates[qualKey][catKey][0].fields.every( + (field: any) => !field.value + ) + ) { + const multipleQuals: boolean = !!qualifiers?.length; + const multipleCats: boolean = !!categories?.length; + errors.push({ + errorLocation: location, + errorMessage: errorMessageFunc( + multipleQuals, + locationDictionary([qualKey]), + multipleCats, + locationDictionary([catKey]) + ), + }); + } + } + } + + return errors; +}; + +export const validatePartialRateCompletionOMS = + ( + singleValueFieldFlag?: "iuhh-rate" | "aifhh-rate", + errorMessageFunc?: ErrorMessageFunc + ): OmsValidationCallback => + ({ categories, isOPM, label, locationDictionary, qualifiers, rateData }) => { + return [ + ...(!!singleValueFieldFlag + ? _singleValueValidation({ + location: `Optional Measure Stratification: ${locationDictionary([ + ...label, + ])}`, + rateData: rateData?.[singleValueFieldFlag], + categories: !!( + isOPM || + categories[0].label === SINGLE_CATEGORY || + !categories[0].label + ) + ? undefined + : categories, + qualifiers: !!isOPM ? undefined : qualifiers, + locationDictionary, + errorMessageFunc, + }) + : _validation({ + location: `Optional Measure Stratification: ${locationDictionary([ + ...label, + ])}`, + rateData: convertOmsDataToRateArray( + categories, + qualifiers, + rateData + ), + categories: !!( + isOPM || + categories[0].label === SINGLE_CATEGORY || + !categories[0].label + ) + ? undefined + : categories, + qualifiers: !!isOPM ? undefined : qualifiers, + errorMessageFunc, + })), + ]; + }; + +/** + * Checks for fields that have been partially filled out and reports them. + * + * @param performanceMeasureArray pm data + * @param OPM opm data + * @param qualifiers required field for parsing PM data + * @param categories optional for further locating an error + */ +export const validatePartialRateCompletionPM = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + qualifiers: LabelData[], + categories?: LabelData[], + errorMessageFunc?: ErrorMessageFunc +) => { + return [ + ..._validation({ + location: "Performance Measure", + rateData: performanceMeasureArray, + categories, + qualifiers, + errorMessageFunc, + }), + ..._validation({ + location: "Other Performance Measure", + rateData: getOtherPerformanceMeasureRateArray(OPM), + errorMessageFunc, + }), + ]; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.test.ts new file mode 100644 index 0000000000..13507a2d25 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.test.ts @@ -0,0 +1,167 @@ +import { validateRateNotZeroOMS, validateRateNotZeroPM } from "."; +import { + generateOmsQualifierRateData, + locationDictionary, + manualZeroRate, + manualNonZeroRate, + simpleRate, + partialRate, + generateOtherPerformanceMeasureData, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Non-Zero/No Zero Numerator/Rate Validation", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM/OPM Validation", () => { + it("should return NO errors", () => { + const errors = validateRateNotZeroPM( + [[simpleRate, simpleRate]], + undefined, + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should have NO error for zero numerator but rate non-zero - Hybrid", () => { + const errors = validateRateNotZeroPM( + [], + generateOtherPerformanceMeasureData([ + manualNonZeroRate, + manualNonZeroRate, + manualNonZeroRate, + ]), + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error for zero rate but numerator non-zero", () => { + const errors = validateRateNotZeroPM( + [ + [manualZeroRate, manualZeroRate], + [manualZeroRate, manualZeroRate], + ], + undefined, + qualifiers + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + `Performance Measure/Other Performance Measure` + ); + expect(errors[0].errorMessage).toBe( + "Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation." + ); + }); + + it("should have error for zero rate but numerator non-zero - OPM", () => { + const errors = validateRateNotZeroPM( + [], + generateOtherPerformanceMeasureData([ + manualZeroRate, + manualZeroRate, + manualZeroRate, + ]), + qualifiers + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + `Performance Measure/Other Performance Measure` + ); + expect(errors[0].errorMessage).toBe( + "Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation." + ); + }); + + it("should NOT have error from empty rate value", () => { + const errors = validateRateNotZeroPM( + [ + [partialRate, partialRate], + [partialRate, partialRate], + ], + undefined, + qualifiers + ); + + expect(errors).toHaveLength(0); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const errors = validateRateNotZeroPM( + [ + [manualZeroRate, manualZeroRate], + [manualZeroRate, manualZeroRate], + ], + undefined, + qualifiers, + errorMessage + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + `Performance Measure/Other Performance Measure` + ); + expect(errors[0].errorMessage).toBe(errorMessage); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should have error for zero numerator but rate non-zero", () => { + const data = generateOmsQualifierRateData(categories, qualifiers, [ + manualZeroRate, + manualZeroRate, + ]); + const errors = validateRateNotZeroOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification: TestLabel" + ); + expect(errors[0].errorMessage).toBe( + "Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation." + ); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const data = generateOmsQualifierRateData(categories, qualifiers, [ + manualZeroRate, + manualZeroRate, + ]); + const errors = validateRateNotZeroOMS(errorMessage)({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification: TestLabel" + ); + expect(errors[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.ts new file mode 100644 index 0000000000..0e6d6515b9 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.ts @@ -0,0 +1,79 @@ +import { + OmsValidationCallback, + FormRateField, + UnifiedValFuncProps as UVFP, +} from "../types"; +import { + convertOmsDataToRateArray, + getOtherPerformanceMeasureRateArray, +} from "../dataDrivenTools"; +import { LabelData } from "utils"; + +export const validationRateNotZero = ({ + location, + rateData, + errorMessage, +}: UVFP) => { + const errorArray: FormError[] = []; + + for (const ratefields of rateData) { + for (const rate of ratefields) { + if (rate && rate.denominator && rate.numerator && rate.rate) { + if ( + parseFloat(rate.numerator) > 0 && + parseFloat(rate.denominator) > 0 && + parseFloat(rate.rate) === 0 + ) { + errorArray.push({ + errorLocation: location, + errorMessage: + errorMessage ?? + "Rate should not be 0 if numerator and denominator are not 0. If the calculated rate is less than 0.5, disregard this validation.", + }); + } + } + } + } + + return errorArray; +}; + +export const validateRateNotZeroOMS = + (errorMessage?: string): OmsValidationCallback => + ({ categories, qualifiers, rateData, label, locationDictionary }) => { + return validationRateNotZero({ + categories, + qualifiers, + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + errorMessage, + }).filter((v, i, a) => i === 0 || a[0].errorLocation !== v.errorLocation); + }; + +// If a user manually over-rides a rate it must not violate two rules: +// It must be zero if the numerator is zero +export const validateRateNotZeroPM = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + _qualifiers: LabelData[], + errorMessage?: string +) => { + const errorArray: FormError[] = []; + const location = `Performance Measure/Other Performance Measure`; + const rateDataOPM = getOtherPerformanceMeasureRateArray(OPM); + + const errors = [ + ...validationRateNotZero({ + location, + rateData: performanceMeasureArray, + errorMessage, + }), + ...validationRateNotZero({ + location, + rateData: rateDataOPM, + errorMessage, + }), + ]; + if (!!errors.length) errorArray.push(errors[0]); + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.test.ts new file mode 100644 index 0000000000..c7ec64219f --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.test.ts @@ -0,0 +1,198 @@ +import * as DC from "dataConstants"; +import { validateRateZeroOMS, validateRateZeroPM } from "."; + +import { testFormData } from "../testHelpers/_testFormData"; + +import { + generateOmsQualifierRateData, + locationDictionary, + manualNonZeroRate, + simpleRate, + partialRate, + generateOtherPerformanceMeasureData, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Non-Zero/No Zero Numerator/Rate Validation", () => { + const categories = [ + { label: "Test Cat 1", text: "Test Cat 1", id: "Test Cat 1" }, + { label: "Test Cat 2", text: "Test Cat 2", id: "Test Cat 2" }, + ]; + const qualifiers = [ + { label: "Test Qual 1", text: "Test Qual 1", id: "Test Qual 1" }, + { label: "Test Qual 2", text: "Test Qual 2", id: "Test Qual 2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + // PM + describe("PM/OPM Validation", () => { + it("should return NO errors", () => { + const errors = validateRateZeroPM( + [[simpleRate, simpleRate]], + undefined, + qualifiers, + { ...testFormData } + ); + + expect(errors).toHaveLength(0); + }); + + it("should have error for zero numerator but rate non-zero", () => { + const errors = validateRateZeroPM( + [ + [manualNonZeroRate, manualNonZeroRate], + [manualNonZeroRate, manualNonZeroRate], + ], + undefined, + qualifiers, + { ...testFormData } + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + `Performance Measure/Other Performance Measure` + ); + expect(errors[0].errorMessage).toBe( + "Manually entered rate should be 0 if numerator is 0" + ); + }); + + it("should have error for zero numerator but rate non-zero - OPM", () => { + const errors = validateRateZeroPM( + [], + generateOtherPerformanceMeasureData([ + manualNonZeroRate, + manualNonZeroRate, + manualNonZeroRate, + ]), + qualifiers, + { ...testFormData } + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + `Performance Measure/Other Performance Measure` + ); + expect(errors[0].errorMessage).toBe( + "Manually entered rate should be 0 if numerator is 0" + ); + }); + + it("should have NO error for zero numerator but rate non-zero - Hybrid", () => { + const errors = validateRateZeroPM( + [], + generateOtherPerformanceMeasureData([ + manualNonZeroRate, + manualNonZeroRate, + manualNonZeroRate, + ]), + qualifiers, + { + ...testFormData, + DataSource: [DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA], + } + ); + + expect(errors).toHaveLength(0); + }); + + it("should NOT have error from empty rate value", () => { + const errors = validateRateZeroPM( + [ + [partialRate, partialRate], + [partialRate, partialRate], + ], + undefined, + qualifiers, + testFormData + ); + + expect(errors).toHaveLength(0); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const errors = validateRateZeroPM( + [ + [manualNonZeroRate, manualNonZeroRate], + [manualNonZeroRate, manualNonZeroRate], + ], + undefined, + qualifiers, + { ...testFormData }, + errorMessage + ); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toBe( + `Performance Measure/Other Performance Measure` + ); + expect(errors[0].errorMessage).toBe(errorMessage); + }); + }); + + // OMS + describe("OMS Validation", () => { + it("should return NO errors", () => { + const data = generateOmsQualifierRateData(categories, qualifiers, [ + simpleRate, + simpleRate, + ]); + const errors = [ + ...validateRateZeroOMS()({ + ...baseOMSInfo, + rateData: data, + }), + ...validateRateZeroOMS()({ + ...baseOMSInfo, + rateData: data, + }), + ]; + + expect(errors).toHaveLength(0); + }); + + it("should have error for zero rate but numerator non-zero", () => { + const data = generateOmsQualifierRateData(categories, qualifiers, [ + manualNonZeroRate, + manualNonZeroRate, + ]); + const errors = validateRateZeroOMS()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification: TestLabel" + ); + expect(errors[0].errorMessage).toBe( + "Manually entered rate should be 0 if numerator is 0" + ); + }); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust."; + const data = generateOmsQualifierRateData(categories, qualifiers, [ + manualNonZeroRate, + manualNonZeroRate, + ]); + const errors = validateRateZeroOMS(errorMessage)({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(1); + expect(errors[0].errorLocation).toContain( + "Optional Measure Stratification: TestLabel" + ); + expect(errors[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.ts new file mode 100644 index 0000000000..7bd0610a2b --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.ts @@ -0,0 +1,105 @@ +import * as DC from "dataConstants"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { + OmsValidationCallback, + FormRateField, + UnifiedValFuncProps as UVFP, +} from "../types"; +import { + convertOmsDataToRateArray, + getOtherPerformanceMeasureRateArray, +} from "../dataDrivenTools"; +import { LabelData } from "utils"; + +interface ValProps extends UVFP { + hybridData?: boolean; +} + +export const validationRateZero = ({ + location, + rateData, + hybridData, + errorMessage, +}: ValProps) => { + const errorArray: FormError[] = []; + + for (const ratefields of rateData) { + for (const rate of ratefields) { + if (rate && rate.denominator && rate.numerator && rate.rate) { + if ( + parseFloat(rate.numerator) === 0 && + parseFloat(rate.denominator) > 0 && + parseFloat(rate.rate) !== 0 && + !hybridData + ) { + errorArray.push({ + errorLocation: location, + errorMessage: + errorMessage ?? + "Manually entered rate should be 0 if numerator is 0", + }); + } + } + } + } + + return errorArray; +}; + +export const validateRateZeroOMS = + (errorMessage?: string): OmsValidationCallback => + ({ + categories, + qualifiers, + rateData, + label, + locationDictionary, + dataSource, + }) => { + const hybridData = dataSource?.includes( + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA + ); + return validationRateZero({ + categories, + qualifiers, + hybridData, + location: `Optional Measure Stratification: ${locationDictionary(label)}`, + rateData: convertOmsDataToRateArray(categories, qualifiers, rateData), + errorMessage, + }).filter((v, i, a) => i === 0 || a[0].errorLocation !== v.errorLocation); + }; + +// If a user manually over-rides a rate it must not violate two rules: +// It must be zero if the numerator is zero +export const validateRateZeroPM = ( + performanceMeasureArray: FormRateField[][], + OPM: any, + _qualifiers: LabelData[], + data: Types.DefaultFormData, + errorMessage?: string +): FormError[] => { + const errorArray: FormError[] = []; + const hybridData = data?.[DC.DATA_SOURCE]?.includes( + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA + ); + const location = `Performance Measure/Other Performance Measure`; + const rateDataOPM = getOtherPerformanceMeasureRateArray(OPM); + + const errors = [ + ...validationRateZero({ + location, + rateData: performanceMeasureArray, + hybridData, + errorMessage, + }), + ...validationRateZero({ + location, + rateData: rateDataOPM, + hybridData, + errorMessage, + }), + ]; + + if (!!errors.length) errorArray.push(errors[0]); + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.test.ts new file mode 100644 index 0000000000..be2f5489ff --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.test.ts @@ -0,0 +1,44 @@ +import * as DC from "dataConstants"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateReasonForNotReporting } from "."; + +describe("validateReasonForNotReporting", () => { + let formData: string[]; + let errorArray: FormError[]; + + const _check_errors = ( + data: string[], + numErrors: number, + collecting?: boolean + ) => { + errorArray = [...validateReasonForNotReporting(data, collecting)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = testFormData[DC.WHY_ARE_YOU_NOT_REPORTING]; // reset data + errorArray = []; + }); + + it("Default form data", () => { + _check_errors(["Sample Error Reason"], 0, false); + }); + + it("Reason for not reporting not selected (reporting)", () => { + _check_errors(formData, 1); + }); + + it("Reason for not reporting not selected (collecting)", () => { + _check_errors(formData, 1, true); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessageFunc = (collecting?: boolean) => { + return `Another ${collecting} bites the dust`; + }; + errorArray = [ + ...validateReasonForNotReporting(formData, true, errorMessageFunc), + ]; + expect(errorArray[0].errorMessage).toBe(errorMessageFunc(true)); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.ts new file mode 100644 index 0000000000..ac081cc31d --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.ts @@ -0,0 +1,27 @@ +const validateReasonForNotReportingErrorMessage = (collecting?: boolean) => { + return `You must select at least one reason for not ${ + collecting ? "collecting" : "reporting" + } on this measure`; +}; + +export const validateReasonForNotReporting = ( + whyNotReporting: any, + collecting?: boolean, + errorMessageFunc = validateReasonForNotReportingErrorMessage +) => { + let error = false; + const errorArray: FormError[] = []; + + if (!(whyNotReporting && whyNotReporting.length > 0)) { + error = true; + } + if (error) { + errorArray.push({ + errorLocation: `Why Are You Not ${ + collecting ? "Collecting" : "Reporting" + } On This Measure`, + errorMessage: errorMessageFunc(collecting), + }); + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.test.ts new file mode 100644 index 0000000000..69f1093419 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.test.ts @@ -0,0 +1,47 @@ +import * as DC from "dataConstants"; +import { test_setup } from "../testHelpers/_helper"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateRequiredRadioButtonForCombinedRates } from "."; + +describe("validateRequiredRadioButtonForCombinedRates", () => { + let formData: any; + let errorArray: FormError[]; + + const _check_errors = (data: any, numErrors: number) => { + const { ageGroups, performanceMeasureArray, OPM } = test_setup(data); + ageGroups; + performanceMeasureArray; + OPM; + + errorArray = [...validateRequiredRadioButtonForCombinedRates(data)]; + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = { ...testFormData }; + errorArray = []; + }); + + it("Default form data", () => { + _check_errors(formData, 0); + }); + + it("No error if combined rate not checked", () => { + delete formData[DC.COMBINED_RATES]; + _check_errors(formData, 0); + }); + + it("Should throw error for missing field", () => { + delete formData[DC.COMBINED_RATES_COMBINED_RATES]; + _check_errors(formData, 1); + }); + + it("Error message text should match provided errorMessage", () => { + const errorMessage = "Another one bites the dust"; + delete formData[DC.COMBINED_RATES_COMBINED_RATES]; + errorArray = [ + ...validateRequiredRadioButtonForCombinedRates(formData, errorMessage), + ]; + expect(errorArray[0].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.ts new file mode 100644 index 0000000000..f7ad439e76 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.ts @@ -0,0 +1,22 @@ +import * as DC from "dataConstants"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export const validateRequiredRadioButtonForCombinedRates = ( + data: Types.CombinedRates, + errorMessage?: string +) => { + const errorArray: FormError[] = []; + + if (data.CombinedRates && data.CombinedRates === DC.YES) { + if (!data["CombinedRates-CombinedRates"]) { + errorArray.push({ + errorLocation: "Combined Rate(s)", + errorMessage: + errorMessage ?? + "You must select at least one option for Combined Rate(s) Details if Yes is selected.", + }); + } + } + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.test.ts new file mode 100644 index 0000000000..a4eb4e53be --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.test.ts @@ -0,0 +1,56 @@ +import { validateSameDenominatorSets } from "."; +import { + generateOmsCategoryRateData, + locationDictionary, + simpleRate, + doubleRate, + lowerRate, + higherRate, +} from "utils/testUtils/2024/validationHelpers"; + +describe("Testing Same Denominator Set Validation", () => { + const categories = [ + { label: "TestCat1", text: "TestCat1", id: "TestCat1" }, + { label: "TestCat2", text: "TestCat2", id: "TestCat2" }, + ]; + const qualifiers = [ + { label: "TestQual1", text: "TestQual1", id: "TestQual1" }, + { label: "TestQual2", text: "TestQual2", id: "TestQual2" }, + ]; + + const baseOMSInfo = { + categories, + qualifiers, + locationDictionary, + isOPM: false, + label: ["TestLabel"], + }; + + it("Denominator should return no errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + lowerRate, + higherRate, + ]); + + const errors = validateSameDenominatorSets()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(0); + }); + + it("Denominator should return errors", () => { + const data = generateOmsCategoryRateData(categories, qualifiers, [ + simpleRate, + doubleRate, + ]); + + const errors = validateSameDenominatorSets()({ + ...baseOMSInfo, + rateData: data, + }); + + expect(errors).toHaveLength(2); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.ts new file mode 100644 index 0000000000..c5cd70b733 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.ts @@ -0,0 +1,44 @@ +import { OmsValidationCallback } from "../types"; + +export const validateSameDenominatorSets = + (errorMessage?: string): OmsValidationCallback => + ({ rateData, locationDictionary, categories, qualifiers, isOPM, label }) => { + if (isOPM) return []; + const errorArray: FormError[] = []; + + for (const qual of qualifiers) { + for ( + let initiation = 0; + initiation < categories.length; + initiation += 2 + ) { + const engagement = initiation + 1; + + const initRate = + rateData.rates?.[categories[initiation].id]?.[qual.id]?.[0]; + const engageRate = + rateData.rates?.[categories[engagement].id]?.[qual.id]?.[0]; + + if ( + initRate && + initRate.denominator && + engageRate && + engageRate.denominator && + initRate.denominator !== engageRate.denominator + ) { + errorArray.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + [...label, qual.id] + )}`, + errorMessage: + errorMessage ?? + `Denominators must be the same for ${locationDictionary([ + categories[initiation].label, + ])} and ${locationDictionary([categories[engagement].label])}.`, + }); + } + } + } + + return errorArray; + }; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.test.tsx b/services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.test.tsx new file mode 100644 index 0000000000..e0220813fa --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.test.tsx @@ -0,0 +1,357 @@ +import { validateTotalNDR, validateOMSTotalNDR } from "."; + +import * as VH from "utils/testUtils/2024/validationHelpers"; + +describe("Testing PM/OMS Total Validations", () => { + describe("PM validation", () => { + it("should return no errors", () => { + const basePM = [VH.simpleRate, VH.simpleRate, VH.doubleRate]; + const singleResult = validateTotalNDR([basePM]); + const multiResults = validateTotalNDR([basePM, basePM, basePM]); + + expect(singleResult.length).toBe(0); + expect(multiResults.length).toBe(0); + }); + + it("should return numerator error", () => { + const basePM = [VH.simpleRate, VH.simpleRate, VH.incorrectNumeratorRate]; + const singleResults = validateTotalNDR([basePM]); + const multiResults = validateTotalNDR([basePM, basePM, basePM]); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const result of [...singleResults, ...multiResults]) { + expect(result.errorLocation).toBe("Performance Measure"); + expect(result.errorMessage).toBe( + `${VH.incorrectNumeratorRate.label} numerator field is not equal to the sum of other numerators.` + ); + } + }); + + it("should return denominator error", () => { + const basePM = [ + VH.simpleRate, + VH.simpleRate, + VH.incorrectDenominatorRate, + ]; + const singleResults = validateTotalNDR([basePM]); + const multiResults = validateTotalNDR([basePM, basePM, basePM]); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const result of [...singleResults, ...multiResults]) { + expect(result.errorLocation).toBe("Performance Measure"); + expect(result.errorMessage).toBe( + `${VH.incorrectDenominatorRate.label} denominator field is not equal to the sum of other denominators.` + ); + } + }); + + it("should return field empty error", () => { + const basePM = [VH.simpleRate, VH.simpleRate, VH.emptyRate]; + + // single PM check + const singleResults = validateTotalNDR([basePM]); + const multiResults = validateTotalNDR([basePM, basePM, basePM]); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const result of [...singleResults, ...multiResults]) { + expect(result.errorLocation).toBe("Performance Measure"); + expect(result.errorMessage).toBe( + `${VH.emptyRate.label} must contain values if other fields are filled.` + ); + } + }); + + it("should return no errors for partial state", () => { + const basePM = [VH.partialRate, VH.simpleRate, VH.simpleRate]; + const singleResult = validateTotalNDR([basePM]); + const multiResults = validateTotalNDR([basePM, basePM, basePM]); + + expect(singleResult.length).toBe(0); + expect(multiResults.length).toBe(0); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = (qualifier: string, fieldType: string) => { + return `Another ${qualifier} bites the ${fieldType}.`; + }; + + const basePM = [ + VH.simpleRate, + VH.simpleRate, + VH.incorrectDenominatorRate, + ]; + const singleResults = validateTotalNDR( + [basePM], + undefined, + undefined, + errorMessageFunc + ); + const multiResults = validateTotalNDR( + [basePM, basePM, basePM], + undefined, + undefined, + errorMessageFunc + ); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const result of [...singleResults, ...multiResults]) { + expect(result.errorLocation).toBe("Performance Measure"); + expect(result.errorMessage).toBe( + errorMessageFunc(VH.incorrectDenominatorRate.label!, "Denominator") + ); + } + }); + }); + + describe("OMS validation", () => { + const label = ["TestLabel"]; + + const noCategories = [ + { label: "singleCategory", text: "singleCategory", id: "singleCategory" }, + ]; + const categories = [ + { label: "test1", text: "test1", id: "test1" }, + { label: "test2", text: "test2", id: "test2" }, + { label: "test3", text: "test3", id: "test3" }, + ]; + const qualifiers = [ + { label: "test1", text: "test1", id: "test1" }, + { label: "test2", text: "test2", id: "test2" }, + { label: "testTotal", text: "testTotal", id: "testTotal" }, + ]; + + const locationDictionary = (s: string[]) => { + return s[0]; + }; + + const baseMultiFunctionInfo = { + label, + categories, + qualifiers, + locationDictionary, + rateData: {}, + isOPM: false, + }; + const baseSingleFunctionInfo = { + label, + categories: noCategories, + qualifiers, + locationDictionary, + rateData: {}, + isOPM: false, + }; + + it("should stop if this is OPM", () => { + const results = validateOMSTotalNDR()({ + ...baseSingleFunctionInfo, + isOPM: true, + }); + + expect(results.length).toBe(0); + }); + + it("should return no errors", () => { + const basePMData = [VH.simpleRate, VH.simpleRate, VH.doubleRate]; + + const singleResult = validateOMSTotalNDR()({ + ...baseSingleFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + noCategories, + qualifiers, + basePMData + ), + }); + const multiResults = validateOMSTotalNDR()({ + ...baseMultiFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + categories, + qualifiers, + basePMData + ), + }); + + expect(singleResult.length).toBe(0); + expect(multiResults.length).toBe(0); + }); + + it("should return numerator error", () => { + const basePMData = [ + VH.simpleRate, + VH.simpleRate, + VH.incorrectNumeratorRate, + ]; + + const singleResults = validateOMSTotalNDR()({ + ...baseSingleFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + noCategories, + qualifiers, + basePMData + ), + }); + const multiResults = validateOMSTotalNDR()({ + ...baseMultiFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + categories, + qualifiers, + basePMData + ), + }); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const error of [...singleResults, ...multiResults]) { + expect(error.errorLocation).toBe( + "Optional Measure Stratification: TestLabel" + ); + expect(error.errorMessage).toBe( + "Total numerator field is not equal to the sum of other numerators." + ); + } + }); + + it("should return denominator error", () => { + const basePMData = [ + VH.simpleRate, + VH.simpleRate, + VH.incorrectDenominatorRate, + ]; + + const singleResults = validateOMSTotalNDR()({ + ...baseSingleFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + noCategories, + qualifiers, + basePMData + ), + }); + const multiResults = validateOMSTotalNDR()({ + ...baseMultiFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + categories, + qualifiers, + basePMData + ), + }); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const error of [...singleResults, ...multiResults]) { + expect(error.errorLocation).toBe( + "Optional Measure Stratification: TestLabel" + ); + expect(error.errorMessage).toBe( + "Total denominator field is not equal to the sum of other denominators." + ); + } + }); + + it("should return field empty error", () => { + const basePMData = [VH.simpleRate, VH.simpleRate, VH.emptyRate]; + + const singleResults = validateOMSTotalNDR()({ + ...baseSingleFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + noCategories, + qualifiers, + basePMData + ), + }); + const multiResults = validateOMSTotalNDR()({ + ...baseMultiFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + categories, + qualifiers, + basePMData + ), + }); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const error of [...singleResults, ...multiResults]) { + expect(error.errorLocation).toBe( + "Optional Measure Stratification: TestLabel" + ); + expect(error.errorMessage).toBe( + "Total must contain values if other fields are filled." + ); + } + }); + + it("should return no errors for a partial state", () => { + const basePMData = [VH.partialRate, VH.simpleRate, VH.simpleRate]; + + const singleResult = validateOMSTotalNDR()({ + ...baseSingleFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + noCategories, + qualifiers, + basePMData + ), + }); + const multiResults = validateOMSTotalNDR()({ + ...baseMultiFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + categories, + qualifiers, + basePMData + ), + }); + + expect(singleResult.length).toBe(0); + expect(multiResults.length).toBe(0); + }); + + it("Error message text should match provided errorMessageFunc", () => { + const errorMessageFunc = (fieldType: string, totalLabel?: string) => { + return `Another ${fieldType} bites the ${totalLabel ?? ""}.`; + }; + + const basePMData = [ + VH.simpleRate, + VH.simpleRate, + VH.incorrectNumeratorRate, + ]; + + const singleResults = validateOMSTotalNDR(errorMessageFunc)({ + ...baseSingleFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + noCategories, + qualifiers, + basePMData + ), + }); + const multiResults = validateOMSTotalNDR(errorMessageFunc)({ + ...baseMultiFunctionInfo, + rateData: VH.generateOmsQualifierRateData( + categories, + qualifiers, + basePMData + ), + }); + + expect(singleResults.length).toBe(1); + expect(multiResults.length).toBe(3); + for (const error of [...singleResults, ...multiResults]) { + expect(error.errorLocation).toBe( + "Optional Measure Stratification: TestLabel" + ); + expect(error.errorMessage).toBe( + errorMessageFunc("numerator", undefined) + ); + } + for (const result of [...singleResults, ...multiResults]) { + expect(result.errorLocation).toBe( + "Optional Measure Stratification: TestLabel" + ); + expect(result.errorMessage).toBe( + errorMessageFunc("numerator", undefined) + ); + } + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.ts new file mode 100644 index 0000000000..1cac61d3fb --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.ts @@ -0,0 +1,179 @@ +import { OmsValidationCallback, FormRateField } from "../types"; +import { LabelData } from "utils"; + +const validateOMSTotalNDRErrorMessage = ( + fieldType: string, + totalLabel?: string +) => { + if (fieldType === "Total") { + return `${ + totalLabel ? `${totalLabel} ` : "" + }Total must contain values if other fields are filled.`; + } + return `${ + totalLabel ? `${totalLabel} ` : "" + }Total ${fieldType} field is not equal to the sum of other ${fieldType}s.`; +}; + +export const validateOMSTotalNDR = + (errorMessageFunc = validateOMSTotalNDRErrorMessage): OmsValidationCallback => + ({ + categories, + qualifiers, + rateData, + label, + locationDictionary, + isOPM, + customTotalLabel, + }) => { + if (isOPM) return []; + + const error: FormError[] = []; + + for (const cat of categories.map((s) => s.id)) { + const ndrSets = []; + let numeratorSum: any = null; // initialized as a non-zero value to accurately compare + let denominatorSum: any = null; + for (const qual of qualifiers.map((s) => s.id)) { + ndrSets.push(rateData.rates?.[cat]?.[qual]?.[0]); + } + + // The last NDR set is the total + const totalNDR = ndrSets.pop(); + + // Calculate numerator and denominator totals + ndrSets.forEach((set) => { + if (set && set.denominator && set.numerator && set.rate) { + numeratorSum += parseFloat(set.numerator); + denominatorSum += parseFloat(set.denominator); + } + }); + + if (totalNDR && totalNDR.numerator && totalNDR.denominator) { + let x; + if ( + (x = parseFloat(totalNDR.numerator)) !== numeratorSum && + numeratorSum !== null && + !isNaN(x) + ) { + error.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + [...label, qualifiers.slice(-1)[0].label] + )}`, + errorMessage: errorMessageFunc("numerator", customTotalLabel), + }); + } + if ( + (x = parseFloat(totalNDR.denominator)) !== denominatorSum && + denominatorSum !== null && + !isNaN(x) + ) { + error.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + [...label, qualifiers.slice(-1)[0].label] + )}`, + errorMessage: errorMessageFunc("denominator", customTotalLabel), + }); + } + } else if (numeratorSum && denominatorSum) { + error.push({ + errorLocation: `Optional Measure Stratification: ${locationDictionary( + [...label, qualifiers.slice(-1)[0].label] + )}`, + errorMessage: errorMessageFunc("Total", customTotalLabel), + }); + } + } + + return error; + }; + +const validateTotalNDRErrorMessage = (qualifier: string, fieldType: string) => { + if (fieldType === "Total") { + return `${qualifier} must contain values if other fields are filled.`; + } + return `${qualifier} ${fieldType.toLowerCase()} field is not equal to the sum of other ${fieldType.toLowerCase()}s.`; +}; + +/* +Validate that the values represented in the Total NDR fields are the sum of the respective non-total fields. +e.g. numerator === sumOfAllOtherNumerators + +This validation can be applied for both Performance Measure and OMS sections. +Default assumption is that this is run for Performance Measure unless specified. +*/ +export const validateTotalNDR = ( + performanceMeasureArray: FormRateField[][], + errorLocation = "Performance Measure", + categories?: LabelData[], + errorMessageFunc = validateTotalNDRErrorMessage +): FormError[] => { + let errorArray: FormError[] = []; + + performanceMeasureArray.forEach((ndrSet, idx) => { + // If this measure has a totalling NDR, the last NDR set is the total. + let numeratorSum: any = null; + let denominatorSum: any = null; + ndrSet.slice(0, -1).forEach((item: any) => { + if ( + item !== undefined && + item !== null && + !item["isTotal"] && + item.rate + ) { + let x; + if (!isNaN((x = parseFloat(item["numerator"])))) { + numeratorSum = numeratorSum + x; // += syntax does not work if default value is null + } + if (!isNaN((x = parseFloat(item["denominator"])))) { + denominatorSum = denominatorSum + x; // += syntax does not work if default value is null + } + } + }); + + let totalNDR = ndrSet[ndrSet.length - 1]; + if (totalNDR?.denominator && totalNDR?.numerator) { + // If we wanted to get fancy we could offer expected values in here quite easily. + + const parsedNum = parseFloat(totalNDR.numerator ?? ""); + const parsedDen = parseFloat(totalNDR.denominator ?? ""); + if ( + parsedNum !== numeratorSum && + numeratorSum !== null && + !isNaN(parsedNum) + ) { + const qualifier = + (categories && categories[idx].label) || totalNDR.label || ""; + errorArray.push({ + errorLocation: errorLocation, + errorMessage: errorMessageFunc(qualifier, "Numerator"), + }); + } + if ( + parsedDen !== denominatorSum && + denominatorSum !== null && + !isNaN(parsedDen) + ) { + const qualifier = + (categories && categories[idx].label) || totalNDR.label || ""; + errorArray.push({ + errorLocation: errorLocation, + errorMessage: errorMessageFunc(qualifier, "Denominator"), + }); + } + } else if (numeratorSum && denominatorSum) { + const fieldLabel = + (categories && + categories[idx]?.label && + `${categories[idx].label} - ${totalNDR.label}`) || + totalNDR.label || + ""; + errorArray.push({ + errorLocation: errorLocation, + errorMessage: errorMessageFunc(fieldLabel, "Total"), + }); + } + }); + + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.test.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.test.ts new file mode 100644 index 0000000000..532ff5205e --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.test.ts @@ -0,0 +1,87 @@ +import * as DC from "dataConstants"; +import { testFormData } from "../testHelpers/_testFormData"; +import { validateYearFormat } from "."; + +/* This validation checks that start and end date years match YYYY format. */ +describe("ensureBothYearsMatchYYYYFormat", () => { + let formData: any; + + const run_validation = (data: any, errorMessage?: string): FormError[] => { + const dateRange = data[DC.DATE_RANGE]; + return [...validateYearFormat(dateRange, errorMessage)]; + }; + + const check_errors = (data: any, numErrors: number) => { + const errorArray: FormError[] = run_validation(data); + expect(errorArray.length).toBe(numErrors); + }; + + beforeEach(() => { + formData = JSON.parse(JSON.stringify(testFormData)); // reset data + }); + + it("when DATE_RANGE is undefined", () => { + delete formData[DC.DATE_RANGE]; + check_errors(formData, 0); + }); + + it("when START_DATE is undefined and END_DATE is undefined", () => { + delete formData[DC.DATE_RANGE][DC.START_DATE]; + delete formData[DC.DATE_RANGE][DC.END_DATE]; + check_errors(formData, 0); + }); + + it("when START_DATE is complete and END_DATE is complete", () => { + check_errors(formData, 0); + }); + + it("when START_DATE and END_DATE years have one digit values", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 1; + + // End Date + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = 1; + check_errors(formData, 2); + }); + + it("when START_DATE and END_DATE have two digit values", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 22; + + // End Date + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = 22; + check_errors(formData, 2); + }); + + it("when START_DATE and END_DATE have three digit values", () => { + // Start Date + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 333; + + // End Date + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = 333; + check_errors(formData, 2); + }); + + it("Error message text should match default errorMessage", () => { + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 1; + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = 1; + const errorArray = run_validation(formData); + expect(errorArray.length).toBe(2); + expect(errorArray[0].errorMessage).toBe( + "Please enter start date year in YYYY-format" + ); + expect(errorArray[1].errorMessage).toBe( + "Please enter end date year in YYYY-format" + ); + }); + + it("Error message text should match provided errorMessage", () => { + formData[DC.DATE_RANGE][DC.START_DATE][DC.SELECTED_YEAR] = 1; + formData[DC.DATE_RANGE][DC.END_DATE][DC.SELECTED_YEAR] = 1; + const errorMessage = "Another one bites the dust."; + const errorArray = run_validation(formData, errorMessage); + expect(errorArray.length).toBe(2); + expect(errorArray[0].errorMessage).toBe(errorMessage); + expect(errorArray[1].errorMessage).toBe(errorMessage); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.ts b/services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.ts new file mode 100644 index 0000000000..fee817281a --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.ts @@ -0,0 +1,30 @@ +import { DateRange } from "measures/2024/shared/CommonQuestions/types"; + +export const validateYearFormat = ( + dateRange: DateRange["DateRange"], + errorMessage?: string +) => { + const errorArray: FormError[] = []; + if ( + dateRange?.startDate?.selectedYear && + dateRange.startDate.selectedYear.toString().length !== 4 + ) { + errorArray.push({ + errorLocation: "Date Range", + errorMessage: + errorMessage ?? "Please enter start date year in YYYY-format", + errorType: "Warning", + }); + } + if ( + dateRange?.endDate?.selectedYear && + dateRange.endDate.selectedYear.toString().length !== 4 + ) { + errorArray.push({ + errorLocation: "Date Range", + errorMessage: errorMessage ?? "Please enter end date year in YYYY-format", + errorType: "Warning", + }); + } + return errorArray; +}; diff --git a/services/ui-src/src/measures/2024/shared/util/validationsMock.tsx b/services/ui-src/src/measures/2024/shared/util/validationsMock.tsx new file mode 100644 index 0000000000..94b21e2da3 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/util/validationsMock.tsx @@ -0,0 +1,178 @@ +import * as validateAtLeastOneDataSource from "measures/2024/shared/globalValidations/validateAtLeastOneDataSource"; +import * as validateAtLeastOneDefinitionOfPopulation from "measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation"; +import * as validateAtLeastOneDataSourceType from "measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType"; +import * as validateAtLeastOneDeliverySystem from "measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem"; +import * as validateAtLeastOneDeviationFieldFilled from "measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled"; +import * as validateAtLeastOneRateComplete from "measures/2024/shared/globalValidations/validateAtLeastOneRateComplete"; +import * as validateBothDatesInRange from "measures/2024/shared/globalValidations/validateBothDatesInRange"; +import * as validateDualPopInformation from "measures/2024/shared/globalValidations/validateDualPopInformation"; +import * as validateEqualCategoryDenominators from "measures/2024/shared/globalValidations/validateEqualCategoryDenominators"; +import * as validateEqualQualifierDenominators from "measures/2024/shared/globalValidations/validateEqualQualifierDenominators"; +import * as validateFfsRadioButtonCompletion from "measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion"; +import * as validateRateNotZero from "measures/2024/shared/globalValidations/validateRateNotZero"; +import * as validateRateZero from "measures/2024/shared/globalValidations/validateRateZero"; +import * as validateNumeratorsLessThanDenominators from "measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators"; +import * as validateOneCatRateHigherThanOtherCat from "measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat"; +import * as validateOneQualDenomHigherThanOtherDenomOMS from "measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS"; +import * as validateOneQualRateHigherThanOtherQual from "measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual"; +import * as validateReasonForNotReporting from "measures/2024/shared/globalValidations/validateReasonForNotReporting"; +import * as validateRequiredRadioButtonForCombinedRates from "measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates"; +import * as validateTotals from "measures/2024/shared/globalValidations/validateTotals"; +import * as PCRatLeastOneRateComplete from "measures/2024/shared/globalValidations/PCRValidations/PCRatLeastOneRateComplete"; +import * as PCRnoNonZeroNumOrDenom from "measures/2024/shared/globalValidations/PCRValidations/PCRnoNonZeroNumOrDenom"; +import * as ComplexAtLeastOneRateComplete from "measures/2024/shared/globalValidations/ComplexValidations/ComplexAtLeastOneRateComplete"; +import * as ComplexNoNonZeroNumOrDenom from "measures/2024/shared/globalValidations/ComplexValidations/ComplexNoNonZeroNumOrDenom"; +import * as ComplexValidateNDRTotals from "measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateNDRTotals"; +import * as ComplexValidateDualPopInformation from "measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateDualPopInformation"; +import * as ComplexValueSameCrossCategory from "measures/2024/shared/globalValidations/ComplexValidations/ComplexValueSameCrossCategory"; +import { DefaultFormData } from "measures/2024/shared/CommonQuestions/types"; + +/** + * Replicate the behavior of the validateAndSetErrors() function in the MeasureWrapper + */ +export const mockValidateAndSetErrors = ( + validationFunctions: any, + data: DefaultFormData | {} = {} +) => { + validationFunctions.reduce((_a: any, current: any) => current(data), []); +}; + +export const clearMocks = () => { + for (const mock in validationsMockObj) { + validationsMockObj[mock].mockClear(); + } +}; + +/** + * Spies for all the validation functions in the globalValidations folder + */ +export const validationsMockObj: any = { + validateAtLeastOneDataSource: jest.spyOn( + validateAtLeastOneDataSource, + "validateAtLeastOneDataSource" + ), + validateAtLeastOneDefinitionOfPopulation: jest.spyOn( + validateAtLeastOneDefinitionOfPopulation, + "validateAtLeastOneDefinitionOfPopulation" + ), + validateAtLeastOneDataSourceType: jest.spyOn( + validateAtLeastOneDataSourceType, + "validateAtLeastOneDataSourceType" + ), + validateAtLeastOneDeliverySystem: jest.spyOn( + validateAtLeastOneDeliverySystem, + "validateAtLeastOneDeliverySystem" + ), + validateAtLeastOneDeviationFieldFilled: jest.spyOn( + validateAtLeastOneDeviationFieldFilled, + "validateAtLeastOneDeviationFieldFilled" + ), + validateAtLeastOneRateComplete: jest.spyOn( + validateAtLeastOneRateComplete, + "validateAtLeastOneRateComplete" + ), + validateBothDatesCompleted: jest.spyOn( + validateBothDatesInRange, + "validateBothDatesCompleted" + ), + validateDualPopInformationPM: jest.spyOn( + validateDualPopInformation, + "validateDualPopInformationPM" + ), + validateEqualCategoryDenominatorsPM: jest.spyOn( + validateEqualCategoryDenominators, + "validateEqualCategoryDenominatorsPM" + ), + validateEqualCategoryDenominatorsOMS: jest.spyOn( + validateEqualCategoryDenominators, + "validateEqualCategoryDenominatorsOMS" + ), + validateEqualQualifierDenominatorsPM: jest.spyOn( + validateEqualQualifierDenominators, + "validateEqualQualifierDenominatorsPM" + ), + validateEqualQualifierDenominatorsOMS: jest.spyOn( + validateEqualQualifierDenominators, + "validateEqualQualifierDenominatorsOMS" + ), + validateFfsRadioButtonCompletion: jest.spyOn( + validateFfsRadioButtonCompletion, + "validateFfsRadioButtonCompletion" + ), + validateRateNotZeroPM: jest.spyOn( + validateRateNotZero, + "validateRateNotZeroPM" + ), + validateRateNotZeroOMS: jest.spyOn( + validateRateNotZero, + "validateRateNotZeroOMS" + ), + validateRateZeroPM: jest.spyOn(validateRateZero, "validateRateZeroPM"), + validateRateZeroOMS: jest.spyOn(validateRateZero, "validateRateZeroOMS"), + validateNumeratorsLessThanDenominatorsPM: jest.spyOn( + validateNumeratorsLessThanDenominators, + "validateNumeratorsLessThanDenominatorsPM" + ), + validateNumeratorLessThanDenominatorOMS: jest.spyOn( + validateNumeratorsLessThanDenominators, + "validateNumeratorLessThanDenominatorOMS" + ), + validateOneCatRateHigherThanOtherCatPM: jest.spyOn( + validateOneCatRateHigherThanOtherCat, + "validateOneCatRateHigherThanOtherCatPM" + ), + validateOneCatRateHigherThanOtherCatOMS: jest.spyOn( + validateOneCatRateHigherThanOtherCat, + "validateOneCatRateHigherThanOtherCatOMS" + ), + validateOneQualDenomHigherThanOtherDenomOMS: jest.spyOn( + validateOneQualDenomHigherThanOtherDenomOMS, + "validateOneQualDenomHigherThanOtherDenomOMS" + ), + validateOneQualRateHigherThanOtherQualPM: jest.spyOn( + validateOneQualRateHigherThanOtherQual, + "validateOneQualRateHigherThanOtherQualPM" + ), + validateOneQualRateHigherThanOtherQualOMS: jest.spyOn( + validateOneQualRateHigherThanOtherQual, + "validateOneQualRateHigherThanOtherQualOMS" + ), + validateReasonForNotReporting: jest.spyOn( + validateReasonForNotReporting, + "validateReasonForNotReporting" + ), + validateRequiredRadioButtonForCombinedRates: jest.spyOn( + validateRequiredRadioButtonForCombinedRates, + "validateRequiredRadioButtonForCombinedRates" + ), + validateTotalNDR: jest.spyOn(validateTotals, "validateTotalNDR"), + validateOMSTotalNDR: jest.spyOn(validateTotals, "validateOMSTotalNDR"), + PCRatLeastOneRateComplete: jest.spyOn( + PCRatLeastOneRateComplete, + "PCRatLeastOneRateComplete" + ), + PCRnoNonZeroNumOrDenom: jest.spyOn( + PCRnoNonZeroNumOrDenom, + "PCRnoNonZeroNumOrDenom" + ), + ComplexAtLeastOneRateComplete: jest.spyOn( + ComplexAtLeastOneRateComplete, + "ComplexAtLeastOneRateComplete" + ), + ComplexNoNonZeroNumOrDenom: jest.spyOn( + ComplexNoNonZeroNumOrDenom, + "ComplexNoNonZeroNumOrDenom" + ), + ComplexValidateNDRTotals: jest.spyOn( + ComplexValidateNDRTotals, + "ComplexValidateNDRTotals" + ), + ComplexValidateDualPopInformation: jest.spyOn( + ComplexValidateDualPopInformation, + "ComplexValidateDualPopInformation" + ), + ComplexValueSameCrossCategory: jest.spyOn( + ComplexValueSameCrossCategory, + "ComplexValueSameCrossCategory" + ), +}; diff --git a/services/ui-src/src/measures/index.tsx b/services/ui-src/src/measures/index.tsx index 363274009f..8c7ea7deed 100644 --- a/services/ui-src/src/measures/index.tsx +++ b/services/ui-src/src/measures/index.tsx @@ -3,6 +3,7 @@ import twentyTwentyOneMeasures, { QualifierData as data2021 } from "./2021"; import twentyTwentyTwoMeasures, { QualifierData as data2022 } from "./2022"; import twentyTwentyThreeMeasures, { QualifierData as data2023 } from "./2023"; import * as QMR from "components"; +import twentyTwentyFourMeasures, { QualifierData as data2024 } from "./2024"; export type CustomValidator = (res: ResolverResult) => ResolverResult; @@ -21,6 +22,7 @@ const measuresByYear: MeasuresByYear = { 2021: twentyTwentyOneMeasures, 2022: twentyTwentyTwoMeasures, 2023: twentyTwentyThreeMeasures, + 2024: twentyTwentyFourMeasures, }; export default measuresByYear; @@ -28,4 +30,5 @@ export const QualifierData: IQualifierData[] = [ { year: "2021", data: data2021 }, { year: "2022", data: data2022 }, { year: "2023", data: data2023 }, + { year: "2024", data: data2024 }, ]; diff --git a/services/ui-src/src/measures/measureDescriptions.ts b/services/ui-src/src/measures/measureDescriptions.ts index e8723e1d4e..fb71725c62 100644 --- a/services/ui-src/src/measures/measureDescriptions.ts +++ b/services/ui-src/src/measures/measureDescriptions.ts @@ -197,7 +197,6 @@ export const measureDescriptions: MeasureList = { "Prevention Quality Indicator (PQI) 92: Chronic Conditions Composite", }, - // New measures will be added here "2023": { // Adult "AAB-AD": @@ -299,4 +298,106 @@ export const measureDescriptions: MeasureList = { "PQI92-HH": "Prevention Quality Indicator (PQI) 92: Chronic Conditions Composite", }, + + "2024": { + // Adult + "AAB-AD": + "Avoidance of Antibiotic Treatment for Acute Bronchitis/Bronchiolitis: Age 18 And Older", + "AMM-AD": "Antidepressant Medication Management", + "AMR-AD": "Asthma Medication Ratio: Ages 19 to 64", + "BCS-AD": "Breast Cancer Screening", + "CBP-AD": "Controlling High Blood Pressure", + "CCP-AD": "Contraceptive Care - Postpartum Women Ages 21 to 44", + "CCS-AD": "Cervical Cancer Screening", + "CCW-AD": "Contraceptive Care - All Women Ages 21 to 44", + "CDF-AD": "Screening for Depression and Follow-Up Plan: Age 18 and Older", + "CHL-AD": "Chlamydia Screening in Women Ages 21 to 24", + "COB-AD": "Concurrent Use of Opioids and Benzodiazepines", + "COL-AD": "Colorectal Cancer Screening", + "CPA-AD": + "Consumer Assessment of Healthcare Providers and Systems (CAHPS®) Health Plan Survey 5.1H, Adult Version (Medicaid)", + "CPU-AD": + "Long-Term Services and Supports Comprehensive Care Plan and Update", + "FUA-AD": + "Follow-up After Emergency Department Visit for Substance Use: Age 18 and Older", + "FUH-AD": + "Follow-Up After Hospitalization for Mental Illness: Age 18 and Older", + "FUM-AD": + "Follow-Up After Emergency Department Visit for Mental Illness: Age 18 and Older", + "FUM-HH": "Follow-Up After Emergency Department Visit for Mental Illness", + "FVA-AD": "Flu Vaccinations for Adults Ages 18 to 64", + "HBD-AD": "Hemoglobin A1c Control for Patients with Diabetes", + "HPCMI-AD": + "Diabetes Care for People with Serious Mental Illness: Hemoglobin A1C (HbA1c) Poor Control (>9.0%)", + "HVL-AD": "HIV Viral Load Suppression", + "IET-AD": "Initiation and Engagement of Substance Use Disorder Treatment", + "MSC-AD": "Medical Assistance with Smoking and Tobacco Use Cessation", + "NCIDDS-AD": "National Core Indicators Survey", + "OHD-AD": "Use of Opioids at High Dosage in Persons Without Cancer", + "OUD-AD": "Use of Pharmacotherapy for Opioid Use Disorder", + "PCR-AD": "Plan All-Cause Readmissions", + "PPC-AD": "Prenatal and Postpartum Care: Postpartum Care", + "PQI01-AD": "PQI 01: Diabetes Short-Term Complications Admission Rate", + "PQI05-AD": + "PQI 05: Chronic Obstructive Pulmonary Disease (COPD) or Asthma in Older Adults Admission Rate", + "PQI08-AD": "PQI 08: Heart Failure Admission Rate", + "PQI15-AD": "PQI 15: Asthma in Younger Adults Admission Rate", + "SAA-AD": + "Adherence to Antipsychotic Medications for Individuals With Schizophrenia", + "SSD-AD": + "Diabetes Screening for People with Schizophrenia or Bipolar Disorder Who Are Using Antipsychotic Medications", + + // Child + "AAB-CH": + "Avoidance of Antibiotic Treatment for Acute Bronchitis/Bronchiolitis: Ages 3 Months to 17 Years", + "ADD-CH": + "Follow-Up Care for Children Prescribed Attention-Deficit/Hyperactivity Disorder (ADHD) Medication", + "AMB-CH": "Ambulatory Care: Emergency Department (ED) Visits", + "AMR-CH": "Asthma Medication Ratio: Ages 5 to 18", + "APM-CH": + "Metabolic Monitoring for Children and Adolescents on Antipsychotics", + "APP-CH": + "Use of First-Line Psychosocial Care for Children and Adolescents on Antipsychotics", + "CCP-CH": "Contraceptive Care - Postpartum Women Ages 15 to 20", + "CCW-CH": "Contraceptive Care - All Women Ages 15 to 20", + "CDF-CH": "Screening for Depression and Follow-Up Plan: Ages 12 to 17", + "CHL-CH": "Chlamydia Screening in Women Ages 16 to 20", + "CIS-CH": "Childhood Immunization Status", + "CPC-CH": + "Consumer Assessment of Healthcare Providers and Systems (CAHPS®) Health Plan Survey 5.1H - Child Version Including Medicaid and Children with Chronic Conditions Supplemental Items", + "DEV-CH": "Developmental Screening in the First Three Years of Life", + "FUA-CH": + "Follow-Up After Emergency Department Visit for Substance Use: Ages 13 to 17", + "FUH-CH": + "Follow-Up After Hospitalization for Mental Illness: Ages 6 to 17", + "FUM-CH": + "Follow-Up After Emergency Department Visit for Mental Illness: Ages 6 to 17", + "IMA-CH": "Immunizations for Adolescents", + "LBW-CH": "Live Births Weighing Less Than 2,500 Grams", + "LRCD-CH": "Low-Risk Cesarean Delivery", + "LSC-CH": "Lead Screening in Children", + "OEV-CH": "Oral Evaluation, Dental Services", + "PPC2-CH": "Prenatal and Postpartum Care: Under Age 21", + "SFM-CH": "Sealant Receipt on Permanent First Molars", + "TFL-CH": "Prevention: Topical Fluoride for Children", + "W30-CH": "Well-Child Visits in the First 30 Months of Life", + "WCC-CH": + "Weight Assessment and Counseling for Nutrition and Physical Activity for Children/Adolescents", + "WCV-CH": "Child and Adolescent Well-Care Visits", + + // Health Home + "AIF-HH": "Admission to a Facility from the Community", + "AMB-HH": "Ambulatory Care: Emergency Department (ED) Visits", + "CBP-HH": "Controlling High Blood Pressure", + "CDF-HH": "Screening for Depression and Follow-Up Plan", + "COL-HH": "Colorectal Cancer Screening", + "FUA-HH": "Follow-Up After Emergency Department Visit for Substance Use", + "FUH-HH": "Follow-Up After Hospitalization for Mental Illness", + "IET-HH": "Initiation and Engagement of Substance Use Disorder Treatment", + "IU-HH": "Inpatient Utilization", + "OUD-HH": "Use of Pharmacotherapy for Opioid Use Disorder", + "PCR-HH": "Plan All-Cause Readmissions", + "PQI92-HH": + "Prevention Quality Indicator (PQI) 92: Chronic Conditions Composite", + }, }; diff --git a/services/ui-src/src/shared/SharedContext.tsx b/services/ui-src/src/shared/SharedContext.tsx new file mode 100644 index 0000000000..4e76f24f68 --- /dev/null +++ b/services/ui-src/src/shared/SharedContext.tsx @@ -0,0 +1,6 @@ +import { createContext } from "react"; + +//WIP: this is scaffolding code as there's to many files to update at once, will remove once refactor is nearing complete +const SharedContext = createContext({}); + +export default SharedContext; diff --git a/services/ui-src/src/measures/2023/shared/CommonQuestions/AdditionalNotes/index.test.tsx b/services/ui-src/src/shared/commonQuestions/AdditionalNotes.test.tsx similarity index 72% rename from services/ui-src/src/measures/2023/shared/CommonQuestions/AdditionalNotes/index.test.tsx rename to services/ui-src/src/shared/commonQuestions/AdditionalNotes.test.tsx index d3114df0e7..d96a25095e 100644 --- a/services/ui-src/src/measures/2023/shared/CommonQuestions/AdditionalNotes/index.test.tsx +++ b/services/ui-src/src/shared/commonQuestions/AdditionalNotes.test.tsx @@ -1,19 +1,24 @@ import fireEvent from "@testing-library/user-event"; -import { AdditionalNotes } from "."; -import { Reporting } from "../Reporting"; +import { AdditionalNotes } from "./AdditionalNotes"; +import { Reporting } from "measures/2024/shared/CommonQuestions/Reporting"; import { screen } from "@testing-library/react"; import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import SharedContext from "shared/SharedContext"; +import commonQuestionsLabel from "labels/2024/commonQuestionsLabel"; describe("Test AdditionalNotes component", () => { beforeEach(() => { - renderWithHookForm([ - <Reporting - measureName="My Test Measure" - reportingYear="2023" - measureAbbreviation="MTM" - />, - <AdditionalNotes />, - ]); + renderWithHookForm( + <SharedContext.Provider value={commonQuestionsLabel}> + <Reporting + measureName="My Test Measure" + reportingYear="2024" + measureAbbreviation="MTM" + /> + , + <AdditionalNotes /> + </SharedContext.Provider> + ); }); it("component renders", () => { @@ -37,7 +42,7 @@ describe("Test AdditionalNotes component", () => { it("input is deleted when measure reporting radio option is changed", async () => { const reportingNo = await screen.findByLabelText( - "No, I am not reporting My Test Measure (MTM) for FFY 2023 quality measure reporting." + "No, I am not reporting My Test Measure (MTM) for FFY 2024 quality measure reporting." ); const textArea = await screen.findByLabelText( @@ -50,7 +55,7 @@ describe("Test AdditionalNotes component", () => { // change reporting radio button option from no to yes const reportingYes = await screen.findByLabelText( - "Yes, I am reporting My Test Measure (MTM) for FFY 2023 quality measure reporting." + "Yes, I am reporting My Test Measure (MTM) for FFY 2024 quality measure reporting." ); fireEvent.click(reportingYes); diff --git a/services/ui-src/src/measures/2023/shared/CommonQuestions/AdditionalNotes/index.tsx b/services/ui-src/src/shared/commonQuestions/AdditionalNotes.tsx similarity index 59% rename from services/ui-src/src/measures/2023/shared/CommonQuestions/AdditionalNotes/index.tsx rename to services/ui-src/src/shared/commonQuestions/AdditionalNotes.tsx index 2ff4bc2554..b046798d8e 100644 --- a/services/ui-src/src/measures/2023/shared/CommonQuestions/AdditionalNotes/index.tsx +++ b/services/ui-src/src/shared/commonQuestions/AdditionalNotes.tsx @@ -2,10 +2,12 @@ import * as QMR from "components"; import * as CUI from "@chakra-ui/react"; import { useCustomRegister } from "hooks/useCustomRegister"; import { Upload } from "components/Upload"; -import * as Types from "../types"; +import * as Types from "shared/types"; import * as DC from "dataConstants"; import { useFormContext } from "react-hook-form"; -import { useEffect } from "react"; +import { useContext, useEffect } from "react"; +import { parseLabelToHTML } from "utils/parser"; +import SharedContext from "shared/SharedContext"; export const AdditionalNotes = () => { const register = useCustomRegister<Types.AdditionalNotes>(); @@ -16,28 +18,21 @@ export const AdditionalNotes = () => { resetField("AdditionalNotes-AdditionalNotes"); }, [didReport, resetField]); + //WIP: using form context to get the labels for this component temporarily. + const labels: any = useContext(SharedContext); + return ( <QMR.CoreQuestionWrapper testid="additional-notes" - label="Additional Notes/Comments on the measure (optional)" + label={labels?.AdditonalNotes?.header} > <QMR.TextArea - label={ - <> - Please add any additional notes or comments on the measure not - otherwise captured above ( - <em> - text in this field is included in publicly-reported state-specific - comments - </em> - ): - </> - } + label={parseLabelToHTML(labels?.AdditonalNotes?.section)} {...register(DC.ADDITIONAL_NOTES)} /> <CUI.Box marginTop={10}> <Upload - label="If you need additional space to include comments or supplemental information, please attach further documentation below." + label={labels?.AdditonalNotes?.upload} {...register(DC.ADDITIONAL_NOTES_UPLOAD)} /> </CUI.Box> diff --git a/services/ui-src/src/shared/types/TypeAdditionalNotes.tsx b/services/ui-src/src/shared/types/TypeAdditionalNotes.tsx new file mode 100644 index 0000000000..8a67b053f0 --- /dev/null +++ b/services/ui-src/src/shared/types/TypeAdditionalNotes.tsx @@ -0,0 +1,6 @@ +import * as DC from "dataConstants"; + +export interface AdditionalNotes { + [DC.ADDITIONAL_NOTES]: string; // Additional notes or comments on the measure + [DC.ADDITIONAL_NOTES_UPLOAD]: File[]; // Additional attachments upload +} diff --git a/services/ui-src/src/shared/types/index.tsx b/services/ui-src/src/shared/types/index.tsx new file mode 100644 index 0000000000..ca46df167f --- /dev/null +++ b/services/ui-src/src/shared/types/index.tsx @@ -0,0 +1 @@ +export * from "shared/types/TypeAdditionalNotes"; diff --git a/services/ui-src/src/utils/parser.test.ts b/services/ui-src/src/utils/parser.test.ts new file mode 100644 index 0000000000..f26348edeb --- /dev/null +++ b/services/ui-src/src/utils/parser.test.ts @@ -0,0 +1,37 @@ +import { render } from "@testing-library/react"; +import { parseLabelToHTML } from "./parser"; + +describe("Test parseLabelToHTML", () => { + it("Test that label text with tags will return JSX elements", () => { + const label = "Hello <em>I am a tag</em> and I am a string"; + const parsedLabel = parseLabelToHTML(label); + expect(parsedLabel).toBeInstanceOf(Object); + }); + + it("Test that it renders tags recursively", () => { + const label = "<em><strong>hello</strong></em>"; + + const parsedLabel = parseLabelToHTML(label); + const { container } = render(parsedLabel); + + const outerTag = container.querySelector("em"); + expect(outerTag).toBeTruthy(); + const innerTag = container.querySelector("strong"); + expect(innerTag).toBeTruthy(); + }); + + it("Test that error is thrown when data is not an html element", () => { + const label = "I am a bad set of <![CDATA[data]]>"; + expect(() => parseLabelToHTML(label)).toThrow( + "Unrecognized node type in label:\n" + label + ); + }); + + it("Test that attributes are copied", () => { + const label = `<a href="https://example.com/">test</a>`; + const parsedLabel = parseLabelToHTML(label); + const { container } = render(parsedLabel); + const link = container.querySelector("a")!; + expect(link.href).toBe("https://example.com/"); + }); +}); diff --git a/services/ui-src/src/utils/parser.tsx b/services/ui-src/src/utils/parser.tsx new file mode 100644 index 0000000000..5e2d263304 --- /dev/null +++ b/services/ui-src/src/utils/parser.tsx @@ -0,0 +1,41 @@ +import React, { JSXElementConstructor, ReactElement } from "react"; +import { v4 as uuidv4 } from "uuid"; + +/** + * Convert labels with HTML tags to JSX elements to be rendered. + * + * Doesn't work with `<br>` tags. + */ +export const parseLabelToHTML = (label: string) => { + const attributesOf = (element: Element) => { + return Object.fromEntries( + [...element.attributes].map((attr) => [attr.name, attr.value]) + ); + }; + + const convertNodetoReactElement = (node: Node): StringOrElement => { + if (node instanceof Text) { + // Text nodes always have textContent. + return node.textContent!; + } else if (node instanceof Element) { + const tagName = node.tagName.toLowerCase(); + const props = { + // The random key prevents browser warnings. + key: uuidv4(), + ...attributesOf(node), + }; + const children = [...node.childNodes].map(convertNodetoReactElement); + return React.createElement(tagName, props, children); + } else { + throw new Error(`Unrecognized node type in label:\n${label}`); + } + }; + + const body = new DOMParser().parseFromString(label, "text/html").body; + const elements = [...body.childNodes].map(convertNodetoReactElement); + return <>{elements}</>; +}; + +type StringOrElement = + | string + | ReactElement<Record<string, any>, string | JSXElementConstructor<any>>; diff --git a/services/ui-src/src/utils/testUtils/2024/validationHelper.test.ts b/services/ui-src/src/utils/testUtils/2024/validationHelper.test.ts new file mode 100644 index 0000000000..7e61c3a738 --- /dev/null +++ b/services/ui-src/src/utils/testUtils/2024/validationHelper.test.ts @@ -0,0 +1,29 @@ +import * as VH from "./validationHelpers"; + +// mock and suppress console calls +const mockedConsoleError = jest.fn(); +(global as any).console = { + error: mockedConsoleError, +}; + +describe("Test Validation Helpers", () => { + it("should create valid rateData", () => { + const data = VH.generateOmsQualifierRateData( + [{ label: "test1", id: "test1", text: "test1" }], + [{ label: "test2", id: "test2", text: "test2" }], + [{ numerator: "5" }] + ); + expect(data.rates?.["test1"]?.["test2"]?.[0]?.numerator).toBe("5"); + }); + + it("should not create data if no data passed", () => { + const data = VH.generateOmsQualifierRateData( + [{ label: "test1", id: "test1", text: "test1" }], + [{ label: "test2", id: "test2", text: "test2" }], + [] + ); + + expect(data).toEqual({}); + expect(mockedConsoleError).toHaveBeenCalled(); + }); +}); diff --git a/services/ui-src/src/utils/testUtils/2024/validationHelpers.ts b/services/ui-src/src/utils/testUtils/2024/validationHelpers.ts new file mode 100644 index 0000000000..90fea47b8d --- /dev/null +++ b/services/ui-src/src/utils/testUtils/2024/validationHelpers.ts @@ -0,0 +1,313 @@ +/** +NOTE: This validationHelper was cloned when categories & qualifiers were updated to use LabelData[] as type instead of string[] +This should be the file used for some of the unit test from year 2024 onward +*/ + +import * as DC from "dataConstants"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { + OMSData, + OmsNode, +} from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; +import { LabelData } from "utils"; +import { + RateFields, + OmsNodes as OMS, + DataDrivenTypes as DDT, + PerformanceMeasure, +} from "measures/2024/shared/CommonQuestions/types"; + +// Test Rate Objects +export const partialRate: RateFields = { + numerator: "5", +}; +export const badNumeratorRate: RateFields = { + numerator: "7", + denominator: "5", + rate: "33.3", +}; +export const manualZeroRate: RateFields = { + numerator: "7", + denominator: "5", + rate: "0.0", +}; +export const manualNonZeroRate: RateFields = { + numerator: "0", + denominator: "5", + rate: "55.0", +}; +export const simpleRate: RateFields = { + numerator: "1", + denominator: "2", + rate: "50.0", +}; +export const doubleRate: RateFields = { + numerator: "2", + denominator: "4", + rate: "50.0", + label: "Double Test Label", +}; +export const lowerRate: RateFields = { + numerator: "1", + denominator: "4", + rate: "25.0", + label: "Lower Rate Label", +}; +export const higherRate: RateFields = { + numerator: "3", + denominator: "4", + rate: "75.0", + label: "Higher Rate Label", +}; +export const incorrectNumeratorRate: RateFields = { + numerator: "3", + denominator: "4", + rate: "50.0", + label: "Numerator Test Label", +}; +export const incorrectDenominatorRate: RateFields = { + numerator: "2", + denominator: "5", + rate: "50.0", + label: "Denominator Test Label", +}; +export const emptyRate: RateFields = { + numerator: "", + denominator: "", + rate: "", + label: "Empty Test Label", +}; + +/** + * Helper function to prep oms validation test data by slotting test data in qualifier order + * + * @param categories should always at least contain "singleCategory" + * @param qualifiers a non-negotiable LabelData array + * @param testData what test data to place in the qualifier location in rate data + * + * @note testData MUST be the same length as chosen qualifiers + */ +export const generateOmsQualifierRateData = ( + categories: LabelData[], + qualifiers: LabelData[], + testData: RateFields[] +) => { + if (testData.length !== qualifiers.length) { + console.error("Mismatch in test data length"); + return {}; + } + const rateData: OMS.OmsRateFields = {}; + const cats = categories.length + ? categories.map((item) => item.id) + : [DC.SINGLE_CATEGORY]; + rateData.options = qualifiers.map((s) => s.id); + + for (const [i, q] of qualifiers.map((q) => q.id).entries()) { + for (const c of cats) { + rateData.rates ??= {}; + rateData.rates[c] ??= {}; + rateData.rates[c][q] = [testData[i]]; + } + } + + return rateData; +}; + +/** + * Helper function to prep oms validation test data by slotting test data in category order + * + * @param categories should be longer than just singleCategory + * @param qualifiers a non-negotiable LabelData array + * @param testData what test data to place in the category location in rate data + * + * @note testData MUST be the same length as chosen categories + */ +export const generateOmsCategoryRateData = ( + categories: LabelData[], + qualifiers: LabelData[], + testData: RateFields[] +) => { + if (testData.length !== categories.length) { + console.error("Mismatch in test data length"); + return {}; + } + + const rateData: OMS.OmsRateFields = {}; + rateData.options = qualifiers.map((s) => s.id); + + for (const [i, c] of categories.map((c) => c.id).entries()) { + for (const q of qualifiers.map((q) => q.id)) { + rateData.rates ??= {}; + rateData.rates[c] ??= {}; + rateData.rates[c][q] = [testData[i]]; + } + } + + return rateData; +}; + +/** + * Helper function to prep pm validation test data by slotting test data in qualifier order + * + * @param pmd needs to contain the qualifiers and categories + * @param testData an array of rate objects that is the same length as qualifiers + */ +export const generatePmQualifierRateData = ( + pmd: DDT.PerformanceMeasure, + testData: RateFields[] +) => { + if (testData.length !== pmd?.qualifiers?.length) { + console.error("Mismatch in test data length"); + return {}; + } + const rateData: PerformanceMeasure = { PerformanceMeasure: { rates: {} } }; + const cats = pmd.categories?.length + ? pmd.categories.map((item) => item.id) + : [DC.SINGLE_CATEGORY]; + + for (let i = 0; i < pmd.qualifiers.length; i++) { + for (const c of cats ?? []) { + rateData.PerformanceMeasure!.rates![c] ??= []; + rateData?.PerformanceMeasure?.rates?.[c]?.push(testData[i]); + } + } + + return rateData; +}; + +/** + * Helper function to prep pm validation test data by slotting test data in category order + * + * @param pmd needs to contain the categories and qualifiers + * @param testData an array of rate objects that is the same length as categories + */ +export const generatePmCategoryRateData = ( + pmd: DDT.PerformanceMeasure, + testData: RateFields[] +) => { + if (testData.length !== pmd?.categories?.length) { + console.error("Mismatch in test data length"); + return {}; + } + + const rateData: PerformanceMeasure = { PerformanceMeasure: { rates: {} } }; + + for (const [i, c] of pmd.categories.map((c) => c.id).entries()) { + pmd.qualifiers?.forEach(() => { + rateData.PerformanceMeasure!.rates![c] ??= []; + rateData?.PerformanceMeasure?.rates?.[c]?.push(testData[i]); + }); + } + + return rateData; +}; + +/** + * Helper function to prep opm validation test data by slotting test data into x number of fields + * @param testData object to be pushed onto OPM rates + * @param numberOfFields how many field sets to make + */ +export const generateOtherPerformanceMeasureData = ( + testData: RateFields[], + numberOfFields = 3 +) => { + const data: Types.OtherRatesFields[] = []; + + for (let index = 0; index < numberOfFields; index++) { + data.push({ rate: testData }); + } + + return data; +}; + +/** + * Dummy location dictionary function for testing utility + */ +export const locationDictionary = (s: string[]) => { + return s[0]; +}; + +/** + * Generates a filled OMS form data object + * + * @param rateData test data that is applied to all nodes + * @param addToSelections should every node be added to checkbox selections + * @param renderData in case you want a specific OMS layout + */ +export const generateOmsFormData = ( + rateData: OMS.OmsRateFields, + addToSelections = true, + renderData?: Types.DataDrivenTypes.OptionalMeasureStrat +) => { + const data = renderData ?? OMSData(); + const description = "TestAdditionalCategoryOrSubCategory"; + const omsData: Types.OptionalMeasureStratification = { + OptionalMeasureStratification: { options: [], selections: {} }, + }; + + // urban/domestic - english/spanish - etc + const createMidLevelNode = ( + node: OmsNode + ): Types.OmsNodes.MidLevelOMSNode => { + const midNode: Types.OmsNodes.MidLevelOMSNode = {}; + if (!!node.options?.length) { + midNode.aggregate = "NoIndependentData"; + for (const opt of node.options) { + midNode.options ??= []; + midNode.label ??= ""; + midNode.selections ??= {}; + addToSelections && midNode.options.push(opt.id); + midNode.selections[opt.id] = { + rateData, + additionalSubCategories: [{ description, rateData }], + }; + } + } else { + midNode.rateData = rateData; + midNode.additionalSubCategories = [{ description, rateData }]; + } + + return midNode; + }; + + // race - sex - geography - etc + const createTopLevelNode = ( + node: OmsNode + ): Types.OmsNodes.TopLevelOmsNode => { + const topNode: Types.OmsNodes.TopLevelOmsNode = {}; + if (!node.options) { + topNode.rateData = rateData; + } + if (!!node.options?.length) { + for (const opt of node.options) { + topNode.options ??= []; + topNode.label ??= ""; + topNode.selections ??= {}; + addToSelections && topNode.options.push(opt.id); + topNode.selections[opt.id] = createMidLevelNode(opt); + } + } + if (node.addMore) { + const tempAdd: Types.OmsNodes.AddtnlOmsNode = { + description, + rateData, + additionalSubCategories: [{ description, rateData }], + }; + topNode.additionalSelections = [tempAdd, tempAdd]; + } + return topNode; + }; + + // makes top level node for each omsnode + const createBaseOMS = (node: OmsNode) => { + addToSelections && + omsData.OptionalMeasureStratification.options.push(node.id); + omsData.OptionalMeasureStratification.selections ??= {}; + omsData.OptionalMeasureStratification.selections[node.id] = + createTopLevelNode(node); + }; + + data.forEach((node) => createBaseOMS(node)); + + return omsData; +}; diff --git a/services/ui-src/src/utils/tracking/tealium.js b/services/ui-src/src/utils/tracking/tealium.js index fdbc37759a..7a734ca925 100644 --- a/services/ui-src/src/utils/tracking/tealium.js +++ b/services/ui-src/src/utils/tracking/tealium.js @@ -7,6 +7,11 @@ export const fireTealiumPageView = (user, url, pathname) => { pathname.endsWith("CSQ"); const contentType = isReportPage ? "form" : "app"; const sectionName = isReportPage ? pathname.split("/")[1] : "main app"; + const tealiumEnvMap = { + "mdctqmr.cms.gov": "production", + "mdctqmrval.cms.gov": "qa", + }; + const tealiumEnv = tealiumEnvMap[window.location.hostname] || "dev"; const { host: siteDomain } = url ? new URL(url) : null; if (window.utag) { window.utag.view({ @@ -15,7 +20,7 @@ export const fireTealiumPageView = (user, url, pathname) => { page_name: sectionName + ":" + pathname, page_path: pathname, site_domain: siteDomain, - site_environment: process.env.NODE_ENV, + site_environment: tealiumEnv, site_section: sectionName, logged_in: !!user, }); diff --git a/services/ui-src/src/views/AdminHome/index.tsx b/services/ui-src/src/views/AdminHome/index.tsx index e9ff405dc7..743797194c 100644 --- a/services/ui-src/src/views/AdminHome/index.tsx +++ b/services/ui-src/src/views/AdminHome/index.tsx @@ -6,10 +6,13 @@ import config from "config"; import { useUser } from "hooks/authHooks"; import { BannerCard } from "components/Banner/BannerCard"; import { UserRoles } from "types"; +import { useFlags } from "launchdarkly-react-client-sdk"; export const AdminHome = () => { const [locality, setLocality] = useState("AL"); - const releaseYearByFlag = parseInt(config.currentReportingYear); + const releaseYearByFlag = useFlags()?.["release2024"] + ? config.currentReportingYear + : parseInt(config.currentReportingYear) - 1; const navigate = useNavigate(); const { userRole } = useUser(); diff --git a/services/ui-src/src/views/Home/index.tsx b/services/ui-src/src/views/Home/index.tsx index cf67108a56..d897782503 100644 --- a/services/ui-src/src/views/Home/index.tsx +++ b/services/ui-src/src/views/Home/index.tsx @@ -5,10 +5,13 @@ import * as CUI from "@chakra-ui/react"; import "./index.module.scss"; import * as QMR from "components"; import { useUser } from "hooks/authHooks"; +import { useFlags } from "launchdarkly-react-client-sdk"; export function Home() { const { userRole, userState } = useUser(); - const releaseYearByFlag = parseInt(config.currentReportingYear); + const releaseYearByFlag = useFlags()?.["release2024"] + ? config.currentReportingYear + : parseInt(config.currentReportingYear) - 1; if ( userRole === UserRoles.ADMIN || diff --git a/services/ui-src/src/views/StateHome/AddCoreSetCards.tsx b/services/ui-src/src/views/StateHome/AddCoreSetCards.tsx index 130567698a..a3f032e4e9 100644 --- a/services/ui-src/src/views/StateHome/AddCoreSetCards.tsx +++ b/services/ui-src/src/views/StateHome/AddCoreSetCards.tsx @@ -65,6 +65,7 @@ export const AddCoreSetCards = ({ healthHomesCoreSetExists, renderHealthHomeCoreSet = true, }: Props) => { + const { year } = useParams(); return ( <> <AddCoreSetCard @@ -81,11 +82,13 @@ export const AddCoreSetCards = ({ coreSetExists={healthHomesCoreSetExists} /> )} - <CUI.Center w="44" textAlign="center"> - <CUI.Text fontStyle="italic" fontSize="sm"> - Only one group of Adult Core Set Measures can be submitted per FFY - </CUI.Text> - </CUI.Center> + {year && parseInt(year) < 2024 && ( + <CUI.Center w="44" textAlign="center"> + <CUI.Text fontStyle="italic" fontSize="sm"> + Only one group of Adult Core Set Measures can be submitted per FFY + </CUI.Text> + </CUI.Center> + )} </> ); }; diff --git a/services/ui-src/src/views/StateHome/index.tsx b/services/ui-src/src/views/StateHome/index.tsx index f1c030b179..c9755a0dfa 100644 --- a/services/ui-src/src/views/StateHome/index.tsx +++ b/services/ui-src/src/views/StateHome/index.tsx @@ -17,6 +17,8 @@ import { useUpdateAllMeasures } from "hooks/useUpdateAllMeasures"; import { useResetCoreSet } from "hooks/useResetCoreSet"; import { BannerCard } from "components/Banner/BannerCard"; +import { useFlags } from "launchdarkly-react-client-sdk"; + interface HandleDeleteData { state: string; year: string; @@ -39,6 +41,7 @@ const ReportingYear = () => { const navigate = useNavigate(); const { state, year } = useParams(); const { data: reportingYears } = useGetReportingYears(); + const releasedTwentyTwentyFour = useFlags()?.["release2024"]; let reportingyearOptions: IRepYear[] = reportingYears && reportingYears.length @@ -48,6 +51,12 @@ const ReportingYear = () => { })) : [{ displayValue: `${year} Core Set`, value: `${year}` }]; + if (!releasedTwentyTwentyFour) { + reportingyearOptions = reportingyearOptions.filter( + (entry) => entry.value !== "2024" + ); + } + return ( <CUI.Box w={{ base: "full", md: "48" }}> <CUI.Text fontSize="sm" fontWeight="600" mb="2"> @@ -98,7 +107,10 @@ const StateHome = () => { const queryClient = useQueryClient(); const mutation = useUpdateAllMeasures(); const resetCoreSetMutation = useResetCoreSet(); - const { data, error, isLoading } = Api.useGetCoreSets(true); + const releasedTwentyTwentyFour = useFlags()?.["release2024"]; + const { data, error, isLoading } = Api.useGetCoreSets( + releasedTwentyTwentyFour + ); const { userState, userRole } = useUser(); const deleteCoreSet = Api.useDeleteCoreSet(); diff --git a/services/ui-src/tsconfig.json b/services/ui-src/tsconfig.json index 604f29458a..5793b28f1e 100644 --- a/services/ui-src/tsconfig.json +++ b/services/ui-src/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "incremental": true, "target": "ES2020", - "lib": ["es6", "dom", "ES2021"], + "lib": ["es6", "dom", "ES2021", "dom.iterable"], "jsx": "react-jsx", "sourceMap": true, "module": "esnext", diff --git a/services/ui-src/yarn.lock b/services/ui-src/yarn.lock index 22b5bee99d..3f78257faf 100644 --- a/services/ui-src/yarn.lock +++ b/services/ui-src/yarn.lock @@ -6956,7 +6956,7 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= -degenerator@^2.2.0, degenerator@^4.0.4: +degenerator@^2.2.0, degenerator@^4.0.4, degenerator@^5.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-4.0.4.tgz#dbeeb602c64ce543c1f17e2c681d1d0cc9d4a0ac" integrity sha512-MTZdZsuNxSBL92rsjx3VFWe57OpRlikyLbcx2B5Dmdv6oScqpMrvpY7zHLMymrUxo3U5+suPUMsNgW/+SZB1lg== @@ -8867,14 +8867,9 @@ invariant@^2.2.4: loose-envify "^1.0.0" ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ip@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" - integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== + version "1.1.9" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== ipaddr.js@1.9.1: version "1.9.1" @@ -10930,13 +10925,12 @@ pac-proxy-agent@^4.1.0: raw-body "^2.2.0" socks-proxy-agent "5" -pac-resolver@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-6.0.2.tgz#742ef24d2805b18c0a684ac02bcb0b5ce9644648" - integrity sha512-EQpuJ2ifOjpZY5sg1Q1ZeAxvtLwR7Mj3RgY8cysPGbsRu3RBXyJFWxnMus9PScjxya/0LzvVDxNh/gl0eXBU4w== +pac-resolver@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== dependencies: - degenerator "^4.0.4" - ip "^1.1.8" + degenerator "^5.0.0" netmask "^2.0.2" pac-resolver@^4.1.0: diff --git a/services/ui/serverless.yml b/services/ui/serverless.yml index 5831f7dcf9..9c672a5eda 100644 --- a/services/ui/serverless.yml +++ b/services/ui/serverless.yml @@ -17,6 +17,7 @@ plugins: - serverless-stack-termination-protection - serverless-iam-helper - serverless-s3-bucket-helper + - "@enterprise-cmcs/serverless-waf-plugin" custom: stage: ${opt:stage, self:provider.stage} @@ -30,18 +31,69 @@ custom: route53DomainName: ${ssm:/configuration/${self:custom.stage}/route53/domainName, ""} cloudfrontCertificateArn: ${ssm:/configuration/${self:custom.stage}/cloudfront/certificateArn, ssm:/configuration/default/cloudfront/certificateArn, ""} cloudfrontDomainName: ${ssm:/configuration/${self:custom.stage}/cloudfront/domainName, ""} - webAclName: ${self:service}-${self:custom.stage}-webacl + vpnIpSetArn: ${ssm:/configuration/${self:custom.stage}/vpnIpSetArn, ssm:/configuration/default/vpnIpSetArn, ""} + vpnIpv6SetArn: ${ssm:/configuration/${self:custom.stage}/vpnIpv6SetArn, ssm:/configuration/default/vpnIpv6SetArn, ""} + + wafExcludeRules: + wafScope: CLOUDFRONT + wafPlugin: + name: ${self:service}-${self:custom.stage}-webacl-waf + rules: + - enable: ${param:restrictToVpn} + rule: + Name: vpn-only + Priority: 0 + Action: + Allow: {} + VisibilityConfig: + SampledRequestsEnabled: true + CloudWatchMetricsEnabled: true + MetricName: ${self:service}-${self:custom.stage}-webacl-vpn-only + Statement: + OrStatement: + Statements: + - IPSetReferenceStatement: + ARN: ${self:custom.vpnIpSetArn} + - IPSetReferenceStatement: + ARN: !GetAtt GitHubIPSet.Arn + - IPSetReferenceStatement: + ARN: ${self:custom.vpnIpv6SetArn} + - enable: ${param:restrictToVpn} + rule: + Name: block-all-other-traffic + Priority: 3 + Action: + Block: + CustomResponse: + ResponseCode: 403 + VisibilityConfig: + SampledRequestsEnabled: true + CloudWatchMetricsEnabled: true + MetricName: ${self:service}-${self:custom.stage}-block-traffic + Statement: + NotStatement: + Statement: + IPSetReferenceStatement: + ARN: ${self:custom.vpnIpSetArn} firehoseStreamName: aws-waf-logs-${self:service}-${self:custom.stage}-firehose scripts: hooks: # Associate the WAF ACL with the Firehose Delivery Stream deploy:finalize: | - wafAclArn=`aws wafv2 list-web-acls --scope CLOUDFRONT | jq -r '.WebACLs | .[] | select(.Name=="${self:custom.webAclName}") | .ARN'` + wafAclArn=`aws wafv2 list-web-acls --scope CLOUDFRONT | jq -r '.WebACLs | .[] | select(.Name=="${self:custom.wafPlugin.name}") | .ARN'` firehoseStreamArn=`aws firehose describe-delivery-stream --delivery-stream-name ${self:custom.firehoseStreamName} | jq -r '.DeliveryStreamDescription.DeliveryStreamARN'` aws wafv2 put-logging-configuration \ --logging-configuration ResourceArn=$wafAclArn,LogDestinationConfigs=$firehoseStreamArn \ --region ${self:provider.region} - +params: + prod: + restrictToVpn: false + val: + restrictToVpn: false + master: + restrictToVpn: true + default: + restrictToVpn: true resources: Conditions: CreateDnsRecord: @@ -65,6 +117,13 @@ resources: - "" - ${self:custom.cloudfrontDomainName} Resources: + GitHubIPSet: + Type: AWS::WAFv2::IPSet + Properties: + Name: ${self:custom.stage}-gh-ipset + Scope: CLOUDFRONT + IPAddressVersion: IPV4 + Addresses: [] S3Bucket: Type: AWS::S3::Bucket Properties: @@ -137,35 +196,6 @@ resources: Bool: aws:SecureTransport: false Bucket: !Ref CloudFrontLoggingBucket - CloudFrontWebAcl: - Type: AWS::WAFv2::WebACL - Properties: - Name: ${self:custom.webAclName} - DefaultAction: - Block: {} - Rules: - - Action: - Allow: {} - Name: ${self:custom.webAclName}-allow-usa-plus-territories - Priority: 0 - Statement: - GeoMatchStatement: - CountryCodes: - - GU # Guam - - PR # Puerto Rico - - US # USA - - UM # US Minor Outlying Islands - - VI # US Virgin Islands - - MP # Northern Mariana Islands - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: WafWebAcl - Scope: CLOUDFRONT - VisibilityConfig: - CloudWatchMetricsEnabled: true - SampledRequestsEnabled: true - MetricName: ${self:custom.stage}-webacl CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: @@ -214,7 +244,7 @@ resources: - ErrorCode: 403 ResponseCode: 403 ResponsePagePath: /index.html - WebACLId: !GetAtt CloudFrontWebAcl.Arn + WebACLId: !GetAtt WafPluginAcl.Arn Logging: Bucket: !Sub "${CloudFrontLoggingBucket}.s3.amazonaws.com" Prefix: AWSLogs/CLOUDFRONT/${self:custom.stage}/ diff --git a/tests/cypress/cypress/e2e/a11y/PPC2CHpage.spec.ts b/tests/cypress/cypress/e2e/a11y/PPC2CHpage.spec.ts new file mode 100644 index 0000000000..37252b79c8 --- /dev/null +++ b/tests/cypress/cypress/e2e/a11y/PPC2CHpage.spec.ts @@ -0,0 +1,11 @@ +import { testingYear } from "../../../support/constants"; + +describe("PPC-CH Page 508 Compliance Test", () => { + it("Check a11y on PPC-CH Page", () => { + cy.login(); + cy.selectYear(testingYear); + cy.goToChildCoreSetMeasures(); + cy.goToMeasure("PPC2-CH"); + cy.checkA11yOfPage(); + }); +}); diff --git a/tests/cypress/cypress/e2e/a11y/PPCCHpage.spec.ts b/tests/cypress/cypress/e2e/a11y/PPCCHpage.spec.ts index 2d2b2118ae..6ed71a13dc 100644 --- a/tests/cypress/cypress/e2e/a11y/PPCCHpage.spec.ts +++ b/tests/cypress/cypress/e2e/a11y/PPCCHpage.spec.ts @@ -1,9 +1,7 @@ -import { testingYear } from "../../../support/constants"; - describe("PPC-CH Page 508 Compliance Test", () => { it("Check a11y on PPC-CH Page", () => { cy.login(); - cy.selectYear(testingYear); + cy.selectYear("2023"); cy.goToChildCoreSetMeasures(); cy.goToMeasure("PPC-CH"); cy.checkA11yOfPage(); diff --git a/tests/cypress/cypress/e2e/features/export_all_measures.spec.ts b/tests/cypress/cypress/e2e/features/export_all_measures.spec.ts index 5956ef19cd..de0e2dd071 100644 --- a/tests/cypress/cypress/e2e/features/export_all_measures.spec.ts +++ b/tests/cypress/cypress/e2e/features/export_all_measures.spec.ts @@ -1,4 +1,4 @@ -import { measureAbbrList2023, testingYear } from "../../../support/constants"; +import { measureAbbrList2024, testingYear } from "../../../support/constants"; describe("Export All Measures", () => { beforeEach(() => { @@ -16,7 +16,7 @@ describe("Export All Measures", () => { cy.get('[data-cy="Export"]').first().click(); // Check all measures + CSQ present - for (const measureAbbr of measureAbbrList2023.ADULT) { + for (const measureAbbr of measureAbbrList2024.ADULT) { cy.get(`#${measureAbbr}`).should("be.visible"); } cy.get("#CSQ").should("be.visible"); @@ -38,7 +38,7 @@ describe("Export All Measures", () => { }); // Check all measures + CSQ present - for (const measureAbbr of measureAbbrList2023.CHILD) { + for (const measureAbbr of measureAbbrList2024.CHILD) { cy.get(`#${measureAbbr}`).should("be.visible"); } cy.get("#CSQ").should("be.visible"); @@ -60,7 +60,7 @@ describe("Export All Measures", () => { }); // Check all measures + CSQ present - for (const measureAbbr of measureAbbrList2023.HEALTH_HOME) { + for (const measureAbbr of measureAbbrList2024.HEALTH_HOME) { cy.get(`#${measureAbbr}`).should("be.visible"); } cy.get("#CSQ").should("be.visible"); diff --git a/tests/cypress/cypress/e2e/features/kebab_menu_measures.spec.ts b/tests/cypress/cypress/e2e/features/kebab_menu_measures.spec.ts index 8902e45b73..a9c1cce383 100644 --- a/tests/cypress/cypress/e2e/features/kebab_menu_measures.spec.ts +++ b/tests/cypress/cypress/e2e/features/kebab_menu_measures.spec.ts @@ -24,7 +24,7 @@ describe("Measure kebab menus", () => { cy.get('button[aria-label="View for AMM-AD"]').click(); cy.get('[data-cy="state-layout-container"').should( "include.text", - "FFY 2023AMM-AD - Antidepressant Medication Management" + "FFY 2024AMM-AD - Antidepressant Medication Management" ); }); }); diff --git a/tests/cypress/cypress/e2e/features/reporting_in_FY21_tag_for_alt_data_sources.spec.ts b/tests/cypress/cypress/e2e/features/reporting_in_FY24_tag_for_alt_data_sources.spec.ts similarity index 76% rename from tests/cypress/cypress/e2e/features/reporting_in_FY21_tag_for_alt_data_sources.spec.ts rename to tests/cypress/cypress/e2e/features/reporting_in_FY24_tag_for_alt_data_sources.spec.ts index 1c8fb02371..8f82fa50b4 100644 --- a/tests/cypress/cypress/e2e/features/reporting_in_FY21_tag_for_alt_data_sources.spec.ts +++ b/tests/cypress/cypress/e2e/features/reporting_in_FY24_tag_for_alt_data_sources.spec.ts @@ -1,6 +1,6 @@ import { testingYear } from "../../../support/constants"; -describe("OY2 15211 Reporting in FY22 Tag for Alt Data Sources", () => { +describe("OY2 15211 Reporting in FY24 Tag for Alt Data Sources", () => { beforeEach(() => { cy.login("stateuser2"); cy.selectYear(testingYear); @@ -8,7 +8,7 @@ describe("OY2 15211 Reporting in FY22 Tag for Alt Data Sources", () => { it("N/A And Completed Statuses", () => { cy.get("a > [data-cy='ACS']").click(); - cy.get("[data-cy='Reporting FFY 2023-NCIDDS-AD'] > p").should( + cy.get("[data-cy='Reporting FFY 2024-NCIDDS-AD'] > p").should( "have.text", "N/A" ); diff --git a/tests/cypress/cypress/e2e/features/submit_coreset.spec.ts b/tests/cypress/cypress/e2e/features/submit_coreset.spec.ts index 592c130018..c96fd2c4b7 100644 --- a/tests/cypress/cypress/e2e/features/submit_coreset.spec.ts +++ b/tests/cypress/cypress/e2e/features/submit_coreset.spec.ts @@ -1,4 +1,4 @@ -import { measureAbbrList2023, testingYear } from "../../../support/constants"; +import { measureAbbrList2024, testingYear } from "../../../support/constants"; describe.skip("Submit Core Set button", () => { beforeEach(() => { @@ -44,7 +44,7 @@ describe.skip("Submit Core Set button", () => { it("should require qualifiers to submit the core set", () => { // complete all measures cy.goToAdultMeasures(); - for (const abbr of measureAbbrList2023.ADULT) { + for (const abbr of measureAbbrList2024.ADULT) { completeMeasure(abbr); } cy.get('[data-cy="Submit Core Set"]').should("be.disabled"); @@ -92,7 +92,7 @@ describe.skip("Submit Core Set button", () => { ); // Edit a measure - cy.goToMeasure(measureAbbrList2023.ADULT[0]); + cy.goToMeasure(measureAbbrList2024.ADULT[0]); cy.get('[data-cy="Save"]').click(); cy.wait(1000); @@ -292,7 +292,7 @@ const completeMeasure = (measureName: string) => { const qualifierTestSetup = (abbrList: string, statusString: string) => { // complete all measures - for (const abbr of measureAbbrList2023[abbrList]) { + for (const abbr of measureAbbrList2024[abbrList]) { completeMeasure(abbr); } @@ -312,13 +312,13 @@ const qualifierTestSetup = (abbrList: string, statusString: string) => { let numComplete = 0; switch (abbrList) { case "ADULT": - numComplete = measureAbbrList2023[abbrList].length + 1; + numComplete = measureAbbrList2024[abbrList].length + 1; break; case "CHILD": - numComplete = measureAbbrList2023[abbrList].length + 3; + numComplete = measureAbbrList2024[abbrList].length + 3; break; default: - numComplete = measureAbbrList2023[abbrList].length; + numComplete = measureAbbrList2024[abbrList].length; } cy.get(`[data-cy="Status-${statusString}"]`).should( "contain.text", diff --git a/tests/cypress/cypress/e2e/init/create_delete_child.spec.ts b/tests/cypress/cypress/e2e/init/create_delete_child.spec.ts index fabf885bf5..faa0d38ea1 100644 --- a/tests/cypress/cypress/e2e/init/create_delete_child.spec.ts +++ b/tests/cypress/cypress/e2e/init/create_delete_child.spec.ts @@ -27,4 +27,5 @@ const createChildCoreSetByYear = (year) => { }; createChildCoreSetByYear(testingYear); +createChildCoreSetByYear("2023"); createChildCoreSetByYear("2021"); diff --git a/tests/cypress/cypress/e2e/measures/adult/PPCAD.spec.ts b/tests/cypress/cypress/e2e/measures/adult/PPCAD.spec.ts index ee12f6ca77..ec246112be 100644 --- a/tests/cypress/cypress/e2e/measures/adult/PPCAD.spec.ts +++ b/tests/cypress/cypress/e2e/measures/adult/PPCAD.spec.ts @@ -31,6 +31,8 @@ describe("Measure: PPC-AD", () => { cy.enterValidDateRange(); + cy.get('[data-cy="HybridMeasurePopulationIncluded"]').type("0"); + cy.get('[data-cy="PerformanceMeasure.rates.SyrrI1.0.numerator"]').type("0"); cy.get('[data-cy="PerformanceMeasure.rates.SyrrI1.0.denominator"]').type( "5" diff --git a/tests/cypress/cypress/e2e/measures/child/ADDCH.spec.ts b/tests/cypress/cypress/e2e/measures/child/ADDCH.spec.ts index 1b354e3bd5..a0ca282832 100644 --- a/tests/cypress/cypress/e2e/measures/child/ADDCH.spec.ts +++ b/tests/cypress/cypress/e2e/measures/child/ADDCH.spec.ts @@ -156,13 +156,13 @@ describe("Measure: oy2-9921 ADD-CH", () => { "have.text", "Percentage of children newly prescribed attention-deficit/hyperactivity disorder (ADHD) medication who had at least three follow-up care visits within a 10-month period, one of which was within 30 days of when the first ADHD medication was dispensed. Two rates are reported." ); - cy.get(".css-1af6wus > :nth-child(1) > .css-0").should( + cy.get(".css-1crglu1 > .css-0:nth-child(1)").should( "have.text", - "Percentage of children ages 6 to 12 with a prescription dispensed for ADHD medication, who had one follow-up visit with a practitioner with prescribing authority during the 30-day Initiation Phase." + "Initiation PhasePercentage of children ages 6 to 12 with a prescription dispensed for ADHD medication, who had one follow-up visit with a practitioner with prescribing authority during the 30-day Initiation Phase." ); - cy.get(".css-1af6wus > :nth-child(2) > .css-0").should( + cy.get(".css-1crglu1 > .css-0:nth-child(2)").should( "have.text", - "Percentage of children ages 6 to 12 with a prescription dispensed for ADHD medication who remained on the medication for at least 210 days and who, in addition to the visit in the Initiation Phase, had at least two follow-up visits with a practitioner within 270 days (9 months) after the Initiation Phase ended." + "Continuation and Maintenance (C&M) PhasePercentage of children ages 6 to 12 with a prescription dispensed for ADHD medication who remained on the medication for at least 210 days and who, in addition to the visit in the Initiation Phase, had at least two follow-up visits with a practitioner within 270 days (9 months) after the Initiation Phase ended." ); cy.get( '[data-cy="If this measure has been reported by the state previously and there has been a substantial change in the rate or measure-eligible population, please provide any available context below:"]' diff --git a/tests/cypress/cypress/e2e/measures/child/PPC2CH.spec.ts b/tests/cypress/cypress/e2e/measures/child/PPC2CH.spec.ts new file mode 100644 index 0000000000..3f02ee01f6 --- /dev/null +++ b/tests/cypress/cypress/e2e/measures/child/PPC2CH.spec.ts @@ -0,0 +1,239 @@ +import { testingYear } from "../../../../support/constants"; + +describe("Measure: PPC-CH", () => { + beforeEach(() => { + cy.login(); + cy.selectYear(testingYear); + cy.goToChildCoreSetMeasures(); + cy.goToMeasure("PPC2-CH"); + }); + + it("Ensure correct sections display if user is/not reporting", () => { + cy.displaysSectionsWhenUserNotReporting(); + cy.displaysSectionsWhenUserIsReporting(); + }); + + it("If not reporting and not why not -> show error", () => { + cy.get('[data-cy="DidReport1"]').click(); + cy.get('[data-cy="Validate Measure"]').click(); + cy.get( + '[data-cy="Why Are You Not Reporting On This Measure Error"]' + ).should("have.text", "Why Are You Not Reporting On This Measure Error"); + }); + + it("should show correct data source options", () => { + cy.get('[data-cy="DataSource1"] > .chakra-checkbox__control').click(); + cy.get( + '[data-cy="DataSource0"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Administrative Data"); + /* ==== Generated with Cypress Studio ==== */ + cy.get( + '[data-cy="DataSourceSelections.HybridAdministrativeandMedicalRecordsData0.selected0"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Medicaid Management Information System (MMIS)"); + cy.get( + '[data-cy="DataSourceSelections.HybridAdministrativeandMedicalRecordsData0.selected1"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Vital Records"); + cy.get( + '[data-cy="DataSourceSelections.HybridAdministrativeandMedicalRecordsData0.selected2"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Other"); + cy.get( + '[data-cy="DataSourceSelections.HybridAdministrativeandMedicalRecordsData1.selected0"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Electronic Health Record (EHR) Data"); + cy.get('[data-cy="What is the Medical Records Data Source?"]').should( + "have.text", + "What is the Medical Records Data Source?" + ); + cy.get( + '[data-cy="DataSourceSelections.HybridAdministrativeandMedicalRecordsData1.selected1"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Paper"); + cy.get( + '[data-cy="DataSource2"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "Other Data Source"); + /* ==== End Cypress Studio ==== */ + }); + + it("if primary measurement spec is selected -> show performance measures", () => { + cy.get('[data-cy="MeasurementSpecification0"]').click(); + cy.get(`#MeasurementSpecification-NCQAHEDIS`).should( + "have.text", + "National Committee for Quality Assurance (NCQA)/Healthcare Effectiveness Data and Information Set (HEDIS)" + ); + cy.get('[data-cy="Performance Measure"]').should( + "have.text", + "Performance Measure" + ); + cy.get(".css-xiz5n3 > .css-0:nth-child(1)").should( + "have.text", + "Timeliness of Prenatal Care: Percentage of deliveries that received a prenatal care visit in the first trimester, on or before the enrollment start date or within 42 days of enrollment in Medicaid/CHIP." + ); + cy.get(".css-xiz5n3 > .css-0:nth-child(2)").should( + "have.text", + "Postpartum Care: Percentage of deliveries that had a postpartum visit on or between 7 and 84 days after delivery." + ); + }); + + it("at least one dnr set if reporting and measurement spec or error.", () => { + cy.get('[data-cy="DidReport0"]').click(); + cy.get('[data-cy="Validate Measure"]').click(); + cy.get( + '[data-cy="Performance Measure/Other Performance Measure Error"]' + ).should("be.visible"); + }); + + it("if yes for combined rates → and no additional selection → show warning", () => { + cy.get('[data-cy="DidReport0"]').click(); + cy.get('[data-cy="MeasurementSpecification0"]').click(); + cy.get('[data-cy="CombinedRates0"]').click(); + cy.get('[data-cy="Validate Measure"]').click(); + cy.get( + '[data-cy="You must select at least one option for Combined Rate(s) Details if Yes is selected."]' + ).should( + "have.text", + "You must select at least one option for Combined Rate(s) Details if Yes is selected." + ); + }); + + it("Ensure that numerical value after decimal is rounded up/down for auto calculated rate.", () => { + cy.get('[data-cy="MeasurementSpecification0"]').click(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').type( + "555" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').type( + "10000" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "5.6" + ); + }); + + it("Rate calculation should be correct", () => { + // select is reporting + cy.get('[data-cy="DidReport0"]').click({ force: true }); + + // select AHRQ for measurement specification + cy.get('[data-cy="MeasurementSpecification0"]').click(); + + cy.get('[id="DataSource0-checkbox"]').check({ force: true }); + cy.get('[id="DataSource1-checkbox"]').uncheck({ force: true }); + + // Rate calculation should be = (N/D*100) + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').type("5"); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').type( + "10" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "50.0" + ); + + // Ensure that auto calculate rate displays 1 decimal (even if the value is zero) + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').type("8"); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "80.0" + ); + + // Ensure that numerical value after decimal is rounded up/down for auto calculated rate (up) + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').type( + "9" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "88.9" + ); + + // Ensure that numerical value after decimal is rounded up/down for auto calculated rate (down) + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').type( + "18" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "44.4" + ); + + // Ensure that user cannot manually enter rates if admin data is selected - (already selected) + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.attr", + "readonly" + ); + }); + + it("User can manually adjust rate if other data source is selected", () => { + cy.get('[data-cy="DidReport0"]').click(); + cy.get('[data-cy="DataSource1"]').click(); + cy.get('[data-cy="MeasurementSpecification0"]').click(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.numerator"]').type( + "10" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.denominator"]').type( + "20" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "50.0" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "not.have.attr", + "readonly" + ); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').clear(); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').type("48.1"); + cy.get('[data-cy="PerformanceMeasure.rates.fcjCsg.0.rate"]').should( + "have.value", + "48.1" + ); + }); + // if Other measure spec is selected each age range/ custom description for which there are n/d/r + // values entered in other performance measure are presented in: + // - Optional Measure specification + it("Age ranges are represented in OMS when other measure spec", () => { + cy.wait(2000); + // cy.get('[data-cy="DidReport0"]').click({ force: true }); + cy.get('[data-cy="MeasurementSpecification1"]').click(); + cy.get('[id="DataSource0-checkbox"]').check({ force: true }); + cy.get('[id="DataSource1-checkbox"]').uncheck({ force: true }); + cy.get('[data-cy="OtherPerformanceMeasure-Rates.0.description"]').clear(); + cy.get('[data-cy="OtherPerformanceMeasure-Rates.0.description"]').type( + "example 1" + ); + cy.get( + '[data-cy="OtherPerformanceMeasure-Rates.0.rate.0.numerator"]' + ).clear(); + cy.get('[data-cy="OtherPerformanceMeasure-Rates.0.rate.0.numerator"]').type( + "5" + ); + cy.get( + '[data-cy="OtherPerformanceMeasure-Rates.0.rate.0.denominator"]' + ).clear(); + cy.get( + '[data-cy="OtherPerformanceMeasure-Rates.0.rate.0.denominator"]' + ).type("10"); + cy.get('[data-cy="OtherPerformanceMeasure-Rates.0.rate.0.rate"]').should( + "have.value", + "50.0" + ); + cy.get( + '[data-cy="OptionalMeasureStratification.options0"] > .chakra-checkbox__control' + ).click(); + cy.get( + '[data-cy="OptionalMeasureStratification.selections.3dpUZu.options0"] > .chakra-checkbox__control' + ).click(); + cy.get( + '[data-cy="OptionalMeasureStratification.selections.3dpUZu.selections.ll9YP8.rateData.options0"] > .chakra-checkbox__label > .chakra-text' + ).should("have.text", "example 1"); + cy.get( + '[data-cy="OptionalMeasureStratification.selections.3dpUZu.selections.ll9YP8.rateData.options0"] > .chakra-checkbox__control' + ).click(); + cy.get( + '[data-cy="OptionalMeasureStratification.selections.3dpUZu.selections.ll9YP8.rateData.rates.OPM.OPM_example1.0.numerator"]' + ).type("3"); + }); +}); diff --git a/tests/cypress/cypress/e2e/measures/child/PPCCH.spec.ts b/tests/cypress/cypress/e2e/measures/child/PPCCH.spec.ts index 74dd018cfa..a169f23fda 100644 --- a/tests/cypress/cypress/e2e/measures/child/PPCCH.spec.ts +++ b/tests/cypress/cypress/e2e/measures/child/PPCCH.spec.ts @@ -1,9 +1,7 @@ -import { testingYear } from "../../../../support/constants"; - describe("Measure: PPC-CH", () => { beforeEach(() => { cy.login(); - cy.selectYear(testingYear); + cy.selectYear("2023"); cy.goToChildCoreSetMeasures(); cy.goToMeasure("PPC-CH"); }); diff --git a/tests/cypress/cypress/e2e/measures/child/QUALIFIERS_child.spec.ts b/tests/cypress/cypress/e2e/measures/child/QUALIFIERS_child.spec.ts index 76e35d7252..7da1883f42 100644 --- a/tests/cypress/cypress/e2e/measures/child/QUALIFIERS_child.spec.ts +++ b/tests/cypress/cypress/e2e/measures/child/QUALIFIERS_child.spec.ts @@ -16,7 +16,7 @@ describe("Child Measure Qualifier: CH", () => { cy.get("body").should("include.text", "Delivery System"); cy.get("body").should( "include.text", - "As of September 30, 2022 what percentage of your Medicaid/CHIP enrollees (under age 21) were enrolled in each delivery system?" + "As of September 30, 2023 what percentage of your Medicaid/CHIP enrollees (under age 21) were enrolled in each delivery system?" ); cy.get('[data-cy="PercentageEnrolledInEachDeliverySystem.0.label"]').should( "have.value", @@ -122,7 +122,7 @@ describe("Child Measure Qualifier: CH", () => { '[data-cy="CoreSetMeasuresAuditedOrValidatedDetails.0.MeasuresAuditedOrValidated-OEV-CH - Oral Evaluation, Dental Services"]' ).should("be.visible"); cy.get( - '[data-cy="CoreSetMeasuresAuditedOrValidatedDetails.0.MeasuresAuditedOrValidated-PPC-CH - Prenatal and Postpartum Care: Timeliness of Prenatal Care"]' + '[data-cy="CoreSetMeasuresAuditedOrValidatedDetails.0.MeasuresAuditedOrValidated-PPC2-CH - Prenatal and Postpartum Care: Under Age 21"]' ).should("be.visible"); cy.get( '[data-cy="CoreSetMeasuresAuditedOrValidatedDetails.0.MeasuresAuditedOrValidated-SFM-CH - Sealant Receipt on Permanent First Molars"]' diff --git a/tests/cypress/cypress/e2e/measures/healthhome/QUALIFIERS_healthhome.spec.ts b/tests/cypress/cypress/e2e/measures/healthhome/QUALIFIERS_healthhome.spec.ts index 74496ae7a6..1c8332edfd 100644 --- a/tests/cypress/cypress/e2e/measures/healthhome/QUALIFIERS_healthhome.spec.ts +++ b/tests/cypress/cypress/e2e/measures/healthhome/QUALIFIERS_healthhome.spec.ts @@ -71,9 +71,9 @@ describe("Health Home Measure Qualifier: HH", () => { //testing section 2 with fields inside it cy.get("body").should("include.text", "Cost Savings Data"); - cy.get('[data-cy="Amount of cost savings for FFY 2022"]').should( + cy.get('[data-cy="Amount of cost savings for FFY 2023"]').should( "include.text", - "Amount of cost savings for FFY 2022" + "Amount of cost savings for FFY 2023" ); cy.get('[data-cy="yearlyCostSavings"]').clear(); cy.get('[data-cy="yearlyCostSavings"]').type("1234567890"); @@ -95,7 +95,7 @@ describe("Health Home Measure Qualifier: HH", () => { cy.get("body").should("include.text", "Delivery System"); cy.get("body").should( "include.text", - "As of September 30, 2022 what percentage of your Medicaid Health Home enrollees were enrolled in each delivery system?" + "As of September 30, 2023 what percentage of your Medicaid Health Home enrollees were enrolled in each delivery system?" ); cy.get("body").should("include.text", "Ages 0 to 17"); cy.get("body").should("include.text", "Ages 18 to 64"); diff --git a/tests/cypress/support/commands.ts b/tests/cypress/support/commands.ts index 67fdc68187..e9285d1dc2 100644 --- a/tests/cypress/support/commands.ts +++ b/tests/cypress/support/commands.ts @@ -68,6 +68,12 @@ Cypress.Commands.add("goToHealthHomeSetMeasures", () => { cy.get('[data-cy="tableBody"]').then(($tbody) => { if ($tbody.find('[data-cy^="HHCS"]').length > 0) { cy.get('[data-cy^="HHCS"]').first().click(); + } else { + // adds first available HH core set if no healthhome was made + cy.get('[data-cy="add-hhbutton"]').click(); // clicking on adding child core set measures + cy.get('[data-cy="HealthHomeCoreSet-SPA"]').select(1); // select first available SPA + cy.get('[data-cy="Create"]').click(); //clicking create + cy.get('[data-cy^="HHCS"]').first().click(); } }); }); @@ -272,6 +278,15 @@ const _detailedDescription = Cypress.Commands.add( "addStateSpecificMeasure", (description = _description, detailedDescription = _detailedDescription) => { + //possible fix, cypress sometimes says the add button is disabled, and that usually happens when 5 ss has been added. + cy.get('[data-cy="add-ssm-button"]').then(($btn) => { + //if button turns out to be disable, try deleting a SS before proceeding + if ($btn.is(":disabled")) { + cy.get('tr [data-cy="undefined-kebab-menu"]').last().click(); + cy.get('[data-cy="Delete"]').last().click(); + cy.get('[data-cy="delete-table-item-input"]').type("DELETE{enter}"); + } + }); cy.get('[data-cy="add-ssm-button"]').click(); cy.get('[data-cy="add-ssm.0.description"]').type(description); cy.get('[data-cy="add-ssm.0.detailedDescription"]').type( diff --git a/tests/cypress/support/constants.ts b/tests/cypress/support/constants.ts index acdb0c80ec..4e99bfa0c1 100644 --- a/tests/cypress/support/constants.ts +++ b/tests/cypress/support/constants.ts @@ -1,6 +1,6 @@ -export const testingYear = "2023"; +export const testingYear = "2024"; -export const measureAbbrList2023 = { +export const measureAbbrList2024 = { ADULT: [ "AAB-AD", "AMM-AD", @@ -58,7 +58,7 @@ export const measureAbbrList2023 = { "LRCD-CH", // complete on creation "LSC-CH", "OEV-CH", - "PPC-CH", + "PPC2-CH", "SFM-CH", "TFL-CH", "W30-CH", diff --git a/yarn.lock b/yarn.lock index b4f7504ba7..d701cb90fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1766,6 +1766,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@enterprise-cmcs/serverless-waf-plugin@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@enterprise-cmcs/serverless-waf-plugin/-/serverless-waf-plugin-1.3.2.tgz#66efd0b91326b7d1b045ab7ea7ba5826ed2e635d" + integrity sha512-577MWRddWK2uPEaeUMorOFQq6rhUhGwbdmz+tuKaU9+v77/bDQPqoc6cmhF2oYMswqpxvMgW0P07HAAcmKtquw== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -4691,22 +4696,14 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.12, es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.47, es5-ext@^0.10.49, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@^0.10.61, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.61" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.61.tgz#311de37949ef86b6b0dcea894d1ffedb909d3269" - integrity sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA== - dependencies: - es6-iterator "^2.0.3" - es6-symbol "^3.1.3" - next-tick "^1.1.0" - -es5-ext@^0.10.62: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== +es5-ext@^0.10.12, es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.47, es5-ext@^0.10.49, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@^0.10.61, es5-ext@^0.10.62, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.63" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.63.tgz#9c222a63b6a332ac80b1e373b426af723b895bd6" + integrity sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ== dependencies: es6-iterator "^2.0.3" es6-symbol "^3.1.3" + esniff "^2.0.1" next-tick "^1.1.0" es6-iterator@^2.0.3, es6-iterator@~2.0.3: @@ -5122,6 +5119,16 @@ esniff@^1.1.0: d "1" es5-ext "^0.10.12" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + espree@^7.3.0, espree@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"