diff --git a/common/types.ts b/common/types.ts
index 25029a106..910a32bbd 100644
--- a/common/types.ts
+++ b/common/types.ts
@@ -87,7 +87,8 @@ export type AgencySettingType =
| "HOMEPAGE_URL"
| "ZIPCODE"
| "DATA_SHARING_TYPE"
- | "BIOLOGICAL_SEX_RACE_ETHNICITY_DATA_SOURCE";
+ | "BIOLOGICAL_SEX_RACE_ETHNICITY_DATA_SOURCE"
+ | "SECTOR_INCLUDES_EXCLUDES";
export interface AgencySetting {
setting_type: AgencySettingType;
diff --git a/publisher/src/components/AgencySettings/AgencySettings.styles.tsx b/publisher/src/components/AgencySettings/AgencySettings.styles.tsx
index f708d9df8..5d34a9153 100644
--- a/publisher/src/components/AgencySettings/AgencySettings.styles.tsx
+++ b/publisher/src/components/AgencySettings/AgencySettings.styles.tsx
@@ -444,6 +444,13 @@ export const SupervisionSystemRow = styled(AgencySettingsInfoRow)<{
border: none;
`;
+// Agency Definition
+export const DefinitionDescriptionInputWrapper = styled.div`
+ :not(:last-child) {
+ padding-bottom: 24px;
+ }
+`;
+
// Data Source
export const DataSourceContainer = styled.div`
border-top: 1px solid ${palette.highlight.grey5};
@@ -453,6 +460,7 @@ export const DataSourceContainer = styled.div`
padding: 16px 40px;
`;
export const DataSourceTitle = styled.div`
+ text-transform: capitalize;
font-weight: 700;
padding-bottom: 8px;
`;
diff --git a/publisher/src/components/AgencySettings/AgencySettings.tsx b/publisher/src/components/AgencySettings/AgencySettings.tsx
index 7d283fbe3..1c6add13f 100644
--- a/publisher/src/components/AgencySettings/AgencySettings.tsx
+++ b/publisher/src/components/AgencySettings/AgencySettings.tsx
@@ -27,6 +27,7 @@ import {
} from "./AgencySettings.styles";
import { AgencySettingsBasicInfo } from "./AgencySettingsBasicInfo";
import AgencySettingsDataSource from "./AgencySettingsDataSource";
+import AgencySettingsDefinition from "./AgencySettingsDefinition";
// TODO(#1537) Ungate zipcode and agency data sharing fields
// import AgencySettingsDataSharingType from "./AgencySettingsDataSharingType";
import AgencySettingsDescription from "./AgencySettingsDescription";
@@ -44,6 +45,7 @@ export enum ActiveSetting {
Supervisions = "SUPERVISIONS",
Jurisdictions = "JURISDICTIONS",
DataSource = "BIOLOGICAL_SEX_RACE_ETHNICITY_DATA_SOURCE",
+ Definition = "SECTOR_INCLUDES_EXCLUDES",
}
export type SettingProps = {
@@ -97,6 +99,9 @@ export const AgencySettings: React.FC = observer(() => {
+
{/* TODO(#1537) Ungate zipcode and agency data sharing fields */}
{/* */}
diff --git a/publisher/src/components/AgencySettings/AgencySettingsDefinition.tsx b/publisher/src/components/AgencySettings/AgencySettingsDefinition.tsx
new file mode 100644
index 000000000..5fe9a6be3
--- /dev/null
+++ b/publisher/src/components/AgencySettings/AgencySettingsDefinition.tsx
@@ -0,0 +1,375 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2024 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 { Button } from "@justice-counts/common/components/Button";
+import { CheckboxOptions } from "@justice-counts/common/components/CheckboxOptions";
+import { NewInput } from "@justice-counts/common/components/Input";
+import { Modal } from "@justice-counts/common/components/Modal";
+import {
+ AgencySystems,
+ SupervisionSubsystems,
+} from "@justice-counts/common/types";
+import { removeSnakeCase } from "@justice-counts/common/utils";
+import { observer } from "mobx-react-lite";
+import React, { useEffect, useMemo, useState } from "react";
+import { useParams } from "react-router-dom";
+
+import { useStore } from "../../stores";
+import { ChooseDefaultSettings } from "../MetricsConfiguration/ModalForm.styled";
+import { SettingProps } from "./AgencySettings";
+import {
+ AgencyInfoBlockDescription,
+ AgencySettingsBlock,
+ BasicInfoBlockTitle,
+ DataSourceContainer,
+ DataSourceQuestionWrapper,
+ DataSourceTitle,
+ DefinitionDescriptionInputWrapper,
+ EditButtonContainer,
+} from "./AgencySettings.styles";
+import { AgencySettingsEditModeModal } from "./AgencySettingsEditModeModal";
+import {
+ AgencyIncludesExcludes,
+ boolToYesNoEnum,
+} from "./IncludesExcludes/includesExcludes";
+import {
+ AgencySystemKeys,
+ IncludesExcludesEnum,
+} from "./IncludesExcludes/types";
+
+type IncludedValue = `${IncludesExcludesEnum}`;
+
+type AgencyDefinitionSetting = {
+ sector: AgencySystemKeys;
+ settings: {
+ key: string;
+ included?: IncludedValue;
+ value?: string;
+ }[];
+};
+
+const getDefaultSetting = (
+ systems: AgencySystemKeys[],
+ unconfiguredDefault?: IncludedValue
+): AgencyDefinitionSetting[] => {
+ if (!systems.length) return [];
+
+ return systems.map((system) => {
+ return {
+ sector: system,
+ settings: Object.entries(AgencyIncludesExcludes[system]).map(
+ ([key, obj]) => {
+ return {
+ key,
+ included: unconfiguredDefault ?? obj.default,
+ };
+ }
+ ),
+ };
+ });
+};
+
+const AgencySettingsDefinition: React.FC<{
+ settingProps: SettingProps;
+}> = ({ settingProps }) => {
+ const { isSettingInEditMode, openSetting, removeEditMode, allowEdit } =
+ settingProps;
+
+ const { agencyId } = useParams() as { agencyId: string };
+ const { agencyStore } = useStore();
+ const {
+ currentAgencySystems,
+ currentAgencySettings,
+ updateAgencySettings,
+ saveAgencySettings,
+ } = agencyStore;
+
+ const isSupervisionAgencyWithEnabledSubpopulations =
+ currentAgencySystems?.includes(AgencySystems.SUPERVISION) &&
+ currentAgencySystems?.some((system) =>
+ SupervisionSubsystems.includes(system)
+ );
+ const isCourtAgency = currentAgencySystems?.includes(
+ AgencySystems.COURTS_AND_PRETRIAL
+ );
+ const isCombinedAgency =
+ isSupervisionAgencyWithEnabledSubpopulations && isCourtAgency;
+
+ const discreteAgencyTitle = isSupervisionAgencyWithEnabledSubpopulations
+ ? "Supervision"
+ : "Court";
+ const agencyTitle = isCombinedAgency ? "Combined" : discreteAgencyTitle;
+
+ const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
+ const [currentSystems, setCurrentSystems] = useState([]);
+
+ const agencyDefinitionSetting = useMemo(() => {
+ return (
+ (currentAgencySettings?.find(
+ (setting) => setting.setting_type === "SECTOR_INCLUDES_EXCLUDES"
+ )?.value as AgencyDefinitionSetting[]) || []
+ );
+ }, [currentAgencySettings]);
+
+ useEffect(() => {
+ // When we select agency without applicable sectors we need to reset currentSystems, otherwise it can have values from previous agencies in it.
+ // We also can't do it in reset effect below, because it depends on currentSystems and it'll cause infinite loop.
+ setCurrentSystems([]);
+
+ const supervisionSystems =
+ (currentAgencySystems?.filter((system) =>
+ SupervisionSubsystems.includes(system)
+ ) as AgencySystemKeys[]) || [];
+
+ if (isCombinedAgency) {
+ setCurrentSystems([
+ ...supervisionSystems,
+ AgencySystems.COURTS_AND_PRETRIAL,
+ ]);
+ } else {
+ if (isSupervisionAgencyWithEnabledSubpopulations)
+ setCurrentSystems(supervisionSystems);
+ if (isCourtAgency) setCurrentSystems([AgencySystems.COURTS_AND_PRETRIAL]);
+ }
+ }, [
+ currentAgencySystems,
+ isCourtAgency,
+ isSupervisionAgencyWithEnabledSubpopulations,
+ isCombinedAgency,
+ ]);
+
+ const configuredSystems = agencyDefinitionSetting.map(
+ (setting) => setting.sector
+ );
+ const unconfiguredSystems = currentSystems.filter(
+ (sector) => !configuredSystems.includes(sector)
+ );
+
+ const initialSetting = [
+ ...agencyDefinitionSetting,
+ ...getDefaultSetting(unconfiguredSystems, IncludesExcludesEnum.NO),
+ ];
+
+ const [updatedSetting, setUpdatedSetting] = useState(initialSetting);
+
+ // This is a reset effect and it's used to reset setting for UI to work properly.
+ // The absence of this effect causes visual bug where setting from the previous agency may be displayed.
+ useEffect(() => {
+ setUpdatedSetting(initialSetting);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [agencyDefinitionSetting, currentSystems]);
+
+ const handleSaveClick = () => {
+ const updatedSettings = updateAgencySettings(
+ "SECTOR_INCLUDES_EXCLUDES",
+ updatedSetting,
+ parseInt(agencyId)
+ );
+ saveAgencySettings(updatedSettings, agencyId);
+ removeEditMode();
+ };
+
+ const handleCancelClick = () => {
+ if (JSON.stringify(updatedSetting) === JSON.stringify(initialSetting)) {
+ removeEditMode();
+ } else {
+ setIsConfirmModalOpen(true);
+ }
+ };
+
+ const handleCancelModalConfirm = () => {
+ setUpdatedSetting(initialSetting);
+ setIsConfirmModalOpen(false);
+ removeEditMode();
+ };
+
+ const handleCheckboxChange = (
+ key: string,
+ checked: boolean,
+ system: AgencySystemKeys
+ ) => {
+ setUpdatedSetting((prevSettings) => {
+ const updates = prevSettings.map((prevSetting) => {
+ if (prevSetting.sector === system) {
+ return {
+ ...prevSetting,
+ settings: prevSetting.settings.map((setting) =>
+ setting.key === key
+ ? {
+ ...setting,
+ included: boolToYesNoEnum(!checked),
+ }
+ : setting
+ ),
+ };
+ }
+ return prevSetting;
+ });
+
+ return updates;
+ });
+ };
+
+ const handleDescriptionChange = (system: AgencySystemKeys, value: string) => {
+ setUpdatedSetting((prevSettings) => {
+ return prevSettings.map((prevSetting) => {
+ if (prevSetting.sector === system) {
+ return {
+ ...prevSetting,
+ settings: [
+ ...prevSetting.settings.filter(
+ (setting) => setting.key !== "ADDITIONAL_CONTEXT"
+ ), // Ensure no duplicate keys
+ {
+ key: "ADDITIONAL_CONTEXT",
+ value,
+ },
+ ],
+ };
+ }
+ return prevSetting;
+ });
+ });
+ };
+
+ if (!currentSystems.length) return null;
+
+ return (
+ <>
+ {isSettingInEditMode && (
+ setIsConfirmModalOpen(false)}
+ handleCancelModalConfirm={handleCancelModalConfirm}
+ >
+
+
+ Indicate which of the following categories best defines your
+ agency. Or, choose the{" "}
+
+ setUpdatedSetting(getDefaultSetting(currentSystems))
+ }
+ >
+ Justice Counts definition.
+
+
+
+ {currentSystems?.map((system) => {
+ const defaultDescription = initialSetting
+ .find((setting) => setting.sector === system)
+ ?.settings.find(
+ (setting) => setting.key === "ADDITIONAL_CONTEXT"
+ )?.value;
+
+ return (
+
+ {isSupervisionAgencyWithEnabledSubpopulations && (
+
+ {removeSnakeCase(system).toLocaleLowerCase()}
+
+ )}
+ {
+ const included = updatedSetting
+ .find((sector) => sector.sector === system)
+ ?.settings.find(
+ (setting) => setting.key === key
+ )?.included;
+
+ return {
+ key,
+ label: option.label,
+ checked: included === IncludesExcludesEnum.YES,
+ };
+ }
+ ),
+ ]}
+ onChange={({ key, checked }) =>
+ handleCheckboxChange(key, checked, system)
+ }
+ />
+
+
+ handleDescriptionChange(system, e.target.value)
+ }
+ fullWidth
+ />
+
+
+ );
+ })}
+
+ }
+ buttons={[
+ {
+ label: "Save",
+ onClick: () => {
+ handleSaveClick();
+ },
+ },
+ ]}
+ maxHeight={900}
+ modalBackground="opaque"
+ onClickClose={handleCancelClick}
+ agencySettingsConfigs
+ jurisdictionsSettingsConfigs
+ agencySettingsAndJurisdictionsTitleConfigs
+ customPadding="4px 40px 24px 40px"
+ />
+
+ )}
+
+
+
+ {agencyTitle} Agency Definition
+ {allowEdit && (
+
+
+ )}
+
+
+ Information about how your agency is defined
+
+
+ >
+ );
+};
+
+export default observer(AgencySettingsDefinition);
diff --git a/publisher/src/components/AgencySettings/AgencySettingsSupervisions.tsx b/publisher/src/components/AgencySettings/AgencySettingsSupervisions.tsx
index ae72e6bd9..d20ed4a94 100644
--- a/publisher/src/components/AgencySettings/AgencySettingsSupervisions.tsx
+++ b/publisher/src/components/AgencySettings/AgencySettingsSupervisions.tsx
@@ -19,7 +19,7 @@ import blueCheck from "@justice-counts/common/assets/status-check-icon.png";
import { Button } from "@justice-counts/common/components/Button";
import { Modal } from "@justice-counts/common/components/Modal";
import { AgencySystem } from "@justice-counts/common/types";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useStore } from "../../stores";
@@ -72,6 +72,10 @@ export const AgencySettingsSupervisions: React.FC<{
userStore.isAgencyAdmin(agencyId) ||
userStore.isJusticeCountsAdmin(agencyId);
+ useEffect(() => {
+ setSupervisionSystemsToSave(currentAgencySystems);
+ }, [currentAgencySystems]);
+
const handleSaveClick = () => {
if (supervisionSystemsToSave) {
const updatedSystems = updateAgencySystems(supervisionSystemsToSave);
diff --git a/publisher/src/components/AgencySettings/IncludesExcludes/types.ts b/publisher/src/components/AgencySettings/IncludesExcludes/types.ts
index 344faefad..d703d63ec 100644
--- a/publisher/src/components/AgencySettings/IncludesExcludes/types.ts
+++ b/publisher/src/components/AgencySettings/IncludesExcludes/types.ts
@@ -29,7 +29,7 @@ export type IncludesExcludesWithDefault = {
};
};
-type AgencySystemKeys =
+export type AgencySystemKeys =
| AgencySystems.COURTS_AND_PRETRIAL
| AgencySystems.PAROLE
| AgencySystems.PROBATION
diff --git a/publisher/src/components/AgencySettings/types.ts b/publisher/src/components/AgencySettings/types.ts
index 2f0cf1a50..753c173bc 100644
--- a/publisher/src/components/AgencySettings/types.ts
+++ b/publisher/src/components/AgencySettings/types.ts
@@ -28,4 +28,5 @@ export type AgencySettingType =
| "HOMEPAGE_URL"
| "ZIPCODE"
| "DATA_SHARING_TYPE"
- | "BIOLOGICAL_SEX_RACE_ETHNICITY_DATA_SOURCE";
+ | "BIOLOGICAL_SEX_RACE_ETHNICITY_DATA_SOURCE"
+ | "SECTOR_INCLUDES_EXCLUDES";