-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Publisher][Agency Settings] Add new agency definition section for court and supervision sectors #1594
[Publisher][Agency Settings] Add new agency definition section for court and supervision sectors #1594
Changes from 4 commits
81c8e2d
a418227
f40abce
0d91a80
de7ffec
90d11de
36f5dc2
93008cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,323 @@ | ||
// 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 <https://www.gnu.org/licenses/>. | ||
// ============================================================================= | ||
|
||
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, | ||
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; | ||
}[]; | ||
}; | ||
|
||
const getDefaultSetting = ( | ||
systems: AgencySystemKeys[], | ||
defaultIncluded?: IncludedValue | ||
): AgencyDefinitionSetting[] => { | ||
if (!systems.length) return []; | ||
|
||
return systems.map((system) => { | ||
return { | ||
sector: system, | ||
settings: Object.entries(AgencyIncludesExcludes[system]).map( | ||
([key, obj]) => { | ||
return { | ||
key, | ||
included: defaultIncluded ?? 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 isSupervisionAgency = | ||
mxosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
currentAgencySystems?.includes(AgencySystems.SUPERVISION) && | ||
currentAgencySystems?.some((system) => | ||
SupervisionSubsystems.includes(system) | ||
); | ||
const isCourtAgency = currentAgencySystems?.includes( | ||
AgencySystems.COURTS_AND_PRETRIAL | ||
); | ||
const isCombinedAgency = isSupervisionAgency && isCourtAgency; | ||
|
||
const discreteAgencyTitle = isSupervisionAgency ? "Supervision" : "Court"; | ||
const agencyTitle = isCombinedAgency ? "Combined" : discreteAgencyTitle; | ||
|
||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); | ||
const [currentSystems, setCurrentSystems] = useState<AgencySystemKeys[]>([]); | ||
const [descriptionValue, setDescriptionValue] = useState(""); | ||
|
||
const agencyDefinitionSetting = useMemo(() => { | ||
return ( | ||
(currentAgencySettings?.find( | ||
(setting) => setting.setting_type === "SECTOR_INCLUDES_EXCLUDES" | ||
)?.value as AgencyDefinitionSetting[]) || [] | ||
); | ||
}, [currentAgencySettings]); | ||
|
||
const isSettingConfigured = agencyDefinitionSetting.length > 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this still be a valid way to check this if the user checks and unchecks everything? I think there will still be fields here if the user unchecks everything. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the purpose of this component this would be enough since I just need to know if BE is returning something, and if it's not -- configure default setting object with the correct structure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it! |
||
|
||
useEffect(() => { | ||
setCurrentSystems([]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to reset this every time the effect runs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, unfortunately we need, because when we select agency without applicable sectors we need to reset There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it - can we add a small comment here to clarify? |
||
|
||
if (isCombinedAgency) { | ||
setCurrentSystems([ | ||
...((currentAgencySystems?.filter((system) => | ||
SupervisionSubsystems.includes(system) | ||
) as AgencySystemKeys[]) || []), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could just have one variable that we set outside of this effect that gives us the list of subsystems and use that variable throughout. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This includes supervision systems only, and we using them only in this effect to set |
||
AgencySystems.COURTS_AND_PRETRIAL, | ||
]); | ||
} else { | ||
if (isSupervisionAgency) | ||
setCurrentSystems( | ||
(currentAgencySystems?.filter((system) => | ||
SupervisionSubsystems.includes(system) | ||
) as AgencySystemKeys[]) || [] | ||
); | ||
if (isCourtAgency) setCurrentSystems([AgencySystems.COURTS_AND_PRETRIAL]); | ||
} | ||
}, [ | ||
currentAgencySystems, | ||
isCourtAgency, | ||
isSupervisionAgency, | ||
isCombinedAgency, | ||
]); | ||
|
||
const defaultSetting = isSettingConfigured | ||
mxosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
? agencyDefinitionSetting | ||
: getDefaultSetting(currentSystems, IncludesExcludesEnum.NO); | ||
|
||
const [updatedSetting, setUpdatedSetting] = useState(defaultSetting); | ||
|
||
useEffect(() => { | ||
setUpdatedSetting(defaultSetting); | ||
setDescriptionValue(""); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [agencyDefinitionSetting, currentSystems]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you clarify the purpose of this effect? We already initialize it with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a reset effect used for resetting setting for UI to work properly. The absence of this effect causes visual bug with incorrect working UI when switching agencies like here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for clarifying. I'd add a small comment here too for clarity. |
||
|
||
const handleSaveClick = () => { | ||
const updatedSettings = updateAgencySettings( | ||
"SECTOR_INCLUDES_EXCLUDES", | ||
updatedSetting, | ||
parseInt(agencyId) | ||
); | ||
saveAgencySettings(updatedSettings, agencyId); | ||
removeEditMode(); | ||
}; | ||
|
||
const handleCancelClick = () => { | ||
if (JSON.stringify(updatedSetting) === JSON.stringify(defaultSetting)) { | ||
removeEditMode(); | ||
} else { | ||
setIsConfirmModalOpen(true); | ||
} | ||
}; | ||
|
||
const handleCancelModalConfirm = () => { | ||
setUpdatedSetting(defaultSetting); | ||
setIsConfirmModalOpen(false); | ||
removeEditMode(); | ||
}; | ||
|
||
if (!currentSystems.length) return null; | ||
|
||
return ( | ||
<> | ||
{isSettingInEditMode && ( | ||
<AgencySettingsEditModeModal | ||
openCancelModal={handleCancelClick} | ||
isConfirmModalOpen={isConfirmModalOpen} | ||
closeCancelModal={() => setIsConfirmModalOpen(false)} | ||
handleCancelModalConfirm={handleCancelModalConfirm} | ||
> | ||
<Modal | ||
title={`${agencyTitle} Agency Definition`} | ||
description={ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the modal accept what's currently in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like the modal can accept There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha! Thank you for clarifying - let's keep it as is! |
||
<DataSourceContainer> | ||
<DataSourceQuestionWrapper> | ||
Indicate which of the following categories best defines your | ||
agency. Or, choose the{" "} | ||
<ChooseDefaultSettings | ||
onClick={() => | ||
setUpdatedSetting(getDefaultSetting(currentSystems)) | ||
} | ||
> | ||
Justice Counts definition. | ||
</ChooseDefaultSettings> | ||
</DataSourceQuestionWrapper> | ||
|
||
{currentSystems?.map((system) => { | ||
return ( | ||
<React.Fragment key={system}> | ||
{isSupervisionAgency && ( | ||
<DataSourceTitle> | ||
{removeSnakeCase(system).toLocaleLowerCase()} | ||
</DataSourceTitle> | ||
)} | ||
<CheckboxOptions | ||
options={[ | ||
...Object.entries(AgencyIncludesExcludes[system]).map( | ||
([key, mapObj]) => { | ||
mxosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const included = updatedSetting | ||
.find((sector) => sector.sector === system) | ||
?.settings.find( | ||
(setting) => setting.key === key | ||
)?.included; | ||
|
||
return { | ||
key, | ||
label: mapObj.label, | ||
checked: included === IncludesExcludesEnum.YES, | ||
}; | ||
} | ||
), | ||
]} | ||
onChange={({ key, checked }) => { | ||
setUpdatedSetting((prevSettings) => { | ||
const updates = prevSettings.map((sector) => { | ||
if (sector.sector === system) { | ||
return { | ||
...sector, | ||
settings: sector.settings.map((setting) => | ||
setting.key === key | ||
? { | ||
...setting, | ||
included: boolToYesNoEnum(!checked), | ||
} | ||
: setting | ||
), | ||
}; | ||
} | ||
return sector; | ||
}); | ||
|
||
return updates; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move this to a helper function just for readability that takes in the |
||
}); | ||
}} | ||
/> | ||
</React.Fragment> | ||
); | ||
})} | ||
<NewInput | ||
name="agency_definition_description" | ||
id="agency_definition_description" | ||
type="text" | ||
multiline | ||
placeholder="If the listed categories do not adequately describe your breakdown, please describe additional data elements included in your agency’s definition." | ||
value={descriptionValue} | ||
onChange={(e) => setDescriptionValue(e.target.value)} | ||
fullWidth | ||
/> | ||
</DataSourceContainer> | ||
} | ||
buttons={[ | ||
{ | ||
label: "Save", | ||
onClick: () => { | ||
handleSaveClick(); | ||
}, | ||
}, | ||
]} | ||
maxHeight={900} | ||
modalBackground="opaque" | ||
onClickClose={handleCancelClick} | ||
agencySettingsConfigs | ||
jurisdictionsSettingsConfigs | ||
agencySettingsAndJurisdictionsTitleConfigs | ||
customPadding="4px 40px 24px 40px" | ||
/> | ||
</AgencySettingsEditModeModal> | ||
)} | ||
|
||
<AgencySettingsBlock withBorder id="agency_definition"> | ||
<BasicInfoBlockTitle configured={isSettingConfigured}> | ||
{agencyTitle} Agency Definition | ||
{allowEdit && ( | ||
<EditButtonContainer> | ||
<Button | ||
label={<>Edit</>} | ||
onClick={() => { | ||
openSetting(); | ||
}} | ||
labelColor="blue" | ||
noSidePadding | ||
noHover | ||
/> | ||
</EditButtonContainer> | ||
)} | ||
</BasicInfoBlockTitle> | ||
<AgencyInfoBlockDescription> | ||
Information about how your agency is defined | ||
</AgencyInfoBlockDescription> | ||
</AgencySettingsBlock> | ||
</> | ||
); | ||
}; | ||
|
||
export default observer(AgencySettingsDefinition); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great call on this! |
||
|
||
const handleSaveClick = () => { | ||
if (supervisionSystemsToSave) { | ||
const updatedSystems = updateAgencySystems(supervisionSystemsToSave); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're getting the default settings, why are we also passing in a
defaultIncluded
override param?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When BE returns nothing we need to configure initial object, and since
included
is not an optional field we need to pass "NO" as initial value on the line 159. MaybeinitialIncluded
instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh I see - thank you for explaining! Hmm - how about
unconfiguredDefault
?