diff --git a/i18n/en.pot b/i18n/en.pot index 495e3a38..767d0315 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-09-30T07:39:32.843Z\n" -"PO-Revision-Date: 2024-09-30T07:39:32.843Z\n" +"POT-Creation-Date: 2024-10-06T18:13:02.378Z\n" +"PO-Revision-Date: 2024-10-06T18:13:02.378Z\n" msgid "Low" msgstr "" @@ -90,9 +90,6 @@ msgstr "" msgid "Notes" msgstr "" -msgid "Notes" -msgstr "" - msgid "Create Event" msgstr "" @@ -138,6 +135,9 @@ msgstr "" msgid "7-1-7 performance" msgstr "" +msgid "events" +msgstr "" + msgid "Performance overview" msgstr "" @@ -153,6 +153,12 @@ msgstr "" msgid "Add new Assessment" msgstr "" +msgid "Risk assessment incomplete" +msgstr "" + +msgid "Risks associated with this event have not yet been assessed." +msgstr "" + msgid "N/A" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index e9452c28..1afbac47 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2024-09-12T14:10:04.460Z\n" +"POT-Creation-Date: 2024-10-06T18:13:02.378Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -86,6 +86,9 @@ msgstr "" msgid "Edit Details" msgstr "" +msgid "Notes" +msgstr "" + msgid "Create Event" msgstr "" @@ -131,6 +134,9 @@ msgstr "" msgid "7-1-7 performance" msgstr "" +msgid "events" +msgstr "" + msgid "Performance overview" msgstr "" @@ -146,6 +152,12 @@ msgstr "" msgid "Add new Assessment" msgstr "" +msgid "Risk assessment incomplete" +msgstr "" + +msgid "Risks associated with this event have not yet been assessed." +msgstr "" + msgid "N/A" msgstr "" diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index 39bed03e..5f4a4246 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -21,6 +21,7 @@ import { GetAllDiseaseOutbreaksUseCase } from "./domain/usecases/GetAllDiseaseOu import { MapDiseaseOutbreakToAlertsUseCase } from "./domain/usecases/MapDiseaseOutbreakToAlertsUseCase"; import { AlertRepository } from "./domain/repositories/AlertRepository"; import { AlertTestRepository } from "./data/repositories/test/AlertTestRepository"; +import { Get717PerformanceUseCase } from "./domain/usecases/Get717PerformanceUseCase"; import { GetEntityWithOptionsUseCase } from "./domain/usecases/GetEntityWithOptionsUseCase"; import { SaveEntityUseCase } from "./domain/usecases/SaveEntityUseCase"; import { RiskAssessmentRepository } from "./domain/repositories/RiskAssessmentRepository"; @@ -41,6 +42,15 @@ import { AlertSyncDataStoreTestRepository } from "./data/repositories/test/Alert import { AlertSyncRepository } from "./domain/repositories/AlertSyncRepository"; import { DataStoreClient } from "./data/DataStoreClient"; import { GetTotalCardCountsUseCase } from "./domain/usecases/GetTotalCardCountsUseCase"; +import { ChartConfigRepository } from "./domain/repositories/ChartConfigRepository"; +import { GetChartConfigByTypeUseCase } from "./domain/usecases/GetChartConfigByTypeUseCase"; +import { ChartConfigTestRepository } from "./data/repositories/test/ChartConfigTestRepository"; +import { ChartConfigD2Repository } from "./data/repositories/ChartConfigD2Repository"; +import { GetAnalyticsRuntimeUseCase } from "./domain/usecases/GetAnalyticsRuntimeUseCase"; +import { SystemRepository } from "./domain/repositories/SystemRepository"; +import { SystemD2Repository } from "./data/repositories/SystemD2Repository"; +import { SystemTestRepository } from "./data/repositories/test/SystemTestRepository"; +import { GetOverviewCardsUseCase } from "./domain/usecases/GetOverviewCardsUseCase"; export type CompositionRoot = ReturnType; @@ -55,6 +65,8 @@ type Repositories = { riskAssessmentRepository: RiskAssessmentRepository; mapConfigRepository: MapConfigRepository; performanceOverviewRepository: PerformanceOverviewRepository; + chartConfigRepository: ChartConfigRepository; + systemRepository: SystemRepository; }; function getCompositionRoot(repositories: Repositories) { @@ -81,6 +93,11 @@ function getCompositionRoot(repositories: Repositories) { repositories ), getTotalCardCounts: new GetTotalCardCountsUseCase(repositories), + get717Performance: new Get717PerformanceUseCase(repositories), + getAnalyticsRuntime: new GetAnalyticsRuntimeUseCase(repositories), + getOverviewCards: new GetOverviewCardsUseCase( + repositories.performanceOverviewRepository + ), }, maps: { getConfig: new GetMapConfigUseCase(repositories.mapConfigRepository), @@ -89,6 +106,9 @@ function getCompositionRoot(repositories: Repositories) { getAll: new GetAllOrgUnitsUseCase(repositories.orgUnitRepository), getProvinces: new GetProvincesOrgUnits(repositories.orgUnitRepository), }, + charts: { + getCases: new GetChartConfigByTypeUseCase(repositories.chartConfigRepository), + }, }; } @@ -105,6 +125,8 @@ export function getWebappCompositionRoot(api: D2Api) { riskAssessmentRepository: new RiskAssessmentD2Repository(api), mapConfigRepository: new MapConfigD2Repository(api), performanceOverviewRepository: new PerformanceOverviewD2Repository(api, dataStoreClient), + chartConfigRepository: new ChartConfigD2Repository(dataStoreClient), + systemRepository: new SystemD2Repository(api), }; return getCompositionRoot(repositories); @@ -122,6 +144,8 @@ export function getTestCompositionRoot() { riskAssessmentRepository: new RiskAssessmentTestRepository(), mapConfigRepository: new MapConfigTestRepository(), performanceOverviewRepository: new PerformanceOverviewTestRepository(), + chartConfigRepository: new ChartConfigTestRepository(), + systemRepository: new SystemTestRepository(), }; return getCompositionRoot(repositories); diff --git a/src/data/repositories/ChartConfigD2Repository.ts b/src/data/repositories/ChartConfigD2Repository.ts new file mode 100644 index 00000000..17ac2418 --- /dev/null +++ b/src/data/repositories/ChartConfigD2Repository.ts @@ -0,0 +1,53 @@ +import { DataStoreClient } from "../DataStoreClient"; +import { FutureData } from "../api-futures"; +import { ChartConfigRepository } from "../../domain/repositories/ChartConfigRepository"; +import { Id } from "../../domain/entities/Ref"; + +type ChartConfig = { + key: string; + casesId: Id; + deathsId: Id; + riskAssessmentHistoryId: Id; +}; + +const chartConfigDatastoreKey = "charts-config"; + +export class ChartConfigD2Repository implements ChartConfigRepository { + constructor(private dataStoreClient: DataStoreClient) {} + + public getCases(chartKey: string): FutureData { + return this.dataStoreClient + .getObject(chartConfigDatastoreKey) + .map(chartConfigs => { + const currentChart = chartConfigs?.find( + chartConfig => chartConfig.key === chartKey + ); + if (currentChart) return currentChart.casesId; + else throw new Error(`Chart id not found for ${chartKey}`); + }); + } + + public getDeaths(chartKey: string): FutureData { + return this.dataStoreClient + .getObject(chartConfigDatastoreKey) + .map(chartConfigs => { + const currentChart = chartConfigs?.find( + chartConfig => chartConfig.key === chartKey + ); + if (currentChart) return currentChart.deathsId; + else throw new Error(`Chart id not found for ${chartKey}`); + }); + } + + public getRiskAssessmentHistory(chartKey: string): FutureData { + return this.dataStoreClient + .getObject(chartConfigDatastoreKey) + .map(chartConfigs => { + const currentChart = chartConfigs?.find( + chartConfig => chartConfig.key === chartKey + ); + if (currentChart) return currentChart.riskAssessmentHistoryId; + else throw new Error(`Chart id not found for ${chartKey}`); + }); + } +} diff --git a/src/data/repositories/DiseaseOutbreakEventD2Repository.ts b/src/data/repositories/DiseaseOutbreakEventD2Repository.ts index d0056f75..768b0278 100644 --- a/src/data/repositories/DiseaseOutbreakEventD2Repository.ts +++ b/src/data/repositories/DiseaseOutbreakEventD2Repository.ts @@ -23,7 +23,7 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep program: RTSL_ZEBRA_PROGRAM_ID, orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, trackedEntity: id, - fields: { attributes: true, trackedEntity: true }, + fields: { attributes: true, trackedEntity: true, updatedAt: true }, }) ) .flatMap(response => assertOrError(response.instances[0], "Tracked entity")) diff --git a/src/data/repositories/PerformanceOverviewD2Repository.ts b/src/data/repositories/PerformanceOverviewD2Repository.ts index 44243d8d..f2b82472 100644 --- a/src/data/repositories/PerformanceOverviewD2Repository.ts +++ b/src/data/repositories/PerformanceOverviewD2Repository.ts @@ -7,7 +7,9 @@ import _ from "../../domain/entities/generic/Collection"; import { Future } from "../../domain/entities/generic/Future"; import { eventTrackerCountsIndicatorMap, + PERFORMANCE_METRICS_717_IDS, IndicatorsId, + EVENT_TRACKER_717_IDS, } from "./consts/PerformanceOverviewConstants"; import moment from "moment"; import { @@ -20,9 +22,13 @@ import { HazardNames, PerformanceOverviewMetrics, DiseaseNames, + PerformanceMetrics717, } from "../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; import { AlertSynchronizationData } from "../../domain/entities/alert/AlertData"; import { OrgUnit } from "../../domain/entities/OrgUnit"; +import { Id } from "../../domain/entities/Ref"; +import { OverviewCard } from "../../domain/entities/PerformanceOverview"; +import { assertOrError } from "./utils/AssertOrError"; const formatDate = (date: Date): string => { const year = date.getFullYear(); @@ -32,8 +38,16 @@ const formatDate = (date: Date): string => { }; const DEFAULT_END_DATE: string = formatDate(new Date()); - const DEFAULT_START_DATE = "2000-01-01"; +const EVENT_TRACKER_OVERVIEW_DATASTORE_KEY = "event-tracker-overview-ids"; + +type EventTrackerOverview = { + key: string; + suspectedCasesId: Id; + confirmedCasesId: Id; + deathsId: Id; + probableCasesId: Id; +}; export class PerformanceOverviewD2Repository implements PerformanceOverviewRepository { constructor(private api: D2Api, private datastore: DataStoreClient) {} @@ -86,6 +100,7 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos return Object.values(uniqueTotalCardCounts); }); } + mapAnalyticsRowsToTotalCardCounts = ( rowData: string[][], filters?: Record @@ -139,31 +154,167 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos return filteredCounts; }; + private getAnalyticsApi(caseId: string, startDate: string) { + return apiToFuture( + this.api.analytics.get({ + dimension: [`dx:${caseId}`], + startDate: startDate, + endDate: DEFAULT_END_DATE, + }) + ); + } + + private getEventTrackerOverviewIdsFromDatastore( + type: string + ): FutureData { + return this.datastore + .getObject(EVENT_TRACKER_OVERVIEW_DATASTORE_KEY) + .flatMap(nullableEventTrackerOverviewIds => { + return assertOrError( + nullableEventTrackerOverviewIds, + EVENT_TRACKER_OVERVIEW_DATASTORE_KEY + ).flatMap(eventTrackerOverviewIds => { + const currentEventTrackerOverviewId = eventTrackerOverviewIds?.find( + indicator => indicator.key === type + ); + + if (!currentEventTrackerOverviewId) + return Future.error( + new Error( + `Event Tracke Overview Ids for type ${type} not found in datastore` + ) + ); + return Future.success(currentEventTrackerOverviewId); + }); + }); + } + + getEventTrackerOverviewMetrics(type: string): FutureData { + return this.getEventTrackerOverviewIdsFromDatastore(type).flatMap(eventTrackerOverview => { + const { suspectedCasesId, probableCasesId, confirmedCasesId, deathsId } = + eventTrackerOverview; + + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(new Date().getDate() - 7); + + return Future.joinObj( + { + cumulativeSuspectedCases: this.getAnalyticsApi( + suspectedCasesId, + DEFAULT_START_DATE + ), + newSuspectedCases: this.getAnalyticsApi( + suspectedCasesId, + formatDate(sevenDaysAgo) + ), + cumulativeProbableCases: this.getAnalyticsApi( + probableCasesId, + DEFAULT_START_DATE + ), + newProbableCases: this.getAnalyticsApi( + probableCasesId, + formatDate(sevenDaysAgo) + ), + cumulativeConfirmedCases: this.getAnalyticsApi( + confirmedCasesId, + DEFAULT_START_DATE + ), + newConfirmedCases: this.getAnalyticsApi( + confirmedCasesId, + formatDate(sevenDaysAgo) + ), + cumulativeDeaths: this.getAnalyticsApi(deathsId, DEFAULT_START_DATE), + newDeaths: this.getAnalyticsApi(deathsId, formatDate(sevenDaysAgo)), + }, + { concurrency: 5 } + ).flatMap( + ({ + cumulativeSuspectedCases, + newSuspectedCases, + cumulativeProbableCases, + newProbableCases, + cumulativeConfirmedCases, + newConfirmedCases, + cumulativeDeaths, + newDeaths, + }) => { + return Future.success([ + { + name: "New Suspected Cases", + value: newSuspectedCases?.rows[0]?.[1] + ? parseInt(newSuspectedCases?.rows[0]?.[1]) + : 0, + }, + { + name: "New Probable Cases", + value: newProbableCases?.rows[0]?.[1] + ? parseInt(newProbableCases?.rows[0]?.[1]) + : 0, + }, + { + name: "New Confirmed Cases", + value: newConfirmedCases?.rows[0]?.[1] + ? parseInt(newConfirmedCases?.rows[0]?.[1]) + : 0, + }, + { + name: "New Deaths", + value: newDeaths?.rows[0]?.[1] ? parseInt(newDeaths?.rows[0]?.[1]) : 0, + }, + { + name: "Cumulative Suspected Cases", + value: cumulativeSuspectedCases?.rows[0]?.[1] + ? parseInt(cumulativeSuspectedCases?.rows[0]?.[1]) + : 0, + }, + { + name: "Cumulative Probable Cases", + value: cumulativeProbableCases?.rows[0]?.[1] + ? parseInt(cumulativeProbableCases?.rows[0]?.[1]) + : 0, + }, + { + name: "Cumulative Confirmed Cases", + value: cumulativeConfirmedCases?.rows[0]?.[1] + ? parseInt(cumulativeConfirmedCases?.rows[0]?.[1]) + : 0, + }, + { + name: "Cumulative Deaths", + value: cumulativeDeaths?.rows[0]?.[1] + ? parseInt(cumulativeDeaths?.rows[0]?.[1]) + : 0, + }, + ]); + } + ); + }); + } + getPerformanceOverviewMetrics( diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[] ): FutureData { return apiToFuture( - this.api.get( - `/analytics/enrollments/query/${RTSL_ZEBRA_PROGRAM_ID}`, - { - enrollmentDate: "LAST_12_MONTHS,THIS_MONTH", - dimension: [ - IndicatorsId.suspectedDisease, - IndicatorsId.hazardType, - IndicatorsId.event, - IndicatorsId.era1, - IndicatorsId.era2, - IndicatorsId.era3, - IndicatorsId.era4, - IndicatorsId.era5, - IndicatorsId.era6, - IndicatorsId.era7, - IndicatorsId.detect7d, - IndicatorsId.notify1d, - IndicatorsId.respond7d, - ], - } - ) + this.api.analytics.getEnrollmentsQuery({ + programId: RTSL_ZEBRA_PROGRAM_ID, + dimension: [ + IndicatorsId.suspectedDisease, + IndicatorsId.hazardType, + IndicatorsId.event, + IndicatorsId.era1, + IndicatorsId.era2, + IndicatorsId.era3, + IndicatorsId.era4, + IndicatorsId.era5, + IndicatorsId.era6, + IndicatorsId.era7, + IndicatorsId.detect7d, + IndicatorsId.notify1d, + IndicatorsId.respond7d, + ], + startDate: DEFAULT_START_DATE, + endDate: DEFAULT_END_DATE, + }) ).flatMap(indicatorsProgramFuture => { const mappedIndicators = indicatorsProgramFuture?.rows.map((row: string[]) => @@ -208,6 +359,77 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos }); } + private mapIndicatorsTo717PerformanceMetrics( + performanceMetric717Response: string[][], + metricIdList: PerformanceMetrics717[] + ): PerformanceMetrics717[] { + return _( + performanceMetric717Response.map(([id, value]) => { + const indicator = metricIdList.find(d => d.id === id); + + if (!indicator) throw new Error(`Unknown Indicator with id ${id} `); + + if (!value) { + return undefined; + } + return { + ...indicator, + value: parseFloat(value), + type: indicator.type, + }; + }) + ) + .compact() + .value(); + } + + getDashboard717Performance(): FutureData { + return apiToFuture( + this.api.analytics.get({ + dimension: [`dx:${PERFORMANCE_METRICS_717_IDS.map(({ id }) => id).join(";")}`], + startDate: DEFAULT_START_DATE, + endDate: DEFAULT_END_DATE, + includeMetadataDetails: true, + }) + ).map(res => { + return this.mapIndicatorsTo717PerformanceMetrics(res.rows, PERFORMANCE_METRICS_717_IDS); + }); + } + + getEventTracker717Performance(diseaseOutbreakEventId: Id): FutureData { + return apiToFuture( + this.api.analytics.getEnrollmentsQuery({ + programId: RTSL_ZEBRA_PROGRAM_ID, + dimension: [...EVENT_TRACKER_717_IDS.map(({ id }) => id)], + startDate: DEFAULT_START_DATE, + endDate: DEFAULT_END_DATE, + }) + ).flatMap(response => { + const filteredRow = filterAnalyticsEnrollmentDataByDiseaseOutbreakEvent( + diseaseOutbreakEventId, + response.rows, + response.headers + ); + + if (!filteredRow) + return Future.error(new Error("No data found for event tracker 7-1-7 performance")); + + const mappedIndicatorsToRows: string[][] = EVENT_TRACKER_717_IDS.map(({ id }) => { + return [ + id, + filteredRow[response.headers.findIndex(header => header.name === id)] || "", + ]; + }); + + return Future.success( + this.mapIndicatorsTo717PerformanceMetrics( + mappedIndicatorsToRows, + EVENT_TRACKER_717_IDS + ) + ); + }); + } + private getCasesAndDeathsFromDatastore( key: string | undefined ): FutureData<{ cases: number; deaths: number }> { @@ -263,3 +485,14 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos }, {} as Partial); } } + +function filterAnalyticsEnrollmentDataByDiseaseOutbreakEvent( + diseaseOutbreakEventId: Id, + rows: string[][], + headers: { name: string; column: string }[] +): string[] | undefined { + return rows.filter(row => { + const teiId = row[headers.findIndex(header => header.name === "tei")]; + return teiId === diseaseOutbreakEventId; + })[0]; +} diff --git a/src/data/repositories/RiskAssessmentD2Repository.ts b/src/data/repositories/RiskAssessmentD2Repository.ts index cc7a3b9c..ec42d6f7 100644 --- a/src/data/repositories/RiskAssessmentD2Repository.ts +++ b/src/data/repositories/RiskAssessmentD2Repository.ts @@ -156,12 +156,13 @@ export class RiskAssessmentD2Repository implements RiskAssessmentRepository { dataElement: { id: true, code: true }, value: true, }, + createdAt: true, trackedEntity: true, }, }) ).map(events => { const grading: RiskAssessmentGrading[] = events.instances.map(event => { - return mapDataElementsToRiskAssessmentGrading(event.dataValues); + return mapDataElementsToRiskAssessmentGrading(event.createdAt, event.dataValues); }); return grading; }); diff --git a/src/data/repositories/SystemD2Repository.ts b/src/data/repositories/SystemD2Repository.ts new file mode 100644 index 00000000..f726b072 --- /dev/null +++ b/src/data/repositories/SystemD2Repository.ts @@ -0,0 +1,40 @@ +import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { SystemRepository } from "../../domain/repositories/SystemRepository"; +import { apiToFuture, FutureData } from "../api-futures"; +import { getDateAsLocaleDateTimeString } from "./utils/DateTimeHelper"; + +export class SystemD2Repository implements SystemRepository { + constructor(private api: D2Api) {} + + public getLastAnalyticsRuntime(): FutureData { + return apiToFuture(this.api.system.info).map(info => { + //TO DO : update d2Api repo to add lastAnalyticsTablePartitionSuccess to info + + //@ts-ignore + const lastAnalyticsTablePartitionSuccess = info.lastAnalyticsTablePartitionSuccess; + //If continious analytics is turned on, return it. + if ( + info.lastAnalyticsTableSuccess && + lastAnalyticsTablePartitionSuccess && + new Date(lastAnalyticsTablePartitionSuccess) > + new Date(info.lastAnalyticsTableSuccess) + ) { + return getDateAsLocaleDateTimeString(new Date(lastAnalyticsTablePartitionSuccess)); + } + //Else, return the lastAnalyticsTableSuccess time + else if (info.lastAnalyticsTableSuccess) { + return getDateAsLocaleDateTimeString(new Date(info.lastAnalyticsTableSuccess)); + } else { + return "Unable to fetch last analytics runtime"; + } + + //@ts-ignore + if (info.lastAnalyticsTablePartitionSuccess) + return getDateAsLocaleDateTimeString( + //@ts-ignore + new Date(info.lastAnalyticsTablePartitionSuccess) + ); + else return "Unable to fetch last analytics runtime"; + }); + } +} diff --git a/src/data/repositories/consts/PerformanceOverviewConstants.ts b/src/data/repositories/consts/PerformanceOverviewConstants.ts index 8cf8049c..cab613e6 100644 --- a/src/data/repositories/consts/PerformanceOverviewConstants.ts +++ b/src/data/repositories/consts/PerformanceOverviewConstants.ts @@ -2,6 +2,7 @@ import { DiseaseNames, HazardNames, IncidentStatus, + PerformanceMetrics717, } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; import { Id } from "../../../domain/entities/Ref"; @@ -20,7 +21,6 @@ export enum IndicatorsId { notify1d = "HDa3nE7Elxj", respond7d = "yxVOW4lj4xP", province = "ouname", - creationDate = "lastupdated", id = "tei", nationalIncidentStatus = "incidentStatus", } @@ -264,3 +264,1255 @@ export const eventTrackerCountsIndicatorMap: EventTrackerCountIndicator[] = [ { id: "z3EbI98pgjG", type: "hazard", name: "Environmental", incidentStatus: "Alert" }, { id: "gRcZNqpKyYg", type: "hazard", name: "Environmental", incidentStatus: "Respond" }, ]; + +export const PERFORMANCE_METRICS_717_IDS: PerformanceMetrics717[] = [ + { id: "MFk8jiMSlfC", name: "detection", type: "primary" }, // % of number of alerts that were detected within 7 days of date of emergence + { id: "jD8CfKvvdXt", name: "detection", type: "secondary" }, // Number of alerts notified to public health authorities within 1 day of detection + + { id: "Y6OkqfhGhZb", name: "notification", type: "primary" }, // + { id: "fKvY7kMydl1", name: "notification", type: "secondary" }, // # events response action started 1 day + + { id: "gEVnF77Uz2u", name: "response", type: "primary" }, // % num of alerts responded d within 7d date not + { id: "ZX0uPp3ik81", name: "response", type: "secondary" }, // # events response action started 1 day + + { id: "bs4E7tV8QRN", name: "allTargets", type: "primary" }, // % num of alerts detected within 7d date emergence + { id: "dr4OT0ql4cl", name: "allTargets", type: "secondary" }, +]; + +export const EVENT_TRACKER_717_IDS: PerformanceMetrics717[] = [ + { id: "JuPtc83RFcy", name: "Days to detection", type: "primary" }, + { id: "fNnWRK0SBhD", name: "Days to notification", type: "primary" }, + { id: "dByeVE0Oqtu", name: "Days to early response", type: "primary" }, +]; + +// TODO To be updated with allTargets and event count +export const INDICATORS_717_PERFORMANCE_WIP = [ + { + id: "SnlZWWmSnev", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "AFP", + }, + { + id: "nl6Zlqt3JVM", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + incidentStatus: "Alert", + disease: "AFP", + }, + { + id: "ToQ6DYare5u", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + incidentStatus: "Respond", + disease: "AFP", + }, + { + id: "lFsfgXpIlil", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + incidentStatus: "Watch", + disease: "AFP", + }, + { + id: "nuLeni5o8MP", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute Respiratory", + }, + { + id: "ZPEwiNLRF2f", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute Respiratory", + incidentStatus: "Alert", + }, + { + id: "QNbw22AMaVF", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute Respiratory", + incidentStatus: "Respond", + }, + { + id: "S28T4raFe7I", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute Respiratory", + incidentStatus: "Watch", + }, + { + id: "I7DaAl9tvP7", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute VHF", + }, + { + id: "nkEzRvdeNw0", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute VHF", + incidentStatus: "Alert", + }, + { + id: "b5nzdHny6KE", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute VHF", + incidentStatus: "Respond", + }, + { + id: "ejRL3uLmv7H", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Acute VHF", + incidentStatus: "Watch", + }, + { + id: "zDUstCxMvUO", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Anthrax", + }, + { + id: "uRBJpM3mFIs", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Anthrax", + incidentStatus: "Alert", + }, + { + id: "u4SkulpOQB4", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Anthrax", + incidentStatus: "Watch", + }, + { + id: "XMFJTT0jO4m", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Bacterial Meningitis", + }, + { + id: "KysBTCpMNUz", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Bacterial Meningitis", + incidentStatus: "Alert", + }, + { + id: "NbOk0cxkpvs", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Bacterial Meningitis", + incidentStatus: "Respond", + }, + { + id: "Jlc27BTvUW4", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Bacterial Meningitis", + incidentStatus: "Watch", + }, + { + id: "d4h141lBxPS", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Cholera", + }, + { + id: "rmsp9P4uDPr", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Cholera", + incidentStatus: "Alert", + }, + { + id: "KRMBVYOcdQd", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Cholera", + incidentStatus: "Watch", + }, + { + id: "bXMc1BSXzHp", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Cholera", + incidentStatus: "Respond", + }, + { + id: "AgRt7IJjp0F", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Covid19", + }, + { + id: "kqNy3fZy7RH", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Covid19", + incidentStatus: "Alert", + }, + { + id: "Mf60weIB38l", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Covid19", + incidentStatus: "Respond", + }, + { + id: "xG6Alfb9lK4", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Covid19", + incidentStatus: "Watch", + }, + { + id: "F0n4Cnq7pt3", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Diarrhea with blood", + }, + { + id: "kjvrvYE482f", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Diarrhea with blood", + incidentStatus: "Alert", + }, + { + id: "ip33ZANqpPA", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Diarrhea with blood", + incidentStatus: "Respond", + }, + { + id: "jpsHReIY9Mg", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Diarrhea with blood", + incidentStatus: "Watch", + }, + { + id: "EfAsDeNjNnm", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Measles", + }, + { + id: "U4rOjZVSWp2", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Measles", + incidentStatus: "Alert", + }, + { + id: "ZqtnwH0akDG", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Measles", + incidentStatus: "Respond", + }, + { + id: "dv0JYE1UwXe", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Measles", + incidentStatus: "Watch", + }, + { + id: "L76kMfIlUpY", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Monkeypox", + }, + { + id: "xYYQRymq7iz", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Monkeypox", + incidentStatus: "Alert", + }, + { + id: "QeJBw9kUc9k", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Monkeypox", + incidentStatus: "Respond", + }, + { + id: "jMvLCothXAK", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Monkeypox", + incidentStatus: "Watch", + }, + { + id: "fsUqEop9Fpe", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Neonatal tetanus", + }, + { + id: "DQtrZZXzqvQ", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Neonatal tetanus", + incidentStatus: "Alert", + }, + { + id: "G5iVQOAvV6I", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Neonatal tetanus", + incidentStatus: "Respond", + }, + { + id: "k9HER7gGwx7", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Neonatal tetanus", + incidentStatus: "Watch", + }, + { + id: "zWCJGV5Or6m", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Plague", + }, + { + id: "rFokjuSb3Kx", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Plague", + incidentStatus: "Alert", + }, + { + id: "tMFHefINSiR", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Plague", + incidentStatus: "Respond", + }, + { + id: "ke9HE3bffny", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Plague", + incidentStatus: "Watch", + }, + { + id: "B2qckoEX6m2", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + incidentStatus: "Respond", + }, + { + id: "QZcaLMK9D8A", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "SARIs", + }, + { + id: "gWhrKjOxmAQ", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "SARIs", + incidentStatus: "Alert", + }, + { + id: "ekzSRmlKdm6", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "SARIs", + incidentStatus: "Respond", + }, + { + id: "W0ythNIgdFj", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "SARIs", + incidentStatus: "Watch", + }, + { + id: "t2k9cPlQnns", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Typhoid fever", + }, + { + id: "LRGObRCNgTu", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Typhoid fever", + incidentStatus: "Alert", + }, + { + id: "FVXLF7FqNlJ", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Typhoid fever", + incidentStatus: "Respond", + }, + { + id: "fKmbCH6wv0F", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Typhoid fever", + incidentStatus: "Watch", + }, + { + id: "wTDTbOR8NTz", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Zika fever", + }, + { + id: "MtNNhpMR62L", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Zika fever", + incidentStatus: "Alert", + }, + { + id: "SNPUqZlG5Xl", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Zika fever", + incidentStatus: "Respond", + }, + { + id: "ldTezKD5XYy", + name: "% of number of alerts notified to public health authorities within 1 day of detection", + type: "notification", + disease: "Zika fever", + incidentStatus: "Watch", + }, + { + id: "t4c8Ntac07E", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "notification", + }, + { + id: "RC2jTUJtT7L", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "AFP", + }, + { + id: "Bn9Z9Lr7pZ5", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + incidentStatus: "Alert", + disease: "AFP", + }, + { + id: "Tj04ZY08bPS", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + incidentStatus: "Respond", + disease: "AFP", + }, + { + id: "sHG6xZdYH6A", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + incidentStatus: "Watch", + disease: "AFP", + }, + { + id: "MJ2FPmAWgbd", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute Respiratory", + }, + { + id: "WgTUeyZr3lh", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute Respiratory", + incidentStatus: "Alert", + }, + { + id: "RXa3QVm0iJg", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute Respiratory", + incidentStatus: "Respond", + }, + { + id: "I2OGSbDELey", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute Respiratory", + incidentStatus: "Watch", + }, + { + id: "eoEPuP4f8iK", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute VHF", + }, + { + id: "RUqNg4sGsPq", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute VHF", + incidentStatus: "Alert", + }, + { + id: "APcx2MsAlYr", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute VHF", + incidentStatus: "Respond", + }, + { + id: "D5DiSgxXe1z", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Acute VHF", + incidentStatus: "Watch", + }, + { + id: "pOFn0en3fmJ", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Anthrax", + }, + { + id: "M49f448xNqX", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Anthrax", + incidentStatus: "Alert", + }, + { + id: "lgAGQswA2gz", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Anthrax", + incidentStatus: "Respond", + }, + { + id: "ewJFpaViIfA", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Anthrax", + incidentStatus: "Watch", + }, + { + id: "ytOrBSVhkTM", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Bacterial Meningitis", + }, + { + id: "DsuMoDQd9eG", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Bacterial Meningitis", + incidentStatus: "Alert", + }, + { + id: "qADIfhAuXbE", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Bacterial Meningitis", + incidentStatus: "Respond", + }, + { + id: "PvMa1dFOYIK", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Bacterial Meningitis", + incidentStatus: "Watch", + }, + { + id: "p0W1Run9toi", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Cholera", + }, + { + id: "I94t7uZJJMD", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Cholera", + incidentStatus: "Alert", + }, + { + id: "Wdlal5cmE52", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Cholera", + incidentStatus: "Respond", + }, + { + id: "x8IarVRwu7C", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Cholera", + incidentStatus: "Watch", + }, + { + id: "rgkGha8keXX", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Covid19", + }, + { + id: "QqTuVNZnxy5", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Covid19", + incidentStatus: "Alert", + }, + { + id: "xU5owm2CcDb", + name: "% of number of alerts that were detected within 7 days of date of emergence ", + type: "detection", + disease: "Covid19", + }, + { + id: "UJD6LLiIfKm", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Covid19", + incidentStatus: "Watch", + }, + { + id: "n7Q0XKPhz9D", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Diarrhea with blood", + }, + { + id: "CnfA0qd946B", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Diarrhea with blood", + incidentStatus: "Alert", + }, + { + id: "MNGwSWqwnOZ", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Diarrhea with blood", + incidentStatus: "Respond", + }, + { + id: "OAxlgbhZlb8", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Diarrhea with blood", + incidentStatus: "Watch", + }, + { + id: "MFk8jiMSlfC", + name: "% of number of alerts that were detected within 7 days of date of emergence - Events", + type: "notification", + }, + { + id: "J80pmH2KRcx", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Measles", + }, + { + id: "oxJ5mau1J3x", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Measles", + incidentStatus: "Alert", + }, + { + id: "PXlb8RA8jhM", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Measles", + incidentStatus: "Respond", + }, + { + id: "mP3MBcWPk2x", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Measles", + incidentStatus: "Watch", + }, + { + id: "XMjRmOub0NX", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Monkeypox", + }, + { + id: "ruYWSrhzLgP", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Monkeypox", + incidentStatus: "Alert", + }, + { + id: "q0ZMp98y6y2", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Monkeypox", + incidentStatus: "Respond", + }, + { + id: "rSRYG6orVh9", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Monkeypox", + incidentStatus: "Watch", + }, + { + id: "qI9630naxV6", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Neonatal tetanus", + }, + { + id: "fKirdoLLh1Y", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Neonatal tetanus", + incidentStatus: "Alert", + }, + { + id: "q7oli7A6nn8", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Neonatal tetanus", + incidentStatus: "Respond", + }, + { + id: "PLNANQ0CAPW", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Neonatal tetanus", + incidentStatus: "Watch", + }, + { + id: "lKgkoszdfu7", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Plague", + }, + { + id: "HYmDJlHZd1W", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Plague", + incidentStatus: "Alert", + }, + { + id: "ZQSAJrQMkug", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Plague", + incidentStatus: "Respond", + }, + { + id: "eiQwaMtYq90", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Plague", + incidentStatus: "Watch", + }, + { + id: "UrArvseg6kX", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "SARIs", + }, + { + id: "uXG454CtUma", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "SARIs", + incidentStatus: "Alert", + }, + { + id: "jLXONCZxxfh", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "SARIs", + incidentStatus: "Respond", + }, + { + id: "cQb9Hdw5jql", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "SARIs", + incidentStatus: "Watch", + }, + { + id: "CqH1jf6gGFD", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Typhoid fever", + }, + { + id: "Usju3ALyYYY", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Typhoid fever", + incidentStatus: "Alert", + }, + { + id: "mH1Qofgu1qj", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Typhoid fever", + incidentStatus: "Respond", + }, + { + id: "krtkcFHy5gD", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Typhoid fever", + incidentStatus: "Watch", + }, + { + id: "iBUJyPMvQc0", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Zika fever", + }, + { + id: "PJYXp6lEf33", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Zika fever", + incidentStatus: "Alert", + }, + { + id: "ulx34yw2fhv", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Zika fever", + incidentStatus: "Respond", + }, + { + id: "PMUSuZjMILK", + name: "% of number of alerts that were detected within 7 days of date of emergence", + type: "detection", + disease: "Zika fever", + incidentStatus: "Watch", + }, + { + id: "pAzsfnu4pjN", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + }, + { + id: "gFjSngCJtyc", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "AFP", + }, + { + id: "e7jWFYSjf4G", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + incidentStatus: "Alert", + disease: "AFP", + }, + { + id: "AmiJBU4lLzg", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + incidentStatus: "Respond", + disease: "AFP", + }, + { + id: "Ysu6dNwU8rD", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + incidentStatus: "Watch", + disease: "AFP", + }, + { + id: "ujD1Such0FX", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute Respiratory", + }, + { + id: "rmKCa08KHml", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute Respiratory", + incidentStatus: "Alert", + }, + { + id: "uC4adVvbDmz", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute Respiratory", + incidentStatus: "Respond", + }, + { + id: "XEJCxmOdf8H", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute Respiratory", + incidentStatus: "Watch", + }, + { + id: "siZI9LHQwdp", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute VHF", + }, + { + id: "Dfs66idkbGh", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute VHF", + incidentStatus: "Alert", + }, + { + id: "tfVZ0aXD2nl", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute VHF", + incidentStatus: "Respond", + }, + { + id: "zev7Ksngoex", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Acute VHF", + incidentStatus: "Watch", + }, + { + id: "ViFa8BNG01k", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Anthrax", + }, + { + id: "Ig4wLi4cOsy", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Anthrax", + incidentStatus: "Alert", + }, + { + id: "tDOGR6DjE7D", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Anthrax", + incidentStatus: "Respond", + }, + { + id: "VdxB4vJZ15R", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Anthrax", + incidentStatus: "Watch", + }, + { + id: "a17C88VS6Dy", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Bacterial Meningitis", + }, + { + id: "ZqvCFe2WlAO", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Bacterial Meningitis", + incidentStatus: "Alert", + }, + { + id: "GdO2wxSkV8n", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Bacterial Meningitis", + incidentStatus: "Respond", + }, + { + id: "etcnMIPSSL0", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Bacterial Meningitis", + incidentStatus: "Watch", + }, + { + id: "G2fBbBi6as1", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Cholera", + }, + { + id: "MkEXS8gy8vv", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Cholera", + incidentStatus: "Alert", + }, + { + id: "fHMlOjNZ9S1", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Cholera", + incidentStatus: "Respond", + }, + { + id: "DlDbMhEjoMi", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Cholera", + incidentStatus: "Watch", + }, + { + id: "iQ3thUxI4Bw", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Covid19", + }, + { + id: "cQbg0IJFiko", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Covid19", + incidentStatus: "Alert", + }, + { + id: "BL4G9BPrsWR", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Covid19", + incidentStatus: "Respond", + }, + { + id: "KTZhKNpliQF", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Covid19", + incidentStatus: "Watch", + }, + { + id: "BSmZm0sThuY", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Diarrhea with blood", + }, + { + id: "UpgL6Yu28QQ", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Diarrhea with blood", + incidentStatus: "Alert", + }, + { + id: "jbCO7APEXkx", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Diarrhea with blood", + incidentStatus: "Respond", + }, + { + id: "FRZAF4iK4Ek", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Diarrhea with blood", + incidentStatus: "Watch", + }, + { + id: "AcwSBWB2qlM", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Measles", + }, + { + id: "kxsCnbCHb1b", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Measles", + incidentStatus: "Alert", + }, + { + id: "WIzPozWkU9o", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Measles", + incidentStatus: "Respond", + }, + { + id: "anK7GLeSHF7", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Measles", + incidentStatus: "Watch", + }, + { + id: "AgpPSrHC2pj", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Monkeypox", + }, + { + id: "jT8CcfsJTBi", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Monkeypox", + incidentStatus: "Alert", + }, + { + id: "TLaP8Lu1dhB", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Monkeypox", + incidentStatus: "Respond", + }, + { + id: "ofEzx7UmuDS", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Monkeypox", + incidentStatus: "Watch", + }, + { + id: "sY8hoUE4JoB", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Neonatal tetanus", + }, + { + id: "ZzeMj5kr7wY", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Neonatal tetanus", + incidentStatus: "Alert", + }, + { + id: "ElFKHitzAks", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Neonatal tetanus", + incidentStatus: "Respond", + }, + { + id: "PMS4noi5GN8", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Neonatal tetanus", + incidentStatus: "Watch", + }, + { + id: "NuInWIPC5IT", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Plague", + }, + { + id: "xfR2LhUqkSI", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Plague", + incidentStatus: "Alert", + }, + { + id: "LbHCxDecGTt", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Plague", + incidentStatus: "Respond", + }, + { + id: "J6G42iXjHqN", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Plague", + incidentStatus: "Watch", + }, + { + id: "u8g5YyaZcrI", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "SARIs", + }, + { + id: "ijldyhHh9Yy", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "SARIs", + incidentStatus: "Alert", + }, + { + id: "YCr9TpgA7Lg", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "SARIs", + incidentStatus: "Respond", + }, + { + id: "l5b6LawyANZ", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "SARIs", + incidentStatus: "Watch", + }, + { + id: "WaX2bMKuHGg", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Typhoid fever", + }, + { + id: "LkHrcxooPtt", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Typhoid fever", + incidentStatus: "Alert", + }, + { + id: "ruEiWFop4jQ", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Typhoid fever", + incidentStatus: "Respond", + }, + { + id: "HBFFAFXT4hA", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Typhoid fever", + incidentStatus: "Watch", + }, + { + id: "Wfs9JweVQfv", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Zika fever", + }, + { + id: "AnBWynoByJz", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Zika fever", + incidentStatus: "Alert", + }, + { + id: "LTahquMgu5i", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Zika fever", + incidentStatus: "Respond", + }, + { + id: "ldOsmniN5HQ", + name: "% of number of alerts where Responded date is within 7 days of notification", + type: "response", + disease: "Zika fever", + incidentStatus: "Watch", + }, +]; diff --git a/src/data/repositories/test/ChartConfigTestRepository.ts b/src/data/repositories/test/ChartConfigTestRepository.ts new file mode 100644 index 00000000..8942a9bd --- /dev/null +++ b/src/data/repositories/test/ChartConfigTestRepository.ts @@ -0,0 +1,15 @@ +import { Future } from "../../../domain/entities/generic/Future"; +import { ChartConfigRepository } from "../../../domain/repositories/ChartConfigRepository"; +import { FutureData } from "../../api-futures"; + +export class ChartConfigTestRepository implements ChartConfigRepository { + getRiskAssessmentHistory(_chartKey: string): FutureData { + return Future.success("1"); + } + getCases(_chartkey: string): FutureData { + return Future.success("1"); + } + getDeaths(_chartKey: string): FutureData { + return Future.success("1"); + } +} diff --git a/src/data/repositories/test/PerformanceOverviewTestRepository.ts b/src/data/repositories/test/PerformanceOverviewTestRepository.ts index 4bbc4a4f..311d911c 100644 --- a/src/data/repositories/test/PerformanceOverviewTestRepository.ts +++ b/src/data/repositories/test/PerformanceOverviewTestRepository.ts @@ -1,11 +1,26 @@ +import { PerformanceMetrics717 } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; import { Future } from "../../../domain/entities/generic/Future"; +import { OverviewCard } from "../../../domain/entities/PerformanceOverview"; +import { Id } from "../../../domain/entities/Ref"; import { PerformanceOverviewRepository } from "../../../domain/repositories/PerformanceOverviewRepository"; import { FutureData } from "../../api-futures"; export class PerformanceOverviewTestRepository implements PerformanceOverviewRepository { + getEventTracker717Performance( + _diseaseOutbreakEventId: Id + ): FutureData { + return Future.success([]); + } + getEventTrackerOverviewMetrics(): FutureData { + throw Future.success([]); + } getTotalCardCounts(): FutureData { return Future.success(0); } + getDashboard717Performance(): FutureData { + return Future.success(0); + } + getPerformanceOverviewMetrics(): FutureData { return Future.success([ { diff --git a/src/data/repositories/test/SystemTestRepository.ts b/src/data/repositories/test/SystemTestRepository.ts new file mode 100644 index 00000000..a23138ce --- /dev/null +++ b/src/data/repositories/test/SystemTestRepository.ts @@ -0,0 +1,9 @@ +import { Future } from "../../../domain/entities/generic/Future"; +import { SystemRepository } from "../../../domain/repositories/SystemRepository"; +import { FutureData } from "../../api-futures"; + +export class SystemTestRepository implements SystemRepository { + getLastAnalyticsRuntime(): FutureData { + return Future.success(new Date().toString()); + } +} diff --git a/src/data/repositories/utils/DiseaseOutbreakMapper.ts b/src/data/repositories/utils/DiseaseOutbreakMapper.ts index 1e2950fe..194829fe 100644 --- a/src/data/repositories/utils/DiseaseOutbreakMapper.ts +++ b/src/data/repositories/utils/DiseaseOutbreakMapper.ts @@ -47,8 +47,8 @@ export function mapTrackedEntityAttributesToDiseaseOutbreak( status: trackedEntity.enrollments?.[0]?.status ?? "ACTIVE", //Zebra Outbreak has only one enrollment name: fromMap("name"), dataSource: dataSource, - created: trackedEntity.createdAt ? new Date(trackedEntity.createdAt) : new Date(), - lastUpdated: trackedEntity.updatedAt ? new Date(trackedEntity.updatedAt) : new Date(), + created: trackedEntity.createdAt ? new Date(trackedEntity.createdAt) : undefined, + lastUpdated: trackedEntity.updatedAt ? new Date(trackedEntity.updatedAt) : undefined, createdByName: undefined, hazardType: getHazardTypeByCode(fromMap("hazardType")), mainSyndromeCode: fromMap("mainSyndrome"), diff --git a/src/data/repositories/utils/RiskAssessmentMapper.ts b/src/data/repositories/utils/RiskAssessmentMapper.ts index ae433941..e22e3b13 100644 --- a/src/data/repositories/utils/RiskAssessmentMapper.ts +++ b/src/data/repositories/utils/RiskAssessmentMapper.ts @@ -254,6 +254,7 @@ function getRiskAssessmentTrackerEvent( } export function mapDataElementsToRiskAssessmentGrading( + lastUpdated: string | undefined, dataValues: DataValue[] ): RiskAssessmentGrading { const populationValue = getValueById(dataValues, riskAssessmentGradingIds.populationAtRisk); @@ -273,7 +274,7 @@ export function mapDataElementsToRiskAssessmentGrading( const riskAssessmentGrading: RiskAssessmentGrading = RiskAssessmentGrading.create({ id: "", - lastUpdated: new Date(), + lastUpdated: lastUpdated ? new Date(lastUpdated) : undefined, populationAtRisk: RiskAssessmentGrading.getOptionTypeByCodePopulation(populationValue), attackRate: RiskAssessmentGrading.getOptionTypeByCodeWeighted(attackRateValue), geographicalSpread: diff --git a/src/domain/entities/PerformanceOverview.ts b/src/domain/entities/PerformanceOverview.ts new file mode 100644 index 00000000..1ae41701 --- /dev/null +++ b/src/domain/entities/PerformanceOverview.ts @@ -0,0 +1,4 @@ +export type OverviewCard = { + name: string; + value: number; +}; diff --git a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts index 316d335f..5e5983a8 100644 --- a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts +++ b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts @@ -55,8 +55,8 @@ type EarlyResponseActions = { export type DiseaseOutbreakEventBaseAttrs = NamedRef & { status: "ACTIVE" | "COMPLETED" | "CANCELLED"; - created: Date; - lastUpdated: Date; + created?: Date; + lastUpdated?: Date; createdByName: Maybe; dataSource: DataSource; hazardType: Maybe; diff --git a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts deleted file mode 100644 index 8409e199..00000000 --- a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Maybe } from "../../../utils/ts-utils"; -import { TeamMember } from "../incident-management-team/TeamMember"; -import { Option } from "../Ref"; -import { Rule } from "../Rule"; -import { ValidationErrorKey } from "../ValidationError"; -import { DiseaseOutbreakEventBaseAttrs } from "./DiseaseOutbreakEvent"; - -export type DiseaseOutbreakEventOptions = { - dataSources: Option[]; - hazardTypes: Option[]; - mainSyndromes: Option[]; - suspectedDiseases: Option[]; - notificationSources: Option[]; - incidentStatus: Option[]; - incidentManagers: TeamMember[]; -}; - -export type DiseaseOutbreakEventLabels = { - errors: Record; -}; - -export type DiseaseOutbreakEventWithOptions = { - diseaseOutbreakEvent: Maybe; - options: DiseaseOutbreakEventOptions; - labels: DiseaseOutbreakEventLabels; - rules: Rule[]; -}; diff --git a/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts b/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts index 58e72586..c51caafb 100644 --- a/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts +++ b/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts @@ -41,7 +41,6 @@ export type PerformanceOverviewMetrics = { detect7d: string; notify1d: string; respond7d: string; - creationDate: string; suspectedDisease: DiseaseNames; hazardType: HazardNames; nationalIncidentStatus: string; @@ -67,3 +66,10 @@ type HazardCounts = BaseCounts & { }; export type TotalCardCounts = DiseaseCounts | HazardCounts; + +export type PerformanceMetrics717 = { + id: string; + name: string; + type: "primary" | "secondary"; + value?: number; +}; diff --git a/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts index f1f2b91a..b450c739 100644 --- a/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts +++ b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts @@ -116,7 +116,7 @@ const translations: Record = { }; export interface RiskAssessmentGradingAttrs extends Ref { - lastUpdated: Date; + lastUpdated?: Date; populationAtRisk: LowPopulationAtRisk | MediumPopulationAtRisk | HighPopulationAtRisk; attackRate: LowWeightedOption | MediumWeightedOption | HighWeightedOption; geographicalSpread: LowGeographicalSpread | MediumGeographicalSpread | HighGeographicalSpread; diff --git a/src/domain/repositories/ChartConfigRepository.ts b/src/domain/repositories/ChartConfigRepository.ts new file mode 100644 index 00000000..5f61084a --- /dev/null +++ b/src/domain/repositories/ChartConfigRepository.ts @@ -0,0 +1,7 @@ +import { FutureData } from "../../data/api-futures"; + +export interface ChartConfigRepository { + getCases(chartkey: string): FutureData; + getDeaths(chartKey: string): FutureData; + getRiskAssessmentHistory(chartKey: string): FutureData; +} diff --git a/src/domain/repositories/PerformanceOverviewRepository.ts b/src/domain/repositories/PerformanceOverviewRepository.ts index 174fc4a3..9fba312a 100644 --- a/src/domain/repositories/PerformanceOverviewRepository.ts +++ b/src/domain/repositories/PerformanceOverviewRepository.ts @@ -3,7 +3,10 @@ import { DiseaseOutbreakEventBaseAttrs } from "../entities/disease-outbreak-even import { TotalCardCounts, PerformanceOverviewMetrics, + PerformanceMetrics717, } from "../entities/disease-outbreak-event/PerformanceOverviewMetrics"; +import { OverviewCard } from "../entities/PerformanceOverview"; +import { Id } from "../entities/Ref"; export interface PerformanceOverviewRepository { getPerformanceOverviewMetrics( @@ -15,4 +18,7 @@ export interface PerformanceOverviewRepository { multiSelectFilters?: Record, dateRangeFilter?: string[] ): FutureData; + getDashboard717Performance(): FutureData; + getEventTracker717Performance(diseaseOutbreakEventId: Id): FutureData; + getEventTrackerOverviewMetrics(type: string): FutureData; } diff --git a/src/domain/repositories/SystemRepository.ts b/src/domain/repositories/SystemRepository.ts new file mode 100644 index 00000000..8549a9a1 --- /dev/null +++ b/src/domain/repositories/SystemRepository.ts @@ -0,0 +1,5 @@ +import { FutureData } from "../../data/api-futures"; + +export interface SystemRepository { + getLastAnalyticsRuntime(): FutureData; +} diff --git a/src/domain/usecases/Get717PerformanceUseCase.ts b/src/domain/usecases/Get717PerformanceUseCase.ts new file mode 100644 index 00000000..bdd78f59 --- /dev/null +++ b/src/domain/usecases/Get717PerformanceUseCase.ts @@ -0,0 +1,25 @@ +import { FutureData } from "../../data/api-futures"; +import { PerformanceMetrics717 } from "../entities/disease-outbreak-event/PerformanceOverviewMetrics"; +import { Id } from "../entities/Ref"; +import { PerformanceOverviewRepository } from "../repositories/PerformanceOverviewRepository"; + +export class Get717PerformanceUseCase { + constructor( + private options: { + performanceOverviewRepository: PerformanceOverviewRepository; + } + ) {} + + public execute( + type: "dashboard" | "event_tracker", + diseaseOutbreakEventId: Id | undefined + ): FutureData { + if (type === "event_tracker" && diseaseOutbreakEventId) { + return this.options.performanceOverviewRepository.getEventTracker717Performance( + diseaseOutbreakEventId + ); + } else if (type === "dashboard") { + return this.options.performanceOverviewRepository.getDashboard717Performance(); + } else throw new Error(`Unknown 717 type: ${type} `); + } +} diff --git a/src/domain/usecases/GetAnalyticsRuntimeUseCase.ts b/src/domain/usecases/GetAnalyticsRuntimeUseCase.ts new file mode 100644 index 00000000..428d2b8f --- /dev/null +++ b/src/domain/usecases/GetAnalyticsRuntimeUseCase.ts @@ -0,0 +1,14 @@ +import { FutureData } from "../../data/api-futures"; +import { SystemRepository } from "../repositories/SystemRepository"; + +export class GetAnalyticsRuntimeUseCase { + constructor( + private options: { + systemRepository: SystemRepository; + } + ) {} + + public execute(): FutureData { + return this.options.systemRepository.getLastAnalyticsRuntime(); + } +} diff --git a/src/domain/usecases/GetChartConfigByTypeUseCase.ts b/src/domain/usecases/GetChartConfigByTypeUseCase.ts new file mode 100644 index 00000000..0527ccbd --- /dev/null +++ b/src/domain/usecases/GetChartConfigByTypeUseCase.ts @@ -0,0 +1,19 @@ +import { FutureData } from "../../data/api-futures"; +import { ChartConfigRepository } from "../repositories/ChartConfigRepository"; + +export type ChartType = "deaths" | "cases" | "risk-assessment-history"; +export class GetChartConfigByTypeUseCase { + constructor(private chartConfigRepository: ChartConfigRepository) {} + + public execute(chartType: ChartType, chartKey: string): FutureData { + if (chartType === "deaths") { + return this.chartConfigRepository.getDeaths(chartKey); + } else if (chartType === "cases") { + return this.chartConfigRepository.getCases(chartKey); + } else if (chartType === "risk-assessment-history") { + return this.chartConfigRepository.getRiskAssessmentHistory(chartKey); + } else { + throw new Error(`Invalid chart type: ${chartType}`); + } + } +} diff --git a/src/domain/usecases/GetOverviewCardsUseCase.ts b/src/domain/usecases/GetOverviewCardsUseCase.ts new file mode 100644 index 00000000..2fdfa6ce --- /dev/null +++ b/src/domain/usecases/GetOverviewCardsUseCase.ts @@ -0,0 +1,11 @@ +import { FutureData } from "../../data/api-futures"; +import { OverviewCard } from "../entities/PerformanceOverview"; +import { PerformanceOverviewRepository } from "../repositories/PerformanceOverviewRepository"; + +export class GetOverviewCardsUseCase { + constructor(private performanceOverviewRepository: PerformanceOverviewRepository) {} + + public execute(type: string): FutureData { + return this.performanceOverviewRepository.getEventTrackerOverviewMetrics(type); + } +} diff --git a/src/webapp/components/chart/Chart.tsx b/src/webapp/components/chart/Chart.tsx new file mode 100644 index 00000000..7a8e9580 --- /dev/null +++ b/src/webapp/components/chart/Chart.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Section } from "../section/Section"; +import { Visualisation } from "../visualisation/Visualisation"; +import { useAppContext } from "../../contexts/app-context"; +import { useChart } from "./useChart"; +import { Maybe } from "../../../utils/ts-utils"; +import LoaderContainer from "../loader/LoaderContainer"; +import { ChartType } from "../../../domain/usecases/GetChartConfigByTypeUseCase"; + +type ChartProps = { + title: string; + chartType: ChartType; + chartKey: Maybe; + hasSeparator?: boolean; + lastUpdated?: string; +}; +export const Chart: React.FC = React.memo(props => { + const { api } = useAppContext(); + const { title, hasSeparator, lastUpdated, chartType, chartKey } = props; + + const { id } = useChart(chartType, chartKey); + + const chartUrl = + chartType === "risk-assessment-history" + ? `${api.baseUrl}/dhis-web-event-visualizer/?id=${id}` + : `${api.baseUrl}/dhis-web-data-visualizer/#/${id}`; + + return ( + +
+ +
+
+ ); +}); diff --git a/src/webapp/components/chart/useChart.ts b/src/webapp/components/chart/useChart.ts new file mode 100644 index 00000000..f803749d --- /dev/null +++ b/src/webapp/components/chart/useChart.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from "react"; +import { useAppContext } from "../../contexts/app-context"; +import { Maybe } from "../../../utils/ts-utils"; +import { ChartType } from "../../../domain/usecases/GetChartConfigByTypeUseCase"; + +export function useChart(chartType: ChartType, chartKey: Maybe) { + const { compositionRoot } = useAppContext(); + const [id, setId] = useState(); + + useEffect(() => { + if (!chartKey) { + return; + } + compositionRoot.charts.getCases.execute(chartType, chartKey).run( + chartId => { + setId(chartId); + }, + error => { + console.error(error); + } + ); + }, [chartKey, chartType, compositionRoot.charts.getCases]); + + return { id }; +} diff --git a/src/webapp/components/date-picker/DateRangePicker.tsx b/src/webapp/components/date-picker/DateRangePicker.tsx index a28404ba..629d9c13 100644 --- a/src/webapp/components/date-picker/DateRangePicker.tsx +++ b/src/webapp/components/date-picker/DateRangePicker.tsx @@ -21,8 +21,12 @@ const ID = "date-range-picker"; export const DateRangePicker: React.FC = React.memo( ({ label = "", value, placeholder = "", onChange }) => { const [anchorEl, setAnchorEl] = useState(null); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); + const [startDate, setStartDate] = useState( + value && value[0] ? new Date(value[0]) : null + ); + const [endDate, setEndDate] = useState( + value && value[1] ? new Date(value[1]) : null + ); const handleOpen = useCallback((event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -41,14 +45,14 @@ export const DateRangePicker: React.FC = React.memo( }, [onCleanValues, value.length]); const formatDurationValue = useMemo(() => { - if (!value || value.length !== 2) { + if (!value || value.length !== 2 || !value[0] || !value[1]) { return placeholder; } - return `${moment(startDate).format("DD/MM/yyyy")} — ${moment(endDate).format( - "DD/MM/yyyy" - )}`; - }, [startDate, endDate, placeholder, value]); + return `${moment(new Date(value[0])).format("DD/MM/yyyy")} — ${moment( + new Date(value[1]) + ).format("DD/MM/yyyy")}`; + }, [placeholder, value]); const onReset = useCallback(() => { onChange([]); diff --git a/src/webapp/components/layout/Layout.tsx b/src/webapp/components/layout/Layout.tsx index e8b04bf5..af98ac29 100644 --- a/src/webapp/components/layout/Layout.tsx +++ b/src/webapp/components/layout/Layout.tsx @@ -12,6 +12,7 @@ type LayoutProps = { subtitle?: string; hideSideBarOptions?: boolean; showCreateEvent?: boolean; + lastAnalyticsRuntime?: string; }; export const Layout: React.FC = React.memo( @@ -21,6 +22,7 @@ export const Layout: React.FC = React.memo( subtitle = "", hideSideBarOptions = false, showCreateEvent = false, + lastAnalyticsRuntime = "", }) => { const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); @@ -41,6 +43,7 @@ export const Layout: React.FC = React.memo( showCreateEvent={showCreateEvent} title={title} subtitle={subtitle} + lastAnalyticsRuntime={lastAnalyticsRuntime} > {children} diff --git a/src/webapp/components/layout/main-content/MainContent.tsx b/src/webapp/components/layout/main-content/MainContent.tsx index 696557a2..6e0554e1 100644 --- a/src/webapp/components/layout/main-content/MainContent.tsx +++ b/src/webapp/components/layout/main-content/MainContent.tsx @@ -11,6 +11,7 @@ type MainContentProps = { sideBarOpen: boolean; toggleSideBar: (isOpen: boolean) => void; showCreateEvent?: boolean; + lastAnalyticsRuntime?: string; }; export const MainContent: React.FC = React.memo( @@ -22,6 +23,7 @@ export const MainContent: React.FC = React.memo( showCreateEvent = false, toggleSideBar, sideBarOpen, + lastAnalyticsRuntime = "", }) => { return ( @@ -37,6 +39,13 @@ export const MainContent: React.FC = React.memo( {subtitle && {subtitle}} + {lastAnalyticsRuntime && ( + + Last Analytics Runtime : + {lastAnalyticsRuntime} + + )} + {children} @@ -61,6 +70,23 @@ const SubTitle = styled.span` color: ${props => props.theme.palette.text.secondary}; `; +const AnalyticsRuntime = styled.span` + align-self: flex-end; + margin-block-start: 8px; +`; + +const EmphasisedText = styled.span` + color: ${props => props.theme.palette.common.grey700}; + font-size: 0.875rem; + font-weight: 700; +`; + +const Text = styled.span` + color: ${props => props.theme.palette.common.grey700}; + font-size: 0.875rem; + font-weight: 400; +`; + const PageContent = styled.div` margin-block-start: 48px; `; diff --git a/src/webapp/components/map/Map.tsx b/src/webapp/components/map/Map.tsx deleted file mode 100644 index 6f3e16fe..00000000 --- a/src/webapp/components/map/Map.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { useAppContext } from "../../contexts/app-context"; -import { FilteredMapConfig } from "./useMap"; -import LoaderContainer from "../loader/LoaderContainer"; - -type MapProps = { - config: FilteredMapConfig; -}; - -type State = { - type: "loading" | "loaded"; -}; - -export const Map: React.FC = React.memo(props => { - const { config } = props; - const { api } = useAppContext(); - - const [state, setState] = React.useState({ type: "loading" }); - - const baseUrl = `${api.baseUrl}/api/apps/zebra-custom-maps-app/index.html`; - - const params = { - currentApp: config.currentApp, - currentPage: config.currentPage, - zebraNamespace: config.zebraNamespace, - dashboardDatastoreKey: config.dashboardDatastoreKey, - id: config.mapId, - orgUnits: config.orgUnits.join(","), - programIndicatorId: config.programIndicatorId, - programIndicatorName: config.programIndicatorName, - programId: config.programId, - programName: config.programName, - startDate: config.startDate, - endDate: config.endDate, - timeField: config.timeField, - }; - - const srcUrl = - baseUrl + "?" + new URLSearchParams(removeUndefinedProperties(params)).toString(); - - const iframeRef: React.RefObject = React.createRef(); - - React.useEffect(() => { - const iframe = iframeRef.current; - - if (iframe !== null) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - iframe.addEventListener("load", async () => { - await setMapStyling(iframe); - setState(prevState => ({ ...prevState, type: "loaded" })); - }); - } - }, [iframeRef]); - - const isLoading = state.type === "loading"; - - return ( - - -
- -
-
-
- ); -}); - -const MapEditorIFrame = styled.iframe``; - -const styles: Record = { - wrapperVisible: { width: "100%", height: "80vh" }, - wrapperHidden: { visibility: "hidden", width: "100%" }, -}; - -function removeUndefinedProperties(obj: T): Partial { - return Object.entries(obj).reduce((acc, [key, value]) => { - return value === undefined ? acc : { ...acc, [key]: value }; - }, {} as Partial); -} - -function waitforDocumentToLoad(iframeDocument: Document, selector: string) { - return new Promise(resolve => { - const check = () => { - if (iframeDocument.querySelector(selector)) { - resolve(undefined); - } else { - setTimeout(check, 1000); - } - }; - check(); - }); -} - -function waitforElementToLoad(element: HTMLElement | Document, selector: string) { - return new Promise(resolve => { - const check = () => { - if (element.querySelector(selector)) { - resolve(undefined); - } else { - setTimeout(check, 1000); - } - }; - check(); - }); -} - -async function setMapStyling(iframe: HTMLIFrameElement) { - if (!iframe.contentWindow) return; - const iframeDocument = iframe.contentWindow.document; - - await waitforDocumentToLoad(iframeDocument, "#dhis2-app-root"); - await waitforElementToLoad(iframeDocument, "header"); - await waitforElementToLoad(iframeDocument, ".dhis2-map-container-wrapper"); - - const iFrameRoot = iframeDocument.querySelector("#dhis2-app-root"); - - iframeDocument.querySelectorAll("header").forEach(el => el.remove()); - iFrameRoot?.querySelectorAll("header").forEach(el => el.remove()); - - iframeDocument.querySelectorAll(".app-menu-container").forEach(el => el.remove()); - iFrameRoot?.querySelectorAll(".app-menu-container").forEach(el => el.remove()); - - iframeDocument.querySelectorAll(".layers-toggle-container").forEach(el => el.remove()); - iFrameRoot?.querySelectorAll(".layers-toggle-container").forEach(el => el.remove()); - - iframeDocument.querySelectorAll(".layers-panel-drawer").forEach(el => el.remove()); - iFrameRoot?.querySelectorAll(".layers-panel-drawer").forEach(el => el.remove()); - - const mapContainerWrapper = iframeDocument.querySelector( - ".dhis2-map-container-wrapper" - ); - if (mapContainerWrapper) mapContainerWrapper.style.inset = "0px"; -} diff --git a/src/webapp/components/map/MapSection.tsx b/src/webapp/components/map/MapSection.tsx index ebf58741..8c966ece 100644 --- a/src/webapp/components/map/MapSection.tsx +++ b/src/webapp/components/map/MapSection.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from "react"; import styled from "styled-components"; import { useSnackbar } from "@eyeseetea/d2-ui-components"; -import { Map } from "./Map"; +import { Visualisation } from "../visualisation/Visualisation"; import { useMap } from "./useMap"; import { MapKey } from "../../../domain/entities/MapConfig"; import LoaderContainer from "../loader/LoaderContainer"; @@ -18,6 +18,7 @@ type MapSectionProps = { }; export const MapSection: React.FC = React.memo(props => { + const { api } = useAppContext(); const { mapKey, singleSelectFilters, @@ -44,27 +45,59 @@ export const MapSection: React.FC = React.memo(props => { eventHazardCode: eventHazardCode, }); + const baseUrl = `${api.baseUrl}/api/apps/zebra-custom-maps-app/index.html`; + const [mapUrl, setMapUrl] = React.useState(); + useEffect(() => { if (mapConfigState.kind === "error") { snackbar.error(mapConfigState.message); + } else if (mapConfigState.kind === "loaded") { + const config = mapConfigState.data; + const params = { + currentApp: config.currentApp, + currentPage: config.currentPage, + zebraNamespace: config.zebraNamespace, + dashboardDatastoreKey: config.dashboardDatastoreKey, + id: config.mapId, + orgUnits: config.orgUnits.join(","), + programIndicatorId: config.programIndicatorId, + programIndicatorName: config.programIndicatorName, + programId: config.programId, + programName: config.programName, + startDate: config.startDate, + endDate: config.endDate, + timeField: config.timeField, + }; + const srcUrl = + baseUrl + "?" + new URLSearchParams(removeUndefinedProperties(params)).toString(); + setMapUrl(srcUrl); } - }, [mapConfigState, snackbar]); + }, [baseUrl, mapConfigState, snackbar]); if (mapConfigState.kind === "error") { return
{mapConfigState.message}
; } + function removeUndefinedProperties(obj: T): Partial { + return Object.entries(obj).reduce((acc, [key, value]) => { + return value === undefined ? acc : { ...acc, [key]: value }; + }, {} as Partial); + } + return ( - {mapConfigState.kind === "loaded" && allProvincesIds.length !== 0 ? ( - ) : null} diff --git a/src/webapp/components/table/statistic-table/StatisticTable.tsx b/src/webapp/components/table/statistic-table/StatisticTable.tsx index 15b33579..893f361c 100644 --- a/src/webapp/components/table/statistic-table/StatisticTable.tsx +++ b/src/webapp/components/table/statistic-table/StatisticTable.tsx @@ -19,9 +19,10 @@ import { ColoredCell } from "./ColoredCell"; import { CalculationRow } from "./CalculationRow"; import { Order } from "../../../pages/dashboard/usePerformanceOverview"; import { Option } from "../../utils/option"; -import { Id } from "../../../../domain/entities/Ref"; import { Maybe } from "../../../../utils/ts-utils"; import { PerformanceOverviewMetrics } from "../../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; +import { Link } from "react-router-dom"; +import { RouteName, useRoutes } from "../../../hooks/useRoutes"; export type TableColumn = { value: string; @@ -53,7 +54,6 @@ export type StatisticTableProps = { filters: FiltersConfig[]; order: Maybe; setOrder: Dispatch>>; - goToEvent: (id: Maybe) => void; }; export const StatisticTable: React.FC = React.memo( @@ -65,8 +65,9 @@ export const StatisticTable: React.FC = React.memo( filters: filtersConfig, order, setOrder, - goToEvent, }) => { + const { generatePath } = useRoutes(); + const calculateColumns = [...editRiskAssessmentColumns, ...Object.keys(columnRules)]; const { searchTerm, setSearchTerm, filters, setFilters, filteredRows, filterOptions } = @@ -151,11 +152,21 @@ export const StatisticTable: React.FC = React.memo( /> ) : ( goToEvent(row.id)} key={`${rowIndex}-${column.value}`} $link={columnIndex === 0} > - {row[column.value] || ""} + {row.id ? ( + + {row[column.value]} + + ) : ( + row[column.value] + )} ) )} @@ -218,6 +229,15 @@ const StyledTableCell = styled(TableCell)<{ $link?: boolean }>` font-weight: ${props => (props.$link ? "600" : "initial")}; `; +const StyledLink = styled(Link)<{ $link?: boolean }>` + text-decoration: ${props => (props.$link ? "underline" : "none")}; + cursor: ${props => (props.$link ? "pointer" : "initial")}; + font-weight: ${props => (props.$link ? "600" : "initial")}; + color: ${props => props.theme.palette.text.primary}; + width: 100%; + height: 100%; +`; + const Container = styled.div` display: flex; justify-content: space-between; diff --git a/src/webapp/components/visualisation/Visualisation.tsx b/src/webapp/components/visualisation/Visualisation.tsx index 7f6bf966..a7e2fa7f 100644 --- a/src/webapp/components/visualisation/Visualisation.tsx +++ b/src/webapp/components/visualisation/Visualisation.tsx @@ -1,36 +1,174 @@ import React from "react"; -import { VisualizationTypes } from "../../pages/event-tracker/EventTrackerPage"; import styled from "styled-components"; -import { Section } from "../section/Section"; +import LoaderContainer from "../loader/LoaderContainer"; type VisualisationProps = { - type: VisualizationTypes; - title: string; - hasSeparator?: boolean; - lastUpdated?: string; + type: "map" | "chart"; + srcUrl: string; }; + +type State = { + type: "loading" | "loaded"; +}; + export const Visualisation: React.FC = React.memo(props => { - const { title, hasSeparator, lastUpdated } = props; + const { srcUrl, type } = props; + + const [state, setState] = React.useState({ type: "loading" }); + + const iframeRef: React.RefObject = React.createRef(); + + React.useEffect(() => { + console.debug(`Loading ${type} visualisation from ${srcUrl}`); + const iframe = iframeRef.current; + + if (iframe !== null) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + iframe.addEventListener("load", async () => { + if (type === "map") { + await setMapStyling(iframe); + setState(prevState => ({ ...prevState, type: "loaded" })); + } else { + if (srcUrl.includes("dhis-web-data-visualizer")) { + await setChartStyling(iframe); + setState(prevState => ({ ...prevState, type: "loaded" })); + } else { + await setEventChartStyling(iframe); + setState(prevState => ({ ...prevState, type: "loaded" })); + } + } + }); + } + }, [iframeRef, srcUrl, type]); + + const isLoading = state.type === "loading"; + return ( -
- {`Coming soon!`} -
+ + +
+ +
+
+
); }); -const VisualisationContainer = styled.div` - width: 100%; - height: 25rem; - border: 0.1rem solid ${props => props.theme.palette.divider}; - background: ${props => props.theme.palette.background.paper}; - color: ${props => props.theme.palette.text.disabled}; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 2rem; -`; +const VisualisationIFrame = styled.iframe``; + +const styles: Record = { + wrapperVisible: { width: "100%", height: "80vh" }, + wrapperHidden: { visibility: "hidden", width: "100%" }, +}; + +function waitforDocumentToLoad(iframeDocument: Document, selector: string) { + return new Promise(resolve => { + const check = () => { + if (iframeDocument.querySelector(selector)) { + resolve(undefined); + } else { + setTimeout(check, 1000); + } + }; + check(); + }); +} + +function waitforElementToLoad(element: HTMLElement | Document, selector: string) { + return new Promise(resolve => { + const check = () => { + if (element.querySelector(selector)) { + resolve(undefined); + } else { + setTimeout(check, 1000); + } + }; + check(); + }); +} + +async function setMapStyling(iframe: HTMLIFrameElement) { + if (!iframe.contentWindow) return; + const iframeDocument = iframe.contentWindow.document; + + await waitforDocumentToLoad(iframeDocument, "#dhis2-app-root"); + await waitforElementToLoad(iframeDocument, "header"); + await waitforElementToLoad(iframeDocument, ".dhis2-map-container-wrapper"); + + const iFrameRoot = iframeDocument.querySelector("#dhis2-app-root"); + + iframeDocument.querySelectorAll("header").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll("header").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".app-menu-container").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".app-menu-container").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".layers-toggle-container").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".layers-toggle-container").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".layers-panel-drawer").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".layers-panel-drawer").forEach(el => el.remove()); + + const mapContainerWrapper = iframeDocument.querySelector( + ".dhis2-map-container-wrapper" + ); + if (mapContainerWrapper) mapContainerWrapper.style.inset = "0px"; +} + +async function setChartStyling(iframe: HTMLIFrameElement) { + if (!iframe.contentWindow) return; + const iframeDocument = iframe.contentWindow.document; + + await waitforDocumentToLoad(iframeDocument, "#dhis2-app-root"); + await waitforElementToLoad(iframeDocument, "header"); + await waitforElementToLoad(iframeDocument, ".data-visualizer-app"); + + const iFrameRoot = iframeDocument.querySelector("#dhis2-app-root"); + + iframeDocument.querySelectorAll("header").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll("header").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".main-left").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".main-left").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".section-toolbar").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".section-toolbar").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".main-center-layout").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".main-center-layout").forEach(el => el.remove()); + + iframeDocument.querySelectorAll(".main-center-titlebar").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".main-center-titlebar").forEach(el => el.remove()); +} + +async function setEventChartStyling(iframe: HTMLIFrameElement) { + if (!iframe.contentWindow) return; + const iframeDocument = iframe.contentWindow.document; + + await waitforDocumentToLoad(iframeDocument, "#viewport-1316-embedded-center"); + await waitforDocumentToLoad(iframeDocument, ".x-box-inner"); + + const iFrameRoot = iframeDocument.querySelector("#viewport-1316-embedded-center"); + console.debug(`iFrameRoot: ${iFrameRoot}`); + + console.debug(` toolbar-north : ${iframeDocument.querySelectorAll(".toolbar-north")}`); + iframeDocument.querySelectorAll(".toolbar-north").forEach(el => el.remove()); + iFrameRoot?.querySelectorAll(".toolbar-north").forEach(el => el.remove()); + + console.debug(`#panel-1305 : ${iframeDocument.querySelectorAll("#panel-1305")}`); + iframeDocument.querySelectorAll("#panel-1305").forEach(el => { + console.debug(`Removing element: ${el}`); + el.remove(); + }); + iFrameRoot?.querySelectorAll("#panel-1305").forEach(el => el.remove()); + + const eventChartContainer = iframeDocument.querySelector("#panel-1310"); + console.debug(`eventChartContainer: ${eventChartContainer}`); + if (eventChartContainer) eventChartContainer.style.inset = "0px"; +} diff --git a/src/webapp/hooks/useLastAnalyticsRuntime.ts b/src/webapp/hooks/useLastAnalyticsRuntime.ts new file mode 100644 index 00000000..830aad76 --- /dev/null +++ b/src/webapp/hooks/useLastAnalyticsRuntime.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; +import { useAppContext } from "../contexts/app-context"; + +export function useLastAnalyticsRuntime() { + const { compositionRoot } = useAppContext(); + const [lastAnalyticsRuntime, setLastAnalyticsRuntime] = useState(); + + useEffect(() => { + compositionRoot.performanceOverview.getAnalyticsRuntime.execute().run( + analyticsRuntime => { + setLastAnalyticsRuntime(analyticsRuntime); + }, + err => { + console.debug(err); + } + ); + }, [compositionRoot.performanceOverview.getAnalyticsRuntime]); + + return { lastAnalyticsRuntime }; +} diff --git a/src/webapp/hooks/useRoutes.ts b/src/webapp/hooks/useRoutes.ts index f7285bd8..bfbcc61a 100644 --- a/src/webapp/hooks/useRoutes.ts +++ b/src/webapp/hooks/useRoutes.ts @@ -1,6 +1,6 @@ import React from "react"; import { join } from "string-ts"; -import { generatePath, useHistory } from "react-router-dom"; +import { generatePath as generatePathRRD, useHistory } from "react-router-dom"; import { FormType } from "../pages/form-page/FormPage"; @@ -43,16 +43,29 @@ type RouteParams = { [RouteName.DASHBOARD]: undefined; }; -export function useRoutes() { +type State = { + goTo: (route: T, params?: RouteParams[T]) => void; + generatePath: (route: T, params?: RouteParams[T]) => string; +}; + +export function useRoutes(): State { const history = useHistory(); const goTo = React.useCallback( (route: T, params?: RouteParams[T]) => { - const path = generatePath(routes[route], params as any); + const path = generatePathRRD(routes[route], params as any); history.push(path); }, [history] ); - return { goTo }; + const generatePath = React.useCallback( + (route: T, params?: RouteParams[T]) => { + const path = generatePathRRD(routes[route], params as any); + return path; + }, + [] + ); + + return { goTo, generatePath }; } diff --git a/src/webapp/pages/dashboard/DashboardPage.tsx b/src/webapp/pages/dashboard/DashboardPage.tsx index cc69015f..6dbeadeb 100644 --- a/src/webapp/pages/dashboard/DashboardPage.tsx +++ b/src/webapp/pages/dashboard/DashboardPage.tsx @@ -9,14 +9,15 @@ import { useCardCounts } from "./useCardCounts"; import { StatsCard } from "../../components/stats-card/StatsCard"; import styled from "styled-components"; import { MultipleSelector } from "../../components/selector/MultipleSelector"; -import { Id } from "../../../domain/entities/Ref"; -import { Maybe } from "../../../utils/ts-utils"; import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; -import { RouteName, useRoutes } from "../../hooks/useRoutes"; import { useAlertsActiveVerifiedFilters } from "./useAlertsActiveVerifiedFilters"; import { MapSection } from "../../components/map/MapSection"; import { Selector } from "../../components/selector/Selector"; import { DateRangePicker } from "../../components/date-picker/DateRangePicker"; +import { PerformanceMetric717, use717Performance } from "./use717Performance"; +import { Loader } from "../../components/loader/Loader"; +import { useLastAnalyticsRuntime } from "../../hooks/useLastAnalyticsRuntime"; +import LoaderContainer from "../../components/loader/LoaderContainer"; export const DashboardPage: React.FC = React.memo(() => { const { @@ -36,29 +37,32 @@ export const DashboardPage: React.FC = React.memo(() => { setOrder, columnRules, editRiskAssessmentColumns, + isLoading: performanceOverviewLoading, } = usePerformanceOverview(); - const { cardCounts } = useCardCounts( + const { performanceMetrics717, isLoading: _717CardsLoading } = use717Performance("dashboard"); + const { cardCounts, isLoading: cardCountsLoading } = useCardCounts( singleSelectFilters, multiSelectFilters, dateRangeFilter.value ); - const { goTo } = useRoutes(); const { resetCurrentEventTracker: resetCurrentEventTrackerId } = useCurrentEventTracker(); + const { lastAnalyticsRuntime } = useLastAnalyticsRuntime(); useEffect(() => { //On navigating to the dashboard page, reset the current event tracker id resetCurrentEventTrackerId(); }); - const goToEvent = (id: Maybe) => { - if (!id) return; - goTo(RouteName.EVENT_TRACKER, { id }); - }; - - return ( - + return performanceOverviewLoading || _717CardsLoading ? ( + + ) : ( +
{selectorFiltersConfig.map(({ id, label, placeholder, options, type }) => { @@ -100,16 +104,18 @@ export const DashboardPage: React.FC = React.memo(() => { /> - - {cardCounts.map((cardCount, index) => ( - - ))} - + + + {cardCounts.map((cardCount, index) => ( + + ))} + +
{ dateRangeFilter={dateRangeFilter.value} />
-
TBD
+
+ + {performanceMetrics717.map( + (perfMetric717: PerformanceMetric717, index: number) => ( + + ) + )} + +
{ setOrder={setOrder} columnRules={columnRules} editRiskAssessmentColumns={editRiskAssessmentColumns} - goToEvent={goToEvent} />
@@ -138,14 +159,14 @@ export const DashboardPage: React.FC = React.memo(() => { ); }); -const GridWrapper = styled.div` +export const GridWrapper = styled.div` width: 100%; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; `; -const StyledStatsCard = styled(StatsCard)` +export const StyledStatsCard = styled(StatsCard)` width: 220px; `; diff --git a/src/webapp/pages/dashboard/use717Performance.ts b/src/webapp/pages/dashboard/use717Performance.ts new file mode 100644 index 00000000..4fe86186 --- /dev/null +++ b/src/webapp/pages/dashboard/use717Performance.ts @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useState } from "react"; +import { useAppContext } from "../../contexts/app-context"; +import _ from "../../../domain/entities/generic/Collection"; +import { StatsCardProps } from "../../components/stats-card/StatsCard"; +import { PerformanceMetrics717 } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; +import { Id } from "../../../domain/entities/Ref"; + +type CardColors = StatsCardProps["color"]; + +export type PerformanceMetric717 = { + title: string; + primaryValue: number; + secondaryValue: number; + color: CardColors; +}; +export type PerformanceMetric717State = { + performanceMetrics717: PerformanceMetric717[]; + isLoading: boolean; +}; + +export type Order = { name: string; direction: "asc" | "desc" }; + +export function use717Performance( + type: "dashboard" | "event_tracker", + diseaseOutbreakEventId?: Id +): PerformanceMetric717State { + const { compositionRoot } = useAppContext(); + + const [performanceMetrics717, setPerformanceMetrics717] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const getColor = useCallback((key: string, value: number): CardColors => { + if (key === "allTargets") { + return "grey"; + } else if (value >= 50) { + return "green"; + } else if (value > 0) { + return "red"; + } + return "normal"; + }, []); + + const transformData = useCallback( + (performanceMetrics: PerformanceMetrics717[]) => { + const performanceMetricsByName = _(performanceMetrics).reduce( + (acc: Record, indicator) => { + const key = indicator.name; + const existingGroup = acc[key] || []; + acc[key] = [...existingGroup, indicator]; + return acc; + }, + {} as Record + ); + return Object.entries(performanceMetricsByName).map(([key, values]) => { + const primaryValue = values.find(item => item.type === "primary")?.value ?? 0; + const secondaryValue = values.find(item => item.type === "secondary")?.value ?? 0; + + const title = key + .replace(/([A-Z])/g, match => ` ${match}`) + .replace(/^./, match => match.toUpperCase()) + .trim(); + return { + title: title, + primaryValue: primaryValue, + secondaryValue: secondaryValue, + color: getColor(key, primaryValue), + }; + }); + }, + [getColor] + ); + + useEffect(() => { + setIsLoading(true); + compositionRoot.performanceOverview.get717Performance + .execute(type, diseaseOutbreakEventId) + .run( + performanceMetrics717 => { + setPerformanceMetrics717(transformData(performanceMetrics717)); + setIsLoading(false); + }, + error => { + console.error({ error }); + setIsLoading(false); + } + ); + }, [ + compositionRoot.performanceOverview.get717Performance, + diseaseOutbreakEventId, + transformData, + type, + ]); + + return { + performanceMetrics717, + isLoading, + }; +} diff --git a/src/webapp/pages/event-tracker/EventTrackerPage.tsx b/src/webapp/pages/event-tracker/EventTrackerPage.tsx index 85b1484b..f77d2c6b 100644 --- a/src/webapp/pages/event-tracker/EventTrackerPage.tsx +++ b/src/webapp/pages/event-tracker/EventTrackerPage.tsx @@ -7,10 +7,9 @@ import { AddCircleOutline, EditOutlined } from "@material-ui/icons"; import i18n from "../../../utils/i18n"; import { Layout } from "../../components/layout/Layout"; import { FormSummary } from "../../components/form/form-summary/FormSummary"; -import { Visualisation } from "../../components/visualisation/Visualisation"; +import { Chart } from "../../components/chart/Chart"; import { Section } from "../../components/section/Section"; import { BasicTable, TableColumn } from "../../components/table/BasicTable"; -import { getDateAsLocaleDateTimeString } from "../../../data/repositories/utils/DateTimeHelper"; import { useDiseaseOutbreakEvent } from "./useDiseaseOutbreakEvent"; import { RouteName, useRoutes } from "../../hooks/useRoutes"; import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; @@ -18,16 +17,12 @@ import { MapSection } from "../../components/map/MapSection"; import LoaderContainer from "../../components/loader/LoaderContainer"; import { useMapFilters } from "./useMapFilters"; import { DateRangePicker } from "../../components/date-picker/DateRangePicker"; - -// TODO: Add every section here -export type VisualizationTypes = - | "ALL_EVENTS_MAP" - | "EVENT_TRACKER_AREAS_AFFECTED_MAP" - | "RISK_ASSESSMENT_HISTORY_LINE_CHART" - | "EVENT_TRACKER_CASES_BAR_CHART" - | "EVENT_TRACKER_DEATHS_BAR_CHART" - | "EVENT_TRACKER_OVERVIEW_CARDS" - | "EVENT_TRACKER_717_CARDS"; +import { NoticeBox } from "../../components/notice-box/NoticeBox"; +import { PerformanceMetric717, use717Performance } from "../dashboard/use717Performance"; +import { GridWrapper, StyledStatsCard } from "../dashboard/DashboardPage"; +import { StatsCard } from "../../components/stats-card/StatsCard"; +import { useLastAnalyticsRuntime } from "../../hooks/useLastAnalyticsRuntime"; +import { useOverviewCards } from "./useOverviewCards"; //TO DO : Create Risk assessment section export const riskAssessmentColumns: TableColumn[] = [ @@ -52,6 +47,10 @@ export const EventTrackerPage: React.FC = React.memo(() => { useDiseaseOutbreakEvent(id); const { changeCurrentEventTracker: changeCurrentEventTrackerId, getCurrentEventTracker } = useCurrentEventTracker(); + const currentEventTracker = getCurrentEventTracker(); + const { lastAnalyticsRuntime } = useLastAnalyticsRuntime(); + + const { overviewCards, isLoading: areOverviewCardsLoading } = useOverviewCards(); const { dateRangeFilter } = useMapFilters(); @@ -61,25 +60,24 @@ export const EventTrackerPage: React.FC = React.memo(() => { }); }, [goTo]); + const { performanceMetrics717, isLoading: _717CardsLoading } = use717Performance( + "event_tracker", + id + ); + useEffect(() => { if (eventTrackerDetails) changeCurrentEventTrackerId(eventTrackerDetails); }, [changeCurrentEventTrackerId, eventTrackerDetails, id]); - const lastUpdated = getDateAsLocaleDateTimeString(new Date()); //TO DO : Fetch sync time from datastore once implemented return ( - + -
+
{ @@ -131,28 +130,76 @@ export const EventTrackerPage: React.FC = React.memo(() => { ) } titleVariant="secondary" - lastUpdated={lastUpdated} > - + {riskAssessmentRows.length > 0 ? ( + + ) : ( + + {i18n.t("Risks associated with this event have not yet been assessed.")} + + )} + {!!currentEventTracker?.riskAssessment?.grading?.length && ( + + )}
- - - - - + + {overviewCards?.map((card, index) => ( + + ))} + +
+ +
+ + +
+
+ titleVariant="secondary" + > + + {performanceMetrics717.map( + (perfMetric: PerformanceMetric717, index: number) => ( + + ) + )} + +
); }); diff --git a/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts b/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts index 1c967931..e3daee54 100644 --- a/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts +++ b/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts @@ -71,7 +71,9 @@ export function useDiseaseOutbreakEvent(id: Id) { summary: [ { label: "Last updated", - value: getDateAsLocaleDateTimeString(diseaseOutbreakEvent.lastUpdated), + value: diseaseOutbreakEvent.lastUpdated + ? getDateAsLocaleDateTimeString(diseaseOutbreakEvent.lastUpdated) + : "", }, dataSourceLabelValue, { @@ -103,7 +105,9 @@ export function useDiseaseOutbreakEvent(id: Id) { ) => { if (diseaseOutbreakEvent.riskAssessment) { return diseaseOutbreakEvent.riskAssessment.grading.map(riskAssessmentGrading => ({ - riskAssessmentDate: getDateAsLocaleDateString(riskAssessmentGrading.lastUpdated), + riskAssessmentDate: riskAssessmentGrading.lastUpdated + ? getDateAsLocaleDateString(riskAssessmentGrading.lastUpdated) + : "", grade: RiskAssessmentGrading.getTranslatedLabel( riskAssessmentGrading.getGrade().getOrThrow() ), diff --git a/src/webapp/pages/event-tracker/useOverviewCards.ts b/src/webapp/pages/event-tracker/useOverviewCards.ts new file mode 100644 index 00000000..1ef02b7c --- /dev/null +++ b/src/webapp/pages/event-tracker/useOverviewCards.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { useAppContext } from "../../contexts/app-context"; +import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; +import { OverviewCard } from "../../../domain/entities/PerformanceOverview"; + +export function useOverviewCards() { + const { compositionRoot } = useAppContext(); + const [overviewCards, setOverviewCards] = useState(); + const [isLoading, setIsLoading] = useState(false); + const { getCurrentEventTracker } = useCurrentEventTracker(); + const currentEventTracker = getCurrentEventTracker(); + + useEffect(() => { + const type = currentEventTracker?.suspectedDiseaseCode || currentEventTracker?.hazardType; + if (type) { + setIsLoading(true); + compositionRoot.performanceOverview.getOverviewCards.execute(type).run( + overviewCards => { + setIsLoading(false); + setOverviewCards(overviewCards); + }, + err => { + setIsLoading(false); + console.error(err); + } + ); + } + }, [compositionRoot.performanceOverview.getOverviewCards, currentEventTracker]); + + return { + overviewCards, + isLoading, + }; +} diff --git a/src/webapp/pages/form-page/risk-assessment/mapRiskAssessmentToInitialFormState.ts b/src/webapp/pages/form-page/risk-assessment/mapRiskAssessmentToInitialFormState.ts index 8b01b708..1c52f5ac 100644 --- a/src/webapp/pages/form-page/risk-assessment/mapRiskAssessmentToInitialFormState.ts +++ b/src/webapp/pages/form-page/risk-assessment/mapRiskAssessmentToInitialFormState.ts @@ -12,7 +12,6 @@ import { FormState } from "../../../components/form/FormState"; import { User } from "../../../components/user-selector/UserSelector"; import { Option as UIOption } from "../../../components/utils/option"; import { mapTeamMemberToUser, mapToPresentationOptions } from "../mapEntityToFormState"; -import { getDateAsLocaleDateTimeString } from "../../../../data/repositories/utils/DateTimeHelper"; import { FormSectionState } from "../../../components/form/FormSectionsState"; import { RiskAssessmentQuestionnaire } from "../../../../domain/entities/risk-assessment/RiskAssessmentQuestionnaire"; import { Maybe } from "../../../../utils/ts-utils"; @@ -49,10 +48,6 @@ export function mapRiskGradingToInitialFormState( id: "", title: "Risk Assessment Grading", subtitle: riskFormaData.eventTrackerDetails.name, - titleDescripton: "Last updated", - subtitleDescripton: getDateAsLocaleDateTimeString( - riskFormaData.eventTrackerDetails.lastUpdated - ), saveButtonLabel: "Save", isValid: false, sections: [ @@ -260,10 +255,6 @@ export function mapRiskAssessmentSummaryToInitialFormState( id: riskAssessmentSummary?.id ?? "", title: "Risk Assessment Summary", subtitle: riskFormaData.eventTrackerDetails.name, - titleDescripton: "Last updated", - subtitleDescripton: getDateAsLocaleDateTimeString( - riskFormaData.eventTrackerDetails.lastUpdated - ), saveButtonLabel: "Save & Continue", isValid: riskAssessmentSummary ? true : false, sections: [