Skip to content

Commit

Permalink
Merge pull request #15 from EyeSeeTea/feature/performace-overview-tab…
Browse files Browse the repository at this point in the history
…le-link-indicators

Feature: Performance overview table with indicator linked
  • Loading branch information
bhavananarayanan authored Oct 3, 2024
2 parents aa9994f + 99bad44 commit e07e5f4
Show file tree
Hide file tree
Showing 42 changed files with 1,378 additions and 151 deletions.
20 changes: 16 additions & 4 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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-04T07:57:22.036Z\n"
"PO-Revision-Date: 2024-09-04T07:57:22.036Z\n"
"POT-Creation-Date: 2024-09-30T07:39:32.843Z\n"
"PO-Revision-Date: 2024-09-30T07:39:32.843Z\n"

msgid "Low"
msgstr ""
Expand Down Expand Up @@ -63,6 +63,12 @@ msgstr ""
msgid "Add new option"
msgstr ""

msgid "Reset"
msgstr ""

msgid "Save"
msgstr ""

msgid "There is an error in this field"
msgstr ""

Expand All @@ -72,10 +78,10 @@ msgstr ""
msgid "Cancel"
msgstr ""

msgid "Save"
msgid "Edit Details"
msgstr ""

msgid "Edit Details"
msgid "Notes"
msgstr ""

msgid "Create Event"
Expand Down Expand Up @@ -105,6 +111,12 @@ msgstr ""
msgid "Respond, alert, watch"
msgstr ""

msgid "Duration"
msgstr ""

msgid "7-1-7 performance"
msgstr ""

msgid "Performance overview"
msgstr ""

Expand Down
8 changes: 7 additions & 1 deletion i18n/es.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: i18next-conv\n"
"POT-Creation-Date: 2024-09-04T07:57:22.036Z\n"
"POT-Creation-Date: 2024-09-12T14:10:04.460Z\n"
"PO-Revision-Date: 2018-10-25T09:02:35.143Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -62,6 +62,12 @@ msgstr ""
msgid "Add new option"
msgstr ""

msgid "Reset"
msgstr ""

msgid "Save"
msgstr ""

msgid "There is an error in this field"
msgstr ""

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@dhis2/ui": "6.12.0",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"@eyeseetea/d2-api": "1.16.0-beta.9",
"@eyeseetea/d2-api": "1.16.0-beta.12",
"@eyeseetea/d2-ui-components": "v2.9.0-beta.2",
"@eyeseetea/feedback-component": "0.0.3",
"@material-ui/core": "4.12.4",
Expand All @@ -31,6 +31,7 @@
"d2-manifest": "1.0.0",
"dotenv": "^16.4.5",
"font-awesome": "4.7.0",
"moment": "^2.30.1",
"purify-ts": "1.2.0",
"purify-ts-extra-codec": "0.6.0",
"react": "^18.2.0",
Expand Down
18 changes: 18 additions & 0 deletions src/CompositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ import { OrgUnitTestRepository } from "./data/repositories/test/OrgUnitTestRepos
import { GetAllDiseaseOutbreaksUseCase } from "./domain/usecases/GetAllDiseaseOutbreaksUseCase";
import { SaveDiseaseOutbreakUseCase } from "./domain/usecases/SaveDiseaseOutbreakUseCase";
import { GetDiseaseOutbreakWithOptionsUseCase } from "./domain/usecases/GetDiseaseOutbreakWithOptionsUseCase";
import { PerformanceOverviewRepository } from "./domain/repositories/PerformanceOverviewRepository";
import { GetAllPerformanceOverviewMetricsUseCase } from "./domain/usecases/GetAllPerformanceOverviewMetricsUseCase";
import { PerformanceOverviewD2Repository } from "./data/repositories/PerformanceOverviewD2Repository";
import { PerformanceOverviewTestRepository } from "./data/repositories/test/PerformanceOverviewTestRepository";
import { GetTotalCardCountsUseCase } from "./domain/usecases/GetDiseasesTotalUseCase";
import { MapDiseaseOutbreakToAlertsUseCase } from "./domain/usecases/MapDiseaseOutbreakToAlertsUseCase";
import { AlertRepository } from "./domain/repositories/AlertRepository";
import { AlertTestRepository } from "./data/repositories/test/AlertTestRepository";
import { AlertSyncDataStoreRepository } from "./data/repositories/AlertSyncDataStoreRepository";
import { AlertSyncDataStoreTestRepository } from "./data/repositories/test/AlertSyncDataStoreTestRepository";
import { AlertSyncRepository } from "./domain/repositories/AlertSyncRepository";
import { DataStoreClient } from "./data/DataStoreClient";

