From 2e97e67c70cf4d7efebee6cdf47cc21d5aa20e65 Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Tue, 7 Jan 2025 17:11:13 +0100 Subject: [PATCH] Add Alerts Performance overview table --- src/CompositionRoot.ts | 7 +- src/data/repositories/OrgUnitD2Repository.ts | 7 +- .../PerformanceOverviewD2Repository.ts | 119 +++++++++++- .../AlertsPerformanceOverviewConstants.ts | 25 +++ .../consts/DiseaseOutbreakConstants.ts | 1 + .../consts/PerformanceOverviewConstants.ts | 1 + .../test/PerformanceOverviewTestRepository.ts | 105 +++++----- src/domain/entities/OrgUnit.ts | 9 +- .../alert/AlertsPerformanceOverviewMetrics.ts | 23 +++ .../PerformanceOverviewMetrics.ts | 1 + .../PerformanceOverviewRepository.ts | 4 +- ...AlertsPerformanceOverviewMetricsUseCase.ts | 15 ++ ...ionalPerformanceOverviewMetricsUseCase.ts} | 4 +- .../components/pagination/Pagination.tsx | 31 +++ .../table/statistic-table/StatisticTable.tsx | 126 +++++++----- .../table/statistic-table/useTableFilters.ts | 18 +- src/webapp/pages/dashboard/DashboardPage.tsx | 52 +++-- .../dashboard/useAlertsPerformanceOverview.ts | 182 ++++++++++++++++++ .../useNationalPerformanceOverview.ts | 178 +++++++++++++++++ .../pages/dashboard/usePerformanceOverview.ts | 149 -------------- .../dashboard/usePerformanceOverviewTable.ts | 108 +++++++++++ src/webapp/pages/form-page/useForm.ts | 6 +- 22 files changed, 891 insertions(+), 280 deletions(-) create mode 100644 src/data/repositories/consts/AlertsPerformanceOverviewConstants.ts create mode 100644 src/domain/entities/alert/AlertsPerformanceOverviewMetrics.ts create mode 100644 src/domain/usecases/GetAllAlertsPerformanceOverviewMetricsUseCase.ts rename src/domain/usecases/{GetAllPerformanceOverviewMetricsUseCase.ts => GetAllNationalPerformanceOverviewMetricsUseCase.ts} (89%) create mode 100644 src/webapp/components/pagination/Pagination.tsx create mode 100644 src/webapp/pages/dashboard/useAlertsPerformanceOverview.ts create mode 100644 src/webapp/pages/dashboard/useNationalPerformanceOverview.ts delete mode 100644 src/webapp/pages/dashboard/usePerformanceOverview.ts create mode 100644 src/webapp/pages/dashboard/usePerformanceOverviewTable.ts diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index 59707b48..7df8417a 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -35,7 +35,7 @@ import { MapConfigTestRepository } from "./data/repositories/test/MapConfigTestR import { GetMapConfigUseCase } from "./domain/usecases/GetMapConfigUseCase"; import { GetProvincesOrgUnits } from "./domain/usecases/GetProvincesOrgUnits"; import { PerformanceOverviewRepository } from "./domain/repositories/PerformanceOverviewRepository"; -import { GetAllPerformanceOverviewMetricsUseCase } from "./domain/usecases/GetAllPerformanceOverviewMetricsUseCase"; +import { GetAllNationalPerformanceOverviewMetricsUseCase } from "./domain/usecases/GetAllNationalPerformanceOverviewMetricsUseCase"; import { PerformanceOverviewD2Repository } from "./data/repositories/PerformanceOverviewD2Repository"; import { PerformanceOverviewTestRepository } from "./data/repositories/test/PerformanceOverviewTestRepository"; import { AlertSyncDataStoreRepository } from "./data/repositories/AlertSyncDataStoreRepository"; @@ -73,6 +73,7 @@ import { CasesFileTestRepository } from "./data/repositories/test/CasesFileTestR import { UserGroupD2Repository } from "./data/repositories/UserGroupD2Repository"; import { UserGroupRepository } from "./domain/repositories/UserGroupRepository"; import { UserGroupTestRepository } from "./data/repositories/test/UserGroupTestRepository"; +import { GetAllAlertsPerformanceOverviewMetricsUseCase } from "./domain/usecases/GetAllAlertsPerformanceOverviewMetricsUseCase"; export type CompositionRoot = ReturnType; @@ -128,7 +129,9 @@ function getCompositionRoot(repositories: Repositories) { new DeleteIncidentManagementTeamMemberRolesUseCase(repositories), }, performanceOverview: { - getPerformanceOverviewMetrics: new GetAllPerformanceOverviewMetricsUseCase( + getNationalPerformanceOverviewMetrics: + new GetAllNationalPerformanceOverviewMetricsUseCase(repositories), + getAlertsPerformanceOverviewMetrics: new GetAllAlertsPerformanceOverviewMetricsUseCase( repositories ), getTotalCardCounts: new GetTotalCardCountsUseCase(repositories), diff --git a/src/data/repositories/OrgUnitD2Repository.ts b/src/data/repositories/OrgUnitD2Repository.ts index 9e662355..f47d1d34 100644 --- a/src/data/repositories/OrgUnitD2Repository.ts +++ b/src/data/repositories/OrgUnitD2Repository.ts @@ -1,14 +1,9 @@ import { D2Api, MetadataPick } from "../../types/d2-api"; -import { OrgUnit, OrgUnitLevelType } from "../../domain/entities/OrgUnit"; +import { OrgUnit, orgUnitLevelTypeByLevelNumber } from "../../domain/entities/OrgUnit"; import { Id } from "../../domain/entities/Ref"; import { OrgUnitRepository } from "../../domain/repositories/OrgUnitRepository"; import { apiToFuture, FutureData } from "../api-futures"; -const orgUnitLevelTypeByLevelNumber: Record = { - 1: "National", - 2: "Province", - 3: "District", -}; export class OrgUnitD2Repository implements OrgUnitRepository { constructor(private api: D2Api) {} diff --git a/src/data/repositories/PerformanceOverviewD2Repository.ts b/src/data/repositories/PerformanceOverviewD2Repository.ts index d03c8c3b..7bf94d49 100644 --- a/src/data/repositories/PerformanceOverviewD2Repository.ts +++ b/src/data/repositories/PerformanceOverviewD2Repository.ts @@ -2,7 +2,11 @@ import { Maybe } from "../../utils/ts-utils"; import { AnalyticsResponse, D2Api } from "../../types/d2-api"; import { PerformanceOverviewRepository } from "../../domain/repositories/PerformanceOverviewRepository"; import { apiToFuture, FutureData } from "../api-futures"; -import { RTSL_ZEBRA_PROGRAM_ID } from "./consts/DiseaseOutbreakConstants"; +import { + RTSL_ZEBRA_ALERTS_PROGRAM_ID, + RTSL_ZEBRA_ALERTS_VERIFICATION_STATUS_ID, + RTSL_ZEBRA_PROGRAM_ID, +} from "./consts/DiseaseOutbreakConstants"; import _ from "../../domain/entities/generic/Collection"; import { Future } from "../../domain/entities/generic/Future"; import { @@ -33,6 +37,13 @@ import { ProgramIndicatorsDatastore, ProgramIndicatorsDatastoreKey, } from "./common/getProgramIndicatorsFromDatastore"; +import { AlertsPerformanceOverviewMetrics } from "../../domain/entities/alert/AlertsPerformanceOverviewMetrics"; +import { + AlertsPerformanceOverviewDimensions, + AlertsPerformanceOverviewDimensionsKey, + AlertsPerformanceOverviewDimensionsValue, +} from "./consts/AlertsPerformanceOverviewConstants"; +import { orgUnitLevelTypeByLevelNumber } from "../../domain/entities/OrgUnit"; const formatDate = (date: Date): string => { const year = date.getFullYear(); @@ -55,6 +66,8 @@ const EVENT_TRACKER_PERFORMANCE_717_PROGRAM_INDICATORS_DATASTORE_KEY = const ALERTS_PERFORMANCE_717_PROGRAM_INDICATORS_DATASTORE_KEY = "alerts-717-performance-program-indicators"; const PERFORMANCE_OVERVIEW_DIMENSIONS_DATASTORE_KEY = "performance-overview-dimensions"; +const ALERTS_PERFORMANCE_OVERVIEW_DIMENSIONS_DATASTORE_KEY = + "alerts-performance-overview-dimensions"; type EventTrackerOverviewInDataStore = { key: string; @@ -429,7 +442,7 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos ); } - getPerformanceOverviewMetrics( + getNationalPerformanceOverviewMetrics( diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[] ): FutureData { return this.datastore @@ -571,6 +584,102 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos }); } + getAlertsPerformanceOverviewMetrics(): FutureData { + return this.datastore + .getObject( + ALERTS_PERFORMANCE_OVERVIEW_DIMENSIONS_DATASTORE_KEY + ) + .flatMap(nullablePerformanceOverviewDimensions => { + return assertOrError( + nullablePerformanceOverviewDimensions, + ALERTS_PERFORMANCE_OVERVIEW_DIMENSIONS_DATASTORE_KEY + ).flatMap(performanceOverviewDimensions => { + return apiToFuture( + this.api.get( + `/analytics/enrollments/query/${RTSL_ZEBRA_ALERTS_PROGRAM_ID}`, + { + dimension: [ + performanceOverviewDimensions.eventEBSId, + performanceOverviewDimensions.eventIBSId, + performanceOverviewDimensions.nationalDiseaseOutbreakEventId, + performanceOverviewDimensions.hazardType, + performanceOverviewDimensions.suspectedDisease, + performanceOverviewDimensions.cases, + performanceOverviewDimensions.deaths, + performanceOverviewDimensions.notify1d, + performanceOverviewDimensions.detect7d, + performanceOverviewDimensions.incidentManager, + performanceOverviewDimensions.respond7d, + performanceOverviewDimensions.incidentStatus, + ], + startDate: DEFAULT_START_DATE, + endDate: DEFAULT_END_DATE, + paging: false, + programStatus: "ACTIVE", + filter: `${RTSL_ZEBRA_ALERTS_VERIFICATION_STATUS_ID}:eq:RTSL_ZEB_AL_OS_VERIFICATION_VERIFIED`, + } + ) + ).flatMap(response => { + const mappedIndicators: AlertsPerformanceOverviewMetrics[] = + response.rows.map((row: string[]) => { + return Object.keys(performanceOverviewDimensions).reduce( + (acc, dimensionKey) => { + const dimension: AlertsPerformanceOverviewDimensionsValue = + performanceOverviewDimensions[ + dimensionKey as AlertsPerformanceOverviewDimensionsKey + ]; + + const index = response.headers.findIndex( + header => header.name === dimension + ); + if (dimension === "enrollmentdate") { + const duration = `${moment() + .diff(moment(row[index]), "days") + .toString()}d`; + + const inputDate = row[index]; + const formattedDate = inputDate?.split(" ")[0]; // YYYY-MM-DD + return { + ...acc, + duration: duration, + [dimensionKey]: formattedDate, + }; + } else if (dimension === "ounamehierarchy") { + const hierarchyArray = row[index]?.split("/"); + return { + ...acc, + province: + (hierarchyArray && hierarchyArray.length > 1 + ? hierarchyArray[1] + : row[index]) || "", + orgUnitType: + hierarchyArray && hierarchyArray.length > 0 + ? orgUnitLevelTypeByLevelNumber[ + hierarchyArray.length + ] || "National" + : "National", + }; + } else { + const nameValue = Object.values( + response.metaData.items + ).find(item => item.code === row[index])?.name; + + return { + ...acc, + [dimensionKey]: nameValue || row[index], + }; + } + }, + {} as AlertsPerformanceOverviewMetrics + ); + }); + + return Future.success(mappedIndicators); + }); + }); + }); + } + private getAnalyticsByIndicators(ids: Id[]): FutureData { return apiToFuture( this.api.analytics.get({ @@ -803,6 +912,12 @@ export class PerformanceOverviewD2Repository implements PerformanceOverviewRepos acc.respond7d = row[index]; break; + case "date": { + const inputDate = row[index]; + const formattedDate = inputDate?.split(" ")[0]; // YYYY-MM-DD + acc.date = formattedDate; + break; + } default: acc[key] = row[index]; break; diff --git a/src/data/repositories/consts/AlertsPerformanceOverviewConstants.ts b/src/data/repositories/consts/AlertsPerformanceOverviewConstants.ts new file mode 100644 index 00000000..f5c619bd --- /dev/null +++ b/src/data/repositories/consts/AlertsPerformanceOverviewConstants.ts @@ -0,0 +1,25 @@ +import { Id } from "../../../domain/entities/Ref"; + +export type AlertsPerformanceOverviewDimensions = { + teiId: "tei"; + eventEBSId: Id; + eventIBSId: Id; + nationalDiseaseOutbreakEventId: Id; + hazardType: Id; + suspectedDisease: Id; + orgUnit: "ouname"; + orgUnitHierarchy: "ounamehierarchy"; + cases: Id; + deaths: Id; + date: "enrollmentdate"; + notify1d: Id; + detect7d: Id; + incidentManager: Id; + respond7d: Id; + incidentStatus: Id; +}; + +export type AlertsPerformanceOverviewDimensionsKey = keyof AlertsPerformanceOverviewDimensions; + +export type AlertsPerformanceOverviewDimensionsValue = + AlertsPerformanceOverviewDimensions[AlertsPerformanceOverviewDimensionsKey]; diff --git a/src/data/repositories/consts/DiseaseOutbreakConstants.ts b/src/data/repositories/consts/DiseaseOutbreakConstants.ts index 60e8f11b..840fbc06 100644 --- a/src/data/repositories/consts/DiseaseOutbreakConstants.ts +++ b/src/data/repositories/consts/DiseaseOutbreakConstants.ts @@ -25,6 +25,7 @@ export const RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID = "Pq1d export const RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID = "agsVaIpit4S"; export const RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID = "ydsfY6zyvt7"; export const RTSL_ZEBRA_ALERTS_NATIONAL_INCIDENT_STATUS_TEA_ID = "DzGqKzjhIsz"; +export const RTSL_ZEBRA_ALERTS_VERIFICATION_STATUS_ID = "HvgldgBK8Th"; export const hazardTypeCodeMap: Record = { "Biological:Human": "RTSL_ZEB_OS_HAZARD_TYPE_BIOLOGICAL_HUMAN", diff --git a/src/data/repositories/consts/PerformanceOverviewConstants.ts b/src/data/repositories/consts/PerformanceOverviewConstants.ts index ae9946e0..a1e63ce9 100644 --- a/src/data/repositories/consts/PerformanceOverviewConstants.ts +++ b/src/data/repositories/consts/PerformanceOverviewConstants.ts @@ -22,6 +22,7 @@ export type PerformanceOverviewDimensions = { suspectedDisease: Id; hazardType: Id; nationalIncidentStatus: "incidentStatus"; + date: "enrollmentdate"; }; type EventTrackerCountIndicatorBase = { diff --git a/src/data/repositories/test/PerformanceOverviewTestRepository.ts b/src/data/repositories/test/PerformanceOverviewTestRepository.ts index 986c637c..c5a8f8cc 100644 --- a/src/data/repositories/test/PerformanceOverviewTestRepository.ts +++ b/src/data/repositories/test/PerformanceOverviewTestRepository.ts @@ -1,3 +1,4 @@ +import { AlertsPerformanceOverviewMetrics } from "../../../domain/entities/alert/AlertsPerformanceOverviewMetrics"; import { PerformanceMetrics717 } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; import { Future } from "../../../domain/entities/generic/Future"; import { OverviewCard } from "../../../domain/entities/PerformanceOverview"; @@ -23,7 +24,7 @@ export class PerformanceOverviewTestRepository implements PerformanceOverviewRep return Future.success(0); } - getPerformanceOverviewMetrics(): FutureData { + getNationalPerformanceOverviewMetrics(): FutureData { return Future.success([ { id: "JPenxAnjdhY", @@ -76,56 +77,70 @@ export class PerformanceOverviewTestRepository implements PerformanceOverviewRep detect7d: "", notify1d: "", }, + ]); + } + + getAlertsPerformanceOverviewMetrics(): FutureData { + return Future.success([ { - id: "EtoUrZCn8mP", - manager: "user, dev (dev.user)", - creationDate: "2024-08-22 15:12:06.016", - province: "zm Zambia Ministry of Health", + cases: "1", + teiId: "n3zdthapNzC", + deaths: "3", + province: " ce Central Province ", + orgUnit: "Central Province", + orgUnitType: "Province", + detect7d: "", + duration: "158d", + notify1d: "3", + respond7d: "-2", + eventEBSId: "", + eventIBSId: "OUTBREAK_000005", + hazardType: "", + incidentStatus: "Watch", + incidentManager: "etst", suspectedDisease: "COVID19", - event: "Cholera Aug 2024", - era1: "15", - era2: "14", - era3: "", - era4: "14", - era5: "", - era6: "", - era7: "14", - detect7d: "2", - notify1d: "1", + nationalDiseaseOutbreakEventId: "tnhWg7zKmNF", + date: "2024-08-27", }, { - id: "HNiwOkH3vdJ", - manager: "user, dev (dev.user)", - creationDate: "2024-08-22 13:29:27.734", - province: "zm Zambia Ministry of Health", - suspectedDisease: "Anthrax", - event: "Anthrax July 2024", - era1: "29", - era2: "28", - era3: "", - era4: "", - era5: "", - era6: "", - era7: "17", - detect7d: "10", - notify1d: "21", + cases: "22", + teiId: "ZJylQsTku6z", + deaths: "1", + province: " ce Central Province ", + orgUnit: "Central Province", + orgUnitType: "Province", + detect7d: "", + duration: "139d", + notify1d: "1", + respond7d: "-1", + eventEBSId: "", + eventIBSId: "OUTBREAK_000011", + hazardType: "", + incidentStatus: "Watch", + incidentManager: "Zebra Test 1", + suspectedDisease: "COVID19", + nationalDiseaseOutbreakEventId: "tnhWg7zKmNF", + date: "2024-08-27", }, { - id: "qrezSaY5G0U", - manager: "user, dev (dev.user)", - creationDate: "2024-08-22 13:25:24.505", - province: "zm Zambia Ministry of Health", - suspectedDisease: "Measles", - event: "Measles June 2024", - era1: "63", - era2: "55", - era3: "", - era4: "", - era5: "", - era6: "", - era7: "53", - detect7d: "3", - notify1d: "2", + cases: "5", + teiId: "cOWooRCVHqa", + deaths: "5", + province: " ce Central Province ", + orgUnit: "Central Province", + orgUnitType: "Province", + detect7d: "2", + duration: "111d", + notify1d: "1", + respond7d: "-1", + eventEBSId: "12345", + eventIBSId: "", + hazardType: "Biological: Animal", + incidentStatus: "Alert", + incidentManager: "Zebra Test 2", + suspectedDisease: "Acute VHF", + nationalDiseaseOutbreakEventId: "LALS50e9Zea", + date: "2024-08-27", }, ]); } diff --git a/src/domain/entities/OrgUnit.ts b/src/domain/entities/OrgUnit.ts index 0655de7a..c93be459 100644 --- a/src/domain/entities/OrgUnit.ts +++ b/src/domain/entities/OrgUnit.ts @@ -1,7 +1,14 @@ import { CodedNamedRef } from "./Ref"; -export type OrgUnitLevelType = "National" | "Province" | "District"; +export type OrgUnitLevelType = "National" | "Province" | "District" | "Health Facility"; export type OrgUnit = CodedNamedRef & { level: OrgUnitLevelType; }; + +export const orgUnitLevelTypeByLevelNumber: Record = { + 1: "National", + 2: "Province", + 3: "District", + 4: "Health Facility", +}; diff --git a/src/domain/entities/alert/AlertsPerformanceOverviewMetrics.ts b/src/domain/entities/alert/AlertsPerformanceOverviewMetrics.ts new file mode 100644 index 00000000..eb20bdd7 --- /dev/null +++ b/src/domain/entities/alert/AlertsPerformanceOverviewMetrics.ts @@ -0,0 +1,23 @@ +import { OrgUnitLevelType } from "../OrgUnit"; +import { Id } from "../Ref"; + +export type AlertsPerformanceOverviewMetrics = { + teiId: Id; + eventEBSId: Id; + eventIBSId: Id; + nationalDiseaseOutbreakEventId: Id; + hazardType: string; + suspectedDisease: string; + province: string; + orgUnit: string; + orgUnitType: OrgUnitLevelType; + cases: string; + deaths: string; + duration: string; + date: string; + detect7d: string; + notify1d: string; + incidentManager: string; + respond7d: string; + incidentStatus: string; +}; diff --git a/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts b/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts index b9bdec7b..7a4701df 100644 --- a/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts +++ b/src/domain/entities/disease-outbreak-event/PerformanceOverviewMetrics.ts @@ -51,6 +51,7 @@ export type PerformanceOverviewMetrics = { suspectedDisease: DiseaseNames; hazardType: HazardNames; nationalIncidentStatus: string; + date: string; }; export type IncidentStatus = "Watch" | "Alert" | "Respond" | "ALL"; diff --git a/src/domain/repositories/PerformanceOverviewRepository.ts b/src/domain/repositories/PerformanceOverviewRepository.ts index 8920bab6..a9e0da7f 100644 --- a/src/domain/repositories/PerformanceOverviewRepository.ts +++ b/src/domain/repositories/PerformanceOverviewRepository.ts @@ -1,4 +1,5 @@ import { FutureData } from "../../data/api-futures"; +import { AlertsPerformanceOverviewMetrics } from "../entities/alert/AlertsPerformanceOverviewMetrics"; import { CasesDataSource, DiseaseOutbreakEventBaseAttrs, @@ -12,9 +13,10 @@ import { OverviewCard } from "../entities/PerformanceOverview"; import { Id } from "../entities/Ref"; export interface PerformanceOverviewRepository { - getPerformanceOverviewMetrics( + getNationalPerformanceOverviewMetrics( diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[] ): FutureData; + getAlertsPerformanceOverviewMetrics(): FutureData; getTotalCardCounts( allProvincesIds: string[], singleSelectFilters?: Record, diff --git a/src/domain/usecases/GetAllAlertsPerformanceOverviewMetricsUseCase.ts b/src/domain/usecases/GetAllAlertsPerformanceOverviewMetricsUseCase.ts new file mode 100644 index 00000000..641d5a68 --- /dev/null +++ b/src/domain/usecases/GetAllAlertsPerformanceOverviewMetricsUseCase.ts @@ -0,0 +1,15 @@ +import { FutureData } from "../../data/api-futures"; +import { AlertsPerformanceOverviewMetrics } from "../entities/alert/AlertsPerformanceOverviewMetrics"; +import { PerformanceOverviewRepository } from "../repositories/PerformanceOverviewRepository"; + +export class GetAllAlertsPerformanceOverviewMetricsUseCase { + constructor( + private options: { + performanceOverviewRepository: PerformanceOverviewRepository; + } + ) {} + + public execute(): FutureData { + return this.options.performanceOverviewRepository.getAlertsPerformanceOverviewMetrics(); + } +} diff --git a/src/domain/usecases/GetAllPerformanceOverviewMetricsUseCase.ts b/src/domain/usecases/GetAllNationalPerformanceOverviewMetricsUseCase.ts similarity index 89% rename from src/domain/usecases/GetAllPerformanceOverviewMetricsUseCase.ts rename to src/domain/usecases/GetAllNationalPerformanceOverviewMetricsUseCase.ts index 6bef413e..24438394 100644 --- a/src/domain/usecases/GetAllPerformanceOverviewMetricsUseCase.ts +++ b/src/domain/usecases/GetAllNationalPerformanceOverviewMetricsUseCase.ts @@ -3,7 +3,7 @@ import { PerformanceOverviewMetrics } from "../entities/disease-outbreak-event/P import { DiseaseOutbreakEventRepository } from "../repositories/DiseaseOutbreakEventRepository"; import { PerformanceOverviewRepository } from "../repositories/PerformanceOverviewRepository"; -export class GetAllPerformanceOverviewMetricsUseCase { +export class GetAllNationalPerformanceOverviewMetricsUseCase { constructor( private options: { diseaseOutbreakEventRepository: DiseaseOutbreakEventRepository; @@ -15,7 +15,7 @@ export class GetAllPerformanceOverviewMetricsUseCase { return this.options.diseaseOutbreakEventRepository .getAll() .flatMap(diseaseOutbreakEvents => { - return this.options.performanceOverviewRepository.getPerformanceOverviewMetrics( + return this.options.performanceOverviewRepository.getNationalPerformanceOverviewMetrics( diseaseOutbreakEvents ); }); diff --git a/src/webapp/components/pagination/Pagination.tsx b/src/webapp/components/pagination/Pagination.tsx new file mode 100644 index 00000000..65f7c7c6 --- /dev/null +++ b/src/webapp/components/pagination/Pagination.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Pagination as MUIPagination } from "@material-ui/lab"; +import styled from "styled-components"; + +type PaginationProps = { + totalPages: number; + currentPage: number; + onChange: (event: React.ChangeEvent, page: number) => void; +}; + +export const Pagination: React.FC = React.memo(props => { + const { onChange, totalPages, currentPage } = props; + return ( + + + + ); +}); + +const Container = styled.div` + display: flex; + justify-content: center; +`; + +Pagination.displayName = "Pagination"; diff --git a/src/webapp/components/table/statistic-table/StatisticTable.tsx b/src/webapp/components/table/statistic-table/StatisticTable.tsx index 4d9490da..4dedb7af 100644 --- a/src/webapp/components/table/statistic-table/StatisticTable.tsx +++ b/src/webapp/components/table/statistic-table/StatisticTable.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useCallback } from "react"; +import React, { Dispatch, SetStateAction } from "react"; import styled from "styled-components"; import i18n from "../../../../utils/i18n"; import { @@ -12,17 +12,15 @@ import { } from "@material-ui/core"; import { SearchInput } from "../../search-input/SearchInput"; import { MultipleSelector } from "../../selector/MultipleSelector"; -import { useTableFilters } from "./useTableFilters"; import { useTableCell } from "./useTableCell"; import { useStatisticCalculations } from "./useStatisticCalculations"; import { ColoredCell } from "./ColoredCell"; import { CalculationRow } from "./CalculationRow"; -import { Order } from "../../../pages/dashboard/usePerformanceOverview"; import { Option } from "../../utils/option"; 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"; +import { DateRangePicker } from "../../date-picker/DateRangePicker"; export type TableColumn = { value: string; @@ -30,6 +28,11 @@ export type TableColumn = { dark?: boolean; }; +export type Order = { + name: keyof T; + direction: "asc" | "desc"; +}; + export type FiltersConfig = { value: TableColumn["value"]; label: TableColumn["label"]; @@ -42,74 +45,91 @@ export type FiltersValuesType = { [key: TableColumn["value"]]: string[]; }; +type Row = { [key: TableColumn["value"]]: string }; + export type StatisticTableProps = { columns: TableColumn[]; columnRules: { [key: TableColumn["value"]]: number; }; - editRiskAssessmentColumns: TableColumn["value"][]; - rows: { - [key: TableColumn["value"]]: string; - }[]; - filters: FiltersConfig[]; - order: Maybe; - setOrder: Dispatch>>; + editRiskAssessmentColumns?: TableColumn["value"][]; + rows: Row[]; + paginatedRows?: Row[]; + filtersConfig: FiltersConfig[]; + order: Maybe>; + onOrderBy: (columnValue: string) => void; + isPaginated?: boolean; + searchTerm: string; + setSearchTerm: React.Dispatch>; + filters: FiltersValuesType; + setFilters: Dispatch>; + filterOptions: (column: string) => { value: string; label: string }[]; + allowGoToEventOnClick?: boolean; }; +const DEFAULT_ARRAY_VALUE: string[] = []; + export const StatisticTable: React.FC = React.memo( ({ rows, + paginatedRows, columns, columnRules, - editRiskAssessmentColumns, - filters: filtersConfig, + editRiskAssessmentColumns = DEFAULT_ARRAY_VALUE, + filtersConfig, order, - setOrder, + onOrderBy, + searchTerm, + setSearchTerm, + filters, + setFilters, + filterOptions, + allowGoToEventOnClick = false, }) => { const { generatePath } = useRoutes(); const calculateColumns = [...editRiskAssessmentColumns, ...Object.keys(columnRules)]; - - const { searchTerm, setSearchTerm, filters, setFilters, filteredRows, filterOptions } = - useTableFilters(rows, filtersConfig); const { getCellColor } = useTableCell(editRiskAssessmentColumns, columnRules); const { calculateMedian, calculatePercentTargetMet } = useStatisticCalculations( - filteredRows, + rows, columnRules ); - const onOrderBy = useCallback( - (value: string) => { - setOrder(prevOrder => { - return { - name: value as keyof PerformanceOverviewMetrics, - direction: - prevOrder?.name === value - ? order?.direction === "asc" - ? "desc" - : "asc" - : "asc", - }; - }); - }, - [order, setOrder] - ); - return ( - {filtersConfig.map(({ value, label }) => ( - { - setFilters({ ...filters, [value]: values }); - }} - /> - ))} + {filtersConfig.map(({ value, label, type }) => { + switch (type) { + case "multiselector": + return ( + { + setFilters({ ...filters, [value]: values }); + }} + /> + ); + case "datepicker": + return ( + + { + setFilters({ ...filters, [value]: values }); + }} + placeholder={i18n.t(label)} + /> + + ); + default: + return null; + } + })} setSearchTerm(value)} /> @@ -138,7 +158,7 @@ export const StatisticTable: React.FC = React.memo( - {filteredRows.map((row, rowIndex) => ( + {(paginatedRows || rows).map((row, rowIndex) => ( {columns.map((column, columnIndex) => calculateColumns.includes(column.value) ? ( @@ -153,9 +173,9 @@ export const StatisticTable: React.FC = React.memo( ) : ( - {row.id ? ( + {row.id && allowGoToEventOnClick ? ( { const matchesFilters = Object.keys(filters).every(key => { const filterValues = filters[key] || []; - if (filterValues.length === 0) return true; - return filterValues.includes(row[key] || ""); + const isDatePickerFilter = + filtersConfig.find(filter => filter.value === key)?.type === + "datepicker"; + + if (filterValues.length === 0) { + return true; + } else if (isDatePickerFilter) { + return row[key] && filterValues[0] && filterValues[1] + ? filterValues[0] <= (row[key] || "") && + (row[key] || "") <= filterValues[1] + : true; + } else { + return filterValues.includes(row[key] || ""); + } }); const matchesSearchTerm = _(Object.values(row)).some(cell => @@ -44,7 +56,7 @@ export const useTableFilters = ( }) .value(); } - }, [rows, searchTerm, filters]); + }, [filters, searchTerm, rows, filtersConfig]); const filterOptions = useCallback( (column: TableColumn["value"]) => { diff --git a/src/webapp/pages/dashboard/DashboardPage.tsx b/src/webapp/pages/dashboard/DashboardPage.tsx index 397ae4a4..43b55bfa 100644 --- a/src/webapp/pages/dashboard/DashboardPage.tsx +++ b/src/webapp/pages/dashboard/DashboardPage.tsx @@ -4,7 +4,7 @@ import i18n from "../../../utils/i18n"; import { Layout } from "../../components/layout/Layout"; import { Section } from "../../components/section/Section"; import { StatisticTable } from "../../components/table/statistic-table/StatisticTable"; -import { usePerformanceOverview } from "./usePerformanceOverview"; +import { useNationalPerformanceOverview } from "./useNationalPerformanceOverview"; import { useCardCounts } from "./useCardCounts"; import { StatsCard } from "../../components/stats-card/StatsCard"; import styled from "styled-components"; @@ -18,6 +18,8 @@ import { PerformanceMetric717, use717Performance } from "./use717Performance"; import { Loader } from "../../components/loader/Loader"; import { useLastAnalyticsRuntime } from "../../hooks/useLastAnalyticsRuntime"; import LoaderContainer from "../../components/loader/LoaderContainer"; +import { useAlertsPerformanceOverview } from "./useAlertsPerformanceOverview"; +import { Pagination } from "../../components/pagination/Pagination"; export const DashboardPage: React.FC = React.memo(() => { const { @@ -30,15 +32,21 @@ export const DashboardPage: React.FC = React.memo(() => { } = useAlertsActiveVerifiedFilters(); const { - columns, - dataPerformanceOverview, - filters: performanceOverviewFilters, - order, - setOrder, - columnRules, + dataNationalPerformanceOverview, editRiskAssessmentColumns, isLoading: performanceOverviewLoading, - } = usePerformanceOverview(); + ...restNationalPerformanceOverview + } = useNationalPerformanceOverview(); + + const { + dataAlertsPerformanceOverview, + paginatedDataAlertsPerformanceOverview, + isLoading: alertsPerformanceOverviewLoading, + totalPages, + currentPage, + goToPage, + ...restAlertsPerformanceOverview + } = useAlertsPerformanceOverview(); const { performanceMetrics717: nationalPerformanceMetrics717, @@ -62,7 +70,10 @@ export const DashboardPage: React.FC = React.memo(() => { resetCurrentEventTrackerId(); }, [resetCurrentEventTrackerId]); - return performanceOverviewLoading || national717CardsLoading || alerts717CardsLoading ? ( + return performanceOverviewLoading || + alertsPerformanceOverviewLoading || + national717CardsLoading || + alerts717CardsLoading ? ( ) : ( { )} +
+ + + + +
{nationalPerformanceMetrics717.map( @@ -169,13 +194,9 @@ export const DashboardPage: React.FC = React.memo(() => {
@@ -196,6 +217,7 @@ export const StyledStatsCard = styled(StatsCard)` const StatisticTableWrapper = styled.div` display: grid; + row-gap: 16px; `; const FiltersContainer = styled.div` diff --git a/src/webapp/pages/dashboard/useAlertsPerformanceOverview.ts b/src/webapp/pages/dashboard/useAlertsPerformanceOverview.ts new file mode 100644 index 00000000..d357ef10 --- /dev/null +++ b/src/webapp/pages/dashboard/useAlertsPerformanceOverview.ts @@ -0,0 +1,182 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; + +import { useAppContext } from "../../contexts/app-context"; +import _ from "../../../domain/entities/generic/Collection"; +import { + FiltersConfig, + FiltersValuesType, + TableColumn, +} from "../../components/table/statistic-table/StatisticTable"; +import { Maybe } from "../../../utils/ts-utils"; +import { AlertsPerformanceOverviewMetrics } from "../../../domain/entities/alert/AlertsPerformanceOverviewMetrics"; +import { Id } from "../../../domain/entities/Ref"; +import { TeamMember } from "../../../domain/entities/incident-management-team/TeamMember"; +import { usePerformanceOverviewTable } from "./usePerformanceOverviewTable"; +import { OrgUnitLevelType } from "../../../domain/entities/OrgUnit"; +import i18n from "../../../utils/i18n"; + +type AlertsPerformanceOverviewMetricsTableData = { + event: string; + teiId: Id; + eventEBSId: Id; + eventIBSId: Id; + nationalDiseaseOutbreakEventId: Id; + hazardType: string; + suspectedDisease: string; + province: string; + orgUnit: string; + orgUnitType: OrgUnitLevelType; + cases: string; + deaths: string; + duration: string; + date: string; + notify1d: string; + detect7d: string; + incidentManager: string; + respond7d: string; + incidentStatus: string; +}; + +type State = { + columns: TableColumn[]; + dataAlertsPerformanceOverview: AlertsPerformanceOverviewMetricsTableData[]; + paginatedDataAlertsPerformanceOverview: AlertsPerformanceOverviewMetricsTableData[]; + columnRules: { [key: string]: number }; + order: Maybe; + onOrderBy: (columnValue: string) => void; + isLoading: boolean; + searchTerm: string; + setSearchTerm: React.Dispatch>; + filtersConfig: FiltersConfig[]; + filters: FiltersValuesType; + setFilters: Dispatch>; + filterOptions: (column: string) => { value: string; label: string }[]; + totalPages: number; + currentPage: number; + goToPage: (event: React.ChangeEvent, page: number) => void; +}; + +export type Order = { + name: keyof AlertsPerformanceOverviewMetricsTableData; + direction: "asc" | "desc"; +}; + +export function useAlertsPerformanceOverview(): State { + const { + compositionRoot, + configurations: { teamMembers }, + } = useAppContext(); + const [isLoading, setIsLoading] = useState(true); + + const columnRules = useMemo( + () => ({ + detect7d: 7, + notify1d: 1, + respond7d: 7, + }), + [] + ); + + const filtersConfig = useMemo( + () => [ + { value: "event", label: i18n.t("Disease - Hazard type"), type: "multiselector" }, + { value: "province", label: i18n.t("Province"), type: "multiselector" }, + { value: "date", label: i18n.t("Date"), type: "datepicker" }, + ], + [] + ); + + const { + filteredData: dataAlertsPerformanceOverview, + setData: setAlertsDataPerformanceOverview, + order, + onOrderBy, + searchTerm, + setSearchTerm, + filters, + setFilters, + filterOptions, + paginatedData: paginatedDataAlertsPerformanceOverview, + totalPages, + currentPage, + goToPage, + } = usePerformanceOverviewTable(filtersConfig, true); + + const columns = useMemo( + () => [ + { label: i18n.t("Disease - Hazard type"), value: "event" }, + { label: i18n.t("Location"), value: "province" }, + { label: i18n.t("Organisation unit"), value: "orgUnit" }, + { label: i18n.t("Organisation unit type"), value: "orgUnitType" }, + { label: i18n.t("Cases"), value: "cases" }, + { label: i18n.t("Deaths"), value: "deaths" }, + { label: i18n.t("Duration"), value: "duration" }, + { label: i18n.t("Manager"), value: "incidentManager" }, + { label: i18n.t("Detect 7d"), dark: true, value: "detect7d" }, + { label: i18n.t("Notify 1d"), dark: true, value: "notify1d" }, + { label: i18n.t("Respond 7d"), dark: true, value: "respond7d" }, + { label: i18n.t("Incident Status"), value: "incidentStatus" }, + ], + [] + ); + + const mapEntityToTableData = useCallback( + ( + data: AlertsPerformanceOverviewMetrics, + allTeamMembers: TeamMember[] + ): AlertsPerformanceOverviewMetricsTableData => { + const incidentManagerName = allTeamMembers.find( + tm => tm.name === data.incidentManager + )?.name; + + return { + ...data, + event: data.hazardType || data.suspectedDisease, + incidentManager: incidentManagerName || data.incidentManager, + }; + }, + [] + ); + + useEffect(() => { + setIsLoading(true); + compositionRoot.performanceOverview.getAlertsPerformanceOverviewMetrics.execute().run( + performanceOverviewMetrics => { + const tableData = performanceOverviewMetrics.map(data => + mapEntityToTableData(data, teamMembers.all) + ); + + setAlertsDataPerformanceOverview(tableData); + setIsLoading(false); + }, + error => { + console.error({ error }); + setIsLoading(false); + } + ); + }, [ + compositionRoot.performanceOverview.getAlertsPerformanceOverviewMetrics, + mapEntityToTableData, + setAlertsDataPerformanceOverview, + teamMembers.all, + ]); + + return { + columns, + dataAlertsPerformanceOverview, + paginatedDataAlertsPerformanceOverview, + columnRules, + order, + onOrderBy, + isLoading, + searchTerm, + setSearchTerm, + filtersConfig, + filters, + setFilters, + filterOptions, + currentPage, + totalPages, + goToPage, + }; +} diff --git a/src/webapp/pages/dashboard/useNationalPerformanceOverview.ts b/src/webapp/pages/dashboard/useNationalPerformanceOverview.ts new file mode 100644 index 00000000..e0b452f9 --- /dev/null +++ b/src/webapp/pages/dashboard/useNationalPerformanceOverview.ts @@ -0,0 +1,178 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { useAppContext } from "../../contexts/app-context"; +import _ from "../../../domain/entities/generic/Collection"; +import { + FiltersConfig, + FiltersValuesType, + TableColumn, +} from "../../components/table/statistic-table/StatisticTable"; +import { Maybe } from "../../../utils/ts-utils"; +import { PerformanceOverviewMetrics } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; +import { NationalIncidentStatus } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; +import { usePerformanceOverviewTable } from "./usePerformanceOverviewTable"; +import i18n from "../../../utils/i18n"; + +type State = { + columns: TableColumn[]; + dataNationalPerformanceOverview: PerformanceOverviewMetrics[]; + editRiskAssessmentColumns: string[]; + columnRules: { [key: string]: number }; + order: Maybe; + onOrderBy: (columnValue: string) => void; + isLoading: boolean; + searchTerm: string; + setSearchTerm: React.Dispatch>; + filtersConfig: FiltersConfig[]; + filters: FiltersValuesType; + setFilters: Dispatch>; + filterOptions: (column: string) => { value: string; label: string }[]; + totalPages: number; + currentPage: number; + goToPage: (event: React.ChangeEvent, page: number) => void; + allowGoToEventOnClick: true; +}; + +export type Order = { name: keyof PerformanceOverviewMetrics; direction: "asc" | "desc" }; + +export function useNationalPerformanceOverview(): State { + const { compositionRoot } = useAppContext(); + const { changeExistingEventTrackerTypes } = useExistingEventTrackerTypes(); + + const [isLoading, setIsLoading] = useState(false); + + const filtersConfig: FiltersConfig[] = useMemo( + () => [ + { value: "event", label: i18n.t("Event"), type: "multiselector" }, + { value: "province", label: i18n.t("Province"), type: "multiselector" }, + { value: "date", label: i18n.t("Date"), type: "datepicker" }, + ], + [] + ); + + const columns: TableColumn[] = useMemo( + () => [ + { label: i18n.t("Event"), value: "event" }, + { label: i18n.t("Province"), value: "province" }, + { label: i18n.t("Cases"), value: "cases" }, + { label: i18n.t("Deaths"), value: "deaths" }, + { label: i18n.t("Duration"), value: "duration" }, + { label: i18n.t("Manager"), value: "manager" }, + { label: i18n.t("Detect 7d"), dark: true, value: "detect7d" }, + { label: i18n.t("Notify 1d"), dark: true, value: "notify1d" }, + { label: i18n.t("ERA1"), value: "era1" }, + { label: i18n.t("ERA2"), value: "era2" }, + { label: i18n.t("ERA3"), value: "era3" }, + { label: i18n.t("ERA4"), value: "era4" }, + { label: i18n.t("ERA5"), value: "era5" }, + { label: i18n.t("ERA6"), value: "era6" }, + { label: i18n.t("ERA7"), value: "era7" }, + { label: i18n.t("Respond 7d"), dark: true, value: "respond7d" }, + { label: i18n.t("Incident Status"), value: "nationalIncidentStatus" }, + ], + [] + ); + + const editRiskAssessmentColumns = useMemo( + () => ["era1", "era2", "era3", "era4", "era5", "era6", "era7"], + [] + ); + + const columnRules = useMemo( + () => ({ + detect7d: 7, + notify1d: 1, + respond7d: 7, + }), + [] + ); + + const { + filteredData: dataNationalPerformanceOverview, + setData: setDataPerformanceOverview, + order, + onOrderBy, + searchTerm, + setSearchTerm, + filters, + setFilters, + filterOptions, + totalPages, + currentPage, + goToPage, + } = usePerformanceOverviewTable(filtersConfig); + + const getNationalIncidentStatusString = useCallback((status: string): string => { + switch (status as NationalIncidentStatus) { + case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_ALERT: + return i18n.t("Alert"); + case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_CLOSED: + return i18n.t("Closed"); + case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_DISCARDED: + return i18n.t("Discarded"); + case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_RESPOND: + return i18n.t("Respond"); + case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_WATCH: + return i18n.t("Watch"); + } + }, []); + + const mapEntityToTableData = useCallback( + (programIndicator: PerformanceOverviewMetrics): PerformanceOverviewMetrics => { + return { + ...programIndicator, + nationalIncidentStatus: getNationalIncidentStatusString( + programIndicator.nationalIncidentStatus + ), + event: programIndicator.event, + }; + }, + [getNationalIncidentStatusString] + ); + + useEffect(() => { + setIsLoading(true); + compositionRoot.performanceOverview.getNationalPerformanceOverviewMetrics.execute().run( + performanceOverviewMetrics => { + const existingEventTrackerTypes = performanceOverviewMetrics.map( + metric => metric.suspectedDisease || metric.hazardType + ); + changeExistingEventTrackerTypes(existingEventTrackerTypes); + const mappedData = performanceOverviewMetrics.map( + (data: PerformanceOverviewMetrics) => mapEntityToTableData(data) + ); + setDataPerformanceOverview(mappedData); + setIsLoading(false); + }, + error => { + console.error({ error }); + setIsLoading(false); + } + ); + }, [ + changeExistingEventTrackerTypes, + compositionRoot.performanceOverview.getNationalPerformanceOverviewMetrics, + mapEntityToTableData, + setDataPerformanceOverview, + ]); + + return { + editRiskAssessmentColumns, + dataNationalPerformanceOverview, + columns, + columnRules, + order, + onOrderBy, + isLoading, + searchTerm, + setSearchTerm, + filtersConfig, + filters, + setFilters, + filterOptions, + currentPage, + totalPages, + goToPage, + allowGoToEventOnClick: true, + }; +} diff --git a/src/webapp/pages/dashboard/usePerformanceOverview.ts b/src/webapp/pages/dashboard/usePerformanceOverview.ts deleted file mode 100644 index 4cc227aa..00000000 --- a/src/webapp/pages/dashboard/usePerformanceOverview.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; -import { useAppContext } from "../../contexts/app-context"; -import _ from "../../../domain/entities/generic/Collection"; -import { FiltersConfig, TableColumn } from "../../components/table/statistic-table/StatisticTable"; -import { Maybe } from "../../../utils/ts-utils"; -import { PerformanceOverviewMetrics } from "../../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; -import { NationalIncidentStatus } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; - -type State = { - columns: TableColumn[]; - dataPerformanceOverview: PerformanceOverviewMetrics[]; - columnRules: { [key: string]: number }; - editRiskAssessmentColumns: string[]; - filters: FiltersConfig[]; - order: Maybe; - setOrder: Dispatch>>; - isLoading: boolean; -}; - -export type Order = { name: keyof PerformanceOverviewMetrics; direction: "asc" | "desc" }; - -export function usePerformanceOverview(): State { - const { compositionRoot } = useAppContext(); - - const [dataPerformanceOverview, setDataPerformanceOverview] = useState< - PerformanceOverviewMetrics[] - >([]); - const [isLoading, setIsLoading] = useState(false); - const [order, setOrder] = useState(); - const { changeExistingEventTrackerTypes } = useExistingEventTrackerTypes(); - - const getNationalIncidentStatusString = useCallback((status: string): string => { - switch (status as NationalIncidentStatus) { - case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_ALERT: - return "Alert"; - case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_CLOSED: - return "Closed"; - case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_DISCARDED: - return "Discarded"; - case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_RESPOND: - return "Respond"; - case NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_WATCH: - return "Watch"; - } - }, []); - - const mapEntityToTableData = useCallback( - (programIndicator: PerformanceOverviewMetrics): PerformanceOverviewMetrics => { - return { - ...programIndicator, - nationalIncidentStatus: getNationalIncidentStatusString( - programIndicator.nationalIncidentStatus - ), - event: programIndicator.event, - }; - }, - [getNationalIncidentStatusString] - ); - - useEffect(() => { - if (dataPerformanceOverview.length && order) { - setDataPerformanceOverview( - (prevDataPerformanceOverview: PerformanceOverviewMetrics[]) => { - const newDataPerformanceOverview = _(prevDataPerformanceOverview) - .orderBy([ - [ - item => - Number.isNaN(Number(item[order.name])) - ? item[order.name] - : Number(item[order.name]), - order.direction, - ], - ]) - .toArray(); - - return newDataPerformanceOverview; - } - ); - } - }, [order, dataPerformanceOverview]); - - useEffect(() => { - setIsLoading(true); - compositionRoot.performanceOverview.getPerformanceOverviewMetrics.execute().run( - performanceOverviewMetrics => { - const existingEventTrackerTypes = performanceOverviewMetrics.map( - metric => metric.suspectedDisease || metric.hazardType - ); - changeExistingEventTrackerTypes(existingEventTrackerTypes); - const mappedData = performanceOverviewMetrics.map( - (data: PerformanceOverviewMetrics) => mapEntityToTableData(data) - ); - setDataPerformanceOverview(mappedData); - setIsLoading(false); - }, - error => { - console.error({ error }); - setIsLoading(false); - } - ); - }, [ - changeExistingEventTrackerTypes, - compositionRoot.performanceOverview.getPerformanceOverviewMetrics, - mapEntityToTableData, - ]); - - const columns: TableColumn[] = [ - { label: "Event", value: "event" }, - { label: "Province", value: "province" }, - { label: "Cases", value: "cases" }, - { label: "Deaths", value: "deaths" }, - { label: "Duration", value: "duration" }, - { label: "Manager", value: "manager" }, - { label: "Detect 7d", dark: true, value: "detect7d" }, - { label: "Notify 1d", dark: true, value: "notify1d" }, - { label: "ERA1", value: "era1" }, - { label: "ERA2", value: "era2" }, - { label: "ERA3", value: "era3" }, - { label: "ERA4", value: "era4" }, - { label: "ERA5", value: "era5" }, - { label: "ERA6", value: "era6" }, - { label: "ERA7", value: "era7" }, - { label: "Respond 7d", dark: true, value: "respond7d" }, - { label: "Incident Status", value: "nationalIncidentStatus" }, - ]; - const editRiskAssessmentColumns = ["era1", "era2", "era3", "era4", "era5", "era6", "era7"]; - const columnRules: { [key: string]: number } = { - detect7d: 7, - notify1d: 1, - respond7d: 7, - }; - - const filters: FiltersConfig[] = [ - { value: "event", label: "Event", type: "multiselector" }, - { value: "province", label: "Province", type: "multiselector" }, - ]; - - return { - dataPerformanceOverview, - filters, - order, - setOrder, - columnRules, - editRiskAssessmentColumns, - columns, - isLoading, - }; -} diff --git a/src/webapp/pages/dashboard/usePerformanceOverviewTable.ts b/src/webapp/pages/dashboard/usePerformanceOverviewTable.ts new file mode 100644 index 00000000..0befccd3 --- /dev/null +++ b/src/webapp/pages/dashboard/usePerformanceOverviewTable.ts @@ -0,0 +1,108 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; + +import _ from "../../../domain/entities/generic/Collection"; +import { Maybe } from "../../../utils/ts-utils"; +import { + FiltersConfig, + FiltersValuesType, +} from "../../components/table/statistic-table/StatisticTable"; +import { useTableFilters } from "../../components/table/statistic-table/useTableFilters"; +import { Row } from "../../components/form/FormFieldsState"; + +export type Order = { name: keyof T; direction: "asc" | "desc" }; + +type State = { + filteredData: T[]; + setData: React.Dispatch>; + order: Maybe>; + onOrderBy: (columnValue: string) => void; + searchTerm: string; + setSearchTerm: React.Dispatch>; + filters: FiltersValuesType; + setFilters: Dispatch>; + filterOptions: (column: string) => { value: string; label: string }[]; + paginatedData: T[]; + totalPages: number; + currentPage: number; + goToPage: (event: React.ChangeEvent, page: number) => void; +}; + +const PAGE_SIZE = 20; + +export function usePerformanceOverviewTable( + filtersConfig: FiltersConfig[], + isPaginated?: boolean +): State { + const [data, setData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [order, setOrder] = useState>(); + const [currentPage, setCurrentPage] = useState(1); + + const { searchTerm, setSearchTerm, filters, setFilters, filteredRows, filterOptions } = + useTableFilters(data as Row[], filtersConfig); + + useEffect(() => { + setFilteredData(filteredRows as T[]); + }, [filteredRows]); + + const onOrderBy = useCallback((columnValue: string) => { + setOrder(prevOrder => ({ + name: columnValue as keyof T, + direction: + prevOrder?.name === columnValue && prevOrder.direction === "asc" ? "desc" : "asc", + })); + }, []); + + useEffect(() => { + if (filteredData.length && order) { + setData(prevData => { + const sortedData = _(prevData) + .orderBy([ + [ + item => + Number.isNaN(Number(item[order.name])) + ? item[order.name] + : Number(item[order.name]), + order.direction, + ], + ]) + .toArray(); + return sortedData; + }); + } + }, [filteredData.length, order]); + + const paginatedData = useMemo(() => { + if (isPaginated) { + const startIndex = (currentPage - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + return filteredData.slice(startIndex, endIndex); + } else { + return filteredData; + } + }, [currentPage, filteredData, isPaginated]); + + const totalPages = useMemo(() => Math.ceil(filteredData.length / PAGE_SIZE), [filteredData]); + + const goToPage = (_event: React.ChangeEvent, page: number) => { + if (page >= 1 && page <= totalPages) { + setCurrentPage(page); + } + }; + + return { + filteredData, + setData, + order, + onOrderBy, + searchTerm, + setSearchTerm, + filters, + setFilters, + filterOptions, + paginatedData, + totalPages, + currentPage, + goToPage, + }; +} diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index d1fab7b1..cca5b93c 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -24,7 +24,7 @@ import { } from "./incident-action/mapIncidentActionToInitialFormState"; import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; import { useCheckWritePermission } from "../../hooks/useHasCurrentUserCaptureAccess"; -import { usePerformanceOverview } from "../dashboard/usePerformanceOverview"; +import { useNationalPerformanceOverview } from "../dashboard/useNationalPerformanceOverview"; import { useIncidentActionPlan } from "../incident-action-plan/useIncidentActionPlan"; import { RiskAssessmentQuestionnaire } from "../../../domain/entities/risk-assessment/RiskAssessmentQuestionnaire"; import { ModalData } from "../../components/form/Form"; @@ -73,7 +73,7 @@ export function useForm(formType: FormType, id?: Id): State { const { getCurrentEventTracker } = useCurrentEventTracker(); const currentEventTracker = getCurrentEventTracker(); const { existingEventTrackerTypes } = useExistingEventTrackerTypes(); - const { dataPerformanceOverview } = usePerformanceOverview(); + const { dataNationalPerformanceOverview } = useNationalPerformanceOverview(); const { isIncidentManager } = useIncidentActionPlan(currentEventTracker?.id ?? ""); const snackbar = useSnackbar(); useCheckWritePermission(formType); @@ -96,7 +96,7 @@ export function useForm(formType: FormType, id?: Id): State { setModalData, }); - const allDataPerformanceEvents = dataPerformanceOverview?.map( + const allDataPerformanceEvents = dataNationalPerformanceOverview?.map( event => event.hazardType || event.suspectedDisease );