diff --git a/i18n/en.pot b/i18n/en.pot index 392348f3..383565f7 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-03T18:35:32.151Z\n" -"PO-Revision-Date: 2024-11-03T18:35:32.151Z\n" +"POT-Creation-Date: 2024-11-07T10:23:53.316Z\n" +"PO-Revision-Date: 2024-11-07T10:23:53.316Z\n" msgid "Low" msgstr "" @@ -105,6 +105,9 @@ msgstr "" msgid "Currently assigned:" msgstr "" +msgid "Error loading current Incident Management Team" +msgstr "" + msgid "Create Event" msgstr "" @@ -216,6 +219,12 @@ msgstr "" msgid "Incident Action Plan" msgstr "" +msgid "Team" +msgstr "" + +msgid "Edit Team" +msgstr "" + msgid "Incident Management Team Builder" msgstr "" @@ -225,10 +234,16 @@ msgstr "" msgid "Assign Role" msgstr "" +msgid "Delete Roles" +msgstr "" + msgid "Delete Role" msgstr "" -msgid "Delete team role" +msgid "Confirm deletion" +msgstr "" + +msgid "Delete" msgstr "" msgid "Resources" diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index c4d38f18..d027c1d3 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -53,7 +53,7 @@ import { IncidentManagementTeamTestRepository } from "./data/repositories/test/I import { IncidentManagementTeamD2Repository } from "./data/repositories/IncidentManagementTeamD2Repository"; import { IncidentManagementTeamRepository } from "./domain/repositories/IncidentManagementTeamRepository"; import { GetIncidentManagementTeamByIdUseCase } from "./domain/usecases/GetIncidentManagementTeamByIdUseCase"; -import { DeleteIncidentManagementTeamMemberRoleUseCase } from "./domain/usecases/DeleteIncidentManagementTeamMemberRoleUseCase"; +import { DeleteIncidentManagementTeamMemberRolesUseCase } from "./domain/usecases/DeleteIncidentManagementTeamMemberRolesUseCase"; import { ChartConfigRepository } from "./domain/repositories/ChartConfigRepository"; import { GetChartConfigByTypeUseCase } from "./domain/usecases/GetChartConfigByTypeUseCase"; import { ChartConfigTestRepository } from "./data/repositories/test/ChartConfigTestRepository"; @@ -115,8 +115,8 @@ function getCompositionRoot(repositories: Repositories) { }, incidentManagementTeam: { get: new GetIncidentManagementTeamByIdUseCase(repositories), - deleteIncidentManagementTeamMemberRole: - new DeleteIncidentManagementTeamMemberRoleUseCase(repositories), + deleteIncidentManagementTeamMemberRoles: + new DeleteIncidentManagementTeamMemberRolesUseCase(repositories), }, performanceOverview: { getPerformanceOverviewMetrics: new GetAllPerformanceOverviewMetricsUseCase( diff --git a/src/data/repositories/IncidentManagementTeamD2Repository.ts b/src/data/repositories/IncidentManagementTeamD2Repository.ts index e3a82dae..671e60ae 100644 --- a/src/data/repositories/IncidentManagementTeamD2Repository.ts +++ b/src/data/repositories/IncidentManagementTeamD2Repository.ts @@ -13,7 +13,6 @@ import { IncidentManagementTeam } from "../../domain/entities/incident-managemen import { IncidentManagementTeamRepository } from "../../domain/repositories/IncidentManagementTeamRepository"; import { Id } from "../../domain/entities/Ref"; import { - getTeamMemberIncidentManagementTeamRoles, mapD2EventsToIncidentManagementTeam, mapIncidentManagementTeamMemberToD2Event, } from "./utils/IncidentManagementTeamMapper"; @@ -37,89 +36,49 @@ export class IncidentManagementTeamD2Repository implements IncidentManagementTea }); } - getIncidentManagementTeamMember( - username: Id, - diseaseOutbreakId: Id, - roles: Role[] - ): FutureData { - return this.getIncidentManagementTeamEvents(diseaseOutbreakId).flatMap(d2Events => { - return apiToFuture( - this.api.metadata.get({ - users: { - fields: d2UserFields, - filter: { username: { eq: username } }, - }, - }) - ) - .flatMap(response => - assertOrError(response.users[0], "Incident Management Team Member") - ) - .map(d2User => - this.mapUserToIncidentManagementTeamMember(d2User as D2UserFix, d2Events, roles) - ); - }); - } - - private mapUserToIncidentManagementTeamMember( - d2User: D2UserFix, - events: D2TrackerEvent[], - roles: Role[] - ): TeamMember { - const avatarId = d2User?.avatar?.id; - const photoUrlString = avatarId - ? `${this.api.baseUrl}/api/fileResources/${avatarId}/data` - : undefined; - - const teamMember = new TeamMember({ - id: d2User.id, - username: d2User.username, - name: d2User.name, - email: d2User.email, - phone: d2User.phoneNumber, - status: "Available", // TODO: Get status when defined - photo: - photoUrlString && TeamMember.isValidPhotoUrl(photoUrlString) - ? new URL(photoUrlString) - : undefined, - teamRoles: undefined, - workPosition: undefined, // TODO: Get workPosition when defined - }); - - const teamRoles = getTeamMemberIncidentManagementTeamRoles(teamMember, events, roles); - - return new TeamMember({ - ...teamMember, - teamRoles: teamRoles.length > 0 ? teamRoles : undefined, - }); - } - saveIncidentManagementTeamMemberRole( teamMemberRole: TeamRole, incidentManagementTeamMember: TeamMember, diseaseOutbreakId: Id, roles: Role[] ): FutureData { - return this.saveOrDeleteIncidentManagementTeamMember({ + return this.saveIncidentManagementTeamMember({ teamMemberRole, incidentManagementTeamMember, diseaseOutbreakId, - importStrategy: "CREATE_AND_UPDATE", roles, }); } - deleteIncidentManagementTeamMemberRole( - teamMemberRole: TeamRole, - incidentManagementTeamMember: TeamMember, + deleteIncidentManagementTeamMemberRoles( diseaseOutbreakId: Id, - roles: Role[] + incidentManagementTeamRoleIds: Id[] ): FutureData { - return this.saveOrDeleteIncidentManagementTeamMember({ - teamMemberRole, - incidentManagementTeamMember, - diseaseOutbreakId, - importStrategy: "DELETE", - roles, + const d2IncidentManagementTeamRolesToDelete: D2TrackerEvent[] = + incidentManagementTeamRoleIds.map(id => ({ + event: id, + status: "COMPLETED", + program: RTSL_ZEBRA_PROGRAM_ID, + programStage: RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_BUILDER_PROGRAM_STAGE_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + occurredAt: "", + dataValues: [], + trackedEntity: diseaseOutbreakId, + })); + + return apiToFuture( + this.api.tracker.post( + { importStrategy: "DELETE" }, + { events: d2IncidentManagementTeamRolesToDelete } + ) + ).flatMap(deleteResponse => { + if (deleteResponse.status === "ERROR") { + return Future.error( + new Error(`Error deleting Incident Management Team Member Role`) + ); + } else { + return Future.success(undefined); + } }); } @@ -152,20 +111,13 @@ export class IncidentManagementTeamD2Repository implements IncidentManagementTea }); } - private saveOrDeleteIncidentManagementTeamMember(params: { + private saveIncidentManagementTeamMember(params: { teamMemberRole: TeamRole; incidentManagementTeamMember: TeamMember; diseaseOutbreakId: Id; - importStrategy: "CREATE_AND_UPDATE" | "DELETE"; roles: Role[]; }): FutureData { - const { - teamMemberRole, - incidentManagementTeamMember, - diseaseOutbreakId, - importStrategy, - roles, - } = params; + const { teamMemberRole, incidentManagementTeamMember, diseaseOutbreakId, roles } = params; return getProgramStage( this.api, RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_BUILDER_PROGRAM_STAGE_ID @@ -203,7 +155,10 @@ export class IncidentManagementTeamD2Repository implements IncidentManagementTea ); return apiToFuture( - this.api.tracker.post({ importStrategy: importStrategy }, { events: [d2Event] }) + this.api.tracker.post( + { importStrategy: "CREATE_AND_UPDATE" }, + { events: [d2Event] } + ) ).flatMap(saveResponse => { if (saveResponse.status === "ERROR" || !diseaseOutbreakId) { return Future.error( @@ -218,21 +173,6 @@ export class IncidentManagementTeamD2Repository implements IncidentManagementTea } } -const d2UserFields = { - id: true, - name: true, - email: true, - phoneNumber: true, - username: true, - avatar: true, -} as const; - -type D2User = MetadataPick<{ - users: { fields: typeof d2UserFields }; -}>["users"][number]; - -type D2UserFix = D2User & { username: string }; - const dataElementFields = { id: true, code: true, diff --git a/src/data/repositories/test/IncidentManagementTeamTestRepository.ts b/src/data/repositories/test/IncidentManagementTeamTestRepository.ts index 8782b3ff..67a9b4ef 100644 --- a/src/data/repositories/test/IncidentManagementTeamTestRepository.ts +++ b/src/data/repositories/test/IncidentManagementTeamTestRepository.ts @@ -6,7 +6,6 @@ import { Id } from "../../../domain/entities/Ref"; import { IncidentManagementTeamRepository } from "../../../domain/repositories/IncidentManagementTeamRepository"; import { Maybe } from "../../../utils/ts-utils"; import { FutureData } from "../../api-futures"; -import { INCIDENT_MANAGER_ROLE } from "../consts/IncidentManagementTeamBuilderConstants"; export class IncidentManagementTeamTestRepository implements IncidentManagementTeamRepository { get( @@ -26,38 +25,10 @@ export class IncidentManagementTeamTestRepository implements IncidentManagementT return Future.success(undefined); } - deleteIncidentManagementTeamMemberRole( - _teamMemberRole: TeamRole, - _incidentManagementTeamMember: TeamMember, + deleteIncidentManagementTeamMemberRoles( _diseaseOutbreakId: Id, - _roles: Role[] + _incidentManagementTeamRoleIds: Id[] ): FutureData { return Future.success(undefined); } - - getIncidentManagementTeamMember( - username: Id, - _diseaseOutbreakId: Id, - _roles: Role[] - ): FutureData { - const teamMember: TeamMember = new TeamMember({ - id: username, - username: username, - name: `Team Member Name ${username}`, - email: `email@email.com`, - phone: `121-1234`, - teamRoles: [ - { - id: "role", - name: "role", - roleId: INCIDENT_MANAGER_ROLE, - reportsToUsername: "reportsToUsername", - }, - ], - status: "Available", - photo: new URL("https://www.example.com"), - workPosition: "workPosition", - }); - return Future.success(teamMember); - } } diff --git a/src/domain/entities/ValidationError.ts b/src/domain/entities/ValidationError.ts index 1153cc96..8988c51a 100644 --- a/src/domain/entities/ValidationError.ts +++ b/src/domain/entities/ValidationError.ts @@ -1,6 +1,9 @@ import { Maybe } from "../../utils/ts-utils"; -export type ValidationErrorKey = "field_is_required" | "field_is_required_na"; +export type ValidationErrorKey = + | "field_is_required" + | "field_is_required_na" + | "cannot_create_cyclycal_dependency"; export type ValidationError = { property: string; diff --git a/src/domain/entities/incident-management-team/IncidentManagementTeam.ts b/src/domain/entities/incident-management-team/IncidentManagementTeam.ts index 1764456c..a47ee380 100644 --- a/src/domain/entities/incident-management-team/IncidentManagementTeam.ts +++ b/src/domain/entities/incident-management-team/IncidentManagementTeam.ts @@ -1,5 +1,7 @@ import { Maybe } from "../../../utils/ts-utils"; +import { TEAM_ROLE_FIELD_ID } from "../../../webapp/pages/form-page/incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState"; import { Struct } from "../generic/Struct"; +import { ValidationError } from "../ValidationError"; import { TeamMember } from "./TeamMember"; interface IncidentManagementTeamAttrs { @@ -7,4 +9,63 @@ interface IncidentManagementTeamAttrs { teamHierarchy: TeamMember[]; } -export class IncidentManagementTeam extends Struct() {} +export class IncidentManagementTeam extends Struct() { + static validateNotCyclicalDependency( + teamMember: string | undefined, + reportsToUsername: string | undefined, + currentIncidentManagementTeamHierarchy: TeamMember[], + property: string + ): ValidationError[] { + const descendantsUsernamesByParentUsername = getAllDescendantsUsernamesByParentUsername( + currentIncidentManagementTeamHierarchy + ); + + const descendantsFromTeamMember = + descendantsUsernamesByParentUsername.get(teamMember ?? "") ?? []; + + const isCyclicalDependency = descendantsFromTeamMember.includes(reportsToUsername ?? ""); + + return property === TEAM_ROLE_FIELD_ID || !isCyclicalDependency + ? [] + : [ + { + property: property, + value: "", + errors: ["cannot_create_cyclycal_dependency"], + }, + ]; + } +} + +function getAllDescendantsUsernamesByParentUsername( + teamMembers: TeamMember[] +): Map { + const initialMap = teamMembers.reduce((acc, member) => { + const entries = (member.teamRoles ?? []).map(role => ({ + parentUsername: role.reportsToUsername ?? "PARENT_ROOT", + username: member.username, + })); + + return entries.reduce((mapAcc, { parentUsername, username }) => { + return mapAcc.set(parentUsername, [...(mapAcc.get(parentUsername) ?? []), username]); + }, acc); + }, new Map()); + + const getDescendantUsernames = ( + parent: string, + processedParents = new Set() + ): string[] => { + if (processedParents.has(parent)) return []; + processedParents.add(parent); + + return (initialMap.get(parent) ?? []).flatMap(username => [ + username, + ...getDescendantUsernames(username, processedParents), + ]); + }; + + return Array.from(initialMap.keys()).reduce((descendantsMap, parent) => { + descendantsMap.set(parent, getDescendantUsernames(parent)); + return descendantsMap; + }, new Map()); +} diff --git a/src/domain/repositories/IncidentManagementTeamRepository.ts b/src/domain/repositories/IncidentManagementTeamRepository.ts index becbc70c..2622dae9 100644 --- a/src/domain/repositories/IncidentManagementTeamRepository.ts +++ b/src/domain/repositories/IncidentManagementTeamRepository.ts @@ -17,15 +17,8 @@ export interface IncidentManagementTeamRepository { diseaseOutbreakId: Id, roles: Role[] ): FutureData; - deleteIncidentManagementTeamMemberRole( - teamMemberRole: TeamRole, - incidentManagementTeamMember: TeamMember, + deleteIncidentManagementTeamMemberRoles( diseaseOutbreakId: Id, - roles: Role[] + incidentManagementTeamRoleIds: Id[] ): FutureData; - getIncidentManagementTeamMember( - username: Id, - diseaseOutbreakId: Id, - roles: Role[] - ): FutureData; } diff --git a/src/domain/usecases/DeleteIncidentManagementTeamMemberRoleUseCase.ts b/src/domain/usecases/DeleteIncidentManagementTeamMemberRoleUseCase.ts deleted file mode 100644 index b509ab8a..00000000 --- a/src/domain/usecases/DeleteIncidentManagementTeamMemberRoleUseCase.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FutureData } from "../../data/api-futures"; -import { TeamMember, TeamRole } from "../entities/incident-management-team/TeamMember"; -import { Id } from "../entities/Ref"; -import { IncidentManagementTeamRepository } from "../repositories/IncidentManagementTeamRepository"; -import { RoleRepository } from "../repositories/RoleRepository"; - -export class DeleteIncidentManagementTeamMemberRoleUseCase { - constructor( - private options: { - incidentManagementTeamRepository: IncidentManagementTeamRepository; - roleRepository: RoleRepository; - } - ) {} - - public execute( - teamMemberRole: TeamRole, - incidentManagementTeam: TeamMember, - diseaseOutbreakId: Id - ): FutureData { - return this.options.roleRepository.getAll().flatMap(roles => { - return this.options.incidentManagementTeamRepository.deleteIncidentManagementTeamMemberRole( - teamMemberRole, - incidentManagementTeam, - diseaseOutbreakId, - roles - ); - }); - } -} diff --git a/src/domain/usecases/DeleteIncidentManagementTeamMemberRolesUseCase.ts b/src/domain/usecases/DeleteIncidentManagementTeamMemberRolesUseCase.ts new file mode 100644 index 00000000..142483f3 --- /dev/null +++ b/src/domain/usecases/DeleteIncidentManagementTeamMemberRolesUseCase.ts @@ -0,0 +1,18 @@ +import { FutureData } from "../../data/api-futures"; +import { Id } from "../entities/Ref"; +import { IncidentManagementTeamRepository } from "../repositories/IncidentManagementTeamRepository"; + +export class DeleteIncidentManagementTeamMemberRolesUseCase { + constructor( + private options: { + incidentManagementTeamRepository: IncidentManagementTeamRepository; + } + ) {} + + public execute(diseaseOutbreakId: Id, incidentManagementTeamRoleIds: Id[]): FutureData { + return this.options.incidentManagementTeamRepository.deleteIncidentManagementTeamMemberRoles( + diseaseOutbreakId, + incidentManagementTeamRoleIds + ); + } +} diff --git a/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts b/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts index 76f4bcbc..93fa90e1 100644 --- a/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts +++ b/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts @@ -10,6 +10,7 @@ import { OrgUnitRepository } from "../repositories/OrgUnitRepository"; import { RiskAssessmentRepository } from "../repositories/RiskAssessmentRepository"; import { RoleRepository } from "../repositories/RoleRepository"; import { TeamMemberRepository } from "../repositories/TeamMemberRepository"; +import { getIncidentManagementTeamById } from "./utils/incident-management-team/GetIncidentManagementTeamById"; import { getAll } from "./utils/risk-assessment/GetRiskAssessmentById"; export class GetDiseaseOutbreakByIdUseCase { @@ -60,25 +61,28 @@ export class GetDiseaseOutbreakByIdUseCase { this.options.riskAssessmentRepository, configurations ), - roles: this.options.roleRepository.getAll(), - }).flatMap(({ riskAssessment, roles }) => { - return this.options.incidentManagementTeamRepository - .getIncidentManagementTeamMember(incidentManagerName, id, roles) - .flatMap(incidentManager => { - const diseaseOutbreakEvent: DiseaseOutbreakEvent = - new DiseaseOutbreakEvent({ - ...diseaseOutbreakEventBase, - createdBy: undefined, //TO DO : FIXME populate once metadata change is done. - mainSyndrome: mainSyndrome, - suspectedDisease: suspectedDisease, - notificationSource: notificationSource, - incidentManager: incidentManager, - riskAssessment: riskAssessment, - incidentActionPlan: undefined, //IAP is fetched on menu click. It is not needed here. - incidentManagementTeam: undefined, //IMT is fetched on menu click. It is not needed here. - }); - return Future.success(diseaseOutbreakEvent); - }); + incidentManagementTeam: getIncidentManagementTeamById( + id, + this.options, + configurations + ), + }).flatMap(({ riskAssessment, incidentManagementTeam }) => { + const incidentManager = incidentManagementTeam?.teamHierarchy?.find( + teamMember => teamMember.username === incidentManagerName + ); + + const diseaseOutbreakEvent: DiseaseOutbreakEvent = new DiseaseOutbreakEvent({ + ...diseaseOutbreakEventBase, + createdBy: undefined, //TO DO : FIXME populate once metadata change is done. + mainSyndrome: mainSyndrome, + suspectedDisease: suspectedDisease, + notificationSource: notificationSource, + incidentManager: incidentManager, + riskAssessment: riskAssessment, + incidentActionPlan: undefined, //IAP is fetched on menu click. It is not needed here. + incidentManagementTeam: undefined, //IMT is fetched on menu click. It is not needed here. + }); + return Future.success(diseaseOutbreakEvent); }); }); } diff --git a/src/domain/usecases/utils/disease-outbreak/SaveDiseaseOutbreak.ts b/src/domain/usecases/utils/disease-outbreak/SaveDiseaseOutbreak.ts index 65231991..4bb144ec 100644 --- a/src/domain/usecases/utils/disease-outbreak/SaveDiseaseOutbreak.ts +++ b/src/domain/usecases/utils/disease-outbreak/SaveDiseaseOutbreak.ts @@ -58,8 +58,14 @@ function saveIncidentManagerTeamMemberRole( teamRole => teamRole.roleId === INCIDENT_MANAGER_ROLE ); - if ( - incidentManagerTeamMemberFound && + if (!incidentManagerTeamMemberFound) { + return createNewIncidentManager( + repositories, + diseaseOutbreakEventBaseAttrs, + teamMembers, + roles + ); + } else if ( incidentManagerTeamMemberFound.username !== diseaseOutbreakEventBaseAttrs.incidentManagerName && incidentManagerTeamRole @@ -73,12 +79,7 @@ function saveIncidentManagerTeamMemberRole( roles ); } else { - return createNewIncidentManager( - repositories, - diseaseOutbreakEventBaseAttrs, - teamMembers, - roles - ); + return Future.success(diseaseOutbreakEventBaseAttrs.id); } }); }); @@ -116,12 +117,9 @@ function changeIncidentManager( }; return repositories.incidentManagementTeamRepository - .deleteIncidentManagementTeamMemberRole( - oldIncidentManagerTeamRole, - oldIncidentManager, - diseaseOutbreakEventBaseAttrs.id, - roles - ) + .deleteIncidentManagementTeamMemberRoles(diseaseOutbreakEventBaseAttrs.id, [ + oldIncidentManagerTeamRole.id, + ]) .flatMap(() => { return repositories.incidentManagementTeamRepository .saveIncidentManagementTeamMemberRole( diff --git a/src/domain/usecases/utils/incident-management-team/GetIncidentManagementTeamWithOptions.ts b/src/domain/usecases/utils/incident-management-team/GetIncidentManagementTeamWithOptions.ts index 4cbf362d..41aa9d3b 100644 --- a/src/domain/usecases/utils/incident-management-team/GetIncidentManagementTeamWithOptions.ts +++ b/src/domain/usecases/utils/incident-management-team/GetIncidentManagementTeamWithOptions.ts @@ -51,6 +51,8 @@ export function getIncidentManagementTeamWithOptions( labels: { errors: { field_is_required: "This field is required", + cannot_create_cyclycal_dependency: + "Cannot depend on itself in the team hierarchy", }, }, rules: [ @@ -62,6 +64,14 @@ export function getIncidentManagementTeamWithOptions( ], sectionsWithFieldsToDisableOption: [SECTION_IDS.reportsTo], }, + { + type: "disableFieldOptionWithSameFieldValue", + fieldId: incidentManagementTeamBuilderCodesWithoutRoles.reportsToUsername, + fieldIdsToDisableOption: [ + incidentManagementTeamBuilderCodesWithoutRoles.teamMemberAssigned, + ], + sectionsWithFieldsToDisableOption: [SECTION_IDS.teamMemberAssigned], + }, ], }; return Future.success(incidentManagementTeamMemberFormData); diff --git a/src/utils/tests.tsx b/src/utils/tests.tsx index 71720e0b..2c175a19 100644 --- a/src/utils/tests.tsx +++ b/src/utils/tests.tsx @@ -1,6 +1,7 @@ import { render, RenderResult } from "@testing-library/react"; import { SnackbarProvider } from "@eyeseetea/d2-ui-components"; import { ReactNode } from "react"; +import { MemoryRouter } from "react-router-dom"; import { AppContext, AppContextState } from "../webapp/contexts/app-context"; import { getTestCompositionRoot } from "../CompositionRoot"; import { createAdminUser } from "../domain/entities/__tests__/userFixtures"; @@ -78,9 +79,11 @@ export function getReactComponent(children: ReactNode): RenderResult { - - {children} - + + + {children} + + diff --git a/src/webapp/components/form/FormFieldsState.ts b/src/webapp/components/form/FormFieldsState.ts index 2ce46fd8..2b11d789 100644 --- a/src/webapp/components/form/FormFieldsState.ts +++ b/src/webapp/components/form/FormFieldsState.ts @@ -24,6 +24,7 @@ type FormFieldStateBase = { maxWidth?: string; value: T; type: FieldType; + updateAllStateWithValidationErrors?: boolean; }; export type FormTextFieldState = FormFieldStateBase & { @@ -151,15 +152,20 @@ export function updateFields( fieldValidationErrors?: ValidationError[] ): FormFieldState[] { return formFields.map(field => { + const errors = + fieldValidationErrors?.find(error => error.property === field.id)?.errors || []; if (field.id === updatedField.id) { return { ...updatedField, - errors: - fieldValidationErrors?.find(error => error.property === updatedField.id) - ?.errors || [], + errors: errors, }; } else { - return field; + return updatedField.updateAllStateWithValidationErrors + ? { + ...field, + errors: errors, + } + : field; } }); } diff --git a/src/webapp/components/form/FormSectionsState.ts b/src/webapp/components/form/FormSectionsState.ts index e006b722..43aacd6a 100644 --- a/src/webapp/components/form/FormSectionsState.ts +++ b/src/webapp/components/form/FormSectionsState.ts @@ -30,6 +30,16 @@ function hasSectionAFieldWithNotApplicable(sectionsState: FormSectionState) { return sectionsState.fields.some(field => field.hasNotApplicable); } +export function getFieldValueByIdFromSections( + sectionsState: FormSectionState[], + fieldId: string +): FormFieldState["value"] | undefined { + const section = sectionsState.find(section => + section.fields.some(field => field.id === fieldId) + ); + return section?.fields.find(field => field.id === fieldId)?.value; +} + // UPDATES: export function applyEffectNotApplicableFieldUpdatedInSections( @@ -84,8 +94,11 @@ export function updateSections( fieldValidationErrors?: ValidationError[] ): FormSectionState[] { return formSectionsState.map(section => { + const hasToUpdateSection = + isFieldInSection(section, updatedField) || + updatedField.updateAllStateWithValidationErrors; if (section.subsections?.length) { - const maybeUpdatedSection = isFieldInSection(section, updatedField) + const maybeUpdatedSection = hasToUpdateSection ? updateSectionState(section, updatedField, fieldValidationErrors) : section; return { @@ -97,7 +110,7 @@ export function updateSections( ), }; } else { - return isFieldInSection(section, updatedField) + return hasToUpdateSection ? updateSectionState(section, updatedField, fieldValidationErrors) : section; } @@ -109,7 +122,10 @@ function updateSectionState( updatedField: FormFieldState, fieldValidationErrors?: ValidationError[] ): FormSectionState { - if (isFieldInSection(formSectionState, updatedField)) { + if ( + isFieldInSection(formSectionState, updatedField) || + updatedField.updateAllStateWithValidationErrors + ) { return { ...formSectionState, fields: updateFields(formSectionState.fields, updatedField, fieldValidationErrors), diff --git a/src/webapp/components/form/FormState.ts b/src/webapp/components/form/FormState.ts index b63ca25d..d4e425de 100644 --- a/src/webapp/components/form/FormState.ts +++ b/src/webapp/components/form/FormState.ts @@ -72,8 +72,8 @@ export function isValidForm(formSections: FormSectionState[]): boolean { return allFields.every(field => { const validationErrors = validateField(field, allFields); - - return !validationErrors || validationErrors.errors.length === 0; + const hasErrorsInFields = allFields.some(f => f.errors.length > 0); + return !hasErrorsInFields && (!validationErrors || validationErrors.errors.length === 0); }); } diff --git a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx index a80300c9..e666fc9f 100644 --- a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx +++ b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx @@ -15,10 +15,11 @@ type IMTeamHierarchyItemProps = { teamRole: string; member: Maybe; disabled?: boolean; - onSelectedChange: (nodeId: string, selected: boolean) => void; + onSelectedChange?: (nodeId: string, selected: boolean) => void; subChildren: IMTeamHierarchyOption[]; diseaseOutbreakEventName: string; - selectedItemId: Id; + selectedItemIds?: Id[]; + isSelectable?: boolean; }; export const IMTeamHierarchyItem: React.FC = React.memo(props => { @@ -30,30 +31,36 @@ export const IMTeamHierarchyItem: React.FC = React.mem onSelectedChange, subChildren, diseaseOutbreakEventName, - selectedItemId, + selectedItemIds, + isSelectable = false, } = props; const [openMemberProfile, setOpenMemberProfile] = React.useState(false); const onCheckboxChange = React.useCallback( (isChecked: boolean) => { - !disabled && onSelectedChange(nodeId, isChecked); + if (onSelectedChange && isSelectable && !disabled) { + onSelectedChange(nodeId, isChecked); + } }, - [disabled, nodeId, onSelectedChange] + [disabled, nodeId, onSelectedChange, isSelectable] ); const onTeamRoleClick = React.useCallback( (event: React.MouseEvent) => { - event.preventDefault(); - !disabled && onSelectedChange(nodeId, !(selectedItemId === nodeId)); + if (isSelectable && onSelectedChange && selectedItemIds) { + event.preventDefault(); + !disabled && onSelectedChange(nodeId, !selectedItemIds.includes(nodeId)); + } }, - [disabled, nodeId, onSelectedChange, selectedItemId] + [disabled, nodeId, onSelectedChange, selectedItemIds, isSelectable] ); const onMemberClick = React.useCallback( (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); if (member) { - event.preventDefault(); setOpenMemberProfile(true); } }, @@ -65,14 +72,17 @@ export const IMTeamHierarchyItem: React.FC = React.mem - + {isSelectable && selectedItemIds && ( + + )} @@ -92,10 +102,11 @@ export const IMTeamHierarchyItem: React.FC = React.mem nodeId={child.id} teamRole={child.teamRole} member={child.member} - selectedItemId={selectedItemId} + selectedItemIds={selectedItemIds} onSelectedChange={onSelectedChange} diseaseOutbreakEventName={diseaseOutbreakEventName} subChildren={child.children} + isSelectable={isSelectable} /> ))} diff --git a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx index b15fcfc2..26da4767 100644 --- a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx +++ b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx @@ -8,6 +8,7 @@ import { TeamMember } from "../../../domain/entities/incident-management-team/Te import { IMTeamHierarchyItem } from "./IMTeamHierarchyItem"; import { Id } from "../../../domain/entities/Ref"; import { SearchInput } from "../search-input/SearchInput"; +import i18n from "../../../utils/i18n"; export type IMTeamHierarchyOption = { id: Id; @@ -20,31 +21,48 @@ export type IMTeamHierarchyOption = { type IMTeamHierarchyViewProps = { items: IMTeamHierarchyOption[]; - selectedItemId: Id; - onSelectedItemChange: (nodeId: Id, selected: boolean) => void; + selectedItemIds?: Id[]; + onSelectedItemChange?: (nodeId: Id, selected: boolean) => void; diseaseOutbreakEventName: string; onSearchChange: (term: string) => void; searchTerm: string; + defaultTeamRolesExpanded: Id[]; + isSelectable?: boolean; }; export const IMTeamHierarchyView: React.FC = React.memo(props => { const { onSelectedItemChange, items, - selectedItemId, + selectedItemIds, diseaseOutbreakEventName, searchTerm, onSearchChange, + defaultTeamRolesExpanded, + isSelectable = false, } = props; return ( - + + + + {isSelectable && searchTerm && selectedItemIds && selectedItemIds?.length > 0 ? ( + + {i18n.t( + `{{count}} selected ${selectedItemIds.length > 1 ? "items" : "item"}`, + { + count: selectedItemIds.length, + } + )} + + ) : null} } defaultExpandIcon={} + defaultExpanded={defaultTeamRolesExpanded} > {items.map(item => ( = React.mem nodeId={item.id} teamRole={item.teamRole} member={item.member} - selectedItemId={selectedItemId} + selectedItemIds={selectedItemIds} onSelectedChange={onSelectedItemChange} diseaseOutbreakEventName={diseaseOutbreakEventName} subChildren={item.children} + isSelectable={isSelectable} /> ))} @@ -86,5 +105,14 @@ const StyledIMTeamHierarchyView = styled(TreeViewMUI)` const ContentWrapper = styled.div` display: flex; flex-direction: column; - gap: 8px; +`; + +const CountSelectionText = styled.span` + font-weight: 400; + font-size: 0.875rem; + color: ${props => props.theme.palette.common.grey900}; +`; + +const SearchInputContainer = styled.div` + margin-block-end: 10px; `; diff --git a/src/webapp/components/incident-management-team/IncidentManagementTeamView.tsx b/src/webapp/components/incident-management-team/IncidentManagementTeamView.tsx new file mode 100644 index 00000000..e38aa46a --- /dev/null +++ b/src/webapp/components/incident-management-team/IncidentManagementTeamView.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import styled from "styled-components"; + +import { + IMTeamHierarchyOption, + IMTeamHierarchyView, +} from "../im-team-hierarchy/IMTeamHierarchyView"; +import { Id } from "../../../domain/entities/Ref"; +import { BasicTable, TableColumn, TableRowType } from "../table/BasicTable"; + +type IncidentManagementTeamViewProps = { + selectedHierarchyItemIds?: Id[]; + onSelectHierarchyItem?: (nodeId: string, selected: boolean) => void; + searchTerm: string; + onSearchChange: (term: string) => void; + incidentManagementTeamHierarchyItems: IMTeamHierarchyOption[]; + defaultTeamRolesExpanded: Id[]; + diseaseOutbreakEventName: string; + isSelectable?: boolean; + constactTableColumns: TableColumn[]; + constactTableRows: TableRowType[]; +}; + +export const IncidentManagementTeamView: React.FC = React.memo( + props => { + const { + selectedHierarchyItemIds, + onSelectHierarchyItem, + searchTerm, + onSearchChange, + incidentManagementTeamHierarchyItems, + defaultTeamRolesExpanded, + diseaseOutbreakEventName, + constactTableColumns, + constactTableRows, + isSelectable = false, + } = props; + + return ( + + + + + ); + } +); + +const IncidentManagementTeamViewContainer = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; diff --git a/src/webapp/components/incident-management-team/useIncidentManagementTeamView.ts b/src/webapp/components/incident-management-team/useIncidentManagementTeamView.ts new file mode 100644 index 00000000..97114c23 --- /dev/null +++ b/src/webapp/components/incident-management-team/useIncidentManagementTeamView.ts @@ -0,0 +1,238 @@ +import React from "react"; +import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useSnackbar } from "@eyeseetea/d2-ui-components"; + +import _c from "../../../domain/entities/generic/Collection"; +import i18n from "../../../utils/i18n"; +import { Maybe } from "../../../utils/ts-utils"; +import { Id } from "../../../domain/entities/Ref"; +import { useAppContext } from "../../contexts/app-context"; +import { IMTeamHierarchyOption } from "../im-team-hierarchy/IMTeamHierarchyView"; +import { IncidentManagementTeam } from "../../../domain/entities/incident-management-team/IncidentManagementTeam"; +import { TeamMember, TeamRole } from "../../../domain/entities/incident-management-team/TeamMember"; +import { TableColumn, TableRowType } from "../table/BasicTable"; + +type State = { + incidentManagementTeamHierarchyItems: Maybe; + selectedHierarchyItemIds: Id[]; + setSelectedHierarchyItemIds: Dispatch>; + onSearchChange: (term: string) => void; + getIncidentManagementTeam: () => void; + searchTerm: string; + defaultTeamRolesExpanded: Maybe; + incidentManagementTeam: IncidentManagementTeam | undefined; + constactTableColumns: TableColumn[]; + constactTableRows: TableRowType[]; +}; + +export function useIncidentManagementTeamView(id: Id): State { + const { compositionRoot, configurations } = useAppContext(); + const snackbar = useSnackbar(); + + const [incidentManagementTeamHierarchyItems, setIncidentManagementTeamHierarchyItems] = + useState(); + const [incidentManagementTeam, setIncidentManagementTeam] = useState< + IncidentManagementTeam | undefined + >(); + const [selectedHierarchyItemIds, setSelectedHierarchyItemIds] = useState([]); + const [defaultTeamRolesExpanded, setDefaultTeamRolesExpanded] = useState( + undefined + ); + const [searchTerm, setSearchTerm] = useState(""); + const [constactTableRows, setConstactTableRows] = useState([]); + + const getIncidentManagementTeam = useCallback(() => { + compositionRoot.incidentManagementTeam.get.execute(id, configurations).run( + incidentManagementTeam => { + setIncidentManagementTeam(incidentManagementTeam); + setDefaultTeamRolesExpanded(getDefaultTeamRolesExpanded(incidentManagementTeam)); + setIncidentManagementTeamHierarchyItems( + mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( + incidentManagementTeam?.teamHierarchy + ) + ); + setConstactTableRows( + mapIncidentManagementTeamToTableRows(incidentManagementTeam?.teamHierarchy) + ); + }, + err => { + console.debug(err); + snackbar.error(i18n.t(`Error loading current Incident Management Team`)); + } + ); + }, [compositionRoot.incidentManagementTeam.get, configurations, id, snackbar]); + + useEffect(() => { + getIncidentManagementTeam(); + }, [getIncidentManagementTeam]); + + const onSearchChange = useCallback( + (term: string) => { + setSearchTerm(term); + + if (incidentManagementTeamHierarchyItems) { + const filteredIncidentManagementTeamHierarchyItems = term + ? filterAndGetFlattenIMTeamHierarchyOptions( + incidentManagementTeam?.teamHierarchy, + term + ) + : mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( + incidentManagementTeam?.teamHierarchy + ); + + setIncidentManagementTeamHierarchyItems( + filteredIncidentManagementTeamHierarchyItems + ); + } + }, + [incidentManagementTeam?.teamHierarchy, incidentManagementTeamHierarchyItems] + ); + + const constactTableColumns: TableColumn[] = React.useMemo(() => { + return [ + { value: "role", label: "Role", type: "text" }, + { value: "name", label: "Name", type: "text" }, + { value: "email", label: "Email", type: "text" }, + { value: "phone", label: "Phone", type: "text" }, + ]; + }, []); + + return { + incidentManagementTeamHierarchyItems, + incidentManagementTeam, + selectedHierarchyItemIds, + defaultTeamRolesExpanded, + searchTerm, + constactTableColumns, + constactTableRows, + setSelectedHierarchyItemIds, + onSearchChange, + getIncidentManagementTeam, + }; +} +function createHierarchyItem(item: TeamMember, teamRole: TeamRole): IMTeamHierarchyOption { + return { + id: teamRole.id, + teamRole: teamRole.name, + teamRoleId: teamRole.roleId, + member: new TeamMember({ + id: item.id, + name: item.name, + username: item.username, + phone: item.phone, + email: item.email, + status: item.status, + photo: item.photo, + teamRoles: item.teamRoles, + workPosition: item.workPosition, + }), + parent: teamRole.reportsToUsername, + children: [], + }; +} + +function getTeamRolesMap( + incidentManagementTeamHierarchy: Maybe +): Record | undefined { + if (incidentManagementTeamHierarchy) { + return incidentManagementTeamHierarchy.reduce>( + (map, item) => { + const hierarchyItems = item.teamRoles?.map(teamRole => + createHierarchyItem(item, teamRole) + ); + + return !hierarchyItems || hierarchyItems?.length === 0 + ? map + : hierarchyItems.reduce( + (acc, hierarchyItem) => ({ + ...acc, + [hierarchyItem.id]: hierarchyItem, + }), + map + ); + }, + {} + ); + } +} + +function mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( + incidentManagementTeamHierarchy: Maybe +): IMTeamHierarchyOption[] { + if (incidentManagementTeamHierarchy) { + const teamRolesMap = getTeamRolesMap(incidentManagementTeamHierarchy); + return teamRolesMap ? buildTree(teamRolesMap) : []; + } else { + return []; + } +} + +function buildTree(teamMap: Record): IMTeamHierarchyOption[] { + const findChildren = (parentUsername: string): IMTeamHierarchyOption[] => + Object.values(teamMap) + .filter(item => item.parent === parentUsername) + .reduce((acc, item) => { + const children = findChildren(item.member?.username || ""); + return [...acc, { ...item, children: [...item.children, ...children] }]; + }, []); + + return Object.values(teamMap).reduce((acc, item) => { + const isRoot = !item.parent; + if (isRoot) { + const children = findChildren(item.member?.username || ""); + return [...acc, { ...item, children: [...item.children, ...children] }]; + } + + return acc; + }, []); +} + +function filterAndGetFlattenIMTeamHierarchyOptions( + incidentManagementTeamHierarchy: Maybe, + searchTerm: string +): IMTeamHierarchyOption[] { + if (incidentManagementTeamHierarchy) { + return incidentManagementTeamHierarchy.flatMap((teamMember): IMTeamHierarchyOption[] => { + const hierarchyItems: IMTeamHierarchyOption[] = + teamMember.teamRoles + ?.map((teamRole): IMTeamHierarchyOption | undefined => { + const isMatch = + teamRole.name.toLowerCase().includes(searchTerm.toLowerCase()) || + teamMember?.name.toLowerCase().includes(searchTerm.toLowerCase()); + + if (isMatch) { + return createHierarchyItem(teamMember, teamRole); + } + }) + ?.filter((item): item is IMTeamHierarchyOption => !!item) || []; + return hierarchyItems || []; + }, {}); + } else { + return []; + } +} + +function getDefaultTeamRolesExpanded(incidentManagementTeam: Maybe): Id[] { + return ( + incidentManagementTeam?.teamHierarchy.flatMap(teamMember => { + return teamMember?.teamRoles?.map(teamRole => teamRole.id) || []; + }) || [] + ); +} + +function mapIncidentManagementTeamToTableRows( + incidentManagementTeamHierarchy: Maybe +): TableRowType[] { + const teamRolesMap = getTeamRolesMap(incidentManagementTeamHierarchy); + if (teamRolesMap) { + return Object.values(teamRolesMap).map(teamRole => ({ + id: teamRole.id, + role: teamRole.teamRole, + name: teamRole.member?.name || "", + email: teamRole.member?.email || "", + phone: teamRole.member?.phone || "", + })); + } else { + return []; + } +} diff --git a/src/webapp/components/layout/side-bar/SideBarContent.tsx b/src/webapp/components/layout/side-bar/SideBarContent.tsx index 2bc100eb..4d7310d0 100644 --- a/src/webapp/components/layout/side-bar/SideBarContent.tsx +++ b/src/webapp/components/layout/side-bar/SideBarContent.tsx @@ -1,7 +1,7 @@ import { List, ListItem, ListItemText } from "@material-ui/core"; import React, { useCallback } from "react"; import styled from "styled-components"; -import { NavLink } from "react-router-dom"; +import { NavLink, useLocation } from "react-router-dom"; import { AddCircleOutline } from "@material-ui/icons"; import i18n from "../../../../utils/i18n"; import { Button } from "../../button/Button"; @@ -46,6 +46,7 @@ export const SideBarContent: React.FC = React.memo( ({ children, hideOptions = false, showCreateEvent = false }) => { const { goTo } = useRoutes(); const { getCurrentEventTracker } = useCurrentEventTracker(); + const location = useLocation(); const goToCreateEvent = useCallback(() => { goTo(RouteName.CREATE_FORM, { formType: "disease-outbreak-event" }); @@ -63,24 +64,25 @@ export const SideBarContent: React.FC = React.memo( ) : ( - {DEFAULT_SIDEBAR_OPTIONS.map(({ text, value }) => ( - - - - ))} + {DEFAULT_SIDEBAR_OPTIONS.map(({ text, value }) => { + const route = + value === RouteName.EVENT_TRACKER || + value === RouteName.IM_TEAM_BUILDER + ? routes[value].replace( + ":id", + getCurrentEventTracker()?.id || "" + ) + : routes[value]; + + return ( + + + + ); + })} )} diff --git a/src/webapp/components/table/BasicTable.tsx b/src/webapp/components/table/BasicTable.tsx index 35fa3bd0..b4276169 100644 --- a/src/webapp/components/table/BasicTable.tsx +++ b/src/webapp/components/table/BasicTable.tsx @@ -113,6 +113,7 @@ const StyledTable = styled(Table)` } & .MuiTableBody-root { color: ${props => props.theme.palette.common.grey}; + background-color: ${props => props.theme.palette.common.white}; } & .MuiTableCell-root { font-size: 0.75rem; diff --git a/src/webapp/components/table/statistic-table/StatisticTable.tsx b/src/webapp/components/table/statistic-table/StatisticTable.tsx index 893f361c..4d9490da 100644 --- a/src/webapp/components/table/statistic-table/StatisticTable.tsx +++ b/src/webapp/components/table/statistic-table/StatisticTable.tsx @@ -206,6 +206,7 @@ const StyledTableContainer = styled(TableContainer)` } & .MuiTableBody-root { color: ${props => props.theme.palette.common.grey}; + background-color: ${props => props.theme.palette.common.white}; } & .MuiTableCell-root { min-width: 2rem; diff --git a/src/webapp/contexts/CurrentEventTrackerProvider.tsx b/src/webapp/contexts/CurrentEventTrackerProvider.tsx index 47af0b48..1c8ace4c 100644 --- a/src/webapp/contexts/CurrentEventTrackerProvider.tsx +++ b/src/webapp/contexts/CurrentEventTrackerProvider.tsx @@ -10,6 +10,7 @@ export const CurrentEventTrackerContextProvider: React.FC = ( setCurrentEventTracker(EventTrackerDetails); localStorage.setItem("currentEventTracker", JSON.stringify(EventTrackerDetails)); }, []); + const resetCurrentEventTracker = useCallback(() => { setCurrentEventTracker(undefined); localStorage.removeItem("currentEventTracker"); @@ -19,8 +20,10 @@ export const CurrentEventTrackerContextProvider: React.FC = ( if (currentEventTracker) { return currentEventTracker; } + const localCurrentEventTracker = localStorage.getItem("currentEventTracker"); if (localCurrentEventTracker) { + setCurrentEventTracker(JSON.parse(localCurrentEventTracker) as DiseaseOutbreakEvent); return JSON.parse(localCurrentEventTracker); } return undefined; diff --git a/src/webapp/hooks/useRoutes.ts b/src/webapp/hooks/useRoutes.ts index 4f92a630..c84a319c 100644 --- a/src/webapp/hooks/useRoutes.ts +++ b/src/webapp/hooks/useRoutes.ts @@ -30,7 +30,7 @@ export const routes: Record = { [RouteName.CREATE_FORM]: `/create/${formType}`, [RouteName.EDIT_FORM]: `/edit/${formType}/:id`, [RouteName.EVENT_TRACKER]: "/event-tracker/:id", - [RouteName.IM_TEAM_BUILDER]: "/incident-management-team-builder/:id", + [RouteName.IM_TEAM_BUILDER]: "/:id/incident-management-team-builder", [RouteName.INCIDENT_ACTION_PLAN]: "/incident-action-plan", [RouteName.RESOURCES]: "/resources", [RouteName.DASHBOARD]: "/", diff --git a/src/webapp/pages/form-page/incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState.ts b/src/webapp/pages/form-page/incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState.ts index 5fc25c0b..5ae99d1d 100644 --- a/src/webapp/pages/form-page/incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState.ts @@ -88,12 +88,19 @@ export function mapIncidentManagementTeamMemberToInitialFormState( multiple: false, options: teamRoleToAssing?.roleId === INCIDENT_MANAGER_ROLE - ? incidentManagerOptions - : teamMemberOptions, + ? incidentManagerOptions.map(user => ({ + ...user, + disabled: user.value === teamRoleToAssing?.reportsToUsername, + })) + : teamMemberOptions.map(user => ({ + ...user, + disabled: user.value === teamRoleToAssing?.reportsToUsername, + })), value: incidentManagementTeamMember?.username || "", required: true, showIsRequired: true, disabled: false, + updateAllStateWithValidationErrors: true, }, ], }, @@ -101,7 +108,7 @@ export function mapIncidentManagementTeamMemberToInitialFormState( title: "Reports to...", id: SECTION_IDS.reportsTo, isVisible: true, - required: false, + required: teamRoleToAssing?.roleId !== INCIDENT_MANAGER_ROLE, fields: [ { id: incidentManagementTeamBuilderCodesWithoutRoles.reportsToUsername, @@ -115,9 +122,10 @@ export function mapIncidentManagementTeamMemberToInitialFormState( disabled: user.value === incidentManagementTeamMember?.username, })), value: teamRoleToAssing?.reportsToUsername || "", - required: false, - showIsRequired: false, - disabled: false, + required: teamRoleToAssing?.roleId !== INCIDENT_MANAGER_ROLE, + showIsRequired: teamRoleToAssing?.roleId !== INCIDENT_MANAGER_ROLE, + disabled: teamRoleToAssing?.roleId === INCIDENT_MANAGER_ROLE, + updateAllStateWithValidationErrors: true, }, ], }, diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index ecb103ff..f3b90e47 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -6,7 +6,7 @@ import { Id } from "../../../domain/entities/Ref"; import { FormState } from "../../components/form/FormState"; import { RouteName, useRoutes } from "../../hooks/useRoutes"; import { mapFormStateToEntityData } from "./mapFormStateToEntityData"; -import { updateAndValidateFormState } from "./disease-outbreak-event/utils/updateDiseaseOutbreakEventFormState"; +import { updateAndValidateFormState } from "./utils/updateDiseaseOutbreakEventFormState"; import { FormFieldState } from "../../components/form/FormFieldsState"; import { FormType } from "./FormPage"; import { ConfigurableForm, FormLables } from "../../../domain/entities/ConfigurableForm"; diff --git a/src/webapp/pages/form-page/disease-outbreak-event/utils/applyRulesInFormState.ts b/src/webapp/pages/form-page/utils/applyRulesInFormState.ts similarity index 86% rename from src/webapp/pages/form-page/disease-outbreak-event/utils/applyRulesInFormState.ts rename to src/webapp/pages/form-page/utils/applyRulesInFormState.ts index 522a297c..794eac06 100644 --- a/src/webapp/pages/form-page/disease-outbreak-event/utils/applyRulesInFormState.ts +++ b/src/webapp/pages/form-page/utils/applyRulesInFormState.ts @@ -1,11 +1,11 @@ -import { Rule } from "../../../../../domain/entities/Rule"; -import { FormFieldState } from "../../../../components/form/FormFieldsState"; +import { Rule } from "../../../../domain/entities/Rule"; +import { FormFieldState } from "../../../components/form/FormFieldsState"; import { disableFieldOptionWithSameFieldValueInSection, disableFieldsByFieldValueInSection, toggleSectionVisibilityByFieldValue, -} from "../../../../components/form/FormSectionsState"; -import { FormState } from "../../../../components/form/FormState"; +} from "../../../components/form/FormSectionsState"; +import { FormState } from "../../../components/form/FormState"; export function applyRulesInFormState( currentFormState: FormState, diff --git a/src/webapp/pages/form-page/disease-outbreak-event/utils/updateDiseaseOutbreakEventFormState.ts b/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts similarity index 57% rename from src/webapp/pages/form-page/disease-outbreak-event/utils/updateDiseaseOutbreakEventFormState.ts rename to src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts index 4573a8bc..434e8835 100644 --- a/src/webapp/pages/form-page/disease-outbreak-event/utils/updateDiseaseOutbreakEventFormState.ts +++ b/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts @@ -1,14 +1,17 @@ -import { ConfigurableForm } from "../../../../../domain/entities/ConfigurableForm"; -import { DiseaseOutbreakEvent } from "../../../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { ValidationError } from "../../../../../domain/entities/ValidationError"; -import { FormFieldState } from "../../../../components/form/FormFieldsState"; +import { incidentManagementTeamBuilderCodesWithoutRoles } from "../../../../data/repositories/consts/IncidentManagementTeamBuilderConstants"; +import { ConfigurableForm } from "../../../../domain/entities/ConfigurableForm"; +import { DiseaseOutbreakEvent } from "../../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { IncidentManagementTeam } from "../../../../domain/entities/incident-management-team/IncidentManagementTeam"; +import { ValidationError } from "../../../../domain/entities/ValidationError"; +import { FormFieldState } from "../../../components/form/FormFieldsState"; +import { getFieldValueByIdFromSections } from "../../../components/form/FormSectionsState"; import { FormState, isValidForm, updateFormStateAndApplySideEffects, updateFormStateWithFieldErrors, validateForm, -} from "../../../../components/form/FormState"; +} from "../../../components/form/FormState"; import { applyRulesInFormState } from "./applyRulesInFormState"; export function updateAndValidateFormState( @@ -67,8 +70,24 @@ function validateFormState( break; case "incident-response-action": break; - case "incident-management-team-member-assignment": + case "incident-management-team-member-assignment": { + const reportsToUsername = getFieldValueByIdFromSections( + updatedForm.sections, + incidentManagementTeamBuilderCodesWithoutRoles.reportsToUsername + ) as string | undefined; + const teamMemberAssigned = getFieldValueByIdFromSections( + updatedForm.sections, + incidentManagementTeamBuilderCodesWithoutRoles.teamMemberAssigned + ) as string | undefined; + + return IncidentManagementTeam.validateNotCyclicalDependency( + teamMemberAssigned, + reportsToUsername, + configurableForm?.currentIncidentManagementTeam?.teamHierarchy || [], + updatedField.id + ); break; + } } return [...formValidationErrors, ...entityValidationErrors]; diff --git a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx index 8e5f6b19..beda3408 100644 --- a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx +++ b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx @@ -10,6 +10,7 @@ import { ActionPlanFormSummary } from "../../components/form/form-summary/Action import { IncidentActionNotice } from "./IncidentActionNotice"; import { Loader } from "../../components/loader/Loader"; import { ResponseActionTable } from "./ResponseActionTable"; +import { TeamSection } from "./TeamSection"; export const IncidentActionPlanPage: React.FC = React.memo(() => { const { getCurrentEventTracker } = useCurrentEventTracker(); @@ -42,7 +43,9 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { title={i18n.t("Incident Action Plan")} subtitle={i18n.t(currentEventTracker?.name || "")} > - {!actionPlanSummary && responseActionRows.length === 0 && !summaryError && } + {(!actionPlanSummary && responseActionRows.length === 0 && !summaryError) || !id ? ( + + ) : null} {!incidentActionExists ? ( ) : ( @@ -75,6 +78,7 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { /> )} + {id && } ); }); diff --git a/src/webapp/pages/incident-action-plan/TeamSection.tsx b/src/webapp/pages/incident-action-plan/TeamSection.tsx new file mode 100644 index 00000000..5661b553 --- /dev/null +++ b/src/webapp/pages/incident-action-plan/TeamSection.tsx @@ -0,0 +1,63 @@ +import React, { useCallback } from "react"; +import { IconEditItems24 } from "@dhis2/ui"; + +import i18n from "../../../utils/i18n"; +import { Section } from "../../components/section/Section"; +import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; +import { RouteName, useRoutes } from "../../hooks/useRoutes"; +import { Button } from "../../components/button/Button"; +import { useIncidentManagementTeamView } from "../../components/incident-management-team/useIncidentManagementTeamView"; +import { Id } from "../../../domain/entities/Ref"; +import { IncidentManagementTeamView } from "../../components/incident-management-team/IncidentManagementTeamView"; + +type TeamSectionProps = { + diseaseOutbreakEventId: Id; +}; + +export const TeamSection: React.FC = React.memo(props => { + const { diseaseOutbreakEventId } = props; + const { goTo } = useRoutes(); + const { getCurrentEventTracker } = useCurrentEventTracker(); + const { + incidentManagementTeamHierarchyItems, + onSearchChange, + searchTerm, + defaultTeamRolesExpanded, + constactTableColumns, + constactTableRows, + } = useIncidentManagementTeamView(diseaseOutbreakEventId); + + const goToIncidentManagementTeamBuilder = useCallback(() => { + goTo(RouteName.IM_TEAM_BUILDER, { id: diseaseOutbreakEventId }); + }, [diseaseOutbreakEventId, goTo]); + + return ( +
} + onClick={goToIncidentManagementTeamBuilder} + > + {i18n.t("Edit Team")} + + } + titleVariant="secondary" + > + {!incidentManagementTeamHierarchyItems || !defaultTeamRolesExpanded ? null : ( + + )} +
+ ); +}); diff --git a/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx b/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx index a2a5c4d4..4d5db70a 100644 --- a/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx +++ b/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx @@ -11,10 +11,10 @@ import LoaderContainer from "../../components/loader/LoaderContainer"; import { UserCard } from "../../components/user-selector/UserCard"; import { Section } from "../../components/section/Section"; import { Button } from "../../components/button/Button"; -import { IMTeamHierarchyView } from "../../components/im-team-hierarchy/IMTeamHierarchyView"; import { useIMTeamBuilder } from "./useIMTeamBuilder"; import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; import { SimpleModal } from "../../components/simple-modal/SimpleModal"; +import { IncidentManagementTeamView } from "../../components/incident-management-team/IncidentManagementTeamView"; export const IMTeamBuilderPage: React.FC = React.memo(() => { const { id } = useParams<{ @@ -27,10 +27,13 @@ export const IMTeamBuilderPage: React.FC = React.memo(() => { incidentManagerUser, lastUpdated, incidentManagementTeamHierarchyItems, - selectedHierarchyItemId, + selectedHierarchyItemIds, openDeleteModalData, disableDeletion, searchTerm, + defaultTeamRolesExpanded, + constactTableColumns, + constactTableRows, onSearchChange, onSelectHierarchyItem, goToIncidentManagementTeamRole, @@ -49,7 +52,9 @@ export const IMTeamBuilderPage: React.FC = React.memo(() => { title={i18n.t("Incident Management Team Builder")} subtitle={getCurrentEventTracker()?.name || ""} > - + {incidentManagerUser && } @@ -58,60 +63,81 @@ export const IMTeamBuilderPage: React.FC = React.memo(() => { lastUpdated={lastUpdated} headerButton={ - + {selectedHierarchyItemIds.length > 1 ? null : ( + + )} - {selectedHierarchyItemId && ( + {selectedHierarchyItemIds.length ? ( - )} + ) : null} } > - + {!incidentManagementTeamHierarchyItems || !defaultTeamRolesExpanded ? null : ( + + )} onOpenDeleteModalData(undefined)} - title={i18n.t("Delete team role")} + title={i18n.t("Confirm deletion")} closeLabel={i18n.t("Cancel")} footerButtons={ } > {openDeleteModalData && ( - - {openDeleteModalData.teamRole.name}: - - {openDeleteModalData.teamMember.name}{" "} - - + + {i18n.t( + `Are you sure you want to delete ${ + openDeleteModalData.length > 1 + ? "these team roles" + : "this team role" + }?` + )} + )} @@ -130,19 +156,7 @@ const ButtonsContainer = styled.div` gap: 8px; `; -const RoleAndMemberWrapper = styled.div` - display: flex; - align-items: center; - gap: 4px; -`; - -const RoleWrapper = styled.div` - font-weight: 700; - font-size: 0.875rem; - color: ${props => props.theme.palette.common.grey900}; -`; - -const MemberWrapper = styled.div` +const Text = styled.div` font-weight: 400; font-size: 0.875rem; color: ${props => props.theme.palette.common.grey900}; diff --git a/src/webapp/pages/incident-management-team-builder/useIMTeamBuilder.ts b/src/webapp/pages/incident-management-team-builder/useIMTeamBuilder.ts index 5cab46c6..16fb527d 100644 --- a/src/webapp/pages/incident-management-team-builder/useIMTeamBuilder.ts +++ b/src/webapp/pages/incident-management-team-builder/useIMTeamBuilder.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Id } from "../../../domain/entities/Ref"; import { Maybe } from "../../../utils/ts-utils"; import { useAppContext } from "../../contexts/app-context"; @@ -6,130 +6,119 @@ import { User } from "../../components/user-selector/UserSelector"; import { mapTeamMemberToUser } from "../form-page/mapEntityToFormState"; import { IMTeamHierarchyOption } from "../../components/im-team-hierarchy/IMTeamHierarchyView"; import { RouteName, useRoutes } from "../../hooks/useRoutes"; -import { IncidentManagementTeam } from "../../../domain/entities/incident-management-team/IncidentManagementTeam"; -import { TeamMember, TeamRole } from "../../../domain/entities/incident-management-team/TeamMember"; +import { TeamMember } from "../../../domain/entities/incident-management-team/TeamMember"; import { INCIDENT_MANAGER_ROLE } from "../../../data/repositories/consts/IncidentManagementTeamBuilderConstants"; import _c from "../../../domain/entities/generic/Collection"; +import { useIncidentManagementTeamView } from "../../components/incident-management-team/useIncidentManagementTeamView"; +import { TableColumn, TableRowType } from "../../components/table/BasicTable"; type GlobalMessage = { text: string; type: "warning" | "success" | "error"; }; -export type ProfileModalData = { - teamMember: TeamMember; - teamRole: TeamRole; -}; - type State = { globalMessage: Maybe; incidentManagementTeamHierarchyItems: Maybe; - selectedHierarchyItemId: string; + selectedHierarchyItemIds: Id[]; onSelectHierarchyItem: (nodeId: string, selected: boolean) => void; goToIncidentManagementTeamRole: () => void; onDeleteIncidentManagementTeamMember: () => void; incidentManagerUser: Maybe; lastUpdated: string; - openDeleteModalData: ProfileModalData | undefined; - onOpenDeleteModalData: (selectedHierarchyItemId: Id | undefined) => void; + openDeleteModalData: TeamMember[] | undefined; + onOpenDeleteModalData: (selectedHierarchyItemId: Id[] | undefined) => void; disableDeletion: boolean; onSearchChange: (term: string) => void; searchTerm: string; + defaultTeamRolesExpanded: Maybe; + constactTableColumns: TableColumn[]; + constactTableRows: TableRowType[]; }; -export function useIMTeamBuilder(id: Id): State { - const { compositionRoot, configurations } = useAppContext(); +export function useIMTeamBuilder(diseaseOutbreakEventId: Id): State { + const { compositionRoot } = useAppContext(); const { goTo } = useRoutes(); + const { + incidentManagementTeamHierarchyItems, + incidentManagementTeam, + selectedHierarchyItemIds, + setSelectedHierarchyItemIds, + onSearchChange, + searchTerm, + defaultTeamRolesExpanded, + getIncidentManagementTeam, + constactTableColumns, + constactTableRows, + } = useIncidentManagementTeamView(diseaseOutbreakEventId); + const [globalMessage, setGlobalMessage] = useState>(); - const [incidentManagementTeamHierarchyItems, setIncidentManagementTeamHierarchyItems] = - useState(); - const [incidentManagementTeam, setIncidentManagementTeam] = useState< - IncidentManagementTeam | undefined - >(); - const [selectedHierarchyItemId, setSelectedHierarchyItemId] = useState(""); const [disableDeletion, setDisableDeletion] = useState(false); - const [openDeleteModalData, setOpenDeleteModalData] = useState( + const [openDeleteModalData, setOpenDeleteModalData] = useState( undefined ); - const [searchTerm, setSearchTerm] = useState(""); - - const getIncidentManagementTeam = useCallback(() => { - compositionRoot.incidentManagementTeam.get.execute(id, configurations).run( - incidentManagementTeam => { - setIncidentManagementTeam(incidentManagementTeam); - setIncidentManagementTeamHierarchyItems( - mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( - incidentManagementTeam?.teamHierarchy - ) - ); - }, - err => { - console.debug(err); - setGlobalMessage({ - text: `Error loading current Incident Management Team`, - type: "error", - }); - } - ); - }, [compositionRoot.incidentManagementTeam.get, configurations, id]); - - useEffect(() => { - getIncidentManagementTeam(); - }, [getIncidentManagementTeam]); const goToIncidentManagementTeamRole = useCallback(() => { - if (selectedHierarchyItemId) { + if (selectedHierarchyItemIds.length === 1 && selectedHierarchyItemIds[0]) { goTo(RouteName.EDIT_FORM, { formType: "incident-management-team-member-assignment", - id: selectedHierarchyItemId, + id: selectedHierarchyItemIds[0], }); - } else { + } else if (selectedHierarchyItemIds.length === 0) { goTo(RouteName.CREATE_FORM, { formType: "incident-management-team-member-assignment", }); } - }, [goTo, selectedHierarchyItemId]); + }, [goTo, selectedHierarchyItemIds]); const onSelectHierarchyItem = useCallback( (nodeId: string, selected: boolean) => { - const selection = selected ? nodeId : ""; - const incidentManagementTeamItemSelected = selection - ? incidentManagementTeam?.teamHierarchy.find(teamMember => - teamMember.teamRoles?.some(role => role.id === selection) - ) - : undefined; + const newSelection = selected + ? [...selectedHierarchyItemIds, nodeId] + : selectedHierarchyItemIds.filter(id => id !== nodeId); - const selectedRole = incidentManagementTeamItemSelected?.teamRoles?.find( - role => role.id === selection + const incidentManagementTeamItemsSelected = + incidentManagementTeam?.teamHierarchy.filter(teamMember => + teamMember.teamRoles?.some(role => newSelection.includes(role.id)) + ); + + const isIncidentManagerRoleSelected = !!incidentManagementTeamItemsSelected?.some( + item => { + return item.teamRoles?.some(role => role.roleId === INCIDENT_MANAGER_ROLE); + } ); - const isIncidentManagerRoleSelected = selectedRole?.roleId === INCIDENT_MANAGER_ROLE; + const areAllParentsAndAllChildrenSelected = checkIfParentsAndAllChildrenSelected( + newSelection, + incidentManagementTeam?.teamHierarchy + ); - setSelectedHierarchyItemId(selection); - setDisableDeletion(isIncidentManagerRoleSelected); + setSelectedHierarchyItemIds(newSelection); + setDisableDeletion( + isIncidentManagerRoleSelected || !areAllParentsAndAllChildrenSelected + ); }, - [incidentManagementTeam?.teamHierarchy] + [ + incidentManagementTeam?.teamHierarchy, + selectedHierarchyItemIds, + setSelectedHierarchyItemIds, + ] ); const onOpenDeleteModalData = useCallback( - (selectedHierarchyItemId: Id | undefined) => { - if (!selectedHierarchyItemId) { + (selectedHierarchyItemIds: Id[] | undefined) => { + if (!selectedHierarchyItemIds?.length) { setOpenDeleteModalData(undefined); } else { - const incidentManagementTeamItem = incidentManagementTeam?.teamHierarchy.find( - teamMember => - teamMember.teamRoles?.some(role => role.id === selectedHierarchyItemId) - ); - - const selectedRole = incidentManagementTeamItem?.teamRoles?.find( - role => role.id === selectedHierarchyItemId - ); - - if (incidentManagementTeamItem && selectedRole) { - setOpenDeleteModalData({ - teamRole: selectedRole, - teamMember: incidentManagementTeamItem, - }); + const incidentManagementTeamItemsSelected = + incidentManagementTeam?.teamHierarchy.filter(teamMember => + teamMember.teamRoles?.some(role => + selectedHierarchyItemIds.includes(role.id) + ) + ); + + if (incidentManagementTeamItemsSelected) { + setOpenDeleteModalData(incidentManagementTeamItemsSelected); } } }, @@ -137,51 +126,42 @@ export function useIMTeamBuilder(id: Id): State { ); const onDeleteIncidentManagementTeamMember = useCallback(() => { - if (disableDeletion) return; - - const teamMember = incidentManagementTeam?.teamHierarchy.find(teamMember => - teamMember.teamRoles?.some(role => role.id === selectedHierarchyItemId) - ); - - const teamRoleToDelete = teamMember?.teamRoles?.find( - role => role.id === selectedHierarchyItemId - ); - - if (teamMember && teamRoleToDelete) { - compositionRoot.incidentManagementTeam.deleteIncidentManagementTeamMemberRole - .execute(teamRoleToDelete, teamMember, id) - .run( - () => { - setGlobalMessage({ - text: `${teamMember.name} deleted from Incident Management Team`, - type: "success", - }); - getIncidentManagementTeam(); - onOpenDeleteModalData(undefined); - }, - err => { - console.debug(err); - setGlobalMessage({ - text: `Error deleting ${teamMember.name} from Incident Management Team`, - type: "error", - }); - onOpenDeleteModalData(undefined); - } - ); - } else { - setGlobalMessage({ - text: `Error deleting team member from Incident Management Team`, - type: "error", - }); - } + if (disableDeletion || !selectedHierarchyItemIds.length) return; + + compositionRoot.incidentManagementTeam.deleteIncidentManagementTeamMemberRoles + .execute(diseaseOutbreakEventId, selectedHierarchyItemIds) + .run( + () => { + setGlobalMessage({ + text: `${ + selectedHierarchyItemIds.length > 1 ? "Team members" : "Team member" + } deleted from Incident Management Team`, + type: "success", + }); + getIncidentManagementTeam(); + onOpenDeleteModalData(undefined); + setSelectedHierarchyItemIds([]); + }, + err => { + console.debug(err); + setGlobalMessage({ + text: `Error deleting ${ + selectedHierarchyItemIds.length > 1 ? "team members" : "team member" + } from Incident Management Team`, + type: "error", + }); + onOpenDeleteModalData(undefined); + setSelectedHierarchyItemIds([]); + } + ); }, [ - compositionRoot.incidentManagementTeam.deleteIncidentManagementTeamMemberRole, + compositionRoot.incidentManagementTeam.deleteIncidentManagementTeamMemberRoles, disableDeletion, + diseaseOutbreakEventId, getIncidentManagementTeam, - id, - incidentManagementTeam?.teamHierarchy, onOpenDeleteModalData, - selectedHierarchyItemId, + selectedHierarchyItemIds, + setSelectedHierarchyItemIds, ]); const incidentManagerUser = useMemo(() => { @@ -193,34 +173,12 @@ export function useIMTeamBuilder(id: Id): State { } }, [incidentManagementTeam?.teamHierarchy]); - const onSearchChange = useCallback( - (term: string) => { - setSearchTerm(term); - - if (incidentManagementTeamHierarchyItems) { - const filteredIncidentManagementTeamHierarchyItems = term - ? filterIncidentManagementTeamHierarchy( - incidentManagementTeamHierarchyItems, - term - ) - : mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( - incidentManagementTeam?.teamHierarchy - ); - - setIncidentManagementTeamHierarchyItems( - filteredIncidentManagementTeamHierarchyItems - ); - } - }, - [incidentManagementTeam?.teamHierarchy, incidentManagementTeamHierarchyItems] - ); - const lastUpdated = incidentManagementTeam?.lastUpdated?.toString() ?? ""; return { globalMessage, incidentManagementTeamHierarchyItems, - selectedHierarchyItemId, + selectedHierarchyItemIds, onSelectHierarchyItem, goToIncidentManagementTeamRole, incidentManagerUser, @@ -231,102 +189,53 @@ export function useIMTeamBuilder(id: Id): State { disableDeletion, searchTerm, onSearchChange, + defaultTeamRolesExpanded, + constactTableColumns, + constactTableRows, }; } -function mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( +function checkIfParentsAndAllChildrenSelected( + teamRoleSelection: Id[], incidentManagementTeamHierarchy: Maybe -): IMTeamHierarchyOption[] { - if (incidentManagementTeamHierarchy) { - const createHierarchyItem = ( - item: TeamMember, - teamRole: TeamRole - ): IMTeamHierarchyOption => ({ - id: teamRole.id, - teamRole: teamRole.name, - teamRoleId: teamRole.roleId, - member: new TeamMember({ - id: item.id, - name: item.name, - username: item.username, - phone: item.phone, - email: item.email, - status: item.status, - photo: item.photo, - teamRoles: item.teamRoles, - workPosition: item.workPosition, - }), - parent: teamRole.reportsToUsername, - children: [], - }); - - const teamMap = incidentManagementTeamHierarchy.reduce< - Record - >((map, item) => { - const hierarchyItems = item.teamRoles?.map(teamRole => - createHierarchyItem(item, teamRole) - ); - - return !hierarchyItems || hierarchyItems?.length === 0 - ? map - : hierarchyItems.reduce( - (acc, hierarchyItem) => ({ - ...acc, - [hierarchyItem.id]: hierarchyItem, - }), - map - ); - }, {}); - - return buildTree(teamMap); - } else { - return []; - } -} - -function buildTree(teamMap: Record): IMTeamHierarchyOption[] { - const findChildren = (parentUsername: string): IMTeamHierarchyOption[] => - Object.values(teamMap) - .filter(item => item.parent === parentUsername) - .reduce((acc, item) => { - const children = findChildren(item.member?.username || ""); - return [...acc, { ...item, children: [...item.children, ...children] }]; - }, []); - - return Object.values(teamMap).reduce((acc, item) => { - const isRoot = !item.parent; - if (isRoot) { - const children = findChildren(item.member?.username || ""); - return [...acc, { ...item, children: [...item.children, ...children] }]; - } +): boolean { + if (!incidentManagementTeamHierarchy) return true; + + const selectedItemsInTeamHierarchy = incidentManagementTeamHierarchy + .map(teamMember => ({ + ...teamMember, + teamRoles: teamMember.teamRoles?.filter(teamRole => + teamRoleSelection.includes(teamRole.id) + ), + })) + .filter(teamMember => teamMember.teamRoles?.length); + + const allTeamRoleChildrenIdsByParentUsername = getAllTeamRoleChildrenIdsByParentUsername( + incidentManagementTeamHierarchy + ); - return acc; - }, []); + return selectedItemsInTeamHierarchy.every(teamMember => { + const teamRoleChildrenIds = allTeamRoleChildrenIdsByParentUsername.get(teamMember.username); + return ( + !teamRoleChildrenIds || + teamRoleChildrenIds.every(childId => { + return teamRoleSelection.includes(childId); + }) + ); + }); } -function filterIncidentManagementTeamHierarchy( - items: IMTeamHierarchyOption[], - searchTerm: string -): IMTeamHierarchyOption[] { - return _c( - items.map(item => { - const filteredChildren = filterIncidentManagementTeamHierarchy( - item.children, - searchTerm - ); - - const isMatch = item.teamRole.toLowerCase().includes(searchTerm.toLowerCase()); - - if (isMatch || filteredChildren.length > 0) { - return { - ...item, - children: filteredChildren, - }; - } - - return null; - }) - ) - .compact() - .toArray(); +function getAllTeamRoleChildrenIdsByParentUsername(teamMembers: TeamMember[]): Map { + return teamMembers.reduce((acc, teamMember) => { + return ( + teamMember.teamRoles?.reduce((innerAcc, teamRole) => { + const parentUsername = teamRole.reportsToUsername; + if (parentUsername) { + const existingChildren = innerAcc.get(parentUsername) || []; + innerAcc.set(parentUsername, [...existingChildren, teamRole.id]); + } + return innerAcc; + }, acc) || acc + ); + }, new Map()); }