export type CompositionRoot = ReturnType<typeof getCompositionRoot>;

Expand All @@ -37,6 +43,7 @@ type Repositories = {
optionsRepository: OptionsRepository;
teamMemberRepository: TeamMemberRepository;
orgUnitRepository: OrgUnitRepository;
performanceOverviewRepository: PerformanceOverviewRepository;
};

function getCompositionRoot(repositories: Repositories) {
Expand All @@ -55,10 +62,19 @@ function getCompositionRoot(repositories: Repositories) {
repositories.optionsRepository
),
},
performanceOverview: {
getPerformanceOverviewMetrics: new GetAllPerformanceOverviewMetricsUseCase(
repositories
),
getTotalCardCounts: new GetTotalCardCountsUseCase(
repositories.performanceOverviewRepository
),
},
};
}

export function getWebappCompositionRoot(api: D2Api) {
const dataStoreClient = new DataStoreClient(api);
const repositories: Repositories = {
usersRepository: new UserD2Repository(api),
diseaseOutbreakEventRepository: new DiseaseOutbreakEventD2Repository(api),
Expand All @@ -67,6 +83,7 @@ export function getWebappCompositionRoot(api: D2Api) {
optionsRepository: new OptionsD2Repository(api),
teamMemberRepository: new TeamMemberD2Repository(api),
orgUnitRepository: new OrgUnitD2Repository(api),
performanceOverviewRepository: new PerformanceOverviewD2Repository(api, dataStoreClient),
};

return getCompositionRoot(repositories);
Expand All @@ -81,6 +98,7 @@ export function getTestCompositionRoot() {
optionsRepository: new OptionsTestRepository(),
teamMemberRepository: new TeamMemberTestRepository(),
orgUnitRepository: new OrgUnitTestRepository(),
performanceOverviewRepository: new PerformanceOverviewTestRepository(),
};

return getCompositionRoot(repositories);
Expand Down
8 changes: 5 additions & 3 deletions src/data/repositories/DiseaseOutbreakEventD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep
return Future.fromPromise(
getAllTrackedEntitiesAsync(this.api, RTSL_ZEBRA_PROGRAM_ID, RTSL_ZEBRA_ORG_UNIT_ID)
).map(trackedEntities => {
return trackedEntities.map(trackedEntity => {
return mapTrackedEntityAttributesToDiseaseOutbreak(trackedEntity);
});
return trackedEntities
.map(trackedEntity => {
return mapTrackedEntityAttributesToDiseaseOutbreak(trackedEntity);
})
.filter(outbreak => outbreak.status === "ACTIVE");
});
}

Expand Down
225 changes: 225 additions & 0 deletions src/data/repositories/PerformanceOverviewD2Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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 _ from "../../domain/entities/generic/Collection";
import { Future } from "../../domain/entities/generic/Future";
import { evenTrackerCountsIndicatorMap, IndicatorsId } from "./consts/PerformanceOverviewConstants";
import moment from "moment";
import {
DiseaseOutbreakEventBaseAttrs,
NationalIncidentStatus,
} from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent";
import { DataStoreClient } from "../DataStoreClient";
import {
TotalCardCounts,
HazardNames,
PerformanceOverviewMetrics,
DiseaseNames,
} from "../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics";
import { AlertSynchronizationData } from "../../domain/entities/alert/AlertData";
import { OrgUnit } from "../../domain/entities/OrgUnit";

export class PerformanceOverviewD2Repository implements PerformanceOverviewRepository {
constructor(private api: D2Api, private datastore: DataStoreClient) {}

getTotalCardCounts(filters?: Record<string, string[]>): FutureData<TotalCardCounts[]> {
return apiToFuture(
this.api.analytics.get({
dimension: [
`dx:${evenTrackerCountsIndicatorMap.map(({ id }) => id).join(";")}`,
"ou:LEVEL-2",
"pe:THIS_YEAR",
],
})
).map(analyticsResponse => {
const totalCardCounts =
this.mapAnalyticsRowsToTotalCardCounts(analyticsResponse.rows, filters) || [];

const uniqueTotalCardCounts = totalCardCounts.reduce((acc, totalCardCount) => {
const existingEntry = acc[totalCardCount.name];

if (existingEntry) {
existingEntry.total += totalCardCount.total;
acc[totalCardCount.name] = existingEntry;
} else {
acc[totalCardCount.name] = totalCardCount;
}
return acc;
}, {} as Record<string, TotalCardCounts>);

return Object.values(uniqueTotalCardCounts);
});
}
mapAnalyticsRowsToTotalCardCounts = (
rowData: string[][],
filters?: Record<string, string[]>
): TotalCardCounts[] => {
const counts: TotalCardCounts[] = _(
rowData.map(([id, _orgUnit, _period, total]) => {
const indicator = evenTrackerCountsIndicatorMap.find(d => d.id === id);
if (!indicator || !total) {
return null;
}

if (indicator.type === "hazard") {
const hazardCount = {
id: id,
name: indicator.name,
type: indicator.type,
incidentStatus: indicator.incidentStatus,
total: parseFloat(total),
};
return hazardCount;
} else {
const diseaseCount = {
id: id,
name: indicator.name,
type: indicator.type,
incidentStatus: indicator.incidentStatus,
total: parseFloat(total),
};
return diseaseCount;
}
})
)
.compact()
.value();

const filteredCounts: TotalCardCounts[] = counts.filter(item => {
if (filters && Object.entries(filters).length) {
return Object.entries(filters).every(([key, values]) => {
if (!values.length) {
return true;
}
if (key === "incidentStatus") {
return values.includes(item.incidentStatus as string);
} else if (key === "disease" || key === "hazard") {
return values.includes(item.name as string);
}
});
}
return true;
});
return filteredCounts;
};

getPerformanceOverviewMetrics(
diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[]
): FutureData<PerformanceOverviewMetrics[]> {
return apiToFuture(
this.api.get<AnalyticsResponse>(
`/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,
],
}
)
).flatMap(indicatorsProgramFuture => {
const mappedIndicators =
indicatorsProgramFuture?.rows.map((row: string[]) =>
this.mapRowToBaseIndicator(
row,
indicatorsProgramFuture.headers,
indicatorsProgramFuture.metaData
)
) || [];

const performanceOverviewMetrics = diseaseOutbreakEvents.map(event => {
const baseIndicator = mappedIndicators.find(indicator => indicator.id === event.id);
const key = baseIndicator?.suspectedDisease || baseIndicator?.hazardType;

return this.getCasesAndDeathsFromDatastore(key).map(casesAndDeaths => {
const duration = `${moment()
.diff(moment(event.emerged.date), "days")
.toString()}d`;
if (!baseIndicator) {
return {
id: event.id,
event: event.name,
manager: event.incidentManagerName,
duration: duration,
nationalIncidentStatus: event.incidentStatus,
cases: casesAndDeaths.cases.toString(),
deaths: casesAndDeaths.deaths.toString(),
} as PerformanceOverviewMetrics;
}
return {
...baseIndicator,
nationalIncidentStatus: event.incidentStatus,
manager: event.incidentManagerName,
duration: duration,
cases: casesAndDeaths.cases.toString(),
deaths: casesAndDeaths.deaths.toString(),
} as PerformanceOverviewMetrics;
});
});

return Future.sequential(performanceOverviewMetrics);
});
}

private getCasesAndDeathsFromDatastore(
key: string | undefined
): FutureData<{ cases: number; deaths: number }> {
if (!key) return Future.success({ cases: 0, deaths: 0 });
return this.datastore.getObject<AlertSynchronizationData>(key).flatMap(data => {
if (!data) return Future.success({ cases: 0, deaths: 0 });
const casesDeaths = data.alerts.reduce(
(acc, alert) => {
acc.cases += parseInt(alert.suspectedCases) || 0;
acc.deaths += parseInt(alert.deaths) || 0;
return acc;
},
{ cases: 0, deaths: 0 }
);

return Future.success(casesDeaths);
});
}

private mapRowToBaseIndicator(
row: string[],
headers: { name: string; column: string }[],
metaData: AnalyticsResponse["metaData"]
): Partial<PerformanceOverviewMetrics> {
return headers.reduce((acc, header, index) => {
const key = Object.keys(IndicatorsId).find(
key => IndicatorsId[key as keyof typeof IndicatorsId] === header.name
) as Maybe<keyof PerformanceOverviewMetrics>;

if (!key) return acc;

if (key === "suspectedDisease") {
acc[key] =
(Object.values(metaData.items).find(item => item.code === row[index])
?.name as DiseaseNames) || "";
} else if (key === "hazardType") {
acc[key] =
(Object.values(metaData.items).find(item => item.code === row[index])
?.name as HazardNames) || "";
} else if (key === "nationalIncidentStatus") {
acc[key] = row[index] as NationalIncidentStatus;
} else {
acc[key] = row[index] as (HazardNames & OrgUnit[]) | undefined;
}

return acc;
}, {} as Partial<PerformanceOverviewMetrics>);
}
}
Loading

0 comments on commit e07e5f4

Please sign in to comment.