diff --git a/i18n/en.pot b/i18n/en.pot index 836445da..057dc5b1 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-11-08T16:32:23.115Z\n" -"PO-Revision-Date: 2024-11-08T16:32:23.115Z\n" +"POT-Creation-Date: 2024-11-22T15:21:43.931Z\n" +"PO-Revision-Date: 2024-11-22T15:21:43.931Z\n" msgid "Low" msgstr "" @@ -135,9 +135,6 @@ msgstr "" msgid "Dashboard" msgstr "" -msgid "Respond, alert, watch" -msgstr "" - msgid "Select duration" msgstr "" @@ -165,7 +162,10 @@ msgstr "" msgid "Create Risk Assessment" msgstr "" -msgid "Add new Assessment" +msgid "Edit Risk Assessment" +msgstr "" + +msgid "Add new Grade" msgstr "" msgid "Risk assessment incomplete" diff --git a/i18n/es.po b/i18n/es.po index a4a9ac52..b4406244 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-11-08T16:32:23.115Z\n" +"POT-Creation-Date: 2024-11-22T15:21:43.931Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -134,9 +134,6 @@ msgstr "" msgid "Dashboard" msgstr "" -msgid "Respond, alert, watch" -msgstr "" - msgid "Select duration" msgstr "" @@ -164,7 +161,10 @@ msgstr "" msgid "Create Risk Assessment" msgstr "" -msgid "Add new Assessment" +msgid "Edit Risk Assessment" +msgstr "" + +msgid "Add new Grade" msgstr "" msgid "Risk assessment incomplete" diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index d027c1d3..f54db51d 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -68,6 +68,9 @@ import { ConfigurationsRepository } from "./domain/repositories/ConfigurationsRe import { ConfigurationsD2Repository } from "./data/repositories/ConfigurationsD2Repository"; import { ConfigurationsTestRepository } from "./data/repositories/test/ConfigurationsTestRepository"; import { CompleteEventTrackerUseCase } from "./domain/usecases/CompleteEventTrackerUseCase"; +import { UserGroupD2Repository } from "./data/repositories/UserGroupD2Repository"; +import { UserGroupRepository } from "./domain/repositories/UserGroupRepository"; +import { UserGroupTestRepository } from "./data/repositories/test/UserGroupTestRepository"; export type CompositionRoot = ReturnType; @@ -87,6 +90,7 @@ type Repositories = { chartConfigRepository: ChartConfigRepository; systemRepository: SystemRepository; configurationsRepository: ConfigurationsRepository; + userGroupRepository: UserGroupRepository; }; function getCompositionRoot(repositories: Repositories) { @@ -105,7 +109,8 @@ function getCompositionRoot(repositories: Repositories) { ), getConfigurations: new GetConfigurationsUseCase( repositories.configurationsRepository, - repositories.teamMemberRepository + repositories.teamMemberRepository, + repositories.userGroupRepository ), complete: new CompleteEventTrackerUseCase(repositories), }, @@ -160,6 +165,7 @@ export function getWebappCompositionRoot(api: D2Api) { chartConfigRepository: new ChartConfigD2Repository(dataStoreClient), systemRepository: new SystemD2Repository(api), configurationsRepository: new ConfigurationsD2Repository(api), + userGroupRepository: new UserGroupD2Repository(api), }; return getCompositionRoot(repositories); @@ -182,6 +188,7 @@ export function getTestCompositionRoot() { chartConfigRepository: new ChartConfigTestRepository(), systemRepository: new SystemTestRepository(), configurationsRepository: new ConfigurationsTestRepository(), + userGroupRepository: new UserGroupTestRepository(), }; return getCompositionRoot(repositories); diff --git a/src/data/repositories/IncidentActionD2Repository.ts b/src/data/repositories/IncidentActionD2Repository.ts index c15469f3..20c81878 100644 --- a/src/data/repositories/IncidentActionD2Repository.ts +++ b/src/data/repositories/IncidentActionD2Repository.ts @@ -17,13 +17,18 @@ import { mapDataElementsToIncidentResponseActions, mapIncidentActionToDataElements, } from "./utils/IncidentActionMapper"; -import { ActionPlanFormData, ResponseActionFormData } from "../../domain/entities/ConfigurableForm"; +import { + ActionPlanFormData, + ResponseActionFormData, + SingleResponseActionFormData, +} 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"; +import { FormType } from "../../webapp/pages/form-page/FormPage"; export const incidentActionPlanIds = { iapType: "wr1I51WTHhl", @@ -128,7 +133,7 @@ export class IncidentActionD2Repository implements IncidentActionRepository { } saveIncidentAction( - formData: ActionPlanFormData | ResponseActionFormData, + formData: ActionPlanFormData | ResponseActionFormData | SingleResponseActionFormData, diseaseOutbreakId: Id ): FutureData { const programStageId = this.getProgramStageByFormType(formData.type); @@ -242,10 +247,11 @@ export class IncidentActionD2Repository implements IncidentActionRepository { }); } - private getProgramStageByFormType(formType: string) { + private getProgramStageByFormType(formType: FormType): Id { switch (formType) { case "incident-action-plan": return RTSL_ZEBRA_INCIDENT_ACTION_PLAN_PROGRAM_STAGE_ID; + case "incident-response-actions": case "incident-response-action": return RTSL_ZEBRA_INCIDENT_RESPONSE_ACTION_PROGRAM_STAGE_ID; default: diff --git a/src/data/repositories/TeamMemberD2Repository.ts b/src/data/repositories/TeamMemberD2Repository.ts index 20f69a67..377fd9f4 100644 --- a/src/data/repositories/TeamMemberD2Repository.ts +++ b/src/data/repositories/TeamMemberD2Repository.ts @@ -6,7 +6,7 @@ import { apiToFuture, FutureData } from "../api-futures"; import { assertOrError } from "./utils/AssertOrError"; import { Future } from "../../domain/entities/generic/Future"; -const RTSL_ZEBRA_INCIDENTMANAGER = "RTSL_ZEBRA_INCIDENTMANAGER"; +export const RTSL_ZEBRA_INCIDENTMANAGER = "RTSL_ZEBRA_INCIDENTMANAGER"; const RTSL_ZEBRA_RISKASSESSOR = "RTSL_ZEBRA_RISKASSESSOR"; const RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_MEMBERS = "RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_MEMBERS"; const RTSL_ZEBRA_INCIDENT_RESPONSE_OFFICERS = "RTSL_ZEBRA_INCIDENT_RESPONSE_OFFICERS"; diff --git a/src/data/repositories/UserGroupD2Repository.ts b/src/data/repositories/UserGroupD2Repository.ts new file mode 100644 index 00000000..a94b1718 --- /dev/null +++ b/src/data/repositories/UserGroupD2Repository.ts @@ -0,0 +1,29 @@ +import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { UserGroupRepository } from "../../domain/repositories/UserGroupRepository"; +import { apiToFuture, FutureData } from "../api-futures"; +import { assertOrError } from "./utils/AssertOrError"; +import { UserGroup } from "../../domain/entities/UserGroup"; +import { RTSL_ZEBRA_INCIDENTMANAGER } from "./TeamMemberD2Repository"; + +export class UserGroupD2Repository implements UserGroupRepository { + constructor(private api: D2Api) {} + + getIncidentManagerUserGroupByCode(): FutureData { + return apiToFuture( + this.api.metadata.get({ + userGroups: { + fields: { + id: true, + }, + filter: { + code: { eq: RTSL_ZEBRA_INCIDENTMANAGER }, + }, + }, + }) + ) + .flatMap(response => + assertOrError(response.userGroups[0], `User group ${RTSL_ZEBRA_INCIDENTMANAGER}`) + ) + .map(userGroup => userGroup); + } +} diff --git a/src/data/repositories/consts/IncidentActionConstants.ts b/src/data/repositories/consts/IncidentActionConstants.ts index 87c552b7..e60b9341 100644 --- a/src/data/repositories/consts/IncidentActionConstants.ts +++ b/src/data/repositories/consts/IncidentActionConstants.ts @@ -82,9 +82,9 @@ export const statusCodeMap: Record = { Complete: "RTSL_ZEB_OS_STATUS_COMPLETE", } as const; -export function getStatusTypeByCode(iapTypeCode: string): Maybe { +export function getStatusTypeByCode(statusCode: string): Maybe { return (Object.keys(statusCodeMap) as ResponseActionStatusType[]).find( - key => statusCodeMap[key] === iapTypeCode + key => statusCodeMap[key] === statusCode ); } @@ -94,10 +94,10 @@ export const verificationCodeMap: Record }; export function getVerificationTypeByCode( - iapTypeCode: string + verificationCode: string ): Maybe { return (Object.keys(verificationCodeMap) as ResponseActionVerificationType[]).find( - key => verificationCodeMap[key] === iapTypeCode + key => verificationCodeMap[key] === verificationCode ); } diff --git a/src/data/repositories/test/UserGroupTestRepository.ts b/src/data/repositories/test/UserGroupTestRepository.ts new file mode 100644 index 00000000..c0f579b9 --- /dev/null +++ b/src/data/repositories/test/UserGroupTestRepository.ts @@ -0,0 +1,12 @@ +import { Future } from "../../../domain/entities/generic/Future"; +import { UserGroup } from "../../../domain/entities/UserGroup"; +import { UserGroupRepository } from "../../../domain/repositories/UserGroupRepository"; +import { FutureData } from "../../api-futures"; + +export class UserGroupTestRepository implements UserGroupRepository { + getIncidentManagerUserGroupByCode(): FutureData { + return Future.success({ + id: "1", + }); + } +} diff --git a/src/data/repositories/utils/IncidentActionMapper.ts b/src/data/repositories/utils/IncidentActionMapper.ts index 43d8e881..e8e0883f 100644 --- a/src/data/repositories/utils/IncidentActionMapper.ts +++ b/src/data/repositories/utils/IncidentActionMapper.ts @@ -10,6 +10,7 @@ import { Maybe } from "../../../utils/ts-utils"; import { ActionPlanFormData, ResponseActionFormData, + SingleResponseActionFormData, } from "../../../domain/entities/ConfigurableForm"; import { D2ProgramStageDataElementsMetadata } from "./RiskAssessmentMapper"; import { ActionPlanAttrs } from "../../../domain/entities/incident-action-plan/ActionPlan"; @@ -29,6 +30,7 @@ import { Status, Verification, } from "../../../domain/entities/incident-action-plan/ResponseAction"; +import _c from "../../../domain/entities/generic/Collection"; export function mapDataElementsToIncidentActionPlan( id: Id, @@ -101,7 +103,7 @@ export function mapDataElementsToIncidentResponseActions( } export function mapIncidentActionToDataElements( - formData: ActionPlanFormData | ResponseActionFormData, + formData: ActionPlanFormData | ResponseActionFormData | SingleResponseActionFormData, programStageId: Id, teiId: Id, enrollmentId: Id, @@ -118,6 +120,7 @@ export function mapIncidentActionToDataElements( formData.entity, programStageDataElementsMetadata ); + case "incident-response-actions": case "incident-response-action": return mapIncidentResponseActionToDataElements( programStageId, @@ -164,40 +167,66 @@ export function mapIncidentResponseActionToDataElements( programStageId: Id, teiId: Id, enrollmentId: Id, - incidentResponseActions: ResponseAction[], + incidentResponseActions: ResponseAction | ResponseAction[], programStageDataElementsMetadata: D2ProgramStageDataElementsMetadata[] ): D2TrackerEvent[] { - return incidentResponseActions.map(incidentResponseAction => { - const dataElementValues: Record = - getValueFromIncidentResponseAction(incidentResponseAction); - - const dataValues: DataValue[] = programStageDataElementsMetadata - .filter( - programStageDataElement => - programStageDataElement.dataElement.id !== incidentResponseActionsIds.timeLine - ) - .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] + return Array.isArray(incidentResponseActions) + ? incidentResponseActions.map(incidentResponseAction => { + return buildDataValuesFromResponseAction( + programStageId, + teiId, + enrollmentId, + incidentResponseAction, + programStageDataElementsMetadata + ); + }) + : [ + buildDataValuesFromResponseAction( + programStageId, + teiId, + enrollmentId, + incidentResponseActions, + programStageDataElementsMetadata + ), + ]; +} + +function buildDataValuesFromResponseAction( + programStageId: Id, + teiId: Id, + enrollmentId: Id, + responseAction: ResponseAction, + programStageDataElementsMetadata: D2ProgramStageDataElementsMetadata[] +): D2TrackerEvent { + const dataElementValues: Record = + getValueFromIncidentResponseAction(responseAction); + + const dataValues: DataValue[] = programStageDataElementsMetadata + .filter( + programStageDataElement => + programStageDataElement.dataElement.id !== incidentResponseActionsIds.timeLine + ) + .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 getIncidentActionTrackerEvent( - programStageId, - incidentResponseAction.id, - enrollmentId, - dataValues, - teiId - ); - }); + return getPopulatedDataElement( + programStage.dataElement.id, + dataElementValues[typedCode] + ); + }); + + return getIncidentActionTrackerEvent( + programStageId, + responseAction.id, + enrollmentId, + dataValues, + teiId + ); } function getPopulatedDataElement(dataElement: Id, value: Maybe): DataValue { diff --git a/src/domain/entities/AppConfigurations.ts b/src/domain/entities/AppConfigurations.ts index 5e531355..f181a07b 100644 --- a/src/domain/entities/AppConfigurations.ts +++ b/src/domain/entities/AppConfigurations.ts @@ -23,6 +23,7 @@ import { Capability1, Capability2, } from "./risk-assessment/RiskAssessmentGrading"; +import { UserGroup } from "./UserGroup"; export type SelectableOptions = { eventTrackerConfigurations: DiseaseOutbreakEventOptions; @@ -43,6 +44,7 @@ export type SelectableOptions = { incidentResponseActionConfigurations: IncidentResponseActionOptions; }; export type Configurations = { + incidentManagerUserGroup: UserGroup; selectableOptions: SelectableOptions; teamMembers: { all: TeamMember[]; diff --git a/src/domain/entities/ConfigurableForm.ts b/src/domain/entities/ConfigurableForm.ts index 2434d344..fb472a46 100644 --- a/src/domain/entities/ConfigurableForm.ts +++ b/src/domain/entities/ConfigurableForm.ts @@ -107,12 +107,19 @@ export type ActionPlanFormData = BaseFormData & { }; export type ResponseActionFormData = BaseFormData & { - type: "incident-response-action"; + type: "incident-response-actions"; eventTrackerDetails: DiseaseOutbreakEvent; entity: ResponseAction[]; options: IncidentResponseActionOptions; }; +export type SingleResponseActionFormData = BaseFormData & { + type: "incident-response-action"; + eventTrackerDetails: DiseaseOutbreakEvent; + entity: ResponseAction; + options: IncidentResponseActionOptions; +}; + export type IncidentManagementTeamRoleOptions = { roles: Role[]; teamMembers: TeamMember[]; @@ -135,4 +142,5 @@ export type ConfigurableForm = | RiskAssessmentQuestionnaireFormData | ActionPlanFormData | ResponseActionFormData + | SingleResponseActionFormData | IncidentManagementTeamMemberFormData; diff --git a/src/domain/entities/UserGroup.ts b/src/domain/entities/UserGroup.ts new file mode 100644 index 00000000..4049db80 --- /dev/null +++ b/src/domain/entities/UserGroup.ts @@ -0,0 +1,3 @@ +import { Ref } from "./Ref"; + +export type UserGroup = Ref; diff --git a/src/domain/repositories/IncidentActionRepository.ts b/src/domain/repositories/IncidentActionRepository.ts index dce5bc93..1ef27f31 100644 --- a/src/domain/repositories/IncidentActionRepository.ts +++ b/src/domain/repositories/IncidentActionRepository.ts @@ -4,7 +4,11 @@ import { IncidentResponseActionDataValues, } from "../../data/repositories/IncidentActionD2Repository"; import { Maybe } from "../../utils/ts-utils"; -import { ActionPlanFormData, ResponseActionFormData } from "../entities/ConfigurableForm"; +import { + ActionPlanFormData, + ResponseActionFormData, + SingleResponseActionFormData, +} from "../entities/ConfigurableForm"; import { Id } from "../entities/Ref"; export interface IncidentActionRepository { @@ -13,7 +17,7 @@ export interface IncidentActionRepository { diseaseOutbreakId: Id ): FutureData>; saveIncidentAction( - formData: ActionPlanFormData | ResponseActionFormData, + formData: ActionPlanFormData | ResponseActionFormData | SingleResponseActionFormData, diseaseOutbreakId: Id ): FutureData; updateIncidentResponseAction(options: UpdateIncidentResponseActionOptions): FutureData; diff --git a/src/domain/repositories/UserGroupRepository.ts b/src/domain/repositories/UserGroupRepository.ts new file mode 100644 index 00000000..5693efce --- /dev/null +++ b/src/domain/repositories/UserGroupRepository.ts @@ -0,0 +1,6 @@ +import { FutureData } from "../../data/api-futures"; +import { UserGroup } from "../entities/UserGroup"; + +export interface UserGroupRepository { + getIncidentManagerUserGroupByCode(): FutureData; +} diff --git a/src/domain/usecases/GetConfigurableFormUseCase.ts b/src/domain/usecases/GetConfigurableFormUseCase.ts index bb842569..baf9adf3 100644 --- a/src/domain/usecases/GetConfigurableFormUseCase.ts +++ b/src/domain/usecases/GetConfigurableFormUseCase.ts @@ -13,7 +13,10 @@ import { RoleRepository } from "../repositories/RoleRepository"; import { TeamMemberRepository } from "../repositories/TeamMemberRepository"; import { getDiseaseOutbreakConfigurableForm } from "./utils/disease-outbreak/GetDiseaseOutbreakConfigurableForm"; import { getActionPlanConfigurableForm } from "./utils/incident-action/GetActionPlanConfigurableForm"; -import { getResponseActionConfigurableForm } from "./utils/incident-action/GetResponseActionConfigurableForm"; +import { + getResponseActionConfigurableForm, + getSingleResponseActionConfigurableForm, +} from "./utils/incident-action/GetResponseActionConfigurableForm"; import { getIncidentManagementTeamWithOptions } from "./utils/incident-management-team/GetIncidentManagementTeamWithOptions"; import { getRiskAssessmentGradingConfigurableForm } from "./utils/risk-assessment/GetGradingConfigurableForm"; import { getRiskAssessmentQuestionnaireConfigurableForm } from "./utils/risk-assessment/GetQuestionnaireConfigurableForm"; @@ -30,12 +33,15 @@ export class GetConfigurableFormUseCase { } ) {} - public execute( - formType: FormType, - eventTrackerDetails: Maybe, - configurations: Configurations, - id?: Id - ): FutureData { + public execute(options: { + formType: FormType; + eventTrackerDetails: Maybe; + configurations: Configurations; + id?: Id; + responseActionId?: Id; + }): FutureData { + const { formType, eventTrackerDetails, configurations, id, responseActionId } = options; + switch (formType) { case "disease-outbreak-event": { return getDiseaseOutbreakConfigurableForm(this.options, configurations, id); @@ -74,14 +80,31 @@ export class GetConfigurableFormUseCase { ); return getActionPlanConfigurableForm(eventTrackerDetails, configurations); - case "incident-response-action": + case "incident-response-actions": if (!eventTrackerDetails) return Future.error( new Error("Disease outbreak id is required for incident action plan") ); return getResponseActionConfigurableForm(eventTrackerDetails, configurations); + case "incident-response-action": + if (!eventTrackerDetails) + return Future.error( + new Error("Disease outbreak id is required for incident action plan") + ); + + if (!responseActionId) + return Future.error( + new Error( + "Response action id is required for single incident response action" + ) + ); + return getSingleResponseActionConfigurableForm({ + eventTrackerDetails: eventTrackerDetails, + responseActionId: responseActionId, + configurations: configurations, + }); case "incident-management-team-member-assignment": if (!eventTrackerDetails) return Future.error( diff --git a/src/domain/usecases/GetConfigurationsUseCase.ts b/src/domain/usecases/GetConfigurationsUseCase.ts index 7b6fd86f..11791bb6 100644 --- a/src/domain/usecases/GetConfigurationsUseCase.ts +++ b/src/domain/usecases/GetConfigurationsUseCase.ts @@ -4,11 +4,13 @@ import { Future } from "../entities/generic/Future"; import { TeamMember } from "../entities/incident-management-team/TeamMember"; import { ConfigurationsRepository } from "../repositories/ConfigurationsRepository"; import { TeamMemberRepository } from "../repositories/TeamMemberRepository"; +import { UserGroupRepository } from "../repositories/UserGroupRepository"; export class GetConfigurationsUseCase { constructor( private configurationsRepository: ConfigurationsRepository, - private teamMemberRepository: TeamMemberRepository + private teamMemberRepository: TeamMemberRepository, + private userGroupRepository: UserGroupRepository ) {} public execute(): FutureData { @@ -18,6 +20,7 @@ export class GetConfigurationsUseCase { managers: this.teamMemberRepository.getIncidentManagers(), riskAssessors: this.teamMemberRepository.getRiskAssessors(), selectableOptionsResponse: this.configurationsRepository.getSelectableOptions(), + incidentManagerUserGroup: this.userGroupRepository.getIncidentManagerUserGroupByCode(), }).flatMap( ({ allTeamMembers, @@ -25,6 +28,7 @@ export class GetConfigurationsUseCase { managers, riskAssessors, selectableOptionsResponse, + incidentManagerUserGroup, }) => { const selectableOptions: SelectableOptions = this.mapOptionsAndTeamMembersToSelectableOptions( @@ -36,6 +40,7 @@ export class GetConfigurationsUseCase { const configurations: Configurations = { selectableOptions: selectableOptions, + incidentManagerUserGroup: incidentManagerUserGroup, teamMembers: { all: allTeamMembers, riskAssessors: riskAssessors, diff --git a/src/domain/usecases/SaveEntityUseCase.ts b/src/domain/usecases/SaveEntityUseCase.ts index 629708eb..9d96465c 100644 --- a/src/domain/usecases/SaveEntityUseCase.ts +++ b/src/domain/usecases/SaveEntityUseCase.ts @@ -52,6 +52,7 @@ export class SaveEntityUseCase { formData.eventTrackerDetails.id ); case "incident-action-plan": + case "incident-response-actions": case "incident-response-action": return this.options.incidentActionRepository.saveIncidentAction( formData, diff --git a/src/domain/usecases/utils/incident-action/GetResponseActionConfigurableForm.ts b/src/domain/usecases/utils/incident-action/GetResponseActionConfigurableForm.ts index bfc2c017..50674397 100644 --- a/src/domain/usecases/utils/incident-action/GetResponseActionConfigurableForm.ts +++ b/src/domain/usecases/utils/incident-action/GetResponseActionConfigurableForm.ts @@ -1,6 +1,9 @@ import { FutureData } from "../../../../data/api-futures"; import { Configurations } from "../../../entities/AppConfigurations"; -import { ResponseActionFormData } from "../../../entities/ConfigurableForm"; +import { + ResponseActionFormData, + SingleResponseActionFormData, +} from "../../../entities/ConfigurableForm"; import { DiseaseOutbreakEvent } from "../../../entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { Future } from "../../../entities/generic/Future"; @@ -10,7 +13,7 @@ export function getResponseActionConfigurableForm( ): FutureData { const { incidentResponseActionConfigurations } = configurations.selectableOptions; const incidentResponseActionData: ResponseActionFormData = { - type: "incident-response-action", + type: "incident-response-actions", eventTrackerDetails: eventTrackerDetails, entity: eventTrackerDetails.incidentActionPlan?.responseActions ?? [], options: { @@ -29,3 +32,38 @@ export function getResponseActionConfigurableForm( return Future.success(incidentResponseActionData); } + +export function getSingleResponseActionConfigurableForm(options: { + eventTrackerDetails: DiseaseOutbreakEvent; + responseActionId: string; + configurations: Configurations; +}): FutureData { + const { eventTrackerDetails, responseActionId, configurations } = options; + const { incidentResponseActionConfigurations } = configurations.selectableOptions; + + const incidentResponseActionData: SingleResponseActionFormData = { + type: "incident-response-action", + eventTrackerDetails: eventTrackerDetails, + entity: + eventTrackerDetails.incidentActionPlan?.responseActions.find( + responseAction => responseAction.id === responseActionId + ) ?? + (() => { + throw new Error("Response Action not found"); + })(), + options: { + searchAssignRO: incidentResponseActionConfigurations.searchAssignRO, + status: incidentResponseActionConfigurations.status, + verification: incidentResponseActionConfigurations.verification, + }, + // TODO: Get labels from Datastore used in mapEntityToInitialFormState to create initial form state + labels: { + errors: { + field_is_required: "This field is required", + }, + }, + rules: [], + }; + + return Future.success(incidentResponseActionData); +} diff --git a/src/utils/tests.tsx b/src/utils/tests.tsx index e94989b2..1ad68ef1 100644 --- a/src/utils/tests.tsx +++ b/src/utils/tests.tsx @@ -20,6 +20,7 @@ export function getTestContext() { orgUnits: [], isDev: true, configurations: { + incidentManagerUserGroup: { id: "incidentManagerUserGroup" }, selectableOptions: { eventTrackerConfigurations: { dataSources: [], diff --git a/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx b/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx index 52bc5661..168d69b7 100644 --- a/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx +++ b/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx @@ -84,6 +84,11 @@ export const EventTrackerFormSummary: React.FC = R titleVariant="secondary" > + + {formSummary.incidentManager && ( + + )} + {formSummary.summary.map((labelWithValue, index) => index < ROW_COUNT @@ -106,11 +111,6 @@ export const EventTrackerFormSummary: React.FC = R ) )} - - {formSummary.incidentManager && ( - - )} - diff --git a/src/webapp/components/table/BasicTable.tsx b/src/webapp/components/table/BasicTable.tsx index 4193aca6..fb3ecea7 100644 --- a/src/webapp/components/table/BasicTable.tsx +++ b/src/webapp/components/table/BasicTable.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useState } from "react"; import { + Button, Table, TableBody, TableCell, @@ -13,6 +14,9 @@ import i18n from "../../../utils/i18n"; import { Option } from "../utils/option"; import { Cell } from "./Cell"; import _c from "../../../domain/entities/generic/Collection"; +import { EditOutlined } from "@material-ui/icons"; +import { RouteName, useRoutes } from "../../hooks/useRoutes"; +import { FormType } from "../../pages/form-page/FormPage"; const noop = () => {}; @@ -43,13 +47,16 @@ interface BasicTableProps { onChange?: (cell: Maybe, rowIndex: number, column: TableColumn["value"]) => void; showRowIndex?: boolean; onOrderBy?: (direction: "asc" | "desc") => void; + formType?: FormType; + onClickRow?: (rowId: string) => void; } const sortableColumnLabels = ["Assessment Date", "Due date"]; export const BasicTable: React.FC = React.memo( - ({ columns, rows, onChange = noop, showRowIndex = false, onOrderBy }) => { + ({ columns, rows, onChange = noop, showRowIndex = false, onOrderBy, formType, onClickRow }) => { const [order, setOrder] = useState<"asc" | "desc">(); + const { goTo } = useRoutes(); const orderBy = useCallback(() => { const updatedOrder = order === "asc" ? "desc" : "asc"; @@ -57,6 +64,17 @@ export const BasicTable: React.FC = React.memo( onOrderBy && onOrderBy(updatedOrder); }, [onOrderBy, order]); + const goToEdit = useCallback( + (id: string) => { + if (formType) + goTo(RouteName.EDIT_FORM, { + formType: formType, + id, + }); + }, + [formType, goTo] + ); + return ( @@ -79,6 +97,7 @@ export const BasicTable: React.FC = React.memo( ) )} + {onClickRow && } @@ -94,6 +113,20 @@ export const BasicTable: React.FC = React.memo( onChange={onChange} /> ))} + {onClickRow && ( + + + + )} ))} diff --git a/src/webapp/hooks/useHasCurrentUserCaptureAccess.ts b/src/webapp/hooks/useHasCurrentUserCaptureAccess.ts index 2882d1b4..b1f5cfa1 100644 --- a/src/webapp/hooks/useHasCurrentUserCaptureAccess.ts +++ b/src/webapp/hooks/useHasCurrentUserCaptureAccess.ts @@ -25,6 +25,7 @@ export function useCheckWritePermission(formType: FormType) { "You do not have permission to create/edit incident action plans" ); break; + case "incident-response-actions": case "incident-response-action": snackbar.error( "You do not have permission to create/edit incident response actions" diff --git a/src/webapp/hooks/useRoutes.ts b/src/webapp/hooks/useRoutes.ts index b03d1e9d..9b50504f 100644 --- a/src/webapp/hooks/useRoutes.ts +++ b/src/webapp/hooks/useRoutes.ts @@ -20,6 +20,7 @@ const formTypes = [ "risk-assessment-summary", "risk-assessment-questionnaire", "incident-action-plan", + "incident-response-actions", "incident-response-action", "incident-management-team-member-assignment", ] as const satisfies FormType[]; diff --git a/src/webapp/pages/dashboard/DashboardPage.tsx b/src/webapp/pages/dashboard/DashboardPage.tsx index bdb312ad..93d95dfe 100644 --- a/src/webapp/pages/dashboard/DashboardPage.tsx +++ b/src/webapp/pages/dashboard/DashboardPage.tsx @@ -63,7 +63,7 @@ export const DashboardPage: React.FC = React.memo(() => { showCreateEvent lastAnalyticsRuntime={lastAnalyticsRuntime} > -
+
{selectorFiltersConfig.map(({ id, label, placeholder, options, type }) => { return ( diff --git a/src/webapp/pages/event-tracker/EventTrackerPage.tsx b/src/webapp/pages/event-tracker/EventTrackerPage.tsx index 42e15157..788bebb1 100644 --- a/src/webapp/pages/event-tracker/EventTrackerPage.tsx +++ b/src/webapp/pages/event-tracker/EventTrackerPage.tsx @@ -67,6 +67,12 @@ export const EventTrackerPage: React.FC = React.memo(() => { }); }, [goTo]); + const goToRiskGradingForm = useCallback(() => { + goTo(RouteName.CREATE_FORM, { + formType: "risk-assessment-grading", + }); + }, [goTo]); + const { performanceMetrics717, isLoading: _717CardsLoading } = use717Performance( "event_tracker", id @@ -127,16 +133,28 @@ export const EventTrackerPage: React.FC = React.memo(() => { {i18n.t("Create Risk Assessment")} ) : ( - + + + + ) } titleVariant="secondary" @@ -244,3 +262,8 @@ export const EventTrackerPage: React.FC = React.memo(() => { const DurationFilterContainer = styled.div` max-width: 250px; `; + +const ButtonContainer = styled.div` + display: flex; + gap: 8px; +`; diff --git a/src/webapp/pages/form-page/FormPage.tsx b/src/webapp/pages/form-page/FormPage.tsx index 3d0ad125..39ab385a 100644 --- a/src/webapp/pages/form-page/FormPage.tsx +++ b/src/webapp/pages/form-page/FormPage.tsx @@ -13,6 +13,7 @@ export type FormType = | "risk-assessment-questionnaire" | "risk-assessment-summary" | "incident-action-plan" + | "incident-response-actions" | "incident-response-action" | "incident-management-team-member-assignment"; diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index 0a0f654a..1a414777 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -2,11 +2,14 @@ import { actionPlanConstants, responseActionConstants, } from "../../../../data/repositories/consts/IncidentActionConstants"; +import { Configurations } from "../../../../domain/entities/AppConfigurations"; import { ActionPlanFormData, ResponseActionFormData, + SingleResponseActionFormData, } from "../../../../domain/entities/ConfigurableForm"; import { ResponseAction } from "../../../../domain/entities/incident-action-plan/ResponseAction"; +import { Option } from "../../../../domain/entities/Ref"; import { Maybe } from "../../../../utils/ts-utils"; import { FormSectionState } from "../../../components/form/FormSectionsState"; import { FormState } from "../../../components/form/FormState"; @@ -218,8 +221,9 @@ export function mapIncidentActionPlanToInitialFormState( }; } -export function mapIncidentResponseActionToInitialFormState( - incidentResponseActionFormData: ResponseActionFormData +export function mapIncidentResponseActionsToInitialFormState( + incidentResponseActionFormData: ResponseActionFormData, + isIncidentManager: boolean ): FormState { const { entity: incidentResponseActions, @@ -239,6 +243,7 @@ export function mapIncidentResponseActionToInitialFormState( statusOptions: statusOptions, verificationOptions: verificationOptions, }, + isIncidentManager: isIncidentManager, index: 0, }); @@ -252,6 +257,7 @@ export function mapIncidentResponseActionToInitialFormState( statusOptions: statusOptions, verificationOptions: verificationOptions, }, + isIncidentManager: isIncidentManager, index: index, }); }) @@ -271,6 +277,44 @@ export function mapIncidentResponseActionToInitialFormState( }; } +export function mapSingleIncidentResponseActionToInitialFormState( + incidentResponseActionFormData: SingleResponseActionFormData, + isIncidentManager: boolean +): FormState { + const { + entity: incidentResponseAction, + eventTrackerDetails, + options, + } = incidentResponseActionFormData; + + const { searchAssignRO, status, verification } = options; + const searchAssignROOptions: UIOption[] = mapToPresentationOptions(searchAssignRO); + const statusOptions: UIOption[] = mapToPresentationOptions(status); + const verificationOptions: UIOption[] = mapToPresentationOptions(verification); + + const responseActionSection = getResponseActionSection({ + incidentResponseAction: incidentResponseAction, + options: { + searchAssignROOptions: searchAssignROOptions, + statusOptions: statusOptions, + verificationOptions: verificationOptions, + }, + isIncidentManager: isIncidentManager, + index: 0, + }); + + return { + id: eventTrackerDetails.id ?? "", + title: "Incident Action Plan", + subtitle: eventTrackerDetails.name, + titleDescripton: "Step 2:", + subtitleDescripton: "Edit response action", + saveButtonLabel: "Save plan", + isValid: incidentResponseAction ? true : false, + sections: [responseActionSection], + }; +} + function getResponseActionSection(options: { incidentResponseAction: Maybe; options: { @@ -278,9 +322,10 @@ function getResponseActionSection(options: { statusOptions: UIOption[]; verificationOptions: UIOption[]; }; + isIncidentManager: boolean; index: number; }) { - const { incidentResponseAction, options: formOptions, index } = options; + const { incidentResponseAction, options: formOptions, index, isIncidentManager } = options; const { searchAssignROOptions, statusOptions, verificationOptions } = formOptions; const responseActionSection: FormSectionState = { @@ -365,13 +410,13 @@ function getResponseActionSection(options: { { id: `${responseActionConstants.verification}_${index}`, label: "Verification", - isVisible: true, + isVisible: isIncidentManager ? true : false, errors: [], value: incidentResponseAction?.verification || "", type: "select", multiple: false, options: verificationOptions, - required: true, + required: isIncidentManager ? true : false, showIsRequired: true, disabled: false, }, @@ -381,23 +426,35 @@ function getResponseActionSection(options: { return responseActionSection; } -export function addNewResponseActionSection(sections: FormSectionState[]): FormSectionState { +export function addNewResponseActionSection( + sections: FormSectionState[], + configurations: Configurations, + isIncidentManager: boolean +): FormSectionState { const responseActionSections = sections.filter( section => !section.id.startsWith("addNewResponseActionSection") ); + const { searchAssignRO, status, verification } = + configurations.selectableOptions.incidentResponseActionConfigurations; + const newResponseActionSection = getResponseActionSection({ incidentResponseAction: undefined, options: { - searchAssignROOptions: - sections[0]?.fields[3]?.type === "select" ? sections[0].fields[3].options : [], - statusOptions: - sections[0]?.fields[6]?.type === "select" ? sections[0].fields[6].options : [], - verificationOptions: - sections[0]?.fields[7]?.type === "select" ? sections[0].fields[7].options : [], + searchAssignROOptions: getValueLabelOptions(searchAssignRO), + statusOptions: getValueLabelOptions(status), + verificationOptions: getValueLabelOptions(verification), }, + isIncidentManager: isIncidentManager, index: responseActionSections.length, }); return newResponseActionSection; } + +function getValueLabelOptions(options: Option[]): UIOption[] { + return options.map(option => ({ + value: option.id, + label: option.name, + })); +} diff --git a/src/webapp/pages/form-page/mapEntityToFormState.ts b/src/webapp/pages/form-page/mapEntityToFormState.ts index 1513f837..386ecb7f 100644 --- a/src/webapp/pages/form-page/mapEntityToFormState.ts +++ b/src/webapp/pages/form-page/mapEntityToFormState.ts @@ -11,7 +11,8 @@ import { Option as PresentationOption } from "../../components/utils/option"; import { mapDiseaseOutbreakEventToInitialFormState } from "./disease-outbreak-event/mapDiseaseOutbreakEventToInitialFormState"; import { mapIncidentActionPlanToInitialFormState, - mapIncidentResponseActionToInitialFormState, + mapIncidentResponseActionsToInitialFormState, + mapSingleIncidentResponseActionToInitialFormState, } from "./incident-action/mapIncidentActionToInitialFormState"; import { mapIncidentManagementTeamMemberToInitialFormState } from "./incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState"; import { @@ -20,11 +21,14 @@ import { mapRiskGradingToInitialFormState, } from "./risk-assessment/mapRiskAssessmentToInitialFormState"; -export function mapEntityToFormState( - configurableForm: ConfigurableForm, - editMode?: boolean, - existingEventTrackerTypes?: (DiseaseNames | HazardNames)[] -): FormState { +export function mapEntityToFormState(options: { + configurableForm: ConfigurableForm; + editMode?: boolean; + existingEventTrackerTypes?: (DiseaseNames | HazardNames)[]; + isIncidentManager?: boolean; +}): FormState { + const { configurableForm, editMode, existingEventTrackerTypes, isIncidentManager } = options; + switch (configurableForm.type) { case "disease-outbreak-event": return mapDiseaseOutbreakEventToInitialFormState( @@ -40,8 +44,16 @@ export function mapEntityToFormState( return mapRiskAssessmentQuestionnaireToInitialFormState(configurableForm); case "incident-action-plan": return mapIncidentActionPlanToInitialFormState(configurableForm); + case "incident-response-actions": + return mapIncidentResponseActionsToInitialFormState( + configurableForm, + isIncidentManager ?? false + ); case "incident-response-action": - return mapIncidentResponseActionToInitialFormState(configurableForm); + return mapSingleIncidentResponseActionToInitialFormState( + configurableForm, + isIncidentManager ?? false + ); case "incident-management-team-member-assignment": return mapIncidentManagementTeamMemberToInitialFormState(configurableForm); } diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts index b738a802..4092ea03 100644 --- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts +++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts @@ -24,6 +24,7 @@ import { RiskAssessmentQuestionnaireOptions, RiskAssessmentSummaryFormData, ResponseActionFormData, + SingleResponseActionFormData, } from "../../../domain/entities/ConfigurableForm"; import { Maybe } from "../../../utils/ts-utils"; import { RiskAssessmentGrading } from "../../../domain/entities/risk-assessment/RiskAssessmentGrading"; @@ -40,7 +41,9 @@ import { import { ActionPlanAttrs } from "../../../domain/entities/incident-action-plan/ActionPlan"; import { actionPlanConstants as actionPlanConstants, + getVerificationTypeByCode, responseActionConstants, + verificationCodeMap, } from "../../../data/repositories/consts/IncidentActionConstants"; import { ResponseAction, @@ -106,8 +109,8 @@ export function mapFormStateToEntityData( return actionPlanForm; } - case "incident-response-action": { - const responseActions = mapFormStateToIncidentResponseAction(formState, formData); + case "incident-response-actions": { + const responseActions = mapFormStateToIncidentResponseActions(formState, formData); const responseActionForm: ResponseActionFormData = { ...formData, entity: responseActions, @@ -115,7 +118,15 @@ export function mapFormStateToEntityData( return responseActionForm; } + case "incident-response-action": { + const responseAction = mapFormStateToIncidentResponseAction(formState, formData); + const responseActionForm: SingleResponseActionFormData = { + ...formData, + entity: responseAction, + }; + return responseActionForm; + } case "incident-management-team-member-assignment": { const incidentManagementTeamMember: TeamMember = mapFormStateToIncidentManagementTeamMember(formState, formData); @@ -538,7 +549,7 @@ function mapFormStateToIncidentActionPlan( return incidentActionPlan; } -function mapFormStateToIncidentResponseAction( +function mapFormStateToIncidentResponseActions( formState: FormState, formData: ResponseActionFormData ): ResponseAction[] { @@ -579,7 +590,10 @@ function mapFormStateToIncidentResponseAction( )?.value as string; const verification = formData.options.verification.find( option => option.id === verificationValue - ); + ) ?? { + id: verificationCodeMap.Unverified, + name: getVerificationTypeByCode(verificationCodeMap.Unverified) ?? "", + }; if (!verification) throw new Error("Verification not found"); const responseAction = new ResponseAction({ @@ -599,6 +613,63 @@ function mapFormStateToIncidentResponseAction( return incidentResponseActions; } +function mapFormStateToIncidentResponseAction( + formState: FormState, + formData: SingleResponseActionFormData +): ResponseAction { + const allFields: FormFieldState[] = getAllFieldsFromSections(formState.sections); + + const mainTask = allFields.find(field => field.id.includes(responseActionConstants.mainTask)) + ?.value as string; + + const subActivities = allFields.find(field => + field.id.includes(responseActionConstants.subActivities) + )?.value as string; + + const subPillar = allFields.find(field => field.id.includes(responseActionConstants.subPillar)) + ?.value as string; + + const dueDate = allFields.find(field => field.id.includes(responseActionConstants.dueDate)) + ?.value as Date; + + const searchAssignROValue = allFields.find(field => + field.id.includes(responseActionConstants.searchAssignRO) + )?.value as string; + const searchAssignRO = formData.options.searchAssignRO.find( + option => option.id === searchAssignROValue + ); + if (!searchAssignRO) throw new Error("Responsible officer not found"); + + const statusValue = allFields.find(field => field.id.includes(responseActionConstants.status)) + ?.value as string; + const status = formData.options.status.find(option => option.id === statusValue); + if (!status) throw new Error("Status not found"); + + const verificationValue = allFields.find(field => + field.id.includes(responseActionConstants.verification) + )?.value as string; + const verification = formData.options.verification.find( + option => option.id === verificationValue + ) ?? { + id: verificationCodeMap.Unverified, + name: getVerificationTypeByCode(verificationCodeMap.Unverified) ?? "", + }; + if (!verification) throw new Error("Verification not found"); + + const responseAction = new ResponseAction({ + id: formData.entity?.id ?? "", + mainTask: mainTask, + subActivities: subActivities, + subPillar: subPillar, + dueDate: dueDate, + searchAssignRO: searchAssignRO, + status: status.id as Status, + verification: verification.id as Verification, + }); + + return responseAction; +} + function getRiskAssessmentQuestionsWithOption( questionType: "std" | "custom", allFields: FormFieldState[], diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index d237f8c7..384ac0f4 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -24,6 +24,7 @@ import { useExistingEventTrackerTypes } from "../../contexts/existing-event-trac import { useCheckWritePermission } from "../../hooks/useHasCurrentUserCaptureAccess"; import { useSnackbar } from "@eyeseetea/d2-ui-components"; import { usePerformanceOverview } from "../dashboard/usePerformanceOverview"; +import { useIncidentActionPlan } from "../incident-action-plan/useIncidentActionPlan"; export type GlobalMessage = { text: string; @@ -69,6 +70,7 @@ export function useForm(formType: FormType, id?: Id): State { const currentEventTracker = getCurrentEventTracker(); const { existingEventTrackerTypes } = useExistingEventTrackerTypes(); const { dataPerformanceOverview } = usePerformanceOverview(); + const { isIncidentManager } = useIncidentActionPlan(currentEventTracker?.id ?? ""); useCheckWritePermission(formType); const snackbar = useSnackbar(); @@ -82,14 +84,25 @@ export function useForm(formType: FormType, id?: Id): State { useEffect(() => { compositionRoot.getConfigurableForm - .execute(formType, currentEventTracker, configurations, id) + .execute({ + formType: formType, + eventTrackerDetails: currentEventTracker, + configurations: configurations, + id: id, + responseActionId: id, + }) .run( formData => { setConfigurableForm(formData); setFormLabels(formData.labels); setFormState({ kind: "loaded", - data: mapEntityToFormState(formData, !!id, existingEventTrackers), + data: mapEntityToFormState({ + configurableForm: formData, + editMode: !!id, + existingEventTrackerTypes: existingEventTrackerTypes, + isIncidentManager: isIncidentManager, + }), }); }, error => { @@ -112,6 +125,8 @@ export function useForm(formType: FormType, id?: Id): State { existingEventTrackers, snackbar, goTo, + isIncidentManager, + existingEventTrackerTypes, ]); const handleAddNew = useCallback(() => { @@ -162,7 +177,7 @@ export function useForm(formType: FormType, id?: Id): State { }); break; } - case "incident-response-action": + case "incident-response-actions": setFormState(prevState => { if (prevState.kind === "loaded") { const otherSections = prevState.data.sections.filter( @@ -170,7 +185,9 @@ export function useForm(formType: FormType, id?: Id): State { ); const addAnotherSection = getAnotherResponseActionSection(); const newResponseActionSection = addNewResponseActionSection( - prevState.data.sections + prevState.data.sections, + configurations, + isIncidentManager ); const updatedData = { @@ -207,7 +224,7 @@ export function useForm(formType: FormType, id?: Id): State { default: break; } - }, [configurableForm, formState]); + }, [configurableForm, configurations, formState.kind, isIncidentManager]); const handleFormChange = useCallback( (updatedField: FormFieldState) => { @@ -302,13 +319,23 @@ export function useForm(formType: FormType, id?: Id): State { break; case "incident-action-plan": goTo(RouteName.CREATE_FORM, { - formType: "incident-response-action", + formType: "incident-response-actions", }); setGlobalMessage({ text: i18n.t(`Incident Action Plan saved successfully`), type: "success", }); break; + case "incident-response-actions": + if (currentEventTracker?.id) + goTo(RouteName.INCIDENT_ACTION_PLAN, { + id: currentEventTracker?.id, + }); + setGlobalMessage({ + text: i18n.t(`Incident Response Actions saved successfully`), + type: "success", + }); + break; case "incident-response-action": if (currentEventTracker?.id) goTo(RouteName.INCIDENT_ACTION_PLAN, { @@ -319,7 +346,6 @@ export function useForm(formType: FormType, id?: Id): State { type: "success", }); break; - case "incident-management-team-member-assignment": if (currentEventTracker?.id) goTo(RouteName.IM_TEAM_BUILDER, { @@ -358,8 +384,11 @@ export function useForm(formType: FormType, id?: Id): State { }); break; case "incident-action-plan": + case "incident-response-actions": case "incident-response-action": - goTo(RouteName.INCIDENT_ACTION_PLAN); + goTo(RouteName.INCIDENT_ACTION_PLAN, { + id: currentEventTracker.id, + }); break; default: goTo(RouteName.EVENT_TRACKER, { diff --git a/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts b/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts index 434e8835..c098c297 100644 --- a/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts +++ b/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts @@ -68,7 +68,7 @@ function validateFormState( break; case "incident-action-plan": break; - case "incident-response-action": + case "incident-response-actions": break; case "incident-management-team-member-assignment": { const reportsToUsername = getFieldValueByIdFromSections( diff --git a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx index d2b165cd..93d31c6f 100644 --- a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx +++ b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx @@ -26,6 +26,7 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { responseActionColumns, orderByDueDate, saveTableOption, + onClickResponseActionRow, } = useIncidentActionPlan(id); const getSummaryColumn = useCallback((index: number, label: string, value: string) => { @@ -70,6 +71,7 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { responseActionRows={responseActionRows} onChange={saveTableOption} onOrderBy={orderByDueDate} + onClickResponseActionRow={onClickResponseActionRow} /> void; }; export const ResponseActionTable: React.FC = React.memo( - ({ onChange, onOrderBy, responseActionColumns, responseActionRows }) => { + ({ + onChange, + onClickResponseActionRow, + onOrderBy, + responseActionColumns, + responseActionRows, + }) => { const { goTo } = useRoutes(); const { icon: responseActionIcon, label: responseActionLabel } = @@ -25,7 +32,7 @@ export const ResponseActionTable: React.FC = React.mem const goToCreateResponseAction = useCallback(() => { goTo(RouteName.CREATE_FORM, { - formType: "incident-response-action", + formType: "incident-response-actions", }); }, [goTo]); @@ -50,6 +57,8 @@ export const ResponseActionTable: React.FC = React.mem columns={responseActionColumns} rows={responseActionRows} onOrderBy={onOrderBy} + formType="incident-response-action" + onClickRow={onClickResponseActionRow} />
diff --git a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts index 54753178..92e53919 100644 --- a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts +++ b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts @@ -33,7 +33,7 @@ export type UIIncidentActionOptions = { }; export function useIncidentActionPlan(id: Id) { - const { compositionRoot, configurations: appConfiguration } = useAppContext(); + const { compositionRoot, configurations: appConfiguration, currentUser } = useAppContext(); const { changeCurrentEventTracker, getCurrentEventTracker } = useCurrentEventTracker(); const currentEventTracker = getCurrentEventTracker(); @@ -44,6 +44,12 @@ export function useIncidentActionPlan(id: Id) { const [incidentActionPlan, setIncidentActionPlan] = useState(); const [incidentActionExists, setIncidentActionExists] = useState(false); const [incidentActionOptions, setIncidentActionOptions] = useState(); + const [responseActionRowId, setResponseActionRowId] = useState(); + + const isIncidentManager = useMemo( + () => currentUser.belongToUserGroup(appConfiguration.incidentManagerUserGroup.id), + [currentUser, appConfiguration] + ); const saveTableOption = useCallback( (value: Maybe, rowIndex: number, column: TableColumn["value"]) => { @@ -62,6 +68,10 @@ export function useIncidentActionPlan(id: Id) { [compositionRoot, id, responseActionRows] ); + const onClickResponseActionRow = useCallback((rowId: string) => { + setResponseActionRowId(rowId); + }, []); + const responseActionColumns: TableColumn[] = useMemo(() => { return [ { value: "mainTask", label: "Main task", type: "text" }, @@ -82,14 +92,14 @@ export function useIncidentActionPlan(id: Id) { { value: "verification", label: "Verification", - type: "selector", + type: isIncidentManager ? "selector" : "text", options: incidentActionOptions?.verification ?? [], onChange: saveTableOption, }, { value: "timeLine", label: "Timeline", type: "text" }, { value: "dueDate", label: "Due date", type: "text" }, ]; - }, [incidentActionOptions, saveTableOption]); + }, [incidentActionOptions, saveTableOption, isIncidentManager]); useEffect(() => { compositionRoot.incidentActionPlan.get.execute(id, appConfiguration).run( @@ -164,6 +174,7 @@ export function useIncidentActionPlan(id: Id) { return { incidentActionExists: incidentActionExists, + isIncidentManager: isIncidentManager, saveTableOption: saveTableOption, responseActionColumns: responseActionColumns, actionPlanSummary: actionPlanSummary, @@ -171,6 +182,8 @@ export function useIncidentActionPlan(id: Id) { responseActionRows: responseActionRows, summaryError: globalMessage, orderByDueDate: orderByDueDate, + responseActionRowId: responseActionRowId, + onClickResponseActionRow: onClickResponseActionRow, }; }