department-of-veterans-affairs/vets-website

View on GitHub
src/applications/disability-benefits/all-claims/content/toxicExposure.jsx

Summary

Maintainability
B
5 hrs
Test Coverage
import React from 'react';
import {
  capitalizeEachWord,
  formSubtitle,
  formTitle,
  formatMonthYearDate,
  isClaimingNew,
  makeConditionsSchema,
  sippableId,
  validateConditions,
} from '../utils';
import { NULL_CONDITION_STRING } from '../constants';

/* ---------- content ----------*/
export const conditionsPageTitle = 'Toxic exposure';
export const conditionsQuestion =
  'Are any of your new conditions related to toxic exposure during your military service? Check any that are related.';
export const conditionsDescription = (
  <va-additional-info
    class="vads-u-margin-y--3"
    trigger="What is toxic exposure?"
  >
    <div>
      <p className="vads-u-margin-top--0">
        Toxic exposure includes exposure to substances like Agent Orange, burn
        pits, radiation, asbestos, or contaminated water.
      </p>
      <p className="vads-u-margin-bottom--0">
        <a
          href="https://www.va.gov/disability/eligibility/hazardous-materials-exposure/"
          target="_blank"
          rel="noreferrer"
        >
          Learn more about toxic exposure (opens in new tab)
        </a>
      </p>
    </div>
  </va-additional-info>
);

export const gulfWar1990PageTitle = 'Service after August 2, 1990';
export const gulfWar1990Question =
  'Did you serve in any of these Gulf War locations on or after August 2, 1990? Check any locations where you served.';
export const summaryOfGulfWar1990PageTitle =
  'Summary of service after August 2, 1990';

export const gulfWar2001PageTitle = 'Service post-9/11';
export const gulfWar2001Question =
  'Did you serve in any of these Gulf War locations on or after September 11, 2001? Check any locations where you served.';

export const herbicidePageTitle = 'Agent Orange locations';
export const herbicideQuestion =
  'Did you serve in any of these locations where the military used the herbicide Agent Orange? Check any locations where you served.';

export const additionalExposuresPageTitle = 'Other toxic exposures';
export const additionalExposuresQuestion =
  'Have you been exposed to any of these hazards? Check any that you’ve been exposed to.';
export const specifyOtherExposuresLabel =
  'Other toxic exposures not listed here (250 characters maximum)';

export const noneAndConditionError =
  'You selected a condition, and you also selected “I’m not claiming any conditions related to toxic exposure.” You’ll need to uncheck one of these options to continue.';
export const noneAndLocationError =
  'You selected a location, and you also selected “None of these locations.” You’ll need to uncheck one of these options to continue.';
export const noneAndHazardError =
  'You selected a hazard, and you also selected “None of these.” You’ll need to uncheck one of these options to continue.';

export const otherInvalidCharError =
  'You entered an invalid character in the text field. This field only allows letters, numbers, hyphens, apostrophes, periods, commas, ampersands (& symbol), number signs (# symbol), and spaces.';

export const dateRangeAdditionalInfo = (
  <va-additional-info trigger="What if I have more than one date range?">
    <p>
      You only need to enter one date range. We’ll use this information to find
      your record.
    </p>
  </va-additional-info>
);

export const dateRangeDescriptionWithLocation =
  'Enter any date range you served in this location. You don’t need to have exact dates.';
export const dateRangeDescriptionWithHazard =
  'Enter any date range you were exposed to this hazard. You don’t need to have exact dates.';
export const startDateApproximate = 'Service start date (approximate)';
export const exposureStartDateApproximate = 'Exposure start date (approximate)';
export const exposureEndDateApproximate = 'Exposure end date (approximate)';
export const endDateApproximate = 'Service end date (approximate)';
export const goBackLink = 'Edit locations and dates';
export const goBackLinkExposures = 'Edit exposures and dates';
export const noDatesEntered = 'No dates entered';
export const notSureDatesSummary = 'I’m not sure of the dates';
export const notSureDatesDetails =
  'I’m not sure of the dates I served in this location';
