From 876f56e2607c73727de5718471314fde9d0af17b Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:45:16 +0100 Subject: [PATCH 01/14] feat: implement incident action plan pages (wip) create form summary page, action plan form and response action form --- src/CompositionRoot.ts | 9 +- .../IncidentActionD2Repository.ts | 191 +++++++++ src/data/repositories/OptionsD2Repository.ts | 36 ++ .../consts/DiseaseOutbreakConstants.ts | 2 + .../consts/IncidentActionConstants.ts | 120 ++++++ .../test/IncidentActionTestRepository.ts | 24 ++ .../test/OptionsTestRepository.ts | 26 ++ .../utils/IncidentActionMapper.ts | 233 +++++++++++ .../utils/RiskAssessmentMapper.ts | 2 +- src/domain/entities/ConfigurableForm.ts | 31 +- .../incident-action-plan/ActionPlan.ts | 16 +- .../IncidentActionPlan.ts | 3 +- .../incident-action-plan/ResponseAction.ts | 35 +- .../repositories/IncidentActionRepository.ts | 19 + src/domain/repositories/OptionsRepository.ts | 6 + .../usecases/GetDiseaseOutbreakByIdUseCase.ts | 12 +- .../usecases/GetEntityWithOptionsUseCase.ts | 27 ++ src/domain/usecases/SaveEntityUseCase.ts | 10 +- .../incident-action/GetIncidentActionById.ts | 95 +++++ .../GetIncidentActionPlanWithOptions.ts | 67 +++ .../form-summary/ActionPlanFormSummary.tsx | 71 ++++ .../layout/side-bar/SideBarContent.tsx | 17 +- src/webapp/hooks/useRoutes.ts | 6 +- src/webapp/pages/form-page/FormPage.tsx | 4 +- .../updateDiseaseOutbreakEventFormState.ts | 5 +- .../mapIncidentActionToInitialFormState.ts | 395 ++++++++++++++++++ .../pages/form-page/mapEntityToFormState.ts | 8 + .../form-page/mapFormStateToEntityData.ts | 138 ++++++ src/webapp/pages/form-page/useForm.ts | 19 + .../IncidentActionPlanPage.tsx | 168 +++++++- .../useIncidentActionPlan.ts | 136 ++++++ 31 files changed, 1899 insertions(+), 32 deletions(-) create mode 100644 src/data/repositories/IncidentActionD2Repository.ts create mode 100644 src/data/repositories/consts/IncidentActionConstants.ts create mode 100644 src/data/repositories/test/IncidentActionTestRepository.ts create mode 100644 src/data/repositories/utils/IncidentActionMapper.ts create mode 100644 src/domain/repositories/IncidentActionRepository.ts create mode 100644 src/domain/usecases/utils/incident-action/GetIncidentActionById.ts create mode 100644 src/domain/usecases/utils/incident-action/GetIncidentActionPlanWithOptions.ts create mode 100644 src/webapp/components/form/form-summary/ActionPlanFormSummary.tsx create mode 100644 src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts create mode 100644 src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index a0130516..f62e7d66 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -26,6 +26,9 @@ import { SaveEntityUseCase } from "./domain/usecases/SaveEntityUseCase"; import { RiskAssessmentRepository } from "./domain/repositories/RiskAssessmentRepository"; import { RiskAssessmentD2Repository } from "./data/repositories/RiskAssessmentD2Repository"; import { RiskAssessmentTestRepository } from "./data/repositories/test/RiskAssessmentTestRepository"; +import { IncidentActionRepository } from "./domain/repositories/IncidentActionRepository"; +import { IncidentActionD2Repository } from "./data/repositories/IncidentActionD2Repository"; +import { IncidentActionTestRepository } from "./data/repositories/test/IncidentActionTestRepository"; export type CompositionRoot = ReturnType; @@ -37,6 +40,7 @@ type Repositories = { teamMemberRepository: TeamMemberRepository; orgUnitRepository: OrgUnitRepository; riskAssessmentRepository: RiskAssessmentRepository; + incidentActionRepository: IncidentActionRepository; }; function getCompositionRoot(repositories: Repositories) { @@ -44,7 +48,8 @@ function getCompositionRoot(repositories: Repositories) { getWithOptions: new GetEntityWithOptionsUseCase(repositories), save: new SaveEntityUseCase( repositories.diseaseOutbreakEventRepository, - repositories.riskAssessmentRepository + repositories.riskAssessmentRepository, + repositories.incidentActionRepository ), users: { getCurrent: new GetCurrentUserUseCase(repositories.usersRepository), @@ -68,6 +73,7 @@ export function getWebappCompositionRoot(api: D2Api) { teamMemberRepository: new TeamMemberD2Repository(api), orgUnitRepository: new OrgUnitD2Repository(api), riskAssessmentRepository: new RiskAssessmentD2Repository(api), + incidentActionRepository: new IncidentActionD2Repository(api), }; return getCompositionRoot(repositories); @@ -82,6 +88,7 @@ export function getTestCompositionRoot() { teamMemberRepository: new TeamMemberTestRepository(), orgUnitRepository: new OrgUnitTestRepository(), riskAssessmentRepository: new RiskAssessmentTestRepository(), + incidentActionRepository: new IncidentActionTestRepository(), }; return getCompositionRoot(repositories); diff --git a/src/data/repositories/IncidentActionD2Repository.ts b/src/data/repositories/IncidentActionD2Repository.ts new file mode 100644 index 00000000..66c6ab85 --- /dev/null +++ b/src/data/repositories/IncidentActionD2Repository.ts @@ -0,0 +1,191 @@ +import { D2Api } from "../../types/d2-api"; +import { Maybe } from "../../utils/ts-utils"; +import { apiToFuture, FutureData } from "../api-futures"; +import { Id } from "../../domain/entities/Ref"; +import { + RTSL_ZEBRA_INCIDENT_ACTION_PLAN_PROGRAM_STAGE_ID, + RTSL_ZEBRA_INCIDENT_RESPONSE_ACTION_PROGRAM_STAGE_ID, + RTSL_ZEBRA_ORG_UNIT_ID, + RTSL_ZEBRA_PROGRAM_ID, +} from "./consts/DiseaseOutbreakConstants"; +import { IncidentActionRepository } from "../../domain/repositories/IncidentActionRepository"; +import { + mapDataElementsToIncidentActionPlan, + mapDataElementsToIncidentResponseActions, + mapIncidentActionToDataElements, +} from "./utils/IncidentActionMapper"; +import { ActionPlanFormData, ResponseActionFormData } from "../../domain/entities/ConfigurableForm"; +import { getProgramStage } from "./utils/MetadataHelper"; +import { Future } from "../../domain/entities/generic/Future"; +import { D2TrackerEvent } from "@eyeseetea/d2-api/api/trackerEvents"; +import { Status, Verification } from "../../domain/entities/incident-action-plan/ResponseAction"; + +export const incidentActionPlanIds = { + iapType: "wr1I51WTHhl", + phoecLevel: "KgTXZonQEsm", + criticalInfoRequirements: "sgZ6MgzCI7m", + planningAssumptions: "RZviL2uz1Wa", + responseObjectives: "giq2C0lvCza", + responseStrategies: "lbcbEZ8bEpK", + expectedResults: "sB1N7Nkm5Y1", + responseActivitiesNarrative: "RnWk88dYOXN", +} as const; + +export type IncidentActionPlanDataValues = { + id: string; + iapType: Maybe; + phoecLevel: Maybe; + criticalInfoRequirements: Maybe; + planningAssumptions: Maybe; + responseObjectives: Maybe; + responseStrategies: Maybe; + expectedResults: Maybe; + responseActivitiesNarrative: Maybe; +}; + +export const incidentResponseActionsIds = { + mainTask: "k3FiTDWD18d", + subActivities: "i728CZUYlRB", + subPillar: "BQhCqEHOyej", + searchAssignRO: "Z9a067KbV5J", + dueDate: "i2M51y9qBoC", + timeLine: "xvWvQ3K1GVA", + status: "mUR4eNxgAwg", + verification: "M62NkbKXhqZ", +}; + +export type IncidentResponseActionsDataValues = { + id: string; + mainTask: Maybe; + subActivities: Maybe; + subPillar: Maybe; + searchAssignRO: Maybe; + dueDate: Maybe; + timeLine: Maybe; + status: Maybe; + verification: Maybe; +}; + +export class IncidentActionD2Repository implements IncidentActionRepository { + constructor(private api: D2Api) {} + + private fields = { + event: true, + dataValues: { + dataElement: { id: true, code: true }, + value: true, + }, + trackedEntity: true, + }; + + getIncidentActionPlan(diseaseOutbreakId: Id): FutureData> { + return apiToFuture( + this.api.tracker.events.get({ + program: RTSL_ZEBRA_PROGRAM_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + trackedEntity: diseaseOutbreakId, + programStage: RTSL_ZEBRA_INCIDENT_ACTION_PLAN_PROGRAM_STAGE_ID, + fields: this.fields, + }) + ).map(events => { + if (!events.instances[0]?.event) return undefined; + + const plan: IncidentActionPlanDataValues = mapDataElementsToIncidentActionPlan( + events.instances[0].event, + events.instances[0].dataValues + ); + + return plan; + }); + } + + getIncidentResponseActions( + diseaseOutbreakId: Id + ): FutureData> { + return apiToFuture( + this.api.tracker.events.get({ + program: RTSL_ZEBRA_PROGRAM_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + trackedEntity: diseaseOutbreakId, + programStage: RTSL_ZEBRA_INCIDENT_RESPONSE_ACTION_PROGRAM_STAGE_ID, + fields: this.fields, + }) + ).map(events => { + const responseActions: IncidentResponseActionsDataValues = + mapDataElementsToIncidentResponseActions( + events.instances[0]?.event ?? diseaseOutbreakId, + events.instances[0]?.dataValues ?? [] + ); + + return responseActions; + }); + } + + saveIncidentAction( + formData: ActionPlanFormData | ResponseActionFormData, + diseaseOutbreakId: Id + ): FutureData { + const programStageId = this.getProgramStageByFormType(formData.type); + + return getProgramStage(this.api, programStageId).flatMap(incidentResponse => { + const incidentDataElements = incidentResponse.objects[0]?.programStageDataElements; + + if (!incidentDataElements) + return Future.error( + new Error(` ${formData.type} Program Stage metadata not found`) + ); + + //Get the enrollment Id for the disease outbreak + return apiToFuture( + this.api.tracker.enrollments.get({ + fields: { + enrollment: true, + }, + trackedEntity: diseaseOutbreakId, + enrolledBefore: new Date().toISOString(), + program: RTSL_ZEBRA_PROGRAM_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + }) + ).flatMap(enrollmentResponse => { + const enrollmentId = enrollmentResponse.instances[0]?.enrollment; + if (!enrollmentId) { + return Future.error(new Error(`Enrollment not found for Disease Outbreak`)); + } + + const events: D2TrackerEvent = mapIncidentActionToDataElements( + formData, + programStageId, + diseaseOutbreakId, + enrollmentId, + incidentDataElements + ); + + return apiToFuture( + this.api.tracker.post( + { importStrategy: "CREATE_AND_UPDATE" }, + { events: [events] } + ) + ).flatMap(saveResponse => { + if (saveResponse.status === "ERROR" || !diseaseOutbreakId) { + return Future.error( + new Error(`Error saving Incident Action Plan Risk Assessment Grading`) + ); + } else { + return Future.success(undefined); + } + }); + }); + }); + } + + private getProgramStageByFormType(formType: string) { + switch (formType) { + case "incident-action-plan": + return RTSL_ZEBRA_INCIDENT_ACTION_PLAN_PROGRAM_STAGE_ID; + case "incident-response-action": + return RTSL_ZEBRA_INCIDENT_RESPONSE_ACTION_PROGRAM_STAGE_ID; + default: + throw new Error("Incident Action Form type not supported"); + } + } +} diff --git a/src/data/repositories/OptionsD2Repository.ts b/src/data/repositories/OptionsD2Repository.ts index aa550ad4..274b8373 100644 --- a/src/data/repositories/OptionsD2Repository.ts +++ b/src/data/repositories/OptionsD2Repository.ts @@ -34,6 +34,8 @@ export class OptionsD2Repository implements OptionsRepository { private likelihoodOptions: Map = new Map(); private consequencesOptions: Map = new Map(); private lowMediumHighOptions: Map = new Map(); + private statusOptions: Map = new Map(); + private verificationOptions: Map = new Map(); getMainSyndrome(optionCode: Code): FutureData