From 8980d24d112f6a40e045d73995796067d1fb1adb Mon Sep 17 00:00:00 2001 From: Ilya Date: Fri, 3 May 2024 17:51:31 +0300 Subject: [PATCH] [Publisher][Admin Panel] Add functionality to choose metrics/child agencies during metric copy (#1282) * Add functionality to choose metrics/child agencies during metric copy * [Publisher][Admin Panel] Add functionality to search metrics/child agencies by sectors (#1283) * Add functionality to search metrics/child agencies by sectors * Add sector to metric name * Fix default metrics key propagation * Fixes * Fixes * Fix naming * Block saving if there are no child agencies or metrics selected to copy * FIx disabling button behavior * Small disable save button logic refactor * Small re-refactor of save disabled - I gave bad advice --------- Co-authored-by: Mahmoud --- .../AdminPanel/AdminPanel.styles.tsx | 5 +- .../AdminPanel/AgencyProvisioning.tsx | 285 +++++++++++++++--- publisher/src/components/AdminPanel/types.ts | 14 + .../src/components/assets/close-icon.svg | 3 + publisher/src/stores/AdminPanelStore.ts | 41 ++- 5 files changed, 296 insertions(+), 52 deletions(-) create mode 100644 publisher/src/components/assets/close-icon.svg diff --git a/publisher/src/components/AdminPanel/AdminPanel.styles.tsx b/publisher/src/components/AdminPanel/AdminPanel.styles.tsx index 58867c29d..15c999d95 100644 --- a/publisher/src/components/AdminPanel/AdminPanel.styles.tsx +++ b/publisher/src/components/AdminPanel/AdminPanel.styles.tsx @@ -826,9 +826,8 @@ export const GraphicLines = styled.div<{ type?: SaveConfirmationType }>` export const WarningMessage = styled.div` ${typography.sizeCSS.small} - max-width: 350px; + max-width: 750px; color: ${palette.solid.red}; - margin-top: 5px; display: flex; flex-direction: column; align-items: center; @@ -836,7 +835,7 @@ export const WarningMessage = styled.div` border: 1px solid ${palette.solid.red}; border-radius: 4px; padding: 16px; - margin-left: 12px; + margin: 5px auto 16px; p { text-align: center; diff --git a/publisher/src/components/AdminPanel/AgencyProvisioning.tsx b/publisher/src/components/AdminPanel/AgencyProvisioning.tsx index 3914e9ebf..50583db3f 100644 --- a/publisher/src/components/AdminPanel/AgencyProvisioning.tsx +++ b/publisher/src/components/AdminPanel/AgencyProvisioning.tsx @@ -77,10 +77,12 @@ export const AgencyProvisioning: React.FC = observer( users, agencies, agenciesByID, + metrics, systems, agencyProvisioningUpdates, searchableCounties, searchableSystems, + searchableMetrics, csgAndRecidivizUsers, csgAndRecidivizDefaultRole, updateAgencyName, @@ -124,11 +126,20 @@ export const AgencyProvisioning: React.FC = observer( ? new Set(agencyProvisioningUpdates.child_agency_ids) : new Set() ); + const [selectedChildAgencyIDsToCopy, setSelectedChildAgencyIDsToCopy] = + useState>( + agencyProvisioningUpdates.child_agency_ids + ? new Set(agencyProvisioningUpdates.child_agency_ids) + : new Set() + ); const [selectedSystems, setSelectedSystems] = useState>( agencyProvisioningUpdates.systems ? new Set(agencyProvisioningUpdates.systems) : new Set() ); + const [selectedMetricsKeys, setSelectedMetricsKeys] = useState>( + new Set() + ); const [selectedTeamMembersToAdd, setSelectedTeamMembersToAdd] = useState< Set >(new Set()); @@ -174,9 +185,14 @@ export const AgencyProvisioning: React.FC = observer( ); /** A list of superagencies and child agencies to select from */ - const childAgencies = availableAgencies.filter( - (agency) => !agency.is_superagency - ); + const childAgencies = availableAgencies + .filter((agency) => !agency.is_superagency) + .map((agency) => ({ + ...agency, + sectors: agency.systems.map((system) => + removeSnakeCase(system.toLocaleLowerCase()) + ), + })); const superagencies = availableAgencies.filter( (agency) => agency.is_superagency ); @@ -288,7 +304,8 @@ export const AgencyProvisioning: React.FC = observer( String(agencyProvisioningUpdates.agency_id), agencyProvisioningUpdates.name, userStore.email, - ["ALL"] + Array.from(selectedMetricsKeys), + Array.from(selectedChildAgencyIDsToCopy).map((id) => String(id)) ); } @@ -474,26 +491,38 @@ export const AgencyProvisioning: React.FC = observer( Object.keys(teamMemberRoleUpdates).length > 0 || selectedTeamMembersToAdd.size > 0 || selectedTeamMembersToDelete.size > 0; + /** + * An update has been made when there are child agencies or metrics selected to copy + */ + const hasChildAgenciesCopyUpdates = selectedChildAgencyIDsToCopy.size > 0; + const hasMetricsCopyUpdates = selectedMetricsKeys.size > 0; /** * Saving is disabled if saving is in progress OR an existing agency has made no updates to either the name, state, * county, systems, dashboard enabled checkbox, superagency checkbox and child agencies, child agency's superagency * selection, and team member additions/deletions/role updates, or a newly created agency has no input for both name and state. */ - const isSaveDisabled = - !isCopySuperagencyMetricSettingsSelected && // Allows user to save if all they do is select that they want to copy superagency metric settings - (isSaveInProgress || + const hasCopySuperagencyMetricSettingsUpdates = + hasChildAgenciesCopyUpdates && hasMetricsCopyUpdates; + const hasAgencyInfoUpdates = + hasNameUpdate || + hasStateUpdate || + hasCountyUpdates || + hasSystemUpdates || + hasDashboardEnabledStatusUpdate || + hasIsSuperagencyUpdate || + hasChildAgencyUpdates || + hasSuperagencyUpdate || + hasTeamMemberOrRoleUpdates; + const hasRequiredCreateAgencyFields = + hasNameUpdate && hasStateUpdate && hasSystems; + + const isSaveDisabled = isCopySuperagencyMetricSettingsSelected + ? !hasCopySuperagencyMetricSettingsUpdates + : isSaveInProgress || !hasSystems || (selectedAgency - ? !hasNameUpdate && - !hasStateUpdate && - !hasCountyUpdates && - !hasSystemUpdates && - !hasDashboardEnabledStatusUpdate && - !hasIsSuperagencyUpdate && - !hasChildAgencyUpdates && - !hasSuperagencyUpdate && - !hasTeamMemberOrRoleUpdates - : !(hasNameUpdate && hasStateUpdate && hasSystems))); + ? !hasAgencyInfoUpdates + : !hasRequiredCreateAgencyFields); /** Automatically adds CSG and Recidiviz users to a newly created agency with the proper roles */ useEffect(() => { @@ -520,12 +549,19 @@ export const AgencyProvisioning: React.FC = observer( }; }); } + + if (isCopySuperagencyMetricSettingsSelected && selectedIDToEdit) { + adminPanelStore.fetchAgencyMetrics(String(selectedIDToEdit)); + } }, [ selectedAgency, adminPanelStore, api, csgAndRecidivizUsers, csgAndRecidivizDefaultRole, + secondaryCreatedId, + selectedIDToEdit, + isCopySuperagencyMetricSettingsSelected, ]); /** Here we are making the auto-adding if user was created via the secondary modal */ @@ -546,6 +582,18 @@ export const AgencyProvisioning: React.FC = observer( } }, [users, secondaryCreatedId, csgAndRecidivizDefaultRole]); + /** Here we are making the auto-selecting all metrics by default */ + useEffect(() => { + setSelectedMetricsKeys( + new Set(searchableMetrics.map((metric) => String(metric.id))) + ); + }, [searchableMetrics]); + + const selectedChildAgencies = childAgencies.filter((agency) => + selectedChildAgencyIDs.has(Number(agency.id)) + ); + const hasChildAgencyMetrics = metrics.length > 0; + return ( {showSaveConfirmation.show ? ( @@ -909,37 +957,178 @@ export const AgencyProvisioning: React.FC = observer( {hasSystems && hasChildAgencies && ( - - - setIsCopySuperagencyMetricSettingsSelected( - (prev) => !prev - ) - } - checked={isCopySuperagencyMetricSettingsSelected} - /> - - {isCopySuperagencyMetricSettingsSelected && ( - - -

- WARNING! This action cannot be undone. This - will OVERWRITE all metric settings in all - child agencies. After clicking{" "} - Save, the copying process - will begin and you will receive an email - confirmation once the metrics settings have - been copied over. -

-
- )} -
+ <> + + { + setIsCopySuperagencyMetricSettingsSelected( + (prev) => !prev + ); + }} + checked={ + isCopySuperagencyMetricSettingsSelected + } + /> + + + {isCopySuperagencyMetricSettingsSelected && + hasChildAgencyMetrics && ( + <> + + +

+ WARNING! This action cannot be undone. + This will OVERWRITE metric settings in + child agencies. After clicking{" "} + Save, the copying process + will begin and you will receive an email + confirmation once the metrics settings + have been copied over. +

+
+ + {showSelectionBox === + SelectionInputBoxTypes.COPY_CHILD_AGENCIES && ( + +agency.id + ) + ) + )} + updateSelections={({ id }) => { + setSelectedChildAgencyIDsToCopy( + (prev) => + toggleAddRemoveSetItem(prev, +id) + ); + }} + searchByKeys={["name", "sectors"]} + metadata={{ + listBoxLabel: + "Select child agencies to copy", + searchBoxLabel: "Search agencies", + }} + isActiveBox={ + showSelectionBox === + SelectionInputBoxTypes.COPY_CHILD_AGENCIES + } + /> + )} + { + setShowSelectionBox( + SelectionInputBoxTypes.COPY_CHILD_AGENCIES + ); + }} + fitContentHeight + hoverable + > + {selectedChildAgencyIDsToCopy.size === + 0 ? ( + + No child agencies selected to copy + + ) : ( + Array.from( + selectedChildAgencyIDsToCopy + ).map((agencyID) => ( + + {agenciesByID[agencyID]?.[0].name} + + )) + )} + + + Child agencies to copy + + + + + {showSelectionBox === + SelectionInputBoxTypes.COPY_AGENCY_METRICS && ( + + String(metric.id) + ) + ) + )} + updateSelections={({ id }) => { + setSelectedMetricsKeys((prev) => + toggleAddRemoveSetItem( + prev, + String(id) + ) + ); + }} + searchByKeys={["name", "sectors"]} + metadata={{ + listBoxLabel: + "Select metrics to copy", + searchBoxLabel: "Search metrics", + }} + isActiveBox={ + showSelectionBox === + SelectionInputBoxTypes.COPY_AGENCY_METRICS + } + /> + )} + { + setShowSelectionBox( + SelectionInputBoxTypes.COPY_AGENCY_METRICS + ); + }} + fitContentHeight + hoverable + > + {selectedMetricsKeys.size === 0 ? ( + + No metrics selected to copy + + ) : ( + Array.from(selectedMetricsKeys).map( + (metricKey) => ( + + { + searchableMetrics.find( + (metric) => + metric.id === metricKey + )?.name + } + + ) + ) + )} + + + Metrics to copy + + + + )} + )} )} diff --git a/publisher/src/components/AdminPanel/types.ts b/publisher/src/components/AdminPanel/types.ts index 892209a33..5dc9f0fbd 100644 --- a/publisher/src/components/AdminPanel/types.ts +++ b/publisher/src/components/AdminPanel/types.ts @@ -58,6 +58,8 @@ export enum SelectionInputBoxTypes { SYSTEMS = "SYSTEMS", SUPERAGENCY = "SUPERAGENCY", CHILD_AGENCIES = "CHILD AGENCIES", + COPY_CHILD_AGENCIES = "COPY CHILD AGENCIES", + COPY_AGENCY_METRICS = "COPY AGENCY METRICS", } export type SelectionInputBoxType = `${SelectionInputBoxTypes}`; @@ -86,6 +88,12 @@ export type Agency = { }[]; }; +export type AgencyMetric = { + key: string; + name: string; + sector: string; +}; + export type AgencyWithTeamByID = Omit & { team: Record; }; @@ -95,6 +103,11 @@ export type AgencyResponse = { systems: AgencySystem[]; }; +export type AgencyMetricResponse = { + agency: Agency; + metrics: AgencyMetric[]; +}; + export const AgencyProvisioningSettings = { AGENCY_INFORMATION: "Agency Information", TEAM_MEMBERS_ROLES: "Team Members & Roles", @@ -179,6 +192,7 @@ export type InteractiveSearchListUpdateSelections = ( export type SearchableListItem = { id: string | number; name: string; + sectors?: string | string[]; action?: InteractiveSearchListAction; email?: string; role?: AgencyTeamMemberRole; diff --git a/publisher/src/components/assets/close-icon.svg b/publisher/src/components/assets/close-icon.svg new file mode 100644 index 000000000..01deb4d57 --- /dev/null +++ b/publisher/src/components/assets/close-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/publisher/src/stores/AdminPanelStore.ts b/publisher/src/stores/AdminPanelStore.ts index 5e10d7620..28245aed4 100644 --- a/publisher/src/stores/AdminPanelStore.ts +++ b/publisher/src/stores/AdminPanelStore.ts @@ -17,6 +17,7 @@ import { AgencySystem, + AgencySystems, AgencyTeamMember, AgencyTeamMemberRole, } from "@justice-counts/common/types"; @@ -25,6 +26,8 @@ import { makeAutoObservable, runInAction } from "mobx"; import { Agency, + AgencyMetric, + AgencyMetricResponse, AgencyProvisioningUpdates, AgencyResponse, AgencyTeamUpdates, @@ -76,6 +79,8 @@ class AdminPanelStore { systems: AgencySystem[]; + metrics: AgencyMetric[]; + userProvisioningUpdates: UserProvisioningUpdates; agencyProvisioningUpdates: AgencyProvisioningUpdates; @@ -91,6 +96,7 @@ class AdminPanelStore { this.usersByID = {}; this.agenciesByID = {}; this.systems = []; + this.metrics = []; this.userProvisioningUpdates = initialEmptyUserProvisioningUpdates; this.agencyProvisioningUpdates = initialEmptyAgencyProvisioningUpdates; } @@ -136,6 +142,17 @@ class AdminPanelStore { })); } + get searchableMetrics(): SearchableListItem[] { + return this.metrics + .filter((metric) => metric.sector !== AgencySystems.SUPERAGENCY) + .map((metric) => ({ + ...metric, + id: metric.key, + sectors: metric.sector, + name: `${metric.name}: ${metric.sector.toLocaleLowerCase()}`, + })); + } + /** Returns a list of searchable counties based on the currently selected `state_code` in `agencyProvisioningUpdates` */ get searchableCounties(): SearchableListItem[] { if (!this.agencyProvisioningUpdates.state_code) return []; @@ -224,6 +241,26 @@ class AdminPanelStore { } } + async fetchAgencyMetrics(agencyID: string) { + try { + const response = (await this.api.request({ + path: `/admin/agency/${agencyID}`, + method: "GET", + })) as Response; + const data = (await response.json()) as AgencyMetricResponse; + + if (response.status !== 200) { + throw new Error("There was an issue fetching agency metrics."); + } + + runInAction(() => { + this.metrics = data.metrics; + }); + } catch (error) { + if (error instanceof Error) return new Error(error.message); + } + } + async fetchUsersAndAgencies() { await Promise.all([this.fetchUsers(), this.fetchAgencies()]); runInAction(() => { @@ -235,7 +272,8 @@ class AdminPanelStore { superagencyID: string, agencyName: string, userEmail: string, - metricDefinitionKeySubset: string[] // A list of metric definition keys for future use to update a subset of metrics + metricDefinitionKeySubset: string[], // A list of metric definition keys for future use to update a subset of metrics + childAgencyIdSubset: string[] // A list of child agencies ids for future use to update a subset of metrics ) { try { const response = (await this.api.request({ @@ -245,6 +283,7 @@ class AdminPanelStore { agency_name: agencyName, user_email: userEmail, metric_definition_key_subset: metricDefinitionKeySubset, + child_agency_id_subset: childAgencyIdSubset, }, })) as Response;