export const notSureHazardDetails =
  'I’m not sure of the dates I was exposed to this hazard';

/**
 * Generate the Toxic Exposure subtitle, which is used on Review and Submit and on the pages
 * themselves. If there are item counts, it will display something like 'Location 1 of 3: Location Name'.
 * If either count is invalid, the prefix will be dropped to only display 'Location Name'.
 *
 * @param {number} currentItem - this item's count out of the total selected items
 * @param {number} totalItems - total number of selected items
 * @param {string} itemName - Display name of the location or hazard
 * @param {string} itemType - Name of the item. Defaults to 'Location'
 * @returns {string} subtitle
 */
export function teSubtitle(
  currentItem,
  totalItems,
  itemName,
  itemType = 'Location',
) {
  return (
    (currentItem > 0 &&
      totalItems > 0 &&
      `${itemType} ${currentItem} of ${totalItems}: ${itemName}`) ||
    itemName
  );
}

/* ---------- utils ---------- */
/**
 * Checks if the toxic exposure pages should be displayed using the following criteria
 *  1. 'startedFormVersion' has 2019 or 2022
 *  2. the claim has a claim type of new
 *  3. claiming at least one new disability
 *
 * @returns true if all criteria are met, false otherwise
 */
export function showToxicExposurePages(formData) {
  return (
    (formData?.startedFormVersion === '2019' ||
      formData?.startedFormVersion === '2022') &&
    isClaimingNew(formData) &&
    formData?.newDisabilities?.length > 0
  );
}

/**
 * Checks if
 * 1. TE pages should be showing at all
 * 2. at least one checkbox on the TE conditions page is selected that is not 'none'
 *
 * @param {object} formData
 * @returns true if at least one condition is claimed for toxic exposure, false otherwise
 */
export function isClaimingTECondition(formData) {
  return (
    showToxicExposurePages(formData) &&
    formData?.toxicExposure?.conditions &&
    Object.keys(formData.toxicExposure.conditions).some(
      item =>
        item !== 'none' && formData.toxicExposure.conditions[item] === true,
    )
  );
}

/**
 * Builds the Schema based on user entered condition names
 *
 * Example output:
{
    type: 'object',
    properties: {
      anemia: {
        type: 'boolean'
      },
      tinnitusringingorhissinginears: {
        type: 'boolean'
      },
      none: {
        type: 'boolean'
      }
    }
  }
}
 *
 * @param {object} formData - Full formData for the form
 * @returns {object} Object with id's for each condition
 */
export function makeTEConditionsSchema(formData) {
  return makeConditionsSchema(formData);
}

/**
 * Builds the UI Schema based on user entered condition names.
 *
 * Example output:
 *  {
 *   anemia: {
 *     'ui:title': 'Anemia',
 *   },
 *   tinnitusringingorhissinginears: {
 *     'ui:title': 'Tinnitus (Ringing Or Hissing In Ears)',
 *   },
 *   none: {
 *     'ui:title': 'I am not claiming any conditions related to toxic exposure',
 *   },
 * }
 * @param {*} formData - Full formData for the form
 * @returns {object} Object with id and title for each condition
 */
export function makeTEConditionsUISchema(formData) {
  const { newDisabilities = [] } = formData;
  const options = {};

  newDisabilities.forEach(disability => {
    const { condition } = disability;

    const capitalizedDisabilityName =
      typeof condition === 'string'
        ? capitalizeEachWord(condition)
        : NULL_CONDITION_STRING;

    options[sippableId(condition || NULL_CONDITION_STRING)] = {
      'ui:title': capitalizedDisabilityName,
    };
  });

  options.none = {
    'ui:title': 'I am not claiming any conditions related to toxic exposure',
  };

  return options;
}

/**
 * Validates 'none' checkbox is not selected along with a new condition
 * @param {object} errors - Errors object from rjsf
 * @param {object} formData
 */
export function validateTEConditions(errors, formData) {
  const { conditions = {} } = formData?.toxicExposure;

  validateConditions(
    conditions,
    errors,
    'toxicExposure',
    noneAndConditionError,
  );
}

