diff --git a/i18n/en.pot b/i18n/en.pot index 0090bc99..4d53e015 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-10-09T10:56:24.909Z\n" -"PO-Revision-Date: 2024-10-09T10:56:24.909Z\n" +"POT-Creation-Date: 2024-10-16T14:36:22.158Z\n" +"PO-Revision-Date: 2024-10-16T14:36:22.158Z\n" msgid "Low" msgstr "" @@ -84,6 +84,9 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Edit Action Plan" +msgstr "" + msgid "Edit Details" msgstr "" @@ -186,13 +189,25 @@ msgstr "" msgid "Risk Assessment Questionnaire saved successfully" msgstr "" +msgid "Incident Action Plan saved successfully" +msgstr "" + +msgid "Incident Response Actions saved successfully" +msgstr "" + msgid "Incident Management Team Member saved successfully" msgstr "" -msgid "Incident Action Plan" +msgid "Create an incident action plan" +msgstr "" + +msgid "No plan has been created for this incident" msgstr "" -msgid "Cholera in NW Province, June 2023" +msgid "Create IAP" +msgstr "" + +msgid "Incident Action Plan" msgstr "" msgid "Incident Management Team Builder" diff --git a/i18n/es.po b/i18n/es.po index 9edfa351..d1247b77 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-10-09T10:56:24.909Z\n" +"POT-Creation-Date: 2024-10-15T13:56:24.806Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -83,6 +83,9 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Edit Action Plan" +msgstr "" + msgid "Edit Details" msgstr "" @@ -185,12 +188,30 @@ msgstr "" msgid "Risk Assessment Questionnaire saved successfully" msgstr "" +msgid "Incident Action Plan saved successfully" +msgstr "" + +msgid "Incident Response Actions saved successfully" +msgstr "" + +msgid "Create an incident action plan" +msgstr "" + +msgid "No plan has been created for this incident" +msgstr "" + +msgid "Create IAP" +msgstr "" + msgid "Incident Management Team Member saved successfully" msgstr "" msgid "Incident Action Plan" msgstr "" +msgid "Incident Management Team Builder" +msgstr "" + msgid "Cholera in NW Province, June 2023" msgstr "" diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index 23d578f2..21556037 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -27,6 +27,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"; import { MapConfigRepository } from "./domain/repositories/MapConfigRepository"; import { MapConfigD2Repository } from "./data/repositories/MapConfigD2Repository"; import { MapConfigTestRepository } from "./data/repositories/test/MapConfigTestRepository"; @@ -42,6 +45,8 @@ 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 { GetIncidentActionByIdUseCase } from "./domain/usecases/GetIncidentActionByIdUseCase"; +import { UpdateIncidentResponseActionUseCase } from "./domain/usecases/UpdateIncidentResponseActionUseCase"; import { RoleRepository } from "./domain/repositories/RoleRepository"; import { RoleD2Repository } from "./data/repositories/RoleD2Repository"; import { RoleTestRepository } from "./data/repositories/test/RoleTestRepository"; @@ -71,6 +76,7 @@ type Repositories = { teamMemberRepository: TeamMemberRepository; orgUnitRepository: OrgUnitRepository; riskAssessmentRepository: RiskAssessmentRepository; + incidentActionRepository: IncidentActionRepository; mapConfigRepository: MapConfigRepository; performanceOverviewRepository: PerformanceOverviewRepository; roleRepository: RoleRepository; @@ -95,6 +101,10 @@ function getCompositionRoot(repositories: Repositories) { repositories.optionsRepository ), }, + incidentActionPlan: { + get: new GetIncidentActionByIdUseCase(repositories), + updateResponseAction: new UpdateIncidentResponseActionUseCase(repositories), + }, incidentManagementTeam: { get: new GetIncidentManagementTeamByIdUseCase(repositories), deleteIncidentManagementTeamMemberRole: @@ -135,6 +145,7 @@ export function getWebappCompositionRoot(api: D2Api) { teamMemberRepository: new TeamMemberD2Repository(api), orgUnitRepository: new OrgUnitD2Repository(api), riskAssessmentRepository: new RiskAssessmentD2Repository(api), + incidentActionRepository: new IncidentActionD2Repository(api), mapConfigRepository: new MapConfigD2Repository(api), performanceOverviewRepository: new PerformanceOverviewD2Repository(api, dataStoreClient), roleRepository: new RoleD2Repository(api), @@ -156,6 +167,7 @@ export function getTestCompositionRoot() { teamMemberRepository: new TeamMemberTestRepository(), orgUnitRepository: new OrgUnitTestRepository(), riskAssessmentRepository: new RiskAssessmentTestRepository(), + incidentActionRepository: new IncidentActionTestRepository(), mapConfigRepository: new MapConfigTestRepository(), performanceOverviewRepository: new PerformanceOverviewTestRepository(), roleRepository: new RoleTestRepository(), diff --git a/src/data/repositories/IncidentActionD2Repository.ts b/src/data/repositories/IncidentActionD2Repository.ts new file mode 100644 index 00000000..701edb59 --- /dev/null +++ b/src/data/repositories/IncidentActionD2Repository.ts @@ -0,0 +1,253 @@ +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, + UpdateIncidentResponseActionOptions, +} 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 { Status, Verification } from "../../domain/entities/incident-action-plan/ResponseAction"; +import { assertOrError } from "./utils/AssertOrError"; +import { D2TrackerEvent } from "@eyeseetea/d2-api/api/trackerEvents"; +import { statusCodeMap, verificationCodeMap } from "./consts/IncidentActionConstants"; + +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 IncidentResponseActionDataValues = { + 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 => { + if (events.instances.length === 0) return []; + + const responseActions: IncidentResponseActionDataValues[] = + mapDataElementsToIncidentResponseActions(events.instances); + + 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 = mapIncidentActionToDataElements( + formData, + programStageId, + diseaseOutbreakId, + enrollmentId, + incidentDataElements + ); + + return apiToFuture( + this.api.tracker.post( + { importStrategy: "CREATE_AND_UPDATE" }, + { events: Array.isArray(events) ? events : [events] } + ) + ).flatMap(saveResponse => { + if (saveResponse.status === "ERROR" || !diseaseOutbreakId) { + return Future.error(new Error(`Error saving Incident Action`)); + } else { + return Future.success(undefined); + } + }); + }); + }); + } + + updateIncidentResponseAction(options: UpdateIncidentResponseActionOptions): FutureData { + const { diseaseOutbreakId, eventId, responseAction } = options; + + 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, + event: eventId, + fields: { + enrollment: true, + dataValues: { + dataElement: true, + value: true, + }, + }, + }) + ) + .flatMap(response => assertOrError(response.instances[0], "Event")) + .flatMap(event => { + const enrollmentId = event.enrollment; + if (!enrollmentId) { + return Future.error(new Error(`Enrollment not found for response action`)); + } + + const valueCodeMaps = { ...statusCodeMap, ...verificationCodeMap }; + + const eventToPost: D2TrackerEvent = { + event: eventId, + program: RTSL_ZEBRA_PROGRAM_ID, + programStage: RTSL_ZEBRA_INCIDENT_RESPONSE_ACTION_PROGRAM_STAGE_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + enrollment: enrollmentId, + occurredAt: new Date().toISOString(), + trackedEntity: diseaseOutbreakId, + status: "ACTIVE", + dataValues: [ + { + dataElement: + incidentResponseActionsIds[ + responseAction.type as keyof typeof incidentResponseActionsIds + ], + value: valueCodeMaps[ + responseAction.value as keyof typeof valueCodeMaps + ], + }, + ], + }; + + return apiToFuture( + this.api.tracker.post({ importStrategy: "UPDATE" }, { events: [eventToPost] }) + ).flatMap(saveResponse => { + if (saveResponse.status === "ERROR") { + return Future.error(new Error(`Error saving Incident Response Action`)); + } 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 deaab05c..a0209774 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