From 4f427116300ca4bfbcfcd6fa04f749db2c8b418f Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:20:20 +0100 Subject: [PATCH] feat: incident response action form (wip) --- .../IncidentActionD2Repository.ts | 18 ++- .../consts/IncidentActionConstants.ts | 66 +++++---- .../test/IncidentActionTestRepository.ts | 4 +- .../utils/IncidentActionMapper.ts | 124 ++++++++--------- src/domain/entities/ConfigurableForm.ts | 2 +- .../incident-action-plan/ActionPlan.ts | 3 + .../repositories/IncidentActionRepository.ts | 4 +- .../incident-action/GetIncidentActionById.ts | 54 ++++---- .../GetIncidentActionPlanWithOptions.ts | 2 +- .../mapIncidentActionToInitialFormState.ts | 129 ++++++++++++------ .../form-page/mapFormStateToEntityData.ts | 30 ++-- src/webapp/pages/form-page/useForm.ts | 47 ++++++- .../IncidentActionPlanPage.tsx | 20 +-- .../useIncidentActionPlan.ts | 6 +- 14 files changed, 309 insertions(+), 200 deletions(-) diff --git a/src/data/repositories/IncidentActionD2Repository.ts b/src/data/repositories/IncidentActionD2Repository.ts index 66c6ab85..8b989b68 100644 --- a/src/data/repositories/IncidentActionD2Repository.ts +++ b/src/data/repositories/IncidentActionD2Repository.ts @@ -17,7 +17,6 @@ import { 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 = { @@ -54,7 +53,7 @@ export const incidentResponseActionsIds = { verification: "M62NkbKXhqZ", }; -export type IncidentResponseActionsDataValues = { +export type IncidentResponseActionDataValues = { id: string; mainTask: Maybe; subActivities: Maybe; @@ -101,7 +100,7 @@ export class IncidentActionD2Repository implements IncidentActionRepository { getIncidentResponseActions( diseaseOutbreakId: Id - ): FutureData> { + ): FutureData { return apiToFuture( this.api.tracker.events.get({ program: RTSL_ZEBRA_PROGRAM_ID, @@ -111,11 +110,10 @@ export class IncidentActionD2Repository implements IncidentActionRepository { fields: this.fields, }) ).map(events => { - const responseActions: IncidentResponseActionsDataValues = - mapDataElementsToIncidentResponseActions( - events.instances[0]?.event ?? diseaseOutbreakId, - events.instances[0]?.dataValues ?? [] - ); + if (events.instances.length === 0) return []; + + const responseActions: IncidentResponseActionDataValues[] = + mapDataElementsToIncidentResponseActions(events.instances); return responseActions; }); @@ -152,7 +150,7 @@ export class IncidentActionD2Repository implements IncidentActionRepository { return Future.error(new Error(`Enrollment not found for Disease Outbreak`)); } - const events: D2TrackerEvent = mapIncidentActionToDataElements( + const events = mapIncidentActionToDataElements( formData, programStageId, diseaseOutbreakId, @@ -163,7 +161,7 @@ export class IncidentActionD2Repository implements IncidentActionRepository { return apiToFuture( this.api.tracker.post( { importStrategy: "CREATE_AND_UPDATE" }, - { events: [events] } + { events: Array.isArray(events) ? events : [events] } ) ).flatMap(saveResponse => { if (saveResponse.status === "ERROR" || !diseaseOutbreakId) { diff --git a/src/data/repositories/consts/IncidentActionConstants.ts b/src/data/repositories/consts/IncidentActionConstants.ts index 710b757c..403ca38c 100644 --- a/src/data/repositories/consts/IncidentActionConstants.ts +++ b/src/data/repositories/consts/IncidentActionConstants.ts @@ -1,10 +1,14 @@ -import { ActionPlanAttrs } from "../../../domain/entities/incident-action-plan/ActionPlan"; +import { + ActionPlanAttrs, + ActionPlanIAPType, + ActionPlanPhoecLevel, +} from "../../../domain/entities/incident-action-plan/ActionPlan"; import { ResponseAction, - Status, - Verification, + ResponseActionStatusType, + ResponseActionVerificationType, } from "../../../domain/entities/incident-action-plan/ResponseAction"; -import { GetValue } from "../../../utils/ts-utils"; +import { GetValue, Maybe } from "../../../utils/ts-utils"; export const actionPlanConstants = { iapType: "RTSL_ZEB_DET_IAP_TYPE", @@ -26,30 +30,26 @@ export function isStringInIncidentActionPlanCodes(code: string): code is Inciden return (Object.values(actionPlanConstants) as string[]).includes(code); } -type IAPType = "Initial" | "Update" | "Final"; - -export const iapTypeCodeMap: Record = { +const iapTypeCodeMap: Record = { Initial: "RTSL_ZEB_OS_IAP_TYPE_INITIAL", Update: "RTSL_ZEB_OS_IAP_TYPE_UPDATE", Final: "RTSL_ZEB_OS_IAP_TYPE_FINAL", }; -export function getIAPTypeByCode(iapTypeCode: string): IAPType | undefined { - return (Object.keys(iapTypeCodeMap) as IAPType[]).find( +export function getIAPTypeByCode(iapTypeCode: string): Maybe { + return (Object.keys(iapTypeCodeMap) as ActionPlanIAPType[]).find( key => iapTypeCodeMap[key] === iapTypeCode ); } -type PhoecLevel = "Response" | "Watch" | "Alert"; - -export const phoecLevelCodeMap: Record = { +const phoecLevelCodeMap: Record = { Response: "RTSL_ZEB_OS_PHOEC_ACT_LEVEL_RESPONSE", Watch: "RTSL_ZEB_OS_PHOEC_ACT_LEVEL_WATCH", Alert: "RTSL_ZEB_OS_PHOEC_ACT_LEVEL_ALERT", }; -export function getPhoecLevelByCode(phoecLevelCode: string): PhoecLevel | undefined { - return (Object.keys(phoecLevelCodeMap) as PhoecLevel[]).find( +export function getPhoecLevelByCode(phoecLevelCode: string): ActionPlanPhoecLevel | undefined { + return (Object.keys(phoecLevelCodeMap) as ActionPlanPhoecLevel[]).find( key => phoecLevelCodeMap[key] === phoecLevelCode ); } @@ -76,6 +76,32 @@ export function isStringInIncidentResponseActionCodes( return (Object.values(responseActionConstants) as string[]).includes(code); } +const statusCodeMap: Record = { + "Not done": "RTSL_ZEB_OS_STATUS_NOT_DONE", + Pending: "RTSL_ZEB_OS_STATUS_PENDING", + "In Progress": "RTSL_ZEB_OS_STATUS_IN_PROGRESS", + Complete: "RTSL_ZEB_OS_STATUS_COMPLETE", +} as const; + +export function getStatusTypeByCode(iapTypeCode: string): Maybe { + return (Object.keys(statusCodeMap) as ResponseActionStatusType[]).find( + key => statusCodeMap[key] === iapTypeCode + ); +} + +const verificationCodeMap: Record = { + Verified: "RTSL_ZEB_OS_VERIFICATION_VERIFIED", + Unverified: "RTSL_ZEB_OS_VERIFICATION_UNVERIFIED", +}; + +export function getVerificationTypeByCode( + iapTypeCode: string +): Maybe { + return (Object.keys(verificationCodeMap) as ResponseActionVerificationType[]).find( + key => verificationCodeMap[key] === iapTypeCode + ); +} + export function getValueFromIncidentActionPlan( incidentActionPlan: ActionPlanAttrs ): Record { @@ -106,15 +132,3 @@ export function getValueFromIncidentResponseAction( RTSL_ZEB_DET_VERIFICATION: incidentResponseAction.verification, }; } - -export const statusMap: Record = { - RTSL_ZEB_OS_STATUS_NOT_DONE: Status.RTSL_ZEB_OS_STATUS_NOT_DONE, - RTSL_ZEB_OS_STATUS_PENDING: Status.RTSL_ZEB_OS_STATUS_PENDING, - RTSL_ZEB_OS_STATUS_IN_PROGRESS: Status.RTSL_ZEB_OS_STATUS_IN_PROGRESS, - RTSL_ZEB_OS_STATUS_COMPLETE: Status.RTSL_ZEB_OS_STATUS_COMPLETE, -}; - -export const verificationMap: Record = { - RTSL_ZEB_OS_VERIFICATION_UNVERIFIED: Verification.RTSL_ZEB_OS_VERIFICATION_UNVERIFIED, - RTSL_ZEB_OS_VERIFICATION_VERIFIED: Verification.RTSL_ZEB_OS_VERIFICATION_VERIFIED, -}; diff --git a/src/data/repositories/test/IncidentActionTestRepository.ts b/src/data/repositories/test/IncidentActionTestRepository.ts index b6b78163..2b82fccb 100644 --- a/src/data/repositories/test/IncidentActionTestRepository.ts +++ b/src/data/repositories/test/IncidentActionTestRepository.ts @@ -4,7 +4,7 @@ import { Maybe } from "../../../utils/ts-utils"; import { FutureData } from "../../api-futures"; import { IncidentActionPlanDataValues, - IncidentResponseActionsDataValues, + IncidentResponseActionDataValues, } from "../IncidentActionD2Repository"; export class IncidentActionTestRepository implements IncidentActionRepository { @@ -14,7 +14,7 @@ export class IncidentActionTestRepository implements IncidentActionRepository { getIncidentResponseActions( _diseaseOutbreakId: Id - ): FutureData> { + ): FutureData> { throw new Error("Method not implemented."); } diff --git a/src/data/repositories/utils/IncidentActionMapper.ts b/src/data/repositories/utils/IncidentActionMapper.ts index 42d59cb5..e7e0e6b7 100644 --- a/src/data/repositories/utils/IncidentActionMapper.ts +++ b/src/data/repositories/utils/IncidentActionMapper.ts @@ -2,7 +2,7 @@ import { D2TrackerEvent, DataValue } from "@eyeseetea/d2-api/api/trackerEvents"; import { IncidentActionPlanDataValues, incidentActionPlanIds, - IncidentResponseActionsDataValues, + IncidentResponseActionDataValues, incidentResponseActionsIds, } from "../IncidentActionD2Repository"; import { Id } from "../../../domain/entities/Ref"; @@ -22,12 +22,13 @@ import { isStringInIncidentActionPlanCodes, isStringInIncidentResponseActionCodes, ResponseActionCodes, - responseActionConstants, - statusMap, - verificationMap, } from "../consts/IncidentActionConstants"; import { RTSL_ZEBRA_ORG_UNIT_ID, RTSL_ZEBRA_PROGRAM_ID } from "../consts/DiseaseOutbreakConstants"; -import { ResponseAction } from "../../../domain/entities/incident-action-plan/ResponseAction"; +import { + ResponseAction, + Status, + Verification, +} from "../../../domain/entities/incident-action-plan/ResponseAction"; export function mapDataElementsToIncidentActionPlan( id: Id, @@ -64,43 +65,37 @@ export function mapDataElementsToIncidentActionPlan( } export function mapDataElementsToIncidentResponseActions( - id: Id, - dataValues: DataValue[] -): IncidentResponseActionsDataValues { - const fromMap = (key: keyof typeof responseActionConstants) => getValueFromMap(key, dataValues); - - const mainTask = getValueById(dataValues, incidentResponseActionsIds.mainTask); - const subActivities = getValueById(dataValues, incidentResponseActionsIds.subActivities); - const subPillar = getValueById(dataValues, incidentResponseActionsIds.subPillar); - const searchAssignRO = getValueById(dataValues, incidentResponseActionsIds.searchAssignRO); - const dueDate = getValueById(dataValues, incidentResponseActionsIds.dueDate); - const timeLine = getValueById(dataValues, incidentResponseActionsIds.timeLine); - - const status = statusMap[fromMap("status")]; - const verification = verificationMap[fromMap("verification")]; - - const incidentActionPlan: IncidentResponseActionsDataValues = { - id: id, - mainTask: mainTask, - subActivities: subActivities, - subPillar: subPillar, - searchAssignRO: searchAssignRO, - dueDate: dueDate, - timeLine: timeLine, - status: status, - verification: verification, - }; + instances: D2TrackerEvent[] +): IncidentResponseActionDataValues[] { + const incidentResponseActions: IncidentResponseActionDataValues[] = instances.map(instance => { + const { event, dataValues } = instance; + + const mainTask = getValueById(dataValues, incidentResponseActionsIds.mainTask); + const subActivities = getValueById(dataValues, incidentResponseActionsIds.subActivities); + const subPillar = getValueById(dataValues, incidentResponseActionsIds.subPillar); + const searchAssignRO = getValueById(dataValues, incidentResponseActionsIds.searchAssignRO); + const dueDate = getValueById(dataValues, incidentResponseActionsIds.dueDate); + const timeLine = getValueById(dataValues, incidentResponseActionsIds.timeLine); + const status = getValueById(dataValues, incidentResponseActionsIds.status) as Status; + const verification = getValueById( + dataValues, + incidentResponseActionsIds.verification + ) as Verification; + + return { + id: event, + mainTask, + subActivities, + subPillar, + searchAssignRO, + dueDate, + timeLine, + status, + verification, + }; + }); - return incidentActionPlan; -} - -function getValueFromMap( - key: keyof typeof responseActionConstants, - dataValues: DataValue[] -): string { - return ( - dataValues.find(dataValue => dataValue.value === responseActionConstants[key])?.value ?? "" - ); + return incidentResponseActions; } export function mapIncidentActionToDataElements( @@ -167,30 +162,35 @@ function mapIncidentResponseActionToDataElements( programStageId: Id, teiId: Id, enrollmentId: Id, - incidentResponseAction: ResponseAction, + incidentResponseActions: ResponseAction[], programStageDataElementsMetadata: D2ProgramStageDataElementsMetadata[] -): D2TrackerEvent { - const dataElementValues: Record = - getValueFromIncidentResponseAction(incidentResponseAction); - - const dataValues: DataValue[] = programStageDataElementsMetadata.map(programStage => { - if (!isStringInIncidentResponseActionCodes(programStage.dataElement.code)) { - throw new Error( - `DataElement code ${programStage.dataElement.code} not found in Incident Action Plan Codes` +): D2TrackerEvent[] { + return incidentResponseActions.map(incidentResponseAction => { + const dataElementValues: Record = + getValueFromIncidentResponseAction(incidentResponseAction); + + const dataValues: DataValue[] = programStageDataElementsMetadata.map(programStage => { + if (!isStringInIncidentResponseActionCodes(programStage.dataElement.code)) { + throw new Error( + `DataElement code ${programStage.dataElement.code} not found in Incident Action Plan Codes` + ); + } + const typedCode: IncidentResponseActionKeyCode = programStage.dataElement.code; + + return getPopulatedDataElement( + programStage.dataElement.id, + dataElementValues[typedCode] ); - } - const typedCode: IncidentResponseActionKeyCode = programStage.dataElement.code; - - return getPopulatedDataElement(programStage.dataElement.id, dataElementValues[typedCode]); + }); + + return getIncidentActionTrackerEvent( + programStageId, + incidentResponseAction.id, + enrollmentId, + dataValues, + teiId + ); }); - - return getIncidentActionTrackerEvent( - programStageId, - incidentResponseAction.id, - enrollmentId, - dataValues, - teiId - ); } function getPopulatedDataElement(dataElement: Id, value: Maybe): DataValue { diff --git a/src/domain/entities/ConfigurableForm.ts b/src/domain/entities/ConfigurableForm.ts index 84d5d9f3..884ce812 100644 --- a/src/domain/entities/ConfigurableForm.ts +++ b/src/domain/entities/ConfigurableForm.ts @@ -107,7 +107,7 @@ export type ActionPlanFormData = BaseFormData & { export type ResponseActionFormData = BaseFormData & { type: "incident-response-action"; eventTrackerDetails: DiseaseOutbreakEvent; - entity: Maybe; + entity: ResponseAction[]; options: IncidentResponseActionOptions; }; diff --git a/src/domain/entities/incident-action-plan/ActionPlan.ts b/src/domain/entities/incident-action-plan/ActionPlan.ts index b5f2b84a..64e8fffe 100644 --- a/src/domain/entities/incident-action-plan/ActionPlan.ts +++ b/src/domain/entities/incident-action-plan/ActionPlan.ts @@ -1,6 +1,9 @@ import { Struct } from "../generic/Struct"; import { Id } from "../Ref"; +export type ActionPlanIAPType = "Initial" | "Update" | "Final"; +export type ActionPlanPhoecLevel = "Response" | "Watch" | "Alert"; + export type ActionPlanAttrs = { id: Id; iapType: string; diff --git a/src/domain/repositories/IncidentActionRepository.ts b/src/domain/repositories/IncidentActionRepository.ts index 22fd46c0..9840854a 100644 --- a/src/domain/repositories/IncidentActionRepository.ts +++ b/src/domain/repositories/IncidentActionRepository.ts @@ -1,7 +1,7 @@ import { FutureData } from "../../data/api-futures"; import { IncidentActionPlanDataValues, - IncidentResponseActionsDataValues, + IncidentResponseActionDataValues, } from "../../data/repositories/IncidentActionD2Repository"; import { Maybe } from "../../utils/ts-utils"; import { ActionPlanFormData, ResponseActionFormData } from "../entities/ConfigurableForm"; @@ -11,7 +11,7 @@ export interface IncidentActionRepository { getIncidentActionPlan(diseaseOutbreakId: Id): FutureData>; getIncidentResponseActions( diseaseOutbreakId: Id - ): FutureData>; + ): FutureData>; saveIncidentAction( formData: ActionPlanFormData | ResponseActionFormData, diseaseOutbreakId: Id diff --git a/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts b/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts index 9ee4930d..a149411d 100644 --- a/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts +++ b/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts @@ -1,5 +1,5 @@ import { FutureData } from "../../../../data/api-futures"; -import { IncidentResponseActionsDataValues } from "../../../../data/repositories/IncidentActionD2Repository"; +import { IncidentResponseActionDataValues } from "../../../../data/repositories/IncidentActionD2Repository"; import { Maybe } from "../../../../utils/ts-utils"; import _c from "../../../entities/generic/Collection"; import { Future } from "../../../entities/generic/Future"; @@ -25,7 +25,7 @@ export function getIncidentAction( .getIncidentActionPlan(diseaseOutbreakId) .flatMap(incidentActionPlan => { const actionPlan = new ActionPlan({ - id: diseaseOutbreakId, + id: incidentActionPlan?.id ?? "", iapType: incidentActionPlan?.iapType ?? "", phoecLevel: incidentActionPlan?.phoecLevel ?? "", criticalInfoRequirements: incidentActionPlan?.criticalInfoRequirements ?? "", @@ -47,27 +47,29 @@ export function getIncidentAction( teamMemberRepository ) ).flatMap(responseActionOptions => { - const searchAssignRO = responseActionOptions.searchAssignRO; - - const responseActions = new ResponseAction({ - id: diseaseOutbreakId, - mainTask: responseActionDataValues?.mainTask ?? "", - subActivities: responseActionDataValues?.subActivities ?? "", - subPillar: responseActionDataValues?.subPillar ?? "", - searchAssignRO: searchAssignRO, - dueDate: responseActionDataValues?.dueDate - ? new Date(responseActionDataValues.dueDate) - : new Date(), - timeLine: responseActionDataValues?.timeLine ?? "", - status: responseActionOptions.status?.name as Status, - verification: responseActionOptions.verification?.name as Verification, - }); + const responseActions = + responseActionDataValues?.map(responseActionDataValue => { + return new ResponseAction({ + id: responseActionDataValue?.id ?? "", + mainTask: responseActionDataValue?.mainTask ?? "", + subActivities: responseActionDataValue?.subActivities ?? "", + subPillar: responseActionDataValue?.subPillar ?? "", + searchAssignRO: responseActionOptions.searchAssignRO, + dueDate: responseActionDataValue?.dueDate + ? new Date(responseActionDataValue.dueDate) + : new Date(), + timeLine: responseActionDataValue?.timeLine ?? "", + status: responseActionOptions.status?.id as Status, + verification: responseActionOptions.verification + ?.id as Verification, + }); + }) ?? []; const incidentAction = new IncidentActionPlan({ id: diseaseOutbreakId, lastUpdated: new Date(), actionPlan: actionPlan, - responseActions: [responseActions], + responseActions: responseActions, }); return Future.success(incidentAction); @@ -77,19 +79,21 @@ export function getIncidentAction( } function getIncidentResponseActionOptionFutures( - responseActionsBase: Maybe, + responseActionsBase: Maybe, optionsRepository: OptionsRepository, teamMemberRepository: TeamMemberRepository ) { + const responseActionBase = responseActionsBase ? responseActionsBase[0] : undefined; + return { - searchAssignRO: responseActionsBase?.searchAssignRO - ? teamMemberRepository.get(responseActionsBase.searchAssignRO) + searchAssignRO: responseActionBase?.searchAssignRO + ? teamMemberRepository.get(responseActionBase.searchAssignRO) : Future.success(undefined), - status: responseActionsBase?.status - ? optionsRepository.getStatusOption(responseActionsBase.status) + status: responseActionBase?.status + ? optionsRepository.getStatusOption(responseActionBase.status) : Future.success(undefined), - verification: responseActionsBase?.verification - ? optionsRepository.getVerificationOption(responseActionsBase.verification) + verification: responseActionBase?.verification + ? optionsRepository.getVerificationOption(responseActionBase.verification) : Future.success(undefined), }; } diff --git a/src/domain/usecases/utils/incident-action/GetIncidentActionPlanWithOptions.ts b/src/domain/usecases/utils/incident-action/GetIncidentActionPlanWithOptions.ts index 4c12b379..2485af32 100644 --- a/src/domain/usecases/utils/incident-action/GetIncidentActionPlanWithOptions.ts +++ b/src/domain/usecases/utils/incident-action/GetIncidentActionPlanWithOptions.ts @@ -47,7 +47,7 @@ export function getIncidentResponseActionWithOptions( const incidentResponseActionData: ResponseActionFormData = { type: "incident-response-action", eventTrackerDetails: eventTrackerDetails, - entity: eventTrackerDetails.incidentActionPlan?.responseActions[0], + entity: eventTrackerDetails.incidentActionPlan?.responseActions ?? [], options: { searchAssignRO: responsibleOfficers, status: statusOptions, diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index 996d5ed3..d19f4495 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -6,10 +6,13 @@ import { ActionPlanFormData, ResponseActionFormData, } from "../../../../domain/entities/ConfigurableForm"; +import { ResponseAction } from "../../../../domain/entities/incident-action-plan/ResponseAction"; +import { Maybe } from "../../../../utils/ts-utils"; import { FormSectionState } from "../../../components/form/FormSectionsState"; import { FormState } from "../../../components/form/FormState"; import { Option as UIOption } from "../../../components/utils/option"; import { mapToPresentationOptions } from "../mapEntityToFormState"; +import { getAnotherOptionSection } from "../risk-assessment/mapRiskAssessmentToInitialFormState"; type ActionPlanSectionKeys = | "iapType" @@ -196,8 +199,7 @@ export function mapIncidentActionPlanToInitialFormState( titleDescripton: "Step 1:", subtitleDescripton: "Define the action plan", saveButtonLabel: "Save & Continue", - // isValid: incidentActionPlan ? true : false, - isValid: true, + isValid: incidentActionPlan ? true : false, sections: [ mainSections.iapType, mainSections.phoecLevel, @@ -221,15 +223,57 @@ export function mapIncidentResponseActionToInitialFormState( } = incidentResponseActionFormData; const { searchAssignRO, status, verification } = options; - const searchAssignROOptions: UIOption[] = mapToPresentationOptions(searchAssignRO); const statusOptions: UIOption[] = mapToPresentationOptions(status); const verificationOptions: UIOption[] = mapToPresentationOptions(verification); + const sectionOptions = { + searchAssignROOptions: searchAssignROOptions, + statusOptions: statusOptions, + verificationOptions: verificationOptions, + }; + + const responseActionSections = + incidentResponseActions.flatMap((incidentResponseAction, index) => { + return getIncidentResponseActionFormSections( + { + ...sectionOptions, + incidentResponseAction, + }, + index + ); + }) ?? []; + + const addNewOptionSection: FormSectionState = getAnotherOptionSection(); + + return { + id: eventTrackerDetails.id ?? "", + title: "Incident Action Plan", + subtitle: eventTrackerDetails.name, + titleDescripton: "Step 2:", + subtitleDescripton: "Assign response actions", + saveButtonLabel: "Save plan", + isValid: incidentResponseActions ? true : false, + sections: [...responseActionSections, addNewOptionSection], + }; +} + +function getIncidentResponseActionFormSections( + options: { + incidentResponseAction: Maybe; + searchAssignROOptions: UIOption[]; + statusOptions: UIOption[]; + verificationOptions: UIOption[]; + }, + index: number +): FormSectionState[] { + const { incidentResponseAction, searchAssignROOptions, statusOptions, verificationOptions } = + options; + const mainSections: Record = { mainTask: { title: "Main task", - id: "main_task_section", + id: `main_task_section${index}`, isVisible: true, required: true, fields: [ @@ -238,7 +282,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.mainTask || "", + value: incidentResponseAction?.mainTask || "", type: "text", required: true, }, @@ -246,7 +290,7 @@ export function mapIncidentResponseActionToInitialFormState( }, subActivities: { title: "Sub activities", - id: "sub_activities_section", + id: `sub_activities_section${index}`, isVisible: true, required: true, fields: [ @@ -255,7 +299,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.subActivities || "", + value: incidentResponseAction?.subActivities || "", type: "text", required: true, }, @@ -263,7 +307,7 @@ export function mapIncidentResponseActionToInitialFormState( }, subPillar: { title: "Sub pilar", - id: "sub_pillar_section", + id: `sub_pillar_section${index}`, isVisible: true, required: true, fields: [ @@ -272,7 +316,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.subPillar || "", + value: incidentResponseAction?.subPillar || "", type: "text", required: true, }, @@ -280,7 +324,7 @@ export function mapIncidentResponseActionToInitialFormState( }, searchAssignRO: { title: "Responsible officer", - id: "responsible_officer_section", + id: `responsible_officer_section${index}`, isVisible: true, required: true, fields: [ @@ -289,7 +333,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.searchAssignRO?.name || "", + value: incidentResponseAction?.searchAssignRO?.id || "", type: "select", multiple: false, options: searchAssignROOptions, @@ -299,7 +343,7 @@ export function mapIncidentResponseActionToInitialFormState( }, dueDate: { title: "Due date", - id: "due_date_section", + id: `due_date_section${index}`, isVisible: true, required: true, fields: [ @@ -310,14 +354,14 @@ export function mapIncidentResponseActionToInitialFormState( errors: [], type: "date", required: true, - value: incidentResponseActions?.dueDate || new Date(), + value: incidentResponseAction?.dueDate || new Date(), width: "200px", }, ], }, timeline: { title: "Time line", - id: "time_line_section", + id: `time_line_section${index}`, isVisible: true, required: true, fields: [ @@ -326,7 +370,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.timeLine || "", + value: incidentResponseAction?.timeLine || "", type: "text", required: true, }, @@ -334,7 +378,7 @@ export function mapIncidentResponseActionToInitialFormState( }, status: { title: "Status", - id: "status_section", + id: `status_section${index}`, isVisible: true, required: true, fields: [ @@ -343,7 +387,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.status || "", + value: incidentResponseAction?.status || "", type: "select", multiple: false, options: statusOptions, @@ -353,7 +397,7 @@ export function mapIncidentResponseActionToInitialFormState( }, verification: { title: "Verification", - id: "verification_section", + id: `verification_section${index}`, isVisible: true, required: true, fields: [ @@ -362,7 +406,7 @@ export function mapIncidentResponseActionToInitialFormState( placeholder: "Select..", isVisible: true, errors: [], - value: incidentResponseActions?.verification || "", + value: incidentResponseAction?.verification || "", type: "select", multiple: false, options: verificationOptions, @@ -372,24 +416,31 @@ export function mapIncidentResponseActionToInitialFormState( }, }; - return { - id: eventTrackerDetails.id ?? "", - title: "Incident Action Plan", - subtitle: eventTrackerDetails.name, - titleDescripton: "Step 2:", - subtitleDescripton: "Assign response actions", - saveButtonLabel: "Save plan", - // isValid: incidentResponseActions ? true : false, - isValid: true, - sections: [ - mainSections.mainTask, - mainSections.subActivities, - mainSections.subPillar, - mainSections.searchAssignRO, - mainSections.dueDate, - mainSections.timeline, - mainSections.status, - mainSections.verification, - ], - }; + return [ + mainSections.mainTask, + mainSections.subActivities, + mainSections.subPillar, + mainSections.searchAssignRO, + mainSections.dueDate, + mainSections.timeline, + mainSections.status, + mainSections.verification, + ]; +} + +export function addNewResponseActionSection(sections: FormSectionState[]): FormSectionState[] { + const newResponseActionSection = getIncidentResponseActionFormSections( + { + incidentResponseAction: undefined, + searchAssignROOptions: + sections[0]?.fields[0]?.type === "select" ? sections[0].fields[0].options : [], + statusOptions: + sections[0]?.fields[1]?.type === "select" ? sections[0].fields[1].options : [], + verificationOptions: + sections[0]?.fields[2]?.type === "select" ? sections[0].fields[2].options : [], + }, + sections.length % 8 // not absolutely correct + ); + + return newResponseActionSection; } diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts index 336f39e7..86422864 100644 --- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts +++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts @@ -103,10 +103,10 @@ export function mapFormStateToEntityData( return actionPlanForm; } case "incident-response-action": { - const responseAction = mapFormStateToIncidentResponseAction(formState, formData); + const responseActions = mapFormStateToIncidentResponseAction(formState, formData); const responseActionForm: ResponseActionFormData = { ...formData, - entity: responseAction, + entity: responseActions, }; return responseActionForm; @@ -525,7 +525,7 @@ function mapFormStateToIncidentActionPlan( function mapFormStateToIncidentResponseAction( formState: FormState, formData: ResponseActionFormData -): ResponseAction { +): ResponseAction[] { const allFields: FormFieldState[] = getAllFieldsFromSections(formState.sections); const mainTask = allFields.find(field => field.id.includes(responseActionConstants.mainTask)) @@ -564,19 +564,21 @@ function mapFormStateToIncidentResponseAction( ); if (!verification) throw new Error("Verification not found"); - const incidentResponseAction: ResponseAction = new ResponseAction({ - id: formData.entity?.id ?? "", - mainTask: mainTask, - subActivities: subActivities, - subPillar: subPillar, - searchAssignRO: searchAssignRO, - dueDate: dueDate, - timeLine: timeLine, - status: Status.RTSL_ZEB_OS_STATUS_COMPLETE, - verification: Verification.RTSL_ZEB_OS_VERIFICATION_VERIFIED, + const incidentResponseActions: ResponseAction[] = formData.entity?.map(responseAction => { + return new ResponseAction({ + id: responseAction.id ?? "", + mainTask: mainTask, + subActivities: subActivities, + subPillar: subPillar, + searchAssignRO: searchAssignRO, + dueDate: dueDate, + timeLine: timeLine, + status: status.id as Status, + verification: verification.id as Verification, + }); }); - return incidentResponseAction; + return incidentResponseActions; } function getRiskAssessmentQuestionsWithOption( diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index ee36446a..fa5928bc 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -16,6 +16,7 @@ import { addNewCustomQuestionSection, getAnotherOptionSection, } from "./risk-assessment/mapRiskAssessmentToInitialFormState"; +import { addNewResponseActionSection } from "./incident-action/mapIncidentActionToInitialFormState"; export type GlobalMessage = { text: string; @@ -133,10 +134,54 @@ export function useForm(formType: FormType, id?: Id): State { }); break; } + case "incident-response-action": + setFormState(prevState => { + if (prevState.kind === "loaded") { + const otherSections = prevState.data.sections.filter( + section => section.id !== "addNewOptionSection" + ); + const addAnotherSection = getAnotherOptionSection(); + const newResponseActionSection = addNewResponseActionSection( + prevState.data.sections + ); + + const updatedData = { + ...prevState.data, + sections: [ + ...otherSections, + ...newResponseActionSection, + addAnotherSection, + ], + }; + + const allNewFields = newResponseActionSection.flatMap( + section => section.fields + ); + + const updatedAndValidatedData = allNewFields.reduce( + (acc, updatedFields) => { + return updateAndValidateFormState( + acc, + updatedFields, + configurableForm + ); + }, + updatedData + ); + + return { + kind: "loaded", + data: updatedAndValidatedData, + }; + } else { + return prevState; + } + }); + break; default: break; } - }, [configurableForm, formState.kind]); + }, [configurableForm, formState]); const handleFormChange = useCallback( (updatedField: FormFieldState) => { diff --git a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx index 32808bc3..4412486a 100644 --- a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx +++ b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx @@ -55,30 +55,22 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { ); }, []); - console.log({ - actionPlanSummary, - incidentActionFormSummary, - summaryError, - id, - eventTrackerDetails, - responseActionRows, - }); + const incidentActionExists = eventTrackerDetails?.incidentActionPlan?.actionPlan?.id; return ( - {!incidentActionFormSummary?.summary.every( - summaryLabel => summaryLabel.value !== undefined - ) ? ( + {!actionPlanSummary && responseActionRows.length === 0 && !summaryError && } + {!incidentActionExists ? ( - ) : incidentActionFormSummary ? ( + ) : ( <>
- {incidentActionFormSummary.summary.map((labelWithValue, index) => + {incidentActionFormSummary?.summary.map((labelWithValue, index) => getSummaryColumn( index, labelWithValue.label, @@ -119,8 +111,6 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { id={id} /> - ) : ( - )} ); diff --git a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts index c26ab1e1..7aa4794f 100644 --- a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts +++ b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts @@ -9,6 +9,8 @@ import { TableRowType } from "../../components/table/BasicTable"; import { getIAPTypeByCode, getPhoecLevelByCode, + getStatusTypeByCode, + getVerificationTypeByCode, } from "../../../data/repositories/consts/IncidentActionConstants"; type LabelWithValue = { @@ -130,8 +132,8 @@ const mapIncidentResponseActionToFormSummary = (diseaseOutbreakEvent: DiseaseOut subActivities: responseAction.subActivities, subPillar: responseAction.subPillar, searchAssignRO: responseAction.searchAssignRO?.username ?? "", - status: responseAction.status, - verification: responseAction.verification, + status: getStatusTypeByCode(responseAction.status) ?? "", + verification: getVerificationTypeByCode(responseAction.verification) ?? "", timeLine: responseAction.timeLine, dueDate: responseAction.dueDate.toISOString(), }));