/**
 * Get the value for the 'other' field's description
 * @param {object} formData - full form data
 * @param {string} objectName - name of the object containing the 'other' field
 * @returns {string} sanitized description value if present
 */
export function getOtherFieldDescription(formData, objectName) {
  const description = formData?.toxicExposure?.[objectName]?.description;

  return typeof description === 'string' ? description.trim() : '';
}

/**
 * Given the key for a selected checkbox option, find the index within the selected items. In this
 * example, there are two selected locations. The key='bahrain' would give index of 1, and
 * key='airspace' would give index 2.
 *
 * toxicExposure: {
 *    gulfWar1990: {
 *       bahrain: true,
 *       egypt: false,
 *       airspace: true,
 *    }
 * }
 *
 * @param {string} key - the id for the checkbox option
 * @param {string} objectName - name of the object to look at in the form data
 * @param {object} formData - full formData for the form
 * @returns {number} - index of the key within the list of selected items if found, 0 otherwise
 */
export function getKeyIndex(key, objectName, formData) {
  if (
    !formData ||
    !formData?.toxicExposure ||
    !formData?.toxicExposure[objectName]
  ) {
    return 0;
  }

  let index = 0;
  const properties = Object.keys(formData.toxicExposure[objectName]);
  for (let i = 0; i < properties.length; i += 1) {
    if (formData.toxicExposure[objectName][properties[i]] === true) {
      index += 1;
      if (key === properties[i]) {
        return index;
      }
    }
  }
  return 0;
}

/**
 * Given an object storing checkbox values, get a count of how many values have been selected
 * by the Veteran
 *
 * @param {string} checkboxObjectName - name of the checkbox object to look at in the form data
 * @param {object} formData - full formData for the form
 * @param {string} otherFieldName - name of the 'other' field to look at in the form data
 * @returns {number} count of checkboxes with a value of true
 */
export function getSelectedCount(
  checkboxObjectName,
  formData,
  otherFieldName = '',
) {
  const otherFieldDescription = getOtherFieldDescription(
    formData,
    otherFieldName,
  );
  if (!formData?.toxicExposure?.[checkboxObjectName] && !otherFieldDescription)
    return 0;

  let count = 0;
  const ignoredItems = ['none', 'notsure'];
  for (const [key, value] of Object.entries(
    formData.toxicExposure[checkboxObjectName],
  )) {
    // Skip `none` and `notsure` as non-locations
    if (value === true && !ignoredItems.includes(key)) {
      count += 1;
    }
  }

  return count + (otherFieldDescription ? 1 : 0);
}

/**
 * Validates selected items (e.g. gulfWar1990Locations, gulfWar2001Locations, etc.).
 * If the 'none' checkbox is selected along with another item, adds an error.
 *
 * @param {object} errors - Errors object from rjsf
 * @param {object} formData
 * @param {string} objectName - Name of the object to look at in the form data
 * @param {string} otherObjectName - Name of the object containing other location or other hazard data
 * @param {string} selectionTypes - locations or hazards
 */
export function validateSelections(
  errors,
  formData,
  objectName,
  otherObjectName,
  selectionTypes = 'locations',
) {
  const { [objectName]: items = {} } = formData?.toxicExposure;

  if (
    items?.none === true &&
    !!getSelectedCount(objectName, formData, otherObjectName)
  ) {
    errors.toxicExposure[objectName].addError(
      selectionTypes === 'hazards' ? noneAndHazardError : noneAndLocationError,
    );
  }
}

/**
 * Checks if a specific details page should display. It should display if all
 * the following is true
 * 1. TE pages should be showing at all
 * 2. the given checkbox data is present for the given itemId with a value of true
 * 3. the 'none' checkbox is not true
 *
 * @param {object} formData - full form data
 * @param {string} itemId - unique id for the item
 * @returns {boolean} true if the page should display, false otherwise
 */
export function showCheckboxLoopDetailsPage(
  formData,
  checkboxObjectName,
  itemId,
) {
  return (
    itemId !== 'notsure' &&
    isClaimingTECondition(formData) &&
    formData?.toxicExposure[checkboxObjectName] &&
    formData?.toxicExposure[checkboxObjectName].none !== true &&
    formData?.toxicExposure[checkboxObjectName][itemId] === true
  );
}

/**
 * Checks if the a checkbox and loop's summary page should display. It should display if all the following
 * are true
 * 1. TE pages should be showing at all
 * 2. at least one checkbox item was selected OR an 'other' item input was populated
 * 3. the 'none' checkbox is not true
 * 4. the 'notsure' checkbox is not the only one selected
 *
 * @param {object} formData - full form data
 * @param {string} checkboxObjectName - name of the object containing the checkboxes
 * @param {string} otherObjectName - name of the object containing an 'other' input
 * @returns {boolean} true if the page should display, false otherwise
 */
export function showSummaryPage(
  formData,
  checkboxObjectName,
  otherObjectName = '',
) {
  if (
    isClaimingTECondition(formData) &&
    formData?.toxicExposure[checkboxObjectName]
  ) {
    const checkboxes = formData?.toxicExposure[checkboxObjectName];
    const numSelected = Object.values(
      formData?.toxicExposure[checkboxObjectName],
    ).filter(value => value === true).length;
    return (
      checkboxes.none !== true &&
      ((numSelected > 0 && (checkboxes.notsure !== true || numSelected > 1)) ||
        !!getOtherFieldDescription(formData, otherObjectName))
    );
  }
  return false;
}

/**
 * Takes a date range object with start and end dates and generates a description. Fields are optional so
 * the output format may vary depending on available data. Scenarios
 * startDate: '' and endDate: '' -> 'No dates entered'
 * startDate: '1992-04-01' and endDate: '1995-06-01' -> 'April 1992 - June 1995'
 * startDate: '1992-04-01' and endDate: '' -> 'April 1992 - No end date entered'
 * startDate: '' and endDate: '1995-06-01' -> 'No start date entered - June 1995'
 *
 * @param {object} dates - object containing the date range
 * @returns {string} a description string with month and year, e.g. "September 1992 - September 1993"
 */
export function datesDescription(dates) {
  if (!dates?.startDate && !dates?.endDate) {
    if (dates?.['view:notSure']) {
      return notSureDatesSummary;
    }
    return noDatesEntered;
  }
  const startDate =
    formatMonthYearDate(dates?.startDate) || 'No start date entered';
  const endDate = formatMonthYearDate(dates?.endDate) || 'No end date entered';
  return `${startDate} - ${endDate}`;
}

/**
 * Create a title and subtitle for a page which will be passed into ui:title so that
 * they are grouped in the same legend
 * @param {string} title - the title for the page, which displays below the stepper
 * @param {string} subTitle - the subtitle for the page, which displays below the title
 * @returns {JSX.Element} markup with title and subtitle. example below.
 *
 * <h3 class="...">Service after August 2, 1990</h3>
 * <h4 class="...">Location 2 of 2: Iraq</h4>
 */
export function titleWithSubtitle(title, subTitle) {
  return (
    <>
      {formTitle(title)}
      {formSubtitle(subTitle)}
    </>
  );
}

/**
 * Group together the title, subtitle and description for the details page
 * @param {string} title - the title for the page, which displays below the stepper
 * @param {string} subTitle - markup for the page that displays in between the title and rest of the page
 * @param {string} type - the type of page to generate for, locations or hazards
 * @returns {JSX.Element} legend for the fieldset
 */
export function detailsPageBegin(title, subTitle, type = 'locations') {
  return (
    <legend>
      {formTitle(title)}
      {formSubtitle(subTitle)}
      <p className="vads-u-color--base vads-u-font-weight--normal vads-u-margin-bottom--0">
        {type === 'locations'
          ? dateRangeDescriptionWithLocation
          : dateRangeDescriptionWithHazard}
      </p>
    </legend>
  );
}