From 632e1261c9b56112c5798bc704218feb5e7f51f1 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:23:32 +0100 Subject: [PATCH 01/14] chore: remove dashboard section title --- src/webapp/pages/dashboard/DashboardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( From 53a21c3311e773072f0f5b749b904fd4244c7eac Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:41:13 +0100 Subject: [PATCH 02/14] fix: show correct options for new response action section --- .../mapIncidentActionToInitialFormState.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index 0a0f654a..e984426e 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -8,6 +8,7 @@ import { } from "../../../../domain/entities/ConfigurableForm"; import { ResponseAction } from "../../../../domain/entities/incident-action-plan/ResponseAction"; import { Maybe } from "../../../../utils/ts-utils"; +import { FormFieldState } from "../../../components/form/FormFieldsState"; import { FormSectionState } from "../../../components/form/FormSectionsState"; import { FormState } from "../../../components/form/FormState"; import { Option as UIOption } from "../../../components/utils/option"; @@ -386,18 +387,27 @@ export function addNewResponseActionSection(sections: FormSectionState[]): FormS section => !section.id.startsWith("addNewResponseActionSection") ); + const searchAssignROField = getSectionsField(sections, responseActionConstants.searchAssignRO); + const statusField = getSectionsField(sections, responseActionConstants.status); + const verificationField = getSectionsField(sections, responseActionConstants.verification); + 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 : [], + searchAssignROField?.type === "select" ? searchAssignROField.options : [], + statusOptions: statusField?.type === "select" ? statusField.options : [], verificationOptions: - sections[0]?.fields[7]?.type === "select" ? sections[0].fields[7].options : [], + verificationField?.type === "select" ? verificationField.options : [], }, index: responseActionSections.length, }); return newResponseActionSection; } + +const getSectionsField = (sections: FormSectionState[], fieldId: string): Maybe => { + const sectionFields = sections[0]?.fields ?? []; + + return sectionFields.find(field => field.id.includes(fieldId)); +}; From 86e2a7f5c934968d2e08e9abc2aa3b9b40126819 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:43:50 +0100 Subject: [PATCH 03/14] fix: cancel iap form --- src/webapp/pages/form-page/useForm.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index d237f8c7..1d1f7e26 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -359,7 +359,9 @@ export function useForm(formType: FormType, id?: Id): State { break; case "incident-action-plan": case "incident-response-action": - goTo(RouteName.INCIDENT_ACTION_PLAN); + goTo(RouteName.INCIDENT_ACTION_PLAN, { + id: currentEventTracker.id, + }); break; default: goTo(RouteName.EVENT_TRACKER, { From 9a36a87ca3c0c7443fdda3f3c733c7428dc2656a Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:55:50 +0100 Subject: [PATCH 04/14] feat: make verification field read only for users not in incident manager user group --- src/CompositionRoot.ts | 10 +++++++ .../repositories/TeamMemberD2Repository.ts | 2 +- .../repositories/UserGroupD2Repository.ts | 26 +++++++++++++++++++ .../consts/IncidentActionConstants.ts | 8 +++--- .../test/UserGroupTestRepository.ts | 12 +++++++++ src/domain/entities/UserGroup.ts | 3 +++ .../repositories/UserGroupRepository.ts | 6 +++++ .../usecases/GetUserGroupByCodeUseCase.ts | 12 +++++++++ .../mapIncidentActionToInitialFormState.ts | 16 +++++++++--- .../pages/form-page/mapEntityToFormState.ts | 18 ++++++++----- .../form-page/mapFormStateToEntityData.ts | 7 ++++- src/webapp/pages/form-page/useForm.ts | 16 +++++++++--- .../useIncidentActionPlan.ts | 22 +++++++++++++--- 13 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 src/data/repositories/UserGroupD2Repository.ts create mode 100644 src/data/repositories/test/UserGroupTestRepository.ts create mode 100644 src/domain/entities/UserGroup.ts create mode 100644 src/domain/repositories/UserGroupRepository.ts create mode 100644 src/domain/usecases/GetUserGroupByCodeUseCase.ts diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index d027c1d3..d5c4b8c8 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -68,6 +68,10 @@ 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"; +import { GetUserGroupByCodeUseCase } from "./domain/usecases/GetUserGroupByCodeUseCase"; export type CompositionRoot = ReturnType; @@ -87,6 +91,7 @@ type Repositories = { chartConfigRepository: ChartConfigRepository; systemRepository: SystemRepository; configurationsRepository: ConfigurationsRepository; + userGroupRepository: UserGroupRepository; }; function getCompositionRoot(repositories: Repositories) { @@ -139,6 +144,9 @@ function getCompositionRoot(repositories: Repositories) { charts: { getCases: new GetChartConfigByTypeUseCase(repositories.chartConfigRepository), }, + userGroup: { + getByCode: new GetUserGroupByCodeUseCase(repositories.userGroupRepository), + }, }; } @@ -160,6 +168,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 +191,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/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..4f6caa35 --- /dev/null +++ b/src/data/repositories/UserGroupD2Repository.ts @@ -0,0 +1,26 @@ +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"; + +export class UserGroupD2Repository implements UserGroupRepository { + constructor(private api: D2Api) {} + + getUserGroupByCode(code: string): FutureData { + return apiToFuture( + this.api.metadata.get({ + userGroups: { + fields: { + id: true, + }, + filter: { + code: { eq: code }, + }, + }, + }) + ) + .flatMap(response => assertOrError(response.userGroups[0], `User group ${code}`)) + .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..22e4fc75 --- /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 { + getUserGroupByCode(_code: string): FutureData { + return Future.success({ + id: "1", + }); + } +} 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/UserGroupRepository.ts b/src/domain/repositories/UserGroupRepository.ts new file mode 100644 index 00000000..ac92a475 --- /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 { + getUserGroupByCode(code: string): FutureData; +} diff --git a/src/domain/usecases/GetUserGroupByCodeUseCase.ts b/src/domain/usecases/GetUserGroupByCodeUseCase.ts new file mode 100644 index 00000000..6a3d18cc --- /dev/null +++ b/src/domain/usecases/GetUserGroupByCodeUseCase.ts @@ -0,0 +1,12 @@ +import { FutureData } from "../../data/api-futures"; +import { Code } from "../entities/Ref"; +import { UserGroup } from "../entities/UserGroup"; +import { UserGroupRepository } from "../repositories/UserGroupRepository"; + +export class GetUserGroupByCodeUseCase { + constructor(private userGroupRepository: UserGroupRepository) {} + + public execute(code: Code): FutureData { + return this.userGroupRepository.getUserGroupByCode(code); + } +} diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index e984426e..08b1d96c 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -220,7 +220,8 @@ export function mapIncidentActionPlanToInitialFormState( } export function mapIncidentResponseActionToInitialFormState( - incidentResponseActionFormData: ResponseActionFormData + incidentResponseActionFormData: ResponseActionFormData, + isIncidentManager: boolean ): FormState { const { entity: incidentResponseActions, @@ -240,6 +241,7 @@ export function mapIncidentResponseActionToInitialFormState( statusOptions: statusOptions, verificationOptions: verificationOptions, }, + isIncidentManager: isIncidentManager, index: 0, }); @@ -253,6 +255,7 @@ export function mapIncidentResponseActionToInitialFormState( statusOptions: statusOptions, verificationOptions: verificationOptions, }, + isIncidentManager: isIncidentManager, index: index, }); }) @@ -279,9 +282,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 = { @@ -366,7 +370,7 @@ function getResponseActionSection(options: { { id: `${responseActionConstants.verification}_${index}`, label: "Verification", - isVisible: true, + isVisible: isIncidentManager ? true : false, errors: [], value: incidentResponseAction?.verification || "", type: "select", @@ -382,7 +386,10 @@ function getResponseActionSection(options: { return responseActionSection; } -export function addNewResponseActionSection(sections: FormSectionState[]): FormSectionState { +export function addNewResponseActionSection( + sections: FormSectionState[], + isIncidentManager: boolean +): FormSectionState { const responseActionSections = sections.filter( section => !section.id.startsWith("addNewResponseActionSection") ); @@ -400,6 +407,7 @@ export function addNewResponseActionSection(sections: FormSectionState[]): FormS verificationOptions: verificationField?.type === "select" ? verificationField.options : [], }, + isIncidentManager: isIncidentManager, index: responseActionSections.length, }); diff --git a/src/webapp/pages/form-page/mapEntityToFormState.ts b/src/webapp/pages/form-page/mapEntityToFormState.ts index 1513f837..f98b34a9 100644 --- a/src/webapp/pages/form-page/mapEntityToFormState.ts +++ b/src/webapp/pages/form-page/mapEntityToFormState.ts @@ -20,11 +20,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( @@ -41,7 +44,10 @@ export function mapEntityToFormState( case "incident-action-plan": return mapIncidentActionPlanToInitialFormState(configurableForm); case "incident-response-action": - return mapIncidentResponseActionToInitialFormState(configurableForm); + return mapIncidentResponseActionToInitialFormState( + 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..dc7ae710 100644 --- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts +++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts @@ -40,7 +40,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, @@ -579,7 +581,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({ diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index 1d1f7e26..09e5fc6f 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(); @@ -89,7 +91,12 @@ export function useForm(formType: FormType, id?: Id): State { setFormLabels(formData.labels); setFormState({ kind: "loaded", - data: mapEntityToFormState(formData, !!id, existingEventTrackers), + data: mapEntityToFormState({ + configurableForm: formData, + editMode: !!id, + existingEventTrackerTypes: existingEventTrackerTypes, + isIncidentManager: isIncidentManager, + }), }); }, error => { @@ -112,6 +119,8 @@ export function useForm(formType: FormType, id?: Id): State { existingEventTrackers, snackbar, goTo, + isIncidentManager, + existingEventTrackerTypes, ]); const handleAddNew = useCallback(() => { @@ -170,7 +179,8 @@ export function useForm(formType: FormType, id?: Id): State { ); const addAnotherSection = getAnotherResponseActionSection(); const newResponseActionSection = addNewResponseActionSection( - prevState.data.sections + prevState.data.sections, + isIncidentManager ); const updatedData = { @@ -207,7 +217,7 @@ export function useForm(formType: FormType, id?: Id): State { default: break; } - }, [configurableForm, formState]); + }, [configurableForm, formState.kind, isIncidentManager]); const handleFormChange = useCallback( (updatedField: FormFieldState) => { diff --git a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts index 54753178..dadd4a5e 100644 --- a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts +++ b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts @@ -21,6 +21,7 @@ import { Option } from "../../components/utils/option"; import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; import { DiseaseOutbreakEvent } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import _c from "../../../domain/entities/generic/Collection"; +import { RTSL_ZEBRA_INCIDENTMANAGER } from "../../../data/repositories/TeamMemberD2Repository"; export type IncidentActionFormSummaryData = { subTitle: string; @@ -33,7 +34,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 +45,7 @@ export function useIncidentActionPlan(id: Id) { const [incidentActionPlan, setIncidentActionPlan] = useState(); const [incidentActionExists, setIncidentActionExists] = useState(false); const [incidentActionOptions, setIncidentActionOptions] = useState(); + const [isIncidentManager, setIsIncidentManager] = useState(false); const saveTableOption = useCallback( (value: Maybe, rowIndex: number, column: TableColumn["value"]) => { @@ -82,14 +84,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( @@ -129,6 +131,19 @@ export function useIncidentActionPlan(id: Id) { } }, [changeCurrentEventTracker, currentEventTracker, incidentActionExists, incidentActionPlan]); + useEffect(() => { + compositionRoot.userGroup.getByCode.execute(RTSL_ZEBRA_INCIDENTMANAGER).run( + userGroup => { + const isIncidentManager = currentUser.belongToUserGroup(userGroup.id); + setIsIncidentManager(isIncidentManager); + }, + err => { + console.error(err); + setIsIncidentManager(false); + } + ); + }, [compositionRoot.userGroup.getByCode, currentUser]); + const orderByDueDate = useCallback( (direction: "asc" | "desc") => { setResponseActionRows(prevRows => { @@ -164,6 +179,7 @@ export function useIncidentActionPlan(id: Id) { return { incidentActionExists: incidentActionExists, + isIncidentManager: isIncidentManager, saveTableOption: saveTableOption, responseActionColumns: responseActionColumns, actionPlanSummary: actionPlanSummary, From f1a8ba41fd607eb31f081aa56340c7cdd0ce8379 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:59:13 +0100 Subject: [PATCH 05/14] chore: update .po files --- i18n/en.pot | 7 ++----- i18n/es.po | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 836445da..0ff4e836 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-12T13:57:18.029Z\n" +"PO-Revision-Date: 2024-11-12T13:57:18.029Z\n" msgid "Low" msgstr "" @@ -135,9 +135,6 @@ msgstr "" msgid "Dashboard" msgstr "" -msgid "Respond, alert, watch" -msgstr "" - msgid "Select duration" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index a4a9ac52..68880539 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-12T13:57:18.029Z\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 "" From 9ef3aa2dcd25921f8bf046d8a013008b56012c71 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:32:14 +0100 Subject: [PATCH 06/14] feat: move incident manager user card --- .../form/form-summary/EventTrackerFormSummary.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 && ( - - )} - From a1e6348c0a05b0a662b907c1f6869c67a0aec800 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:32:37 +0100 Subject: [PATCH 07/14] feat: add gotoriskgrading button --- .../pages/event-tracker/EventTrackerPage.tsx | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/webapp/pages/event-tracker/EventTrackerPage.tsx b/src/webapp/pages/event-tracker/EventTrackerPage.tsx index 42e15157..ab3e0995 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; +`; From 85abec6303d585e10e397fb5be8bffd2d3e54e13 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:15:00 +0100 Subject: [PATCH 08/14] chore: revert risk assessment button change --- .../pages/event-tracker/EventTrackerPage.tsx | 43 +++++-------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/src/webapp/pages/event-tracker/EventTrackerPage.tsx b/src/webapp/pages/event-tracker/EventTrackerPage.tsx index ab3e0995..42e15157 100644 --- a/src/webapp/pages/event-tracker/EventTrackerPage.tsx +++ b/src/webapp/pages/event-tracker/EventTrackerPage.tsx @@ -67,12 +67,6 @@ 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 @@ -133,28 +127,16 @@ export const EventTrackerPage: React.FC = React.memo(() => { {i18n.t("Create Risk Assessment")} ) : ( - - - - + ) } titleVariant="secondary" @@ -262,8 +244,3 @@ export const EventTrackerPage: React.FC = React.memo(() => { const DurationFilterContainer = styled.div` max-width: 250px; `; - -const ButtonContainer = styled.div` - display: flex; - gap: 8px; -`; From 856f1df6b96a88d13179212e7165953fc99bf8b3 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:47:21 +0100 Subject: [PATCH 09/14] refactor: get response action options from configurations --- .../mapIncidentActionToInitialFormState.ts | 28 +++++++++---------- src/webapp/pages/form-page/useForm.ts | 3 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index 08b1d96c..a6d61d23 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -2,13 +2,14 @@ import { actionPlanConstants, responseActionConstants, } from "../../../../data/repositories/consts/IncidentActionConstants"; +import { Configurations } from "../../../../domain/entities/AppConfigurations"; import { ActionPlanFormData, ResponseActionFormData, } 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 { FormFieldState } from "../../../components/form/FormFieldsState"; import { FormSectionState } from "../../../components/form/FormSectionsState"; import { FormState } from "../../../components/form/FormState"; import { Option as UIOption } from "../../../components/utils/option"; @@ -388,24 +389,22 @@ function getResponseActionSection(options: { export function addNewResponseActionSection( sections: FormSectionState[], + configurations: Configurations, isIncidentManager: boolean ): FormSectionState { const responseActionSections = sections.filter( section => !section.id.startsWith("addNewResponseActionSection") ); - const searchAssignROField = getSectionsField(sections, responseActionConstants.searchAssignRO); - const statusField = getSectionsField(sections, responseActionConstants.status); - const verificationField = getSectionsField(sections, responseActionConstants.verification); + const { searchAssignRO, status, verification } = + configurations.selectableOptions.incidentResponseActionConfigurations; const newResponseActionSection = getResponseActionSection({ incidentResponseAction: undefined, options: { - searchAssignROOptions: - searchAssignROField?.type === "select" ? searchAssignROField.options : [], - statusOptions: statusField?.type === "select" ? statusField.options : [], - verificationOptions: - verificationField?.type === "select" ? verificationField.options : [], + searchAssignROOptions: getValueLabelOptions(searchAssignRO), + statusOptions: getValueLabelOptions(status), + verificationOptions: getValueLabelOptions(verification), }, isIncidentManager: isIncidentManager, index: responseActionSections.length, @@ -414,8 +413,9 @@ export function addNewResponseActionSection( return newResponseActionSection; } -const getSectionsField = (sections: FormSectionState[], fieldId: string): Maybe => { - const sectionFields = sections[0]?.fields ?? []; - - return sectionFields.find(field => field.id.includes(fieldId)); -}; +function getValueLabelOptions(options: Option[]): UIOption[] { + return options.map(option => ({ + value: option.id, + label: option.name, + })); +} diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index 09e5fc6f..f99b1185 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -180,6 +180,7 @@ export function useForm(formType: FormType, id?: Id): State { const addAnotherSection = getAnotherResponseActionSection(); const newResponseActionSection = addNewResponseActionSection( prevState.data.sections, + configurations, isIncidentManager ); @@ -217,7 +218,7 @@ export function useForm(formType: FormType, id?: Id): State { default: break; } - }, [configurableForm, formState.kind, isIncidentManager]); + }, [configurableForm, configurations, formState.kind, isIncidentManager]); const handleFormChange = useCallback( (updatedField: FormFieldState) => { From 8880da2aae9290d851545aa4090f448d00ceca4c Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:24:43 +0100 Subject: [PATCH 10/14] refactor: get incident manager user group in configurations --- src/CompositionRoot.ts | 3 ++- src/domain/entities/AppConfigurations.ts | 2 ++ .../usecases/GetConfigurationsUseCase.ts | 16 +++++++++++++-- src/utils/tests.tsx | 1 + src/webapp/pages/app/App.tsx | 3 ++- .../mapIncidentActionToInitialFormState.ts | 2 +- .../useIncidentActionPlan.ts | 20 +++++-------------- 7 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index d5c4b8c8..54f09cbf 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -110,7 +110,8 @@ function getCompositionRoot(repositories: Repositories) { ), getConfigurations: new GetConfigurationsUseCase( repositories.configurationsRepository, - repositories.teamMemberRepository + repositories.teamMemberRepository, + repositories.userGroupRepository ), complete: new CompleteEventTrackerUseCase(repositories), }, 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/usecases/GetConfigurationsUseCase.ts b/src/domain/usecases/GetConfigurationsUseCase.ts index 7b6fd86f..c0e8f1a8 100644 --- a/src/domain/usecases/GetConfigurationsUseCase.ts +++ b/src/domain/usecases/GetConfigurationsUseCase.ts @@ -2,22 +2,32 @@ import { FutureData } from "../../data/api-futures"; import { Configurations, SelectableOptions } from "../entities/AppConfigurations"; import { Future } from "../entities/generic/Future"; import { TeamMember } from "../entities/incident-management-team/TeamMember"; +import { Code } from "../entities/Ref"; 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 { + public execute({ + incidentManagerUserGroupCode, + }: { + incidentManagerUserGroupCode: Code; + }): FutureData { return Future.joinObj({ allTeamMembers: this.teamMemberRepository.getAll(), incidentResponseOfficers: this.teamMemberRepository.getIncidentResponseOfficers(), managers: this.teamMemberRepository.getIncidentManagers(), riskAssessors: this.teamMemberRepository.getRiskAssessors(), selectableOptionsResponse: this.configurationsRepository.getSelectableOptions(), + incidentManagerUserGroup: this.userGroupRepository.getUserGroupByCode( + incidentManagerUserGroupCode + ), }).flatMap( ({ allTeamMembers, @@ -25,6 +35,7 @@ export class GetConfigurationsUseCase { managers, riskAssessors, selectableOptionsResponse, + incidentManagerUserGroup, }) => { const selectableOptions: SelectableOptions = this.mapOptionsAndTeamMembersToSelectableOptions( @@ -36,6 +47,7 @@ export class GetConfigurationsUseCase { const configurations: Configurations = { selectableOptions: selectableOptions, + incidentManagerUserGroup: incidentManagerUserGroup, teamMembers: { all: allTeamMembers, riskAssessors: riskAssessors, 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/pages/app/App.tsx b/src/webapp/pages/app/App.tsx index 4d39c89a..b7da6f9f 100644 --- a/src/webapp/pages/app/App.tsx +++ b/src/webapp/pages/app/App.tsx @@ -18,6 +18,7 @@ import { D2Api } from "../../../types/d2-api"; import "./App.css"; import { CurrentEventTrackerContextProvider } from "../../contexts/CurrentEventTrackerProvider"; import { ExistingEventTrackerTypesProvider } from "../../contexts/ExistingEventTrackerTypes"; +import { RTSL_ZEBRA_INCIDENTMANAGER } from "../../../data/repositories/TeamMemberD2Repository"; export interface AppProps { compositionRoot: CompositionRoot; @@ -38,7 +39,7 @@ function App(props: AppProps) { const orgUnits = await compositionRoot.orgUnits.getAll.execute().toPromise(); const configurations = await compositionRoot.diseaseOutbreakEvent.getConfigurations - .execute() + .execute({ incidentManagerUserGroupCode: RTSL_ZEBRA_INCIDENTMANAGER }) .toPromise(); const isDev = process.env.NODE_ENV === "development"; diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index a6d61d23..492040e3 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -377,7 +377,7 @@ function getResponseActionSection(options: { type: "select", multiple: false, options: verificationOptions, - required: true, + required: isIncidentManager ? true : false, showIsRequired: true, disabled: false, }, diff --git a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts index dadd4a5e..71b7bcdd 100644 --- a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts +++ b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts @@ -21,7 +21,6 @@ import { Option } from "../../components/utils/option"; import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; import { DiseaseOutbreakEvent } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import _c from "../../../domain/entities/generic/Collection"; -import { RTSL_ZEBRA_INCIDENTMANAGER } from "../../../data/repositories/TeamMemberD2Repository"; export type IncidentActionFormSummaryData = { subTitle: string; @@ -45,7 +44,11 @@ export function useIncidentActionPlan(id: Id) { const [incidentActionPlan, setIncidentActionPlan] = useState(); const [incidentActionExists, setIncidentActionExists] = useState(false); const [incidentActionOptions, setIncidentActionOptions] = useState(); - const [isIncidentManager, setIsIncidentManager] = useState(false); + + const isIncidentManager = useMemo( + () => currentUser.belongToUserGroup(appConfiguration.incidentManagerUserGroup.id), + [currentUser, appConfiguration] + ); const saveTableOption = useCallback( (value: Maybe, rowIndex: number, column: TableColumn["value"]) => { @@ -131,19 +134,6 @@ export function useIncidentActionPlan(id: Id) { } }, [changeCurrentEventTracker, currentEventTracker, incidentActionExists, incidentActionPlan]); - useEffect(() => { - compositionRoot.userGroup.getByCode.execute(RTSL_ZEBRA_INCIDENTMANAGER).run( - userGroup => { - const isIncidentManager = currentUser.belongToUserGroup(userGroup.id); - setIsIncidentManager(isIncidentManager); - }, - err => { - console.error(err); - setIsIncidentManager(false); - } - ); - }, [compositionRoot.userGroup.getByCode, currentUser]); - const orderByDueDate = useCallback( (direction: "asc" | "desc") => { setResponseActionRows(prevRows => { From b4a41071d220e39716e37623865acd089c161121 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:49:15 +0100 Subject: [PATCH 11/14] refactor: get usergroup by code in data repo --- src/CompositionRoot.ts | 4 ---- src/data/repositories/UserGroupD2Repository.ts | 9 ++++++--- .../repositories/test/UserGroupTestRepository.ts | 2 +- src/domain/repositories/UserGroupRepository.ts | 2 +- src/domain/usecases/GetConfigurationsUseCase.ts | 11 ++--------- src/domain/usecases/GetUserGroupByCodeUseCase.ts | 12 ------------ src/webapp/pages/app/App.tsx | 3 +-- 7 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 src/domain/usecases/GetUserGroupByCodeUseCase.ts diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index 54f09cbf..f54db51d 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -71,7 +71,6 @@ import { CompleteEventTrackerUseCase } from "./domain/usecases/CompleteEventTrac import { UserGroupD2Repository } from "./data/repositories/UserGroupD2Repository"; import { UserGroupRepository } from "./domain/repositories/UserGroupRepository"; import { UserGroupTestRepository } from "./data/repositories/test/UserGroupTestRepository"; -import { GetUserGroupByCodeUseCase } from "./domain/usecases/GetUserGroupByCodeUseCase"; export type CompositionRoot = ReturnType; @@ -145,9 +144,6 @@ function getCompositionRoot(repositories: Repositories) { charts: { getCases: new GetChartConfigByTypeUseCase(repositories.chartConfigRepository), }, - userGroup: { - getByCode: new GetUserGroupByCodeUseCase(repositories.userGroupRepository), - }, }; } diff --git a/src/data/repositories/UserGroupD2Repository.ts b/src/data/repositories/UserGroupD2Repository.ts index 4f6caa35..a94b1718 100644 --- a/src/data/repositories/UserGroupD2Repository.ts +++ b/src/data/repositories/UserGroupD2Repository.ts @@ -3,11 +3,12 @@ import { UserGroupRepository } from "../../domain/repositories/UserGroupReposito 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) {} - getUserGroupByCode(code: string): FutureData { + getIncidentManagerUserGroupByCode(): FutureData { return apiToFuture( this.api.metadata.get({ userGroups: { @@ -15,12 +16,14 @@ export class UserGroupD2Repository implements UserGroupRepository { id: true, }, filter: { - code: { eq: code }, + code: { eq: RTSL_ZEBRA_INCIDENTMANAGER }, }, }, }) ) - .flatMap(response => assertOrError(response.userGroups[0], `User group ${code}`)) + .flatMap(response => + assertOrError(response.userGroups[0], `User group ${RTSL_ZEBRA_INCIDENTMANAGER}`) + ) .map(userGroup => userGroup); } } diff --git a/src/data/repositories/test/UserGroupTestRepository.ts b/src/data/repositories/test/UserGroupTestRepository.ts index 22e4fc75..c0f579b9 100644 --- a/src/data/repositories/test/UserGroupTestRepository.ts +++ b/src/data/repositories/test/UserGroupTestRepository.ts @@ -4,7 +4,7 @@ import { UserGroupRepository } from "../../../domain/repositories/UserGroupRepos import { FutureData } from "../../api-futures"; export class UserGroupTestRepository implements UserGroupRepository { - getUserGroupByCode(_code: string): FutureData { + getIncidentManagerUserGroupByCode(): FutureData { return Future.success({ id: "1", }); diff --git a/src/domain/repositories/UserGroupRepository.ts b/src/domain/repositories/UserGroupRepository.ts index ac92a475..5693efce 100644 --- a/src/domain/repositories/UserGroupRepository.ts +++ b/src/domain/repositories/UserGroupRepository.ts @@ -2,5 +2,5 @@ import { FutureData } from "../../data/api-futures"; import { UserGroup } from "../entities/UserGroup"; export interface UserGroupRepository { - getUserGroupByCode(code: string): FutureData; + getIncidentManagerUserGroupByCode(): FutureData; } diff --git a/src/domain/usecases/GetConfigurationsUseCase.ts b/src/domain/usecases/GetConfigurationsUseCase.ts index c0e8f1a8..11791bb6 100644 --- a/src/domain/usecases/GetConfigurationsUseCase.ts +++ b/src/domain/usecases/GetConfigurationsUseCase.ts @@ -2,7 +2,6 @@ import { FutureData } from "../../data/api-futures"; import { Configurations, SelectableOptions } from "../entities/AppConfigurations"; import { Future } from "../entities/generic/Future"; import { TeamMember } from "../entities/incident-management-team/TeamMember"; -import { Code } from "../entities/Ref"; import { ConfigurationsRepository } from "../repositories/ConfigurationsRepository"; import { TeamMemberRepository } from "../repositories/TeamMemberRepository"; import { UserGroupRepository } from "../repositories/UserGroupRepository"; @@ -14,20 +13,14 @@ export class GetConfigurationsUseCase { private userGroupRepository: UserGroupRepository ) {} - public execute({ - incidentManagerUserGroupCode, - }: { - incidentManagerUserGroupCode: Code; - }): FutureData { + public execute(): FutureData { return Future.joinObj({ allTeamMembers: this.teamMemberRepository.getAll(), incidentResponseOfficers: this.teamMemberRepository.getIncidentResponseOfficers(), managers: this.teamMemberRepository.getIncidentManagers(), riskAssessors: this.teamMemberRepository.getRiskAssessors(), selectableOptionsResponse: this.configurationsRepository.getSelectableOptions(), - incidentManagerUserGroup: this.userGroupRepository.getUserGroupByCode( - incidentManagerUserGroupCode - ), + incidentManagerUserGroup: this.userGroupRepository.getIncidentManagerUserGroupByCode(), }).flatMap( ({ allTeamMembers, diff --git a/src/domain/usecases/GetUserGroupByCodeUseCase.ts b/src/domain/usecases/GetUserGroupByCodeUseCase.ts deleted file mode 100644 index 6a3d18cc..00000000 --- a/src/domain/usecases/GetUserGroupByCodeUseCase.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FutureData } from "../../data/api-futures"; -import { Code } from "../entities/Ref"; -import { UserGroup } from "../entities/UserGroup"; -import { UserGroupRepository } from "../repositories/UserGroupRepository"; - -export class GetUserGroupByCodeUseCase { - constructor(private userGroupRepository: UserGroupRepository) {} - - public execute(code: Code): FutureData { - return this.userGroupRepository.getUserGroupByCode(code); - } -} diff --git a/src/webapp/pages/app/App.tsx b/src/webapp/pages/app/App.tsx index b7da6f9f..4d39c89a 100644 --- a/src/webapp/pages/app/App.tsx +++ b/src/webapp/pages/app/App.tsx @@ -18,7 +18,6 @@ import { D2Api } from "../../../types/d2-api"; import "./App.css"; import { CurrentEventTrackerContextProvider } from "../../contexts/CurrentEventTrackerProvider"; import { ExistingEventTrackerTypesProvider } from "../../contexts/ExistingEventTrackerTypes"; -import { RTSL_ZEBRA_INCIDENTMANAGER } from "../../../data/repositories/TeamMemberD2Repository"; export interface AppProps { compositionRoot: CompositionRoot; @@ -39,7 +38,7 @@ function App(props: AppProps) { const orgUnits = await compositionRoot.orgUnits.getAll.execute().toPromise(); const configurations = await compositionRoot.diseaseOutbreakEvent.getConfigurations - .execute({ incidentManagerUserGroupCode: RTSL_ZEBRA_INCIDENTMANAGER }) + .execute() .toPromise(); const isDev = process.env.NODE_ENV === "development"; From 0b2e427a1e6e55f3ada46a7d4311a207fd1da42b Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:17:02 +0100 Subject: [PATCH 12/14] feat: button to add new grade --- .../pages/event-tracker/EventTrackerPage.tsx | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) 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; +`; From 23de4363926c67fecc06b420760357f3199cb934 Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:03:51 +0100 Subject: [PATCH 13/14] feat: edit single incident response action --- .../IncidentActionD2Repository.ts | 12 ++- .../utils/IncidentActionMapper.ts | 91 ++++++++++++------- src/domain/entities/ConfigurableForm.ts | 10 +- .../repositories/IncidentActionRepository.ts | 8 +- .../usecases/GetConfigurableFormUseCase.ts | 39 ++++++-- src/domain/usecases/SaveEntityUseCase.ts | 1 + .../GetResponseActionConfigurableForm.ts | 42 ++++++++- src/webapp/components/table/BasicTable.tsx | 35 ++++++- .../hooks/useHasCurrentUserCaptureAccess.ts | 1 + src/webapp/hooks/useRoutes.ts | 1 + src/webapp/pages/form-page/FormPage.tsx | 1 + .../mapIncidentActionToInitialFormState.ts | 41 ++++++++- .../pages/form-page/mapEntityToFormState.ts | 10 +- .../form-page/mapFormStateToEntityData.ts | 72 ++++++++++++++- src/webapp/pages/form-page/useForm.ts | 24 ++++- .../updateDiseaseOutbreakEventFormState.ts | 2 +- .../IncidentActionPlanPage.tsx | 2 + .../ResponseActionTable.tsx | 13 ++- .../useIncidentActionPlan.ts | 7 ++ 19 files changed, 351 insertions(+), 61 deletions(-) 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/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/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/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/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/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/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/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 492040e3..1a414777 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -6,6 +6,7 @@ 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"; @@ -220,7 +221,7 @@ export function mapIncidentActionPlanToInitialFormState( }; } -export function mapIncidentResponseActionToInitialFormState( +export function mapIncidentResponseActionsToInitialFormState( incidentResponseActionFormData: ResponseActionFormData, isIncidentManager: boolean ): FormState { @@ -276,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: { diff --git a/src/webapp/pages/form-page/mapEntityToFormState.ts b/src/webapp/pages/form-page/mapEntityToFormState.ts index f98b34a9..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 { @@ -43,8 +44,13 @@ export function mapEntityToFormState(options: { 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( + return mapSingleIncidentResponseActionToInitialFormState( configurableForm, isIncidentManager ?? false ); diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts index dc7ae710..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"; @@ -108,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, @@ -117,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); @@ -540,7 +549,7 @@ function mapFormStateToIncidentActionPlan( return incidentActionPlan; } -function mapFormStateToIncidentResponseAction( +function mapFormStateToIncidentResponseActions( formState: FormState, formData: ResponseActionFormData ): ResponseAction[] { @@ -604,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 f99b1185..384ac0f4 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -84,7 +84,13 @@ 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); @@ -171,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( @@ -313,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, { @@ -330,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, { @@ -369,6 +384,7 @@ 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, { id: currentEventTracker.id, 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 71b7bcdd..92e53919 100644 --- a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts +++ b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts @@ -44,6 +44,7 @@ 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), @@ -67,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" }, @@ -177,6 +182,8 @@ export function useIncidentActionPlan(id: Id) { responseActionRows: responseActionRows, summaryError: globalMessage, orderByDueDate: orderByDueDate, + responseActionRowId: responseActionRowId, + onClickResponseActionRow: onClickResponseActionRow, }; } From 29b39e6ce88b21f268a572bd3a8c74cbf9bde57b Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:52:19 +0100 Subject: [PATCH 14/14] feat: update translation files --- i18n/en.pot | 9 ++++++--- i18n/es.po | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0ff4e836..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-12T13:57:18.029Z\n" -"PO-Revision-Date: 2024-11-12T13:57:18.029Z\n" +"POT-Creation-Date: 2024-11-22T15:21:43.931Z\n" +"PO-Revision-Date: 2024-11-22T15:21:43.931Z\n" msgid "Low" msgstr "" @@ -162,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 68880539..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-12T13:57:18.029Z\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" @@ -161,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"