Skip to content

Commit

Permalink
feat: CE-946-Allow-assignment-of-ceeb-complaints-to-ceeb-officers (#610)
Browse files Browse the repository at this point in the history
Co-authored-by: Scarlett <[email protected]>
Co-authored-by: Barrett Falk <[email protected]>
Co-authored-by: Scarlett <[email protected]>
Co-authored-by: afwilcox <[email protected]>
  • Loading branch information
5 people authored Sep 13, 2024
1 parent 4a59540 commit c3eedad
Show file tree
Hide file tree
Showing 24 changed files with 733 additions and 60 deletions.
1 change: 1 addition & 0 deletions backend/src/enum/role.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export enum Role {
CEEB_COMPLIANCE_COORDINATOR = "CEEB Compliance Coordinator",
CEEB_SECTION_HEAD = "CEEB Section Head",
TEMPORARY_TEST_ADMIN = "TEMPORARY_TEST_ADMIN",
READ_ONLY = "READ ONLY",
}
59 changes: 59 additions & 0 deletions backend/src/external_api/css/css.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,63 @@ export class CssService implements ExternalApiService {
throw new Error(`exception: unable to delete user's role ${userIdir} - error: ${error}`);
}
};

getUserRoleMapping = async (): Promise<AxiosResponse> => {
try {
const apiToken = await this.authenticate();
//Get all roles from NatCom CSS integation
const rolesUrl = `${this.baseUri}/api/v1/integrations/4794/${this.env}/roles`;
const config: AxiosRequestConfig = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`,
},
};
const roleRes = await get(apiToken, rolesUrl, config);
if (roleRes?.data.data.length > 0) {
const {
data: { data: roleList },
} = roleRes;

//Get all users for each role
let usersUrl: string = "";
const usersRoles = await Promise.all(
roleList.map(async (role) => {
usersUrl = `${this.baseUri}/api/v1/integrations/4794/${this.env}/roles/${role.name}/users`;
const userRes = await get(apiToken, encodeURI(usersUrl), config);
if (userRes?.data.data.length > 0) {
const {
data: { data: users },
} = userRes;
let usersRolesTemp = await Promise.all(
users.map((user) => {
return {
userId: user.username
.replace(/@idir$/i, "")
.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5"),
role: role.name,
};
}),
);
return usersRolesTemp;
}
}),
);

//exclude empty roles and concatenate all sub-array elements
const usersRolesFlat = usersRoles.filter((item) => item !== undefined).flat();

//group the array elements by a user id
const usersRolesGroupped = usersRolesFlat.reduce((grouping, item) => {
grouping[item.userId] = [...(grouping[item.userId] || []), item.role];
return grouping;
}, {});

return usersRolesGroupped;
}
} catch (error) {
this.logger.error(`exception: error: ${error}`);
return;
}
};
}
4 changes: 3 additions & 1 deletion backend/src/v1/complaint/complaint.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1293,7 +1293,9 @@ export class ComplaintService {

const agencyCodeInstance = new AgencyCode("COS");

const agencyCode = webeocInd ? agencyCodeInstance : await this._getAgencyByUser();
const { ownedBy } = model;

const agencyCode = webeocInd ? agencyCodeInstance : new AgencyCode(ownedBy);

const queryRunner = this.dataSource.createQueryRunner();
let complaintId = "";
Expand Down
8 changes: 7 additions & 1 deletion backend/src/v1/officer/entities/officer.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { UUID } from "crypto";
import { Office } from "../../office/entities/office.entity";
import { Person } from "../../person/entities/person.entity";
import { Entity, Column, OneToOne, JoinColumn, Unique, PrimaryGeneratedColumn, ManyToOne } from "typeorm";
import { Entity, Column, OneToOne, JoinColumn, Unique, PrimaryGeneratedColumn, ManyToOne, AfterLoad } from "typeorm";

@Entity()
@Unique(["person_guid"])
Expand Down Expand Up @@ -72,5 +72,11 @@ export class Officer {
@Column()
auth_user_guid: UUID;

user_roles: string[];
@AfterLoad()
updateUserRoles() {
this.user_roles = [];
}

constructor() {}
}
9 changes: 9 additions & 0 deletions backend/src/v1/officer/officer.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { PersonService } from "../person/person.service";
import { OfficeService } from "../office/office.service";
import { DataSource } from "typeorm";
import { dataSourceMockFactory } from "../../../test/mocks/datasource";
import { CssService } from "../../external_api/css/css.service";
import { ConfigurationService } from "../configuration/configuration.service";
import { Configuration } from "../configuration/entities/configuration.entity";

describe("OfficerController", () => {
let controller: OfficerController;
Expand Down Expand Up @@ -36,6 +39,12 @@ describe("OfficerController", () => {
provide: DataSource,
useFactory: dataSourceMockFactory,
},
CssService,
ConfigurationService,
{
provide: getRepositoryToken(Configuration),
useValue: {},
},
],
}).compile();

Expand Down
2 changes: 2 additions & 0 deletions backend/src/v1/officer/officer.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { Officer } from "./entities/officer.entity";
import { Person } from "../person/entities/person.entity";
import { Office } from "../office/entities/office.entity";
import { CssModule } from "src/external_api/css/css.module";

@Module({
imports: [
TypeOrmModule.forFeature([Officer]),
TypeOrmModule.forFeature([Person]),
TypeOrmModule.forFeature([Office]),
CssModule,
],
controllers: [OfficerController],
providers: [OfficerService, PersonService, OfficeService],
Expand Down
9 changes: 9 additions & 0 deletions backend/src/v1/officer/officer.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { PersonService } from "../person/person.service";
import { OfficeService } from "../office/office.service";
import { DataSource } from "typeorm";
import { dataSourceMockFactory } from "../../../test/mocks/datasource";
import { CssService } from "../../external_api/css/css.service";
import { ConfigurationService } from "../configuration/configuration.service";
import { Configuration } from "../configuration/entities/configuration.entity";

describe("OfficerService", () => {
let service: OfficerService;
Expand All @@ -34,6 +37,12 @@ describe("OfficerService", () => {
provide: DataSource,
useFactory: dataSourceMockFactory,
},
CssService,
ConfigurationService,
{
provide: getRepositoryToken(Configuration),
useValue: {},
},
],
}).compile();

Expand Down
29 changes: 28 additions & 1 deletion backend/src/v1/officer/officer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DataSource, Repository } from "typeorm";
import { PersonService } from "../person/person.service";
import { OfficeService } from "../office/office.service";
import { UUID } from "crypto";
import { CssService } from "../../external_api/css/css.service";

@Injectable()
export class OfficerService {
Expand All @@ -22,9 +23,11 @@ export class OfficerService {
protected readonly personService: PersonService;
@Inject(OfficeService)
protected readonly officeService: OfficeService;
@Inject(CssService)
private readonly cssService: CssService;

async findAll(): Promise<Officer[]> {
return this.officerRepository
let officers = await this.officerRepository
.createQueryBuilder("officer")
.leftJoinAndSelect("officer.office_guid", "office")
.leftJoinAndSelect("officer.person_guid", "person")
Expand All @@ -33,6 +36,28 @@ export class OfficerService {
.leftJoinAndSelect("office.agency_code", "agency_code")
.orderBy("person.last_name", "ASC")
.getMany();

const roleMapping = await this.cssService.getUserRoleMapping();
if (roleMapping) {
let useGuid: string;
officers = officers.map((officer) => {
useGuid = Object.keys(roleMapping).find((key) => key === officer.auth_user_guid);
return {
officer_guid: officer.officer_guid,
person_guid: officer.person_guid,
office_guid: officer.office_guid,
user_id: officer.user_id,
create_user_id: officer.create_user_id,
create_utc_timestamp: officer.create_utc_timestamp,
update_user_id: officer.update_user_id,
update_utc_timestamp: officer.update_utc_timestamp,
auth_user_guid: officer.auth_user_guid,
user_roles: roleMapping[useGuid] ?? [],
} as Officer;
});
}

return officers;
}

async findByOffice(office_guid: any): Promise<Officer[]> {
Expand Down Expand Up @@ -141,6 +166,8 @@ export class OfficerService {
}

async update(officer_guid: UUID, updateOfficerDto: UpdateOfficerDto): Promise<Officer> {
//exclude roles field populated from keycloak from update
delete (updateOfficerDto as any).user_roles;
await this.officerRepository.update({ officer_guid }, updateOfficerDto);
return this.findOne(officer_guid);
}
Expand Down
15 changes: 15 additions & 0 deletions backend/src/v1/officer/officer.service.v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { PersonService } from "../person/person.service";
import { Person } from "../person/entities/person.entity";
import { OfficeService } from "../office/office.service";
import { Office } from "../office/entities/office.entity";
import { CssService } from "../../external_api/css/css.service";
import { ConfigurationService } from "../configuration/configuration.service";
import { Configuration } from "../configuration/entities/configuration.entity";
import { MockRoleRepository } from "../../../test/mocks/mock-role-repository";
import { REQUEST } from "@nestjs/core";

describe("Testing: OfficerService", () => {
let service: OfficerService;
Expand All @@ -39,6 +44,16 @@ describe("Testing: OfficerService", () => {
provide: DataSource,
useFactory: dataSourceMockFactory,
},
CssService,
{
provide: REQUEST,
useFactory: MockRoleRepository,
},
ConfigurationService,
{
provide: getRepositoryToken(Configuration),
useValue: {},
},
],
})
.compile()
Expand Down
3 changes: 2 additions & 1 deletion backend/src/v1/team/team.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ export class TeamService {
const currentRoles: any = await this.cssService.getUserRoles(userIdir);
for await (const roleItem of currentRoles) {
const rolesMatchWithUpdate = updateRoles.some((updateRole) => updateRole.name === roleItem.name);
if (roleItem.name !== Role.TEMPORARY_TEST_ADMIN && !rolesMatchWithUpdate) {
//Remove existing roles that do not match with updated roles, but still keeps Admin and Read only role
if (roleItem.name !== Role.TEMPORARY_TEST_ADMIN && roleItem.name !== Role.READ_ONLY && !rolesMatchWithUpdate) {
await this.cssService.deleteUserRole(userIdir, roleItem.name);
}
}
Expand Down
3 changes: 3 additions & 0 deletions backend/test/mocks/mock-role-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const MockRoleRepository = () => ({
getUserRoleMapping: jest.fn().mockResolvedValue([]),
});
2 changes: 1 addition & 1 deletion frontend/cypress/e2e/allegation-details-create.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("Complaint Create Page spec - Create View", () => {
region: "West Coast",
status: "Closed",
statusIndex: 1,
assigned: "Benson, Olivia",
assigned: "Kot, Steve",
assignedIndex: 1,
violationInProgressIndex: 1,
violationInProgressString: "No",
Expand Down
2 changes: 1 addition & 1 deletion frontend/cypress/e2e/allegation-details-edit.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const editCallDetails = {
regionCode: "KTNY",
status: "Closed",
statusIndex: 1,
assigned: "Benson, Olivia",
assigned: "Kot, Steve",
assignedIndex: 1,
violationInProgressIndex: 1,
violationInProgressString: "No",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("Complaint Create Page spec - Enter Coordinates - Create View", () => {
speciesIndex: 3,
status: "Closed",
statusIndex: 1,
assigned: "Nesmith, Chris",
assigned: "Kot, Steve",
assignedIndex: 1,
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/cypress/e2e/hwcr-details-create.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("Complaint Create Page spec - Create View", () => {
speciesIndex: 3,
status: "Closed",
statusIndex: 1,
assigned: "Nesmith, Chris",
assigned: "Kot, Steve",
assignedIndex: 1,
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/cypress/e2e/hwcr-details-edit.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe("Complaint Edit Page spec - Edit View", () => {
speciesIndex: 3,
status: "Closed",
statusIndex: 1,
assigned: "Nesmith, Chris",
assigned: "Kot, Steve",
assignedIndex: 1,
};

Expand Down
4 changes: 2 additions & 2 deletions frontend/cypress/e2e/hwcr-outcome-prev-and-educ.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("HWCR Outcome Prevention and Education", () => {
let params = {
section: "PREV&EDUC",
checkboxes: ["#PROVSFTYIN", "#CNTCTBYLAW"],
officer: "Benson, Olivia",
officer: "Kot, Steve",
date: "01",
toastText: "Prevention and education has been saved",
};
Expand Down Expand Up @@ -125,7 +125,7 @@ describe("HWCR Outcome Prevention and Education", () => {
let params = {
section: "PREV&EDUC",
checkboxes: ["#CNTCTBIOVT"],
officer: "Peralta, Jake",
officer: "Kot, Steve",
date: "01",
toastText: "Prevention and education has been updated",
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ Cypress.Commands.add("navigateToTab", (complaintTab: string, removeFilters: bool

//-- verify correct tab
if (complaintTab === "#hwcr-tab") {
cy.get(complaintTab).should("contain.text", "Human Wildlife Conflicts");
cy.get(complaintTab).should("contain.text", "Human Wildlife Conflict");
} else {
cy.get(complaintTab).should("contain.text", "Enforcement");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../../../../../hooks/hooks";
import {
selectDischargeDropdown,
Expand All @@ -19,10 +19,10 @@ import { CompInput } from "../../../../../common/comp-input";
import { openModal } from "../../../../../../store/reducers/app";
import { CANCEL_CONFIRM } from "../../../../../../types/modal/modal-types";
import { getCaseFile, upsertDecisionOutcome } from "../../../../../../store/reducers/case-thunks";
import { assignComplaintToOfficer, selectOfficersDropdown } from "../../../../../../store/reducers/officer";
import { assignComplaintToOfficer, selectOfficersByAgencyDropdown } from "../../../../../../store/reducers/officer";
import { selectCaseId } from "../../../../../../store/reducers/case-selectors";
import { UUID } from "crypto";
import { getComplaintById } from "../../../../../../store/reducers/complaints";
import { getComplaintById, selectComplaintCallerInformation } from "../../../../../../store/reducers/complaints";
import { ToggleError } from "../../../../../../common/toast";
import COMPLAINT_TYPES from "../../../../../../types/app/complaint-types";

Expand Down Expand Up @@ -74,7 +74,8 @@ export const DecisionForm: FC<props> = ({
const schedulesOptions = useAppSelector(selectScheduleDropdown);
const decisionTypeOptions = useAppSelector(selectDecisionTypeDropdown);
const agencyOptions = useAppSelector(selectAgencyDropdown);
const officerOptions = useAppSelector(selectOfficersDropdown(true));
const { ownedByAgencyCode } = useAppSelector(selectComplaintCallerInformation);
const officerOptions = useAppSelector(selectOfficersByAgencyDropdown(ownedByAgencyCode?.agency));

//-- error messgaes
const [scheduleErrorMessage] = useState("");
Expand All @@ -98,11 +99,17 @@ export const DecisionForm: FC<props> = ({
rationale,
inspectionNumber,
leadAgency,
assignedTo: officerAssigned ? officerAssigned : "",
assignedTo: officerAssigned ?? "",
actionTaken,
actionTakenDate,
});

useEffect(() => {
if (officerAssigned) {
applyData({ ...data, assignedTo: officerAssigned });
}
}, [officerAssigned]);

//-- update the decision state by property
const updateModel = (property: string, value: string | Date | undefined) => {
const model = { ...data, [property]: value };
Expand Down
Loading

0 comments on commit c3eedad

Please sign in to comment.