diff --git a/publisher/src/App.tsx b/publisher/src/App.tsx index a46a5a721..6b1c85e00 100644 --- a/publisher/src/App.tsx +++ b/publisher/src/App.tsx @@ -23,6 +23,7 @@ import { trackNavigation } from "./analytics"; import { DataUpload } from "./components/DataUpload"; import { PageWrapper } from "./components/Forms"; import Header from "./components/Header"; +import { MetricsView } from "./components/MetricConfiguration/MetricsView"; import CreateReports from "./components/Reports/CreateReport"; import ReportDataEntry from "./components/Reports/ReportDataEntry"; import ReviewMetrics from "./components/ReviewMetrics/ReviewMetrics"; @@ -42,6 +43,7 @@ const App: React.FC = (): ReactElement => { } /> + } /> } /> } /> } /> diff --git a/publisher/src/components/DataViz/DatapointsView.styles.tsx b/publisher/src/components/DataViz/DatapointsView.styles.tsx index 8c3fd8d9c..1f3362808 100644 --- a/publisher/src/components/DataViz/DatapointsView.styles.tsx +++ b/publisher/src/components/DataViz/DatapointsView.styles.tsx @@ -37,6 +37,7 @@ export const DatapointsViewControlsContainer = styled.div` display: flex; flex-direction: row; border-bottom: 1px solid ${palette.highlight.grey9}; + border-top: 1px solid ${palette.highlight.grey9}; `; const DatapointsViewDropdown = styled(Dropdown)` diff --git a/publisher/src/components/Forms/NotReportedIcon.tsx b/publisher/src/components/Forms/NotReportedIcon.tsx index a16284ef2..0250e4267 100644 --- a/publisher/src/components/Forms/NotReportedIcon.tsx +++ b/publisher/src/components/Forms/NotReportedIcon.tsx @@ -114,8 +114,8 @@ export const NotReportedIcon: React.FC<{ This has been disabled by an admin because the data is unavailable.{" "} If you have the data for this, consider changing the configuration in the{" "} - navigate("/metrics")}> - Metrics View + navigate("/settings")}> + Settings . diff --git a/publisher/src/components/Menu/Menu.tsx b/publisher/src/components/Menu/Menu.tsx index d31816fc1..06d07dd15 100644 --- a/publisher/src/components/Menu/Menu.tsx +++ b/publisher/src/components/Menu/Menu.tsx @@ -37,6 +37,7 @@ enum MenuItems { LearnMore = "LEARN MORE", Settings = "SETTINGS", Agencies = "AGENCIES", + Data = "DATA", } const Menu = () => { @@ -76,6 +77,8 @@ const Menu = () => { setActiveMenuItem(MenuItems.CreateReport); } else if (location.pathname === "/settings") { setActiveMenuItem(MenuItems.Settings); + } else if (location.pathname === "/data") { + setActiveMenuItem(MenuItems.Data); } else { setActiveMenuItem(undefined); } @@ -97,6 +100,14 @@ const Menu = () => { Reports + {/* Data (Visualizations) */} + navigate("/data")} + active={activeMenuItem === MenuItems.Data} + > + Data + + {/* Learn More */} ` } `; +export const MetricViewBoxContainer = styled(MetricBoxContainer)<{ + selected?: boolean; +}>` + max-width: 100%; + min-height: 50px; + border: ${({ selected }) => selected && `1px solid ${palette.solid.blue}`}; + margin-bottom: 5px; +`; + export const MetricBoxWrapper = styled.div` display: flex; `; @@ -88,6 +104,7 @@ export const ActiveMetricSettingHeader = styled.div` z-index: 1; background: ${palette.solid.white}; padding: 10px 15px 0 15px; + margin-bottom: 20px; `; export const MetricNameBadgeToggleWrapper = styled.div` diff --git a/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx b/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx index 47a7c00f6..8b2e75de1 100644 --- a/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx +++ b/publisher/src/components/MetricConfiguration/MetricConfiguration.tsx @@ -70,7 +70,7 @@ export type MetricSettings = { }[]; }; -type MetricSettingsObj = { +export type MetricSettingsObj = { [key: string]: MetricType; }; diff --git a/publisher/src/components/MetricConfiguration/MetricsView.tsx b/publisher/src/components/MetricConfiguration/MetricsView.tsx new file mode 100644 index 000000000..170679ee5 --- /dev/null +++ b/publisher/src/components/MetricConfiguration/MetricsView.tsx @@ -0,0 +1,265 @@ +// 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 { reaction, when } from "mobx"; +import { observer } from "mobx-react-lite"; +import React, { useEffect, useState } from "react"; + +import { Metric, ReportFrequency } from "../../shared/types"; +import { useStore } from "../../stores"; +import { removeSnakeCase } from "../../utils"; +import { Badge, BadgeColorMapping } from "../Badge"; +import ConnectedDatapointsView from "../DataViz/DatapointsView"; +import { NotReportedIcon } from "../Forms"; +import { Loading } from "../Loading"; +import { PageTitle, TabbedBar, TabbedItem, TabbedOptions } from "../Reports"; +import { + ActiveMetricSettingHeader, + MetricBoxWrapper, + MetricDescription, + MetricName, + MetricNameBadgeToggleWrapper, + MetricNameBadgeWrapper, + MetricSettingsObj, + MetricsViewContainer, + MetricsViewControlPanelOverflowHidden, + MetricViewBoxContainer, + PanelContainerLeft, + PanelContainerRight, +} from "."; + +type MetricBoxProps = { + metricKey: string; + displayName: string; + frequency: ReportFrequency; + description: string; + enabled?: boolean; + activeMetricKey: string; + setActiveMetricKey: React.Dispatch>; +}; + +const reportFrequencyBadgeColors: BadgeColorMapping = { + ANNUAL: "ORANGE", + MONTHLY: "GREEN", +}; + +const MetricBox: React.FC = ({ + metricKey, + displayName, + frequency, + description, + enabled, + activeMetricKey, + setActiveMetricKey, +}): JSX.Element => { + return ( + setActiveMetricKey(metricKey)} + enabled={enabled} + selected={metricKey === activeMetricKey} + > + + + {displayName} + + {frequency} + + + + {!enabled && } + + + {description} + + ); +}; + +export const MetricsView: React.FC = observer(() => { + const { reportStore, userStore, datapointsStore } = useStore(); + + const [activeMetricFilter, setActiveMetricFilter] = useState(); + const [isLoading, setIsLoading] = useState(true); + const [loadingError, setLoadingError] = useState( + undefined + ); + const [activeMetricKey, setActiveMetricKey] = useState(""); + const [metricSettings, setMetricSettings] = useState<{ + [key: string]: Metric; + }>({}); + + const filteredMetricSettings: MetricSettingsObj = Object.values( + metricSettings + ) + .filter( + (metric) => + metric.system.toLowerCase() === activeMetricFilter?.toLowerCase() + ) + ?.reduce((res: MetricSettingsObj, metric) => { + res[metric.key] = metric; + return res; + }, {}); + + const fetchAndSetReportSettings = async () => { + const response = (await reportStore.getReportSettings()) as + | Response + | Error; + + setIsLoading(false); + + if (response instanceof Error) { + return setLoadingError(response.message); + } + + const reportSettings = (await response.json()) as Metric[]; + const metricKeyToMetricMap: { [key: string]: Metric } = {}; + + reportSettings?.forEach((metric) => { + metricKeyToMetricMap[metric.key] = metric; + }); + + setMetricSettings(metricKeyToMetricMap); + setActiveMetricKey(Object.keys(metricKeyToMetricMap)[0]); + }; + + useEffect( + () => + // return when's disposer so it is cleaned up if it never runs + when( + () => userStore.userInfoLoaded, + async () => { + fetchAndSetReportSettings(); + datapointsStore.getDatapoints(); + setActiveMetricFilter( + removeSnakeCase(userStore.currentAgency?.systems[0] as string) + ); + } + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + // reload report overviews when the current agency ID changes + useEffect( + () => + // return disposer so it is cleaned up if it never runs + reaction( + () => userStore.currentAgencyId, + async (currentAgencyId, previousAgencyId) => { + // prevents us from calling getDatapoints twice on initial load + if (previousAgencyId !== undefined) { + setIsLoading(true); + fetchAndSetReportSettings(); + await datapointsStore.getDatapoints(); + setActiveMetricFilter( + removeSnakeCase(userStore.currentAgency?.systems[0] as string) + ); + } + } + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [userStore] + ); + + useEffect( + () => { + setActiveMetricKey(Object.keys(filteredMetricSettings)[0]); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [activeMetricFilter] + ); + + if (isLoading) { + return ; + } + + if (!metricSettings[activeMetricKey]) { + return
Error: {loadingError}
; + } + + return ( + <> + + Metrics + + + + {userStore.currentAgency?.systems.map((filterOption) => ( + + setActiveMetricFilter(removeSnakeCase(filterOption)) + } + capitalize + > + {removeSnakeCase(filterOption.toLowerCase())} + + ))} + + + + + {/* List Of Metrics */} + + {filteredMetricSettings && + Object.values(filteredMetricSettings).map((metric) => ( + + + + ))} + + + {/* Data Visualization */} + + + + + {filteredMetricSettings[activeMetricKey]?.display_name} + + {filteredMetricSettings[activeMetricKey]?.frequency && ( + + {filteredMetricSettings[activeMetricKey].frequency} + + )} + + + + + + + + + ); +});