Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes in incident management team builder page #36

Merged
merged 13 commits into from
Nov 11, 2024
21 changes: 18 additions & 3 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down Expand Up @@ -105,6 +105,9 @@ msgstr ""
msgid "Currently assigned:"
msgstr ""

msgid "Error loading current Incident Management Team"
msgstr ""

msgid "Create Event"
msgstr ""

Expand Down Expand Up @@ -216,6 +219,12 @@ msgstr ""
msgid "Incident Action Plan"
msgstr ""

msgid "Team"
msgstr ""

msgid "Edit Team"
msgstr ""

msgid "Incident Management Team Builder"
msgstr ""

Expand All @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions src/CompositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
128 changes: 34 additions & 94 deletions src/data/repositories/IncidentManagementTeamD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -37,89 +36,49 @@ export class IncidentManagementTeamD2Repository implements IncidentManagementTea
});
}

getIncidentManagementTeamMember(
username: Id,
diseaseOutbreakId: Id,
roles: Role[]
): FutureData<TeamMember> {
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<void> {
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<void> {
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);
}
});
}

Expand Down Expand Up @@ -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<void> {
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
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down
33 changes: 2 additions & 31 deletions src/data/repositories/test/IncidentManagementTeamTestRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<void> {
return Future.success(undefined);
}

getIncidentManagementTeamMember(
username: Id,
_diseaseOutbreakId: Id,
_roles: Role[]
): FutureData<TeamMember> {
const teamMember: TeamMember = new TeamMember({
id: username,
username: username,
name: `Team Member Name ${username}`,
email: `[email protected]`,
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);
}
}
5 changes: 4 additions & 1 deletion src/domain/entities/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,71 @@
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 {
lastUpdated: Maybe<Date>;
teamHierarchy: TeamMember[];
}

export class IncidentManagementTeam extends Struct<IncidentManagementTeamAttrs>() {}
export class IncidentManagementTeam extends Struct<IncidentManagementTeamAttrs>() {
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<string, string[]> {
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 }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

return mapAcc.set(parentUsername, [...(mapAcc.get(parentUsername) ?? []), username]);
}, acc);
}, new Map<string, string[]>());

const getDescendantUsernames = (
parent: string,
processedParents = new Set<string>()
): 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<string, string[]>());
}
Loading
Loading