diff --git a/common/types.ts b/common/types.ts index 69a4ba0c6..3818c198e 100644 --- a/common/types.ts +++ b/common/types.ts @@ -140,6 +140,8 @@ export interface MetricDisaggregationDimensions { enabled?: boolean; settings?: MetricConfigurationSettings[]; display_name?: string; + race?: string; + ethnicity?: string; } export interface CreateReportFormValuesType extends Record { diff --git a/publisher/src/components/MetricConfiguration/Configuration.tsx b/publisher/src/components/MetricConfiguration/Configuration.tsx index 74aa5340f..1692e41bc 100644 --- a/publisher/src/components/MetricConfiguration/Configuration.tsx +++ b/publisher/src/components/MetricConfiguration/Configuration.tsx @@ -38,6 +38,8 @@ import { MetricConfigurationContainer, MetricDisaggregations, MetricOnOffWrapper, + RACE_ETHNICITY_DISAGGREGATION_KEY, + RaceEthnicitiesGrid, RadioButtonGroupWrapper, Subheader, } from "."; @@ -160,7 +162,7 @@ export const Configuration: React.FC = observer( Mark (using the checkmark) each of the breakdowns below that your agency will be able to report. Click the arrow to edit the - definition for each metric. + definition for each breakdown. {/* Disaggregations (Enable/Disable) */} @@ -226,66 +228,72 @@ export const Configuration: React.FC = observer( {/* Dimension Fields (Enable/Disable) */} - {activeDimensionKeys?.map((dimensionKey) => { - const currentDisaggregation = - disaggregations[systemMetricKey][activeDisaggregationKey]; - const currentDimension = - dimensions[systemMetricKey][activeDisaggregationKey][ - dimensionKey - ]; + {/* Race & Ethnicities Grid (when active disaggregation is Race / Ethnicity) */} + {activeDisaggregationKey === RACE_ETHNICITY_DISAGGREGATION_KEY ? ( + + ) : ( + activeDimensionKeys?.map((dimensionKey) => { + const currentDisaggregation = + disaggregations[systemMetricKey][activeDisaggregationKey]; + const currentDimension = + dimensions[systemMetricKey][activeDisaggregationKey][ + dimensionKey + ]; - return ( - setActiveDimensionKey(dimensionKey)} - > - - { - if (activeSystem && activeMetricKey) { - const updatedSetting = updateDimensionEnabledStatus( - activeSystem, - activeMetricKey, - activeDisaggregationKey, - dimensionKey, - !currentDimension.enabled - ); - saveMetricSettings(updatedSetting); + return ( + setActiveDimensionKey(dimensionKey)} + > + + - - + onChange={() => { + if (activeSystem && activeMetricKey) { + const updatedSetting = + updateDimensionEnabledStatus( + activeSystem, + activeMetricKey, + activeDisaggregationKey, + dimensionKey, + !currentDimension.enabled + ); + saveMetricSettings(updatedSetting); + } + }} + /> + + - - - {currentDimension.label} - + + + {currentDimension.label} + - - - - ); - })} + + + + ); + }) + )} )} diff --git a/publisher/src/components/MetricConfiguration/MetricConfiguration.styles.tsx b/publisher/src/components/MetricConfiguration/MetricConfiguration.styles.tsx index 360fa8791..b26a0838c 100644 --- a/publisher/src/components/MetricConfiguration/MetricConfiguration.styles.tsx +++ b/publisher/src/components/MetricConfiguration/MetricConfiguration.styles.tsx @@ -607,6 +607,7 @@ export const DefinitionMiniButton = styled(RevertToDefaultButton)<{ showDefault?: boolean; }>` width: unset; + min-width: 60px; padding: 9px 16px; transition: color 0.2s ease; @@ -621,7 +622,7 @@ export const DefinitionMiniButton = styled(RevertToDefaultButton)<{ opacity: 0.9; } - &:nth-child(3) { + &:last-child { background: ${palette.solid.blue}; &:hover { diff --git a/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx b/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx index 556ee0c7e..018b9e3e2 100644 --- a/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx +++ b/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx @@ -40,6 +40,8 @@ import { MetricName, MetricsViewContainer, MetricsViewControlPanel, + RACE_ETHNICITY_DISAGGREGATION_KEY, + RaceEthnicitiesForm, StickyHeader, } from "."; @@ -193,10 +195,16 @@ export const MetricConfiguration: React.FC = observer(() => { {/* Metric/Dimension Definitions (Includes/Excludes) & Context */} - + {/* Race/Ethnicities (when active disaggregation is Race / Ethnicities) */} + {activeDisaggregationKey === RACE_ETHNICITY_DISAGGREGATION_KEY && + activeDimensionKey ? ( + + ) : ( + + )} )} diff --git a/publisher/src/components/MetricConfiguration/RaceEthnicities.styles.tsx b/publisher/src/components/MetricConfiguration/RaceEthnicities.styles.tsx new file mode 100644 index 000000000..480f07fff --- /dev/null +++ b/publisher/src/components/MetricConfiguration/RaceEthnicities.styles.tsx @@ -0,0 +1,144 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2022 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { + palette, + typography, +} from "@justice-counts/common/components/GlobalStyles"; +import styled from "styled-components/macro"; + +import { + DefinitionDisplayName, + DefinitionItem, + DefinitionMiniButton, + Definitions, + DefinitionsDescription, + DefinitionsDisplay, + DefinitionsDisplayContainer, + DefinitionSelection, + DefinitionsTitle, + MetricOnOffWrapper, +} from "."; + +export const RaceEthnicitiesBreakdownContainer = styled.div` + padding-top: 14px; +`; + +export const CalloutBox = styled.div` + width: 100%; + height: 100%; + display: flex; + align-items: center; + padding: 20px 60px 20px 20px; + margin-bottom: 27px; + border-radius: 2px; + border: 1px solid ${palette.solid.blue}; + box-shadow: 0px 2px 4px rgba(0, 115, 229, 0.25); + + svg { + position: absolute; + right: 20px; + } +`; + +export const GridHeaderContainer = styled.div` + ${typography.sizeCSS.small} + color: ${palette.highlight.grey5}; + width: 100%; + display: flex; + padding-bottom: 10px; + border-bottom: 1px solid ${palette.highlight.grey4}; +`; + +export const GridRaceHeader = styled.div` + width: 100%; +`; +export const GridEthnicitiesHeader = styled.div` + display: flex; + gap: 17px; +`; + +export const EthnicityLabel = styled.div` + width: 100%; + display: flex; + align-items: center; + + svg { + margin-left: 3px; + width: 10px; + path { + fill: ${palette.highlight.grey5}; + } + } +`; + +export const Ethnicity = styled.div` + color: ${palette.solid.darkgrey}; + white-space: nowrap; +`; + +export const Description = styled.div` + ${typography.sizeCSS.normal} + + span { + color: ${palette.solid.blue}; + } +`; + +export const RaceEthnicitiesTable = styled.div` + width: 100%; +`; + +export const RaceEthnicitiesRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0; + + &:not(:last-child) { + border-bottom: 1px solid ${palette.highlight.grey4}; + } +`; + +export const RaceCell = styled.div``; + +export const EthnicitiesRow = styled.div` + display: flex; + margin-right: 17px; + gap: 60px; +`; +export const EthnicityCell = styled.div<{ enabled?: boolean }>` + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid ${palette.highlight.grey4}; + ${({ enabled }) => enabled && `background: ${palette.solid.blue};`} +`; + +export const SpecifyEthnicityWrapper = styled(MetricOnOffWrapper)` + margin-bottom: 35px; +`; + +export const RaceEthnicitiesContainer = styled(DefinitionsDisplayContainer)``; +export const RaceEthnicitiesDisplay = styled(DefinitionsDisplay)``; +export const RaceEthnicitiesTitle = styled(DefinitionsTitle)``; +export const RaceEthnicitiesDescription = styled(DefinitionsDescription)``; +export const RaceContainer = styled(Definitions)``; +export const Race = styled(DefinitionItem)``; +export const RaceDisplayName = styled(DefinitionDisplayName)``; +export const RaceSelection = styled(DefinitionSelection)``; +export const RaceSelectionButton = styled(DefinitionMiniButton)``; diff --git a/publisher/src/components/MetricConfiguration/RaceEthnicitiesForm.tsx b/publisher/src/components/MetricConfiguration/RaceEthnicitiesForm.tsx new file mode 100644 index 000000000..d906e34b0 --- /dev/null +++ b/publisher/src/components/MetricConfiguration/RaceEthnicitiesForm.tsx @@ -0,0 +1,255 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2022 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { observer } from "mobx-react-lite"; +import React from "react"; + +import { useStore } from "../../stores"; +import { BinaryRadioButton } from "../Forms"; +import { + Header, + Race, + RaceContainer, + RaceDisplayName, + RaceEthnicitiesContainer, + RaceEthnicitiesDescription, + RaceEthnicitiesDisplay, + RaceEthnicitiesTitle, + raceEthnicityGridStates, + RaceSelection, + RaceSelectionButton, + RadioButtonGroupWrapper, + SpecifyEthnicityWrapper, + Subheader, +} from "."; + +export const RaceEthnicitiesForm = observer(() => { + const { metricConfigStore } = useStore(); + const { + ethnicitiesByRace, + updateAllRaceEthnicitiesToDefaultState, + updateRaceDimensions, + saveMetricSettings, + } = metricConfigStore; + const ethnicitiesByRaceArray = Object.entries(ethnicitiesByRace); + + const canSpecifyEthnicity = + ethnicitiesByRaceArray.filter(([_, ethnicities]) => { + /** At least one Race where all three Ethnicities are enabled */ + return ( + ethnicities.Hispanic.enabled && + ethnicities["Not Hispanic"].enabled && + ethnicities["Unknown Ethnicity"].enabled + ); + }).length > 0; + + const specifiesHispanicAsRace = + ethnicitiesByRaceArray.filter(([race, ethnicities]) => { + /** Unknown Race has both Hispanic and Not Hispanic enabled */ + return ( + race === "Unknown" && + ethnicities.Hispanic.enabled && + ethnicities["Not Hispanic"].enabled + ); + }).length > 0; + + const determineCurrentState = () => { + if (canSpecifyEthnicity) { + return "CAN_SPECIFY_ETHNICITY"; + } + if (specifiesHispanicAsRace) { + return "NO_ETHNICITY_HISPANIC_AS_RACE"; + } + return "NO_ETHNICITY_HISPANIC_NOT_SPECIFIED"; + }; + + const currentState = determineCurrentState(); + + return ( + + + Race and Ethnicity + + + This breakdown asks for combinations of race and ethnicity, and should + be based on what data is available via your case management system. + Answering all of the questions below will fill out the grid for this + breakdown. + + + +
+ Does your case management system allow you to specify an + individual’s ethnicity (Hispanic, Non-Hispanic, or + Unknown) for this metric? +
+ + + { + const updatedDimensions = + updateAllRaceEthnicitiesToDefaultState( + "CAN_SPECIFY_ETHNICITY", + raceEthnicityGridStates + ); + saveMetricSettings(updatedDimensions); + }} + /> + { + const updatedDimensions = + updateAllRaceEthnicitiesToDefaultState( + "NO_ETHNICITY_HISPANIC_AS_RACE", + raceEthnicityGridStates + ); + saveMetricSettings(updatedDimensions); + }} + /> + +
+ +
+ Which of the following categories does that case management system + capture for race? +
+ + Fill out a response for each of the following race categories. + + + + {/* Hispanic/Latino as Race (if user cannot specify ethnicity) */} + {!canSpecifyEthnicity && ( + + Hispanic/Latino + + { + const updatedDimensions = + updateAllRaceEthnicitiesToDefaultState( + "NO_ETHNICITY_HISPANIC_NOT_SPECIFIED", + raceEthnicityGridStates + ); + saveMetricSettings(updatedDimensions); + }} + > + No + + { + const updatedDimensions = + updateAllRaceEthnicitiesToDefaultState( + "NO_ETHNICITY_HISPANIC_AS_RACE", + raceEthnicityGridStates + ); + saveMetricSettings(updatedDimensions); + }} + > + Yes + + + + )} + + {/* Races (Enable/Disable) */} + {Object.entries(ethnicitiesByRace).map(([race, ethnicities]) => { + const raceEnabled = Boolean( + Object.values(ethnicities).find((ethnicity) => ethnicity.enabled) + ); + + return ( + + {race} + + { + let switchedGridStateUpdatedDimensions; + /** + * When Unknown Race is disabled in NO_ETHNICITY_HISPANIC_AS_RACE state, we automatically switch + * to the NO_ETHNICITY_HISPANIC_NOT_SPECIFIED state because the Unknown Race (Hispanic/Latino Ethnicity) + * dimension is the only dimension an agency can specify their numbers for Hispanic/Latino as a Race (while + * in the NO_ETHNICITY_HISPANIC_AS_RACE state). + */ + if ( + race === "Unknown" && + currentState === "NO_ETHNICITY_HISPANIC_AS_RACE" + ) { + switchedGridStateUpdatedDimensions = + updateAllRaceEthnicitiesToDefaultState( + "NO_ETHNICITY_HISPANIC_NOT_SPECIFIED", + raceEthnicityGridStates + ); + } + + const updatedDimensions = updateRaceDimensions( + race, + false, + currentState, + raceEthnicityGridStates + ); + + if (switchedGridStateUpdatedDimensions) { + /** Add the updated dimension from disabling the Unknown race to the switchedGridStateUpdatedDimensions */ + switchedGridStateUpdatedDimensions.disaggregations[0].dimensions.push( + ...updatedDimensions.disaggregations[0].dimensions + ); + return saveMetricSettings( + switchedGridStateUpdatedDimensions + ); + } + saveMetricSettings(updatedDimensions); + }} + > + No + + { + const updatedDimensions = updateRaceDimensions( + race, + true, + currentState, + raceEthnicityGridStates + ); + saveMetricSettings(updatedDimensions); + }} + > + Yes + + + + ); + })} + +
+
+ ); +}); diff --git a/publisher/src/components/MetricConfiguration/RaceEthnicitiesGrid.tsx b/publisher/src/components/MetricConfiguration/RaceEthnicitiesGrid.tsx new file mode 100644 index 000000000..9fb5e6600 --- /dev/null +++ b/publisher/src/components/MetricConfiguration/RaceEthnicitiesGrid.tsx @@ -0,0 +1,82 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2022 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +import { observer } from "mobx-react-lite"; +import React from "react"; + +import { useStore } from "../../stores"; +import { ReactComponent as RightArrowIcon } from "../assets/right-arrow.svg"; +import { + CalloutBox, + Description, + EthnicitiesRow, + Ethnicity, + EthnicityCell, + EthnicityLabel, + GridEthnicitiesHeader, + GridHeaderContainer, + GridRaceHeader, + RaceCell, + RaceEthnicitiesBreakdownContainer, + RaceEthnicitiesRow, + RaceEthnicitiesTable, +} from "."; + +export const RaceEthnicitiesGrid: React.FC = observer(() => { + const { metricConfigStore } = useStore(); + const { ethnicitiesByRace } = metricConfigStore; + + return ( + + + + Answer the questions on the Race and Ethnicity form; the + grid below will reflect your responses. + + + + + + Race + + + Ethnicity + + Hispanic + Not Hispanic + Unknown + + + + + {Object.entries(ethnicitiesByRace).map(([race, ethnicities]) => ( + + {race} + + {Object.values(ethnicities).map((ethnicity) => ( + + ))} + + + ))} + + + ); +}); diff --git a/publisher/src/components/MetricConfiguration/RaceEthnicitiesGridStates.ts b/publisher/src/components/MetricConfiguration/RaceEthnicitiesGridStates.ts new file mode 100644 index 000000000..78c7dc03c --- /dev/null +++ b/publisher/src/components/MetricConfiguration/RaceEthnicitiesGridStates.ts @@ -0,0 +1,172 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2022 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +export type StateKeys = keyof typeof raceEthnicityGridStates; + +export type RaceEthnicitiesGridStates = { + [state: string]: { + [race: string]: { + [ethnicity: string]: boolean; + }; + }; +}; + +/** + * Race x Ethnicity Grid States represent the maximum set of dimensions (of the 24 available + * Race & Ethnicity dimensions) a user can enter data for or disable/enable. + * + * There are 3 states that a user can fall under based on the following conditions: + * State 1 (`CAN_SPECIFY_ETHNICITY`): They record & can report each ethnicity per race (Hispanic/Latino, Not Hispanic/Latino, Unknown Ethnicity) + * State 2 (`NO_ETHNICITY_HISPANIC_AS_RACE`): They cannot record/report each ethnicity and record Hispanic/Latino as a Race + * State 3 (`NO_ETHNICITY_HISPANIC_NOT_SPECIFIED`): They cannot record/report each ethnicity and DO NOT record Hispanic/Latino as a Race + * + * This object serves as a truth table of the 3 states. The boolean represents whether or not that + * particular dimension is available to the user to enter data for or disable/enable. + * + * @example + * State 1 has all 24 available dimensions (Race x All 3 Ethnicities) + * State 2 has only 9 available dimensions (Race x Not Hispanic/Latino Ethnicity, Unknown Race x Hispanic/Latino Ethnicity) - the other 15 dimensions are disabled. + * State 3 has only 8 available dimensions (Race x Unknown Ethnicity) - the other 16 dimensions are disabled. + */ +export const raceEthnicityGridStates = { + CAN_SPECIFY_ETHNICITY: { + "American Indian / Alaskan Native": { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + Asian: { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + Black: { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + "More than one race": { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + "Native Hawaiian / Pacific Islander": { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + White: { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + Other: { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + Unknown: { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": true, + }, + }, + NO_ETHNICITY_HISPANIC_AS_RACE: { + "American Indian / Alaskan Native": { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + Asian: { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + Black: { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + "More than one race": { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + "Native Hawaiian / Pacific Islander": { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + White: { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + Other: { + Hispanic: false, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + Unknown: { + Hispanic: true, + "Not Hispanic": true, + "Unknown Ethnicity": false, + }, + }, + NO_ETHNICITY_HISPANIC_NOT_SPECIFIED: { + "American Indian / Alaskan Native": { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + Asian: { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + Black: { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + "More than one race": { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + "Native Hawaiian / Pacific Islander": { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + White: { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + Other: { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + Unknown: { + Hispanic: false, + "Not Hispanic": false, + "Unknown Ethnicity": true, + }, + }, +}; diff --git a/publisher/src/components/MetricConfiguration/constants.ts b/publisher/src/components/MetricConfiguration/constants.ts new file mode 100644 index 000000000..f746cca81 --- /dev/null +++ b/publisher/src/components/MetricConfiguration/constants.ts @@ -0,0 +1,18 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2022 Recidiviz, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ============================================================================= + +export const RACE_ETHNICITY_DISAGGREGATION_KEY = "global/race_and_ethnicity"; diff --git a/publisher/src/components/MetricConfiguration/index.ts b/publisher/src/components/MetricConfiguration/index.ts index 43890aa5e..4c3802faf 100644 --- a/publisher/src/components/MetricConfiguration/index.ts +++ b/publisher/src/components/MetricConfiguration/index.ts @@ -16,9 +16,14 @@ // ============================================================================= export * from "./Configuration"; +export * from "./constants"; export * from "./ContextConfiguration"; export * from "./MetricBox"; export * from "./MetricConfiguration"; export * from "./MetricConfiguration.styles"; export * from "./MetricDefinitions"; +export * from "./RaceEthnicities.styles"; +export * from "./RaceEthnicitiesForm"; +export * from "./RaceEthnicitiesGrid"; +export * from "./RaceEthnicitiesGridStates"; export * from "./types"; diff --git a/publisher/src/components/MetricConfiguration/types.ts b/publisher/src/components/MetricConfiguration/types.ts index 883ec13b1..014de91fb 100644 --- a/publisher/src/components/MetricConfiguration/types.ts +++ b/publisher/src/components/MetricConfiguration/types.ts @@ -42,3 +42,38 @@ export type MetricSettings = { }[]; }[]; }; + +export type UpdatedDimension = { + key: string; + label: string; + enabled: boolean; + race: Races; + ethnicity: Ethnicities; +}; + +export type UpdatedDisaggregation = { + key: string; + disaggregations: { + key: string; + dimensions: UpdatedDimension[]; + }[]; +}; + +export const races = [ + "American Indian / Alaskan Native", + "Asian", + "Black", + "Native Hawaiian / Pacific Islander", + "White", + "More than one race", + "Other", + "Unknown", +] as const; +export type Races = typeof races[number]; + +export const ethnicities = [ + "Hispanic", + "Not Hispanic", + "Unknown Ethnicity", +] as const; +export type Ethnicities = typeof ethnicities[number]; diff --git a/publisher/src/stores/MetricConfigStore.tsx b/publisher/src/stores/MetricConfigStore.tsx index 1ca98ebe5..9c9be7422 100644 --- a/publisher/src/stores/MetricConfigStore.tsx +++ b/publisher/src/stores/MetricConfigStore.tsx @@ -26,8 +26,16 @@ import { import { makeAutoObservable, runInAction } from "mobx"; import { + Ethnicities, + ethnicities, MetricConfigurationSettingsOptions, MetricSettings, + RACE_ETHNICITY_DISAGGREGATION_KEY, + RaceEthnicitiesGridStates, + Races, + StateKeys, + UpdatedDimension, + UpdatedDisaggregation, } from "../components/MetricConfiguration"; import { isPositiveNumber, removeCommaSpaceAndTrim } from "../utils"; import API from "./API"; @@ -45,7 +53,7 @@ class MetricConfigStore { metrics: { [systemMetricKey: string]: { enabled?: boolean; - label?: Metric["label"]; + label?: string; description?: Metric["description"]; frequency?: Metric["frequency"]; }; @@ -87,7 +95,10 @@ class MetricConfigStore { [disaggregationKey: string]: { [dimensionKey: string]: { enabled?: boolean; - label?: Metric["label"]; + label?: string; + key?: string; + race?: Races; + ethnicity?: Ethnicities; }; }; }; @@ -163,7 +174,7 @@ class MetricConfigStore { key: string; metric: { enabled?: boolean; - label?: Metric["label"]; + label?: string; description?: Metric["description"]; frequency?: Metric["frequency"]; }; @@ -268,6 +279,16 @@ class MetricConfigStore { ); disaggregation.dimensions.forEach((dimension) => { + const dimensionMetadata = + disaggregation.key === RACE_ETHNICITY_DISAGGREGATION_KEY + ? { + label: dimension.label, + key: dimension.key, + race: dimension.race, + ethnicity: dimension.ethnicity, + } + : { label: dimension.label, key: dimension.key }; + /** Initialize Dimension Status (Enabled/Disabled) */ this.updateDimensionEnabledStatus( normalizedMetricSystemName, @@ -275,7 +296,7 @@ class MetricConfigStore { disaggregation.key, dimension.key, dimension.enabled as boolean, - { label: dimension.label } + dimensionMetadata ); dimension.settings?.forEach((setting) => { @@ -450,7 +471,7 @@ class MetricConfigStore { disaggregationKey: string, dimensionKey: string, enabledStatus: boolean, - metadata?: { [key: string]: string } + metadata?: { [key: string]: string | undefined } ): MetricSettings => { const systemMetricKey = MetricConfigStore.getSystemMetricKey( system, @@ -472,6 +493,16 @@ class MetricConfigStore { if (metadata) { this.dimensions[systemMetricKey][disaggregationKey][dimensionKey].label = metadata.label; + this.dimensions[systemMetricKey][disaggregationKey][dimensionKey].key = + metadata.key; + + if (disaggregationKey === RACE_ETHNICITY_DISAGGREGATION_KEY) { + this.dimensions[systemMetricKey][disaggregationKey][dimensionKey].race = + metadata.race as Races; + this.dimensions[systemMetricKey][disaggregationKey][ + dimensionKey + ].ethnicity = metadata.ethnicity as Ethnicities; + } } /** @@ -651,6 +682,166 @@ class MetricConfigStore { contexts: [{ key: contextKey, value }], }; }; + + get ethnicitiesByRace() { + if (!this.activeSystem || !this.activeMetricKey) return {}; + + const systemMetricKey = MetricConfigStore.getSystemMetricKey( + this.activeSystem, + this.activeMetricKey + ); + const raceEthnicitiesDimensions = + this.dimensions[systemMetricKey][RACE_ETHNICITY_DISAGGREGATION_KEY]; + const dimensions = + raceEthnicitiesDimensions && + (Object.values(raceEthnicitiesDimensions) as UpdatedDimension[]); + const ethnicitiesByRaceMap = dimensions?.reduce( + (acc, dimension) => { + acc[dimension.race] = { + ...acc[dimension.race], + [dimension.ethnicity]: dimension, + }; + + return acc; + }, + {} as { + [race: string]: { + [ethnicity: string]: UpdatedDimension; + }; + } + ); + + return ethnicitiesByRaceMap || {}; + } + + updateAllRaceEthnicitiesToDefaultState = ( + state: StateKeys, + gridStates: RaceEthnicitiesGridStates + ): UpdatedDisaggregation => { + const systemMetricKey = MetricConfigStore.getSystemMetricKey( + this.activeSystem as AgencySystems, + this.activeMetricKey as string + ); + const unknownRaceDisabled = !Object.values( + this.ethnicitiesByRace.Unknown + ).find((ethnicity) => ethnicity.enabled); + let sanitizedState = + state === "NO_ETHNICITY_HISPANIC_AS_RACE" && unknownRaceDisabled + ? "NO_ETHNICITY_HISPANIC_NOT_SPECIFIED" + : state; + const updatedDimensions = [] as UpdatedDimension[]; + + /** + * When Unknown Race dimensions are disabled AND user is switching to NO_ETHNICITY_HISPANIC_AS_RACE state, + * re-enable the Unknown Race dimensions for the NO_ETHNICITY_HISPANIC_AS_RACE state. + */ + if (unknownRaceDisabled && state === "NO_ETHNICITY_HISPANIC_AS_RACE") { + this.ethnicitiesByRace.Unknown.Hispanic.enabled = true; + this.ethnicitiesByRace.Unknown["Not Hispanic"].enabled = true; + updatedDimensions.push( + ...[ + { + ...this.ethnicitiesByRace.Unknown.Hispanic, + enabled: true, + }, + { + ...this.ethnicitiesByRace.Unknown["Not Hispanic"], + enabled: true, + }, + ] + ); + sanitizedState = state; + } + + /** Update dimensions to match the specified default grid state */ + Object.keys(this.ethnicitiesByRace).forEach((race) => { + const raceIsEnabled = Boolean( + ethnicities.find( + (ethnicity) => this.ethnicitiesByRace[race][ethnicity].enabled + ) + ); + const disaggregationIsEnabled = + this.disaggregations[systemMetricKey][RACE_ETHNICITY_DISAGGREGATION_KEY] + .enabled; + + /** If the Race is disabled, keep it disabled */ + if (!raceIsEnabled && disaggregationIsEnabled) return; + + ethnicities.forEach((ethnicity) => { + if ( + this.ethnicitiesByRace[race][ethnicity].enabled === + gridStates[sanitizedState][race][ethnicity] + ) + return; + + this.updateDimensionEnabledStatus( + this.activeSystem as AgencySystems, + this.activeMetricKey as string, + RACE_ETHNICITY_DISAGGREGATION_KEY, + this.ethnicitiesByRace[race][ethnicity].key, + gridStates[sanitizedState][race][ethnicity] + ); + + updatedDimensions.push({ + ...this.ethnicitiesByRace[race][ethnicity], + enabled: gridStates[sanitizedState][race][ethnicity], + }); + }); + }); + + /** Return array of dimensions that were updated */ + return { + key: this.activeMetricKey as string, + disaggregations: [ + { + key: RACE_ETHNICITY_DISAGGREGATION_KEY, + dimensions: updatedDimensions, + }, + ], + }; + }; + + updateRaceDimensions = ( + race: string, + enabled: boolean, + state: StateKeys, + gridStates: RaceEthnicitiesGridStates + ): UpdatedDisaggregation => { + const updatedDimensions = [] as UpdatedDimension[]; + + ethnicities.forEach((ethnicity) => { + /** No update if intended update matches the current state (e.g. enabling an already enabled dimension) */ + if (this.ethnicitiesByRace[race][ethnicity].enabled === enabled) return; + /** No update if enabling a disabled dimension that is not available to the user to edit (determined by current grid state) */ + if ( + enabled && + this.ethnicitiesByRace[race][ethnicity].enabled === + gridStates[state][race][ethnicity] + ) + return; + + this.updateDimensionEnabledStatus( + this.activeSystem as AgencySystems, + this.activeMetricKey as string, + RACE_ETHNICITY_DISAGGREGATION_KEY, + this.ethnicitiesByRace[race][ethnicity].key, + enabled + ); + + updatedDimensions.push(this.ethnicitiesByRace[race][ethnicity]); + }); + + /** Return array of dimensions that were updated */ + return { + key: this.activeMetricKey as string, + disaggregations: [ + { + key: RACE_ETHNICITY_DISAGGREGATION_KEY, + dimensions: updatedDimensions, + }, + ], + }; + }; } export default MetricConfigStore;