diff --git a/src/webapp/contexts/CurrentEventTrackerProvider.tsx b/src/webapp/contexts/CurrentEventTrackerProvider.tsx index 2426452c..47af0b48 100644 --- a/src/webapp/contexts/CurrentEventTrackerProvider.tsx +++ b/src/webapp/contexts/CurrentEventTrackerProvider.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, useState } from "react"; +import { PropsWithChildren, useCallback, useState } from "react"; import { CurrentEventTrackerContext } from "./current-event-tracker-context"; import { DiseaseOutbreakEvent } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { Maybe } from "../../utils/ts-utils"; @@ -6,16 +6,16 @@ import { Maybe } from "../../utils/ts-utils"; export const CurrentEventTrackerContextProvider: React.FC = ({ children }) => { const [currentEventTracker, setCurrentEventTracker] = useState(); - const changeCurrentEventTracker = (EventTrackerDetails: DiseaseOutbreakEvent) => { + const changeCurrentEventTracker = useCallback((EventTrackerDetails: DiseaseOutbreakEvent) => { setCurrentEventTracker(EventTrackerDetails); localStorage.setItem("currentEventTracker", JSON.stringify(EventTrackerDetails)); - }; - const resetCurrentEventTracker = () => { + }, []); + const resetCurrentEventTracker = useCallback(() => { setCurrentEventTracker(undefined); localStorage.removeItem("currentEventTracker"); - }; + }, []); - const getCurrentEventTracker = (): Maybe => { + const getCurrentEventTracker = useCallback((): Maybe => { if (currentEventTracker) { return currentEventTracker; } @@ -24,7 +24,7 @@ export const CurrentEventTrackerContextProvider: React.FC = ( return JSON.parse(localCurrentEventTracker); } return undefined; - }; + }, [currentEventTracker]); return ( = ({ children }) => { + const [existingEventTrackerTypes, setExistingEventTrackerTypes] = useState< + (DiseaseNames | HazardNames)[] + >([]); + + const changeExistingEventTrackerTypes = useCallback( + (updatedExistingEventTrackerTypes: (DiseaseNames | HazardNames)[]) => { + setExistingEventTrackerTypes(updatedExistingEventTrackerTypes); + }, + [] + ); + + return ( + + {children} + + ); +}; diff --git a/src/webapp/contexts/existing-event-tracker-types-context.ts b/src/webapp/contexts/existing-event-tracker-types-context.ts new file mode 100644 index 00000000..fcf0bf92 --- /dev/null +++ b/src/webapp/contexts/existing-event-tracker-types-context.ts @@ -0,0 +1,26 @@ +import { createContext, useContext } from "react"; +import { + DiseaseNames, + HazardNames, +} from "../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; + +export interface ExistingEventTrackerTypesProps { + existingEventTrackerTypes: (DiseaseNames | HazardNames)[]; + changeExistingEventTrackerTypes: ( + existingEventTrackerTypes: (DiseaseNames | HazardNames)[] + ) => void; +} + +export const ExistingEventTrackerTypesContext = createContext({ + existingEventTrackerTypes: [], + changeExistingEventTrackerTypes: () => {}, +}); + +export function useExistingEventTrackerTypes() { + const context = useContext(ExistingEventTrackerTypesContext); + if (context) { + return context; + } else { + throw new Error("Existing Event Tracker Types context uninitialized"); + } +} diff --git a/src/webapp/pages/app/App.tsx b/src/webapp/pages/app/App.tsx index 8bc29591..4d39c89a 100644 --- a/src/webapp/pages/app/App.tsx +++ b/src/webapp/pages/app/App.tsx @@ -17,6 +17,7 @@ import { HeaderBar } from "../../components/layout/header-bar/HeaderBar"; import { D2Api } from "../../../types/d2-api"; import "./App.css"; import { CurrentEventTrackerContextProvider } from "../../contexts/CurrentEventTrackerProvider"; +import { ExistingEventTrackerTypesProvider } from "../../contexts/ExistingEventTrackerTypes"; export interface AppProps { compositionRoot: CompositionRoot; @@ -73,7 +74,9 @@ function App(props: AppProps) {
- + + +
diff --git a/src/webapp/pages/dashboard/usePerformanceOverview.ts b/src/webapp/pages/dashboard/usePerformanceOverview.ts index 44dddddf..4cc227aa 100644 --- a/src/webapp/pages/dashboard/usePerformanceOverview.ts +++ b/src/webapp/pages/dashboard/usePerformanceOverview.ts @@ -5,6 +5,7 @@ import { FiltersConfig, TableColumn } from "../../components/table/statistic-tab import { Maybe } from "../../../utils/ts-utils"; import { PerformanceOverviewMetrics } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; import { NationalIncidentStatus } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; type State = { columns: TableColumn[]; @@ -27,28 +28,7 @@ export function usePerformanceOverview(): State { >([]); const [isLoading, setIsLoading] = useState(false); const [order, setOrder] = useState(); - - useEffect(() => { - if (dataPerformanceOverview.length && order) { - setDataPerformanceOverview( - (prevDataPerformanceOverview: PerformanceOverviewMetrics[]) => { - const newDataPerformanceOverview = _(prevDataPerformanceOverview) - .orderBy([ - [ - item => - Number.isNaN(Number(item[order.name])) - ? item[order.name] - : Number(item[order.name]), - order.direction, - ], - ]) - .toArray(); - - return newDataPerformanceOverview; - } - ); - } - }, [order, dataPerformanceOverview]); + const { changeExistingEventTrackerTypes } = useExistingEventTrackerTypes(); const getNationalIncidentStatusString = useCallback((status: string): string => { switch (status as NationalIncidentStatus) { @@ -77,12 +57,39 @@ export function usePerformanceOverview(): State { }, [getNationalIncidentStatusString] ); + + useEffect(() => { + if (dataPerformanceOverview.length && order) { + setDataPerformanceOverview( + (prevDataPerformanceOverview: PerformanceOverviewMetrics[]) => { + const newDataPerformanceOverview = _(prevDataPerformanceOverview) + .orderBy([ + [ + item => + Number.isNaN(Number(item[order.name])) + ? item[order.name] + : Number(item[order.name]), + order.direction, + ], + ]) + .toArray(); + + return newDataPerformanceOverview; + } + ); + } + }, [order, dataPerformanceOverview]); + useEffect(() => { setIsLoading(true); compositionRoot.performanceOverview.getPerformanceOverviewMetrics.execute().run( - programIndicators => { - const mappedData = programIndicators.map((data: PerformanceOverviewMetrics) => - mapEntityToTableData(data) + performanceOverviewMetrics => { + const existingEventTrackerTypes = performanceOverviewMetrics.map( + metric => metric.suspectedDisease || metric.hazardType + ); + changeExistingEventTrackerTypes(existingEventTrackerTypes); + const mappedData = performanceOverviewMetrics.map( + (data: PerformanceOverviewMetrics) => mapEntityToTableData(data) ); setDataPerformanceOverview(mappedData); setIsLoading(false); @@ -92,7 +99,11 @@ export function usePerformanceOverview(): State { setIsLoading(false); } ); - }, [compositionRoot.performanceOverview.getPerformanceOverviewMetrics, mapEntityToTableData]); + }, [ + changeExistingEventTrackerTypes, + compositionRoot.performanceOverview.getPerformanceOverviewMetrics, + mapEntityToTableData, + ]); const columns: TableColumn[] = [ { label: "Event", value: "event" }, diff --git a/src/webapp/pages/event-tracker/EventTrackerPage.tsx b/src/webapp/pages/event-tracker/EventTrackerPage.tsx index fb068730..9265a439 100644 --- a/src/webapp/pages/event-tracker/EventTrackerPage.tsx +++ b/src/webapp/pages/event-tracker/EventTrackerPage.tsx @@ -45,8 +45,7 @@ export const EventTrackerPage: React.FC = React.memo(() => { const { goTo } = useRoutes(); const { formSummary, summaryError, riskAssessmentRows, eventTrackerDetails } = useDiseaseOutbreakEvent(id); - const { changeCurrentEventTracker: changeCurrentEventTrackerId, getCurrentEventTracker } = - useCurrentEventTracker(); + const { changeCurrentEventTracker, getCurrentEventTracker } = useCurrentEventTracker(); const currentEventTracker = getCurrentEventTracker(); const { lastAnalyticsRuntime } = useLastAnalyticsRuntime(); @@ -67,9 +66,9 @@ export const EventTrackerPage: React.FC = React.memo(() => { useEffect(() => { if (eventTrackerDetails) { - changeCurrentEventTrackerId(eventTrackerDetails); + changeCurrentEventTracker(eventTrackerDetails); } - }, [changeCurrentEventTrackerId, eventTrackerDetails]); + }, [changeCurrentEventTracker, eventTrackerDetails]); return ( diff --git a/src/webapp/pages/form-page/disease-outbreak-event/mapDiseaseOutbreakEventToInitialFormState.ts b/src/webapp/pages/form-page/disease-outbreak-event/mapDiseaseOutbreakEventToInitialFormState.ts index a40b9381..999e73c4 100644 --- a/src/webapp/pages/form-page/disease-outbreak-event/mapDiseaseOutbreakEventToInitialFormState.ts +++ b/src/webapp/pages/form-page/disease-outbreak-event/mapDiseaseOutbreakEventToInitialFormState.ts @@ -8,6 +8,10 @@ import { FormState } from "../../../components/form/FormState"; import { User } from "../../../components/user-selector/UserSelector"; import { Option as PresentationOption } from "../../../components/utils/option"; import { mapToPresentationOptions } from "../mapEntityToFormState"; +import { + DiseaseNames, + HazardNames, +} from "../../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; export const diseaseOutbreakEventFieldIds = { name: "name", @@ -83,7 +87,8 @@ type ResponseActionsSubsectionKeys = // TODO: Thinking for the future about generate this FormState by iterating over Object.Keys(diseaseOutbreakEvent) export function mapDiseaseOutbreakEventToInitialFormState( diseaseOutbreakEventWithOptions: DiseaseOutbreakEventFormData, - editMode: boolean + editMode: boolean, + existingEventTrackerTypes: (DiseaseNames | HazardNames)[] ): FormState { const { entity: diseaseOutbreakEvent, options } = diseaseOutbreakEventWithOptions; const { @@ -96,13 +101,21 @@ export function mapDiseaseOutbreakEventToInitialFormState( incidentStatus, } = options; - const teamMemberOptions: User[] = incidentManagers.map(tm => mapTeamMemberToUser(tm)); + //If An Event Tracker has already been created for a given suspected disease or harzd type, + //then do not allow to create another one. Remove it from dropwdown options + const filteredHazardTypes = hazardTypes.filter(hazardType => { + return !existingEventTrackerTypes.includes(hazardType.name as HazardNames); + }); + const filteredSuspectedDiseases = suspectedDiseases.filter(suspectedDisease => { + return !existingEventTrackerTypes.includes(suspectedDisease.name as DiseaseNames); + }); + const teamMemberOptions: User[] = incidentManagers.map(tm => mapTeamMemberToUser(tm)); const dataSourcesOptions: PresentationOption[] = mapToPresentationOptions(dataSources); - const hazardTypesOptions: PresentationOption[] = mapToPresentationOptions(hazardTypes); + const hazardTypesOptions: PresentationOption[] = mapToPresentationOptions(filteredHazardTypes); const mainSyndromesOptions: PresentationOption[] = mapToPresentationOptions(mainSyndromes); const suspectedDiseasesOptions: PresentationOption[] = - mapToPresentationOptions(suspectedDiseases); + mapToPresentationOptions(filteredSuspectedDiseases); const notificationSourcesOptions: PresentationOption[] = mapToPresentationOptions(notificationSources); const incidentStatusOptions: PresentationOption[] = mapToPresentationOptions(incidentStatus); diff --git a/src/webapp/pages/form-page/mapEntityToFormState.ts b/src/webapp/pages/form-page/mapEntityToFormState.ts index fc9ce2be..1513f837 100644 --- a/src/webapp/pages/form-page/mapEntityToFormState.ts +++ b/src/webapp/pages/form-page/mapEntityToFormState.ts @@ -1,4 +1,8 @@ import { ConfigurableForm } from "../../../domain/entities/ConfigurableForm"; +import { + DiseaseNames, + HazardNames, +} from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; import { TeamMember } from "../../../domain/entities/incident-management-team/TeamMember"; import { Option } from "../../../domain/entities/Ref"; import { FormState } from "../../components/form/FormState"; @@ -18,11 +22,16 @@ import { export function mapEntityToFormState( configurableForm: ConfigurableForm, - editMode?: boolean + editMode?: boolean, + existingEventTrackerTypes?: (DiseaseNames | HazardNames)[] ): FormState { switch (configurableForm.type) { case "disease-outbreak-event": - return mapDiseaseOutbreakEventToInitialFormState(configurableForm, editMode ?? false); + return mapDiseaseOutbreakEventToInitialFormState( + configurableForm, + editMode ?? false, + existingEventTrackerTypes ?? [] + ); case "risk-assessment-grading": return mapRiskGradingToInitialFormState(configurableForm); case "risk-assessment-summary": diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index a9271414..bbe752e0 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -20,6 +20,7 @@ import { addNewResponseActionSection, getAnotherResponseActionSection, } from "./incident-action/mapIncidentActionToInitialFormState"; +import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; export type GlobalMessage = { text: string; @@ -65,6 +66,7 @@ export function useForm(formType: FormType, id?: Id): State { const [formLabels, setFormLabels] = useState(); const [isLoading, setIsLoading] = useState(false); const currentEventTracker = getCurrentEventTracker(); + const { existingEventTrackerTypes } = useExistingEventTrackerTypes(); useEffect(() => { compositionRoot.getConfigurableForm @@ -75,7 +77,7 @@ export function useForm(formType: FormType, id?: Id): State { setFormLabels(formData.labels); setFormState({ kind: "loaded", - data: mapEntityToFormState(formData, !!id), + data: mapEntityToFormState(formData, !!id, existingEventTrackerTypes), }); }, error => { @@ -89,7 +91,7 @@ export function useForm(formType: FormType, id?: Id): State { }); } ); - }, [compositionRoot.getConfigurableForm, formType, id, currentEventTracker, configurations]); + }, [compositionRoot.getConfigurableForm, formType, id, currentEventTracker, configurations, existingEventTrackerTypes]); const handleAddNew = useCallback(() => { if (formState.kind !== "loaded" || !configurableForm) return;