diff --git a/backend/src/external_api/css/css.service.ts b/backend/src/external_api/css/css.service.ts index 540667959..01e28b6d7 100644 --- a/backend/src/external_api/css/css.service.ts +++ b/backend/src/external_api/css/css.service.ts @@ -3,6 +3,7 @@ import { ExternalApiService } from "../external-api-service"; import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { get } from "../../helpers/axios-api"; import { ConfigurationService } from "../../v1/configuration/configuration.service"; +import { CssUser } from "src/types/css/cssUser"; @Injectable() export class CssService implements ExternalApiService { @@ -64,6 +65,24 @@ export class CssService implements ExternalApiService { } }; + getUserIdirByEmail = async (email: string): Promise => { + try { + const apiToken = await this.authenticate(); + const url = `${this.baseUri}/api/v1/${this.env}/idir/users?email=${email}`; + const config: AxiosRequestConfig = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiToken}`, + }, + }; + const response = await get(apiToken, url, config); + return response?.data.data; + } catch (error) { + this.logger.error(`exception: unable to get user by email: ${email} - error: ${error}`); + throw new Error(`exception: unable to get user by email: ${email} - error: ${error}`); + } + }; + getUserRoles = async (userIdir): Promise<{ name: string; composite: string }[]> => { try { const apiToken = await this.authenticate(); diff --git a/backend/src/types/css/cssUser.ts b/backend/src/types/css/cssUser.ts new file mode 100644 index 000000000..83bc71c46 --- /dev/null +++ b/backend/src/types/css/cssUser.ts @@ -0,0 +1,11 @@ +export interface CssUser { + username: string; + firstName: string; + lastName: string; + email: string; + attributes: { + idir_user_guid: string[]; + idir_username: string[]; + display_name: string[]; + }; +} diff --git a/backend/src/types/models/people/officer.ts b/backend/src/types/models/people/officer.ts new file mode 100644 index 000000000..e98f0cc9e --- /dev/null +++ b/backend/src/types/models/people/officer.ts @@ -0,0 +1,26 @@ +export interface NewOfficer { + user_id: string; + create_user_id: string; + create_utc_timestamp: Date; + update_user_id: string; + update_utc_timestamp: Date; + auth_user_guid: string; + office_guid: string | null; + team_code: string | null; + person_guid: { + first_name: string; + middle_name_1: null; + middle_name_2: null; + last_name: string; + create_user_id: string; + create_utc_timestamp: Date; + update_user_id: string; + updateTimestamp: Date; + }; + roles: { + user_roles: Array<{ name: string | undefined }>; + user_idir: string; //Example : fohe4m5pn8clhkxmlho33sn1r7vr7m67@idir + }; + coms_enrolled_ind: boolean; + deactivate_ind: boolean; +} diff --git a/backend/src/v1/officer/dto/create-officer.dto.ts b/backend/src/v1/officer/dto/create-officer.dto.ts index 33ecc2fe8..e14ea120d 100644 --- a/backend/src/v1/officer/dto/create-officer.dto.ts +++ b/backend/src/v1/officer/dto/create-officer.dto.ts @@ -5,8 +5,11 @@ export class CreateOfficerDto extends PickType(OfficerDto, [ "user_id", "person_guid", "office_guid", + "auth_user_guid", "create_user_id", "create_utc_timestamp", "update_user_id", "update_utc_timestamp", + "coms_enrolled_ind", + "deactivate_ind", ] as const) {} diff --git a/backend/src/v1/officer/dto/officer.dto.ts b/backend/src/v1/officer/dto/officer.dto.ts index 8afe04cf4..48434bdd7 100644 --- a/backend/src/v1/officer/dto/officer.dto.ts +++ b/backend/src/v1/officer/dto/officer.dto.ts @@ -57,4 +57,16 @@ export class OfficerDto { description: "The keycloak guid for the officer", }) auth_user_guid: UUID; + + @ApiProperty({ + example: "true", + description: "An indicator to determine if the officer has access to COMS", + }) + coms_enrolled_ind: boolean; + + @ApiProperty({ + example: "false", + description: "An indicator to determine if the officer has been deactivated", + }) + deactivate_ind: boolean; } diff --git a/backend/src/v1/officer/dto/update-officer.dto.ts b/backend/src/v1/officer/dto/update-officer.dto.ts index 3c23898bf..77d819844 100644 --- a/backend/src/v1/officer/dto/update-officer.dto.ts +++ b/backend/src/v1/officer/dto/update-officer.dto.ts @@ -1,4 +1,6 @@ import { PartialType } from "@nestjs/swagger"; import { CreateOfficerDto } from "./create-officer.dto"; -export class UpdateOfficerDto extends PartialType(CreateOfficerDto) {} +export class UpdateOfficerDto extends PartialType(CreateOfficerDto) { + user_roles?: string[]; +} diff --git a/backend/src/v1/officer/entities/officer.entity.ts b/backend/src/v1/officer/entities/officer.entity.ts index 64b050523..f79fb5f63 100644 --- a/backend/src/v1/officer/entities/officer.entity.ts +++ b/backend/src/v1/officer/entities/officer.entity.ts @@ -79,6 +79,13 @@ export class Officer { @Column() coms_enrolled_ind: boolean; + @ApiProperty({ + example: false, + description: "Indicates whether an officer has been deactivated", + }) + @Column() + deactivate_ind: boolean; + user_roles: string[]; @AfterLoad() updateUserRoles() { diff --git a/backend/src/v1/officer/officer.controller.ts b/backend/src/v1/officer/officer.controller.ts index 12fc38cc7..48afa45d8 100644 --- a/backend/src/v1/officer/officer.controller.ts +++ b/backend/src/v1/officer/officer.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Put } from "@nestjs/common"; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Put, Inject } from "@nestjs/common"; import { OfficerService } from "./officer.service"; import { CreateOfficerDto } from "./dto/create-officer.dto"; import { UpdateOfficerDto } from "./dto/update-officer.dto"; @@ -9,6 +9,7 @@ import { ApiTags } from "@nestjs/swagger"; import { UUID } from "crypto"; import { User } from "../../auth/decorators/user.decorator"; import { Token } from "../../auth/decorators/token.decorator"; +import { NewOfficer } from "src/types/models/people/officer"; @ApiTags("officer") @UseGuards(JwtRoleGuard) @@ -21,7 +22,7 @@ export class OfficerController { @Post() @Roles(Role.COS_OFFICER) - create(@Body() createOfficerDto: CreateOfficerDto) { + create(@Body() createOfficerDto: NewOfficer) { return this.officerService.create(createOfficerDto); } @@ -61,6 +62,12 @@ export class OfficerController { return this.officerService.findByPersonGuid(person_guid); } + @Get("/find-by-email/:email") + @Roles(Role.TEMPORARY_TEST_ADMIN) + findUserByEmail(@Param("email") email: string) { + return this.officerService.findByCssEmail(email); + } + @Patch(":id") @Roles(Role.COS_OFFICER, Role.CEEB, Role.TEMPORARY_TEST_ADMIN) update(@Param("id") id: UUID, @Body() updateOfficerDto: UpdateOfficerDto) { diff --git a/backend/src/v1/officer/officer.module.ts b/backend/src/v1/officer/officer.module.ts index 8b4561be9..9df735f03 100644 --- a/backend/src/v1/officer/officer.module.ts +++ b/backend/src/v1/officer/officer.module.ts @@ -7,17 +7,23 @@ 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"; +import { CssModule } from "../../external_api/css/css.module"; +import { TeamService } from "src/v1/team/team.service"; +import { Team } from "src/v1/team/entities/team.entity"; +import { OfficerTeamXref } from "src/v1/officer_team_xref/entities/officer_team_xref.entity"; +import { OfficerTeamXrefService } from "src/v1/officer_team_xref/officer_team_xref.service"; @Module({ imports: [ TypeOrmModule.forFeature([Officer]), TypeOrmModule.forFeature([Person]), TypeOrmModule.forFeature([Office]), + TypeOrmModule.forFeature([Team]), + TypeOrmModule.forFeature([OfficerTeamXref]), CssModule, ], controllers: [OfficerController], - providers: [OfficerService, PersonService, OfficeService], + providers: [OfficerService, PersonService, OfficeService, TeamService, OfficerTeamXrefService], exports: [OfficerService], }) export class OfficerModule {} diff --git a/backend/src/v1/officer/officer.service.ts b/backend/src/v1/officer/officer.service.ts index b9ad26c4b..3b74976a5 100644 --- a/backend/src/v1/officer/officer.service.ts +++ b/backend/src/v1/officer/officer.service.ts @@ -3,8 +3,6 @@ import { CreateOfficerDto } from "./dto/create-officer.dto"; import { CreatePersonDto } from "../person/dto/create-person.dto"; import { UpdateOfficerDto } from "./dto/update-officer.dto"; import { Officer } from "./entities/officer.entity"; -import { Office } from "../office/entities/office.entity"; -import { AgencyCode } from "../agency_code/entities/agency_code.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { DataSource, Repository } from "typeorm"; import { PersonService } from "../person/person.service"; @@ -13,18 +11,28 @@ import { UUID } from "crypto"; import { CssService } from "../../external_api/css/css.service"; import { Role } from "../../enum/role.enum"; import { put } from "../../helpers/axios-api"; +import { CssUser } from "../../types/css/cssUser"; +import { CreateOfficerTeamXrefDto } from "../officer_team_xref/dto/create-officer_team_xref.dto"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { NewOfficer } from "../../types/models/people/officer"; @Injectable() export class OfficerService { private readonly logger = new Logger(OfficerService.name); - constructor(private dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) {} @InjectRepository(Officer) - private officerRepository: Repository; + private readonly officerRepository: Repository; + @Inject(PersonService) protected readonly personService: PersonService; @Inject(OfficeService) protected readonly officeService: OfficeService; + @Inject(TeamService) + protected readonly teamService: TeamService; + @Inject(OfficerTeamXrefService) + protected readonly officerTeamXrefService: OfficerTeamXrefService; @Inject(CssService) private readonly cssService: CssService; @@ -55,6 +63,8 @@ export class OfficerService { update_user_id: officer.update_user_id, update_utc_timestamp: officer.update_utc_timestamp, auth_user_guid: officer.auth_user_guid, + coms_enrolled_ind: officer.coms_enrolled_ind, + deactivate_ind: officer.deactivate_ind, user_roles: roleMapping[useGuid] ?? [], } as Officer; }); @@ -93,6 +103,20 @@ export class OfficerService { }); } + async findByCssEmail(email: string): Promise { + const cssUser = await this.cssService.getUserIdirByEmail(email); + if (cssUser.length === 0) return null; + if (cssUser.length > 0) { + //assume email is unique and return only 1 result + const officer = await this.findByAuthUserGuid(cssUser[0].attributes.idir_user_guid[0]); + //if user already exists in officer table, then return officer data + if (officer) { + return officer; + } + } + return cssUser[0]; + } + async findByUserId(userid: string): Promise { userid = userid.toUpperCase(); return this.officerRepository.findOne({ @@ -119,60 +143,79 @@ export class OfficerService { }); } - async create(officer: any): Promise { + async create(officer: NewOfficer): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - let newOfficerString; - let officeObject; + let newOfficerObject; let personObject; + let teamObject; try { - //Look for the Office - officeObject = await this.officeService.findByGeoOrgCode(officer.geo_organization_unit_code); - if (officeObject.length === 0) { - // insertOffice - - let agencyObject = new AgencyCode("COS"); - let officeObject = new Office(); - - officeObject.agency_code = agencyObject; - officeObject.cos_geo_org_unit = officer.geo_organization_unit_code; - officeObject.create_user_id = officer.create_user_id; - officeObject.create_utc_timestamp = officer.create_utc_timestamp; - officeObject.update_user_id = officer.update_user_id; - officeObject.update_utc_timestamp = officer.update_utc_timestamp; - - officeObject = await this.officeService.create(officeObject); - officer.office_guid = officeObject.office_guid; - } else { - // use the existing one - officer.office_guid = officeObject[0].office_guid; - } - //Will always insert the person - personObject = await this.personService.createInTransaction(officer, queryRunner); + personObject = await this.personService.createInTransaction( + (officer.person_guid), + queryRunner, + ); officer.person_guid = personObject.person_guid; - newOfficerString = await this.officerRepository.create(officer); - await queryRunner.manager.save(newOfficerString); + newOfficerObject = this.officerRepository.create((officer)); + await queryRunner.manager.save(newOfficerObject); + + //Create team + if (officer.team_code) { + const teamGuid = await this.teamService.findByTeamCodeAndAgencyCode(officer.team_code, "EPO"); + const teamEntity = { + officer_guid: newOfficerObject.officer_guid, + team_guid: teamGuid, + active_ind: true, + create_user_id: officer.create_user_id, + update_user_id: officer.update_user_id, + }; + teamObject = await this.officerTeamXrefService.createInTransaction( + teamEntity, + queryRunner, + ); + } await queryRunner.commitTransaction(); + + //Create roles + await this.cssService.updateUserRole(officer.roles.user_idir, officer.roles.user_roles); } catch (err) { this.logger.error(err); + //rollback all transactions await queryRunner.rollbackTransaction(); - newOfficerString = "Error Occured"; + newOfficerObject = null; + //remove all css roles + for await (const roleItem of officer.roles.user_roles) { + await this.cssService.deleteUserRole(officer.roles.user_idir, roleItem.name); + } } finally { await queryRunner.release(); } - return newOfficerString; + return newOfficerObject; } async update(officer_guid: UUID, updateOfficerDto: UpdateOfficerDto): Promise { + const userRoles = updateOfficerDto.user_roles; //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); + + try { + await this.officerRepository.update({ officer_guid }, updateOfficerDto); + + //remove all roles if deactivate_ind is true + if (updateOfficerDto.deactivate_ind === true) { + const officerIdirUsername = `${updateOfficerDto.auth_user_guid.split("-").join("")}@idir`; + for await (const roleItem of userRoles) { + await this.cssService.deleteUserRole(officerIdirUsername, roleItem); + } + } + return this.findOne(officer_guid); + } catch (e) { + this.logger.error(e); + } } /** diff --git a/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts b/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts index 70b9261a6..8404e8e22 100644 --- a/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts +++ b/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts @@ -2,7 +2,6 @@ import { PickType } from "@nestjs/swagger"; import { OfficerTeamXrefDto } from "./officer_team_xref.dto"; export class CreateOfficerTeamXrefDto extends PickType(OfficerTeamXrefDto, [ - "officer_team_xref_guid", "officer_guid", "team_guid", "create_user_id", diff --git a/backend/src/v1/officer_team_xref/officer_team_xref.service.ts b/backend/src/v1/officer_team_xref/officer_team_xref.service.ts index 910656673..c83557013 100644 --- a/backend/src/v1/officer_team_xref/officer_team_xref.service.ts +++ b/backend/src/v1/officer_team_xref/officer_team_xref.service.ts @@ -1,23 +1,45 @@ -import { Inject, Injectable, Logger } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { CreateOfficerTeamXrefDto } from "./dto/create-officer_team_xref.dto"; import { UpdateOfficerTeamXrefDto } from "./dto/update-officer_team_xref.dto"; import { OfficerTeamXref } from "./entities/officer_team_xref.entity"; import { DataSource, QueryRunner, Repository } from "typeorm"; import { InjectRepository } from "@nestjs/typeorm"; -import { REQUEST } from "@nestjs/core"; @Injectable() export class OfficerTeamXrefService { private readonly logger = new Logger(OfficerTeamXrefService.name); @InjectRepository(OfficerTeamXref) - private officerTeamXrefRepository: Repository; + private readonly officerTeamXrefRepository: Repository; - constructor(@Inject(REQUEST) private request: Request, private dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) {} - async create(queryRunner: QueryRunner, createOfficerTeamXrefDto: CreateOfficerTeamXrefDto) { - const createdValue = await this.officerTeamXrefRepository.create(createOfficerTeamXrefDto); - queryRunner.manager.save(createdValue); - return createdValue; + async create(newOfficerTeamXref: CreateOfficerTeamXrefDto): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + let result; + try { + result = this.officerTeamXrefRepository.create(newOfficerTeamXref); + await queryRunner.manager.save(result); + await queryRunner.commitTransaction(); + } catch (err) { + this.logger.error(err); + await queryRunner.rollbackTransaction(); + result = null; + } finally { + await queryRunner.release(); + } + return result; + } + + async createInTransaction( + officerTeamXref: CreateOfficerTeamXrefDto, + queryRunner: QueryRunner, + ): Promise { + const newTeam = this.officerTeamXrefRepository.create(officerTeamXref); + await queryRunner.manager.save(newTeam); + return newTeam; } async findAll(): Promise { diff --git a/backend/src/v1/team/team.module.ts b/backend/src/v1/team/team.module.ts index e53b179f3..85450b5b1 100644 --- a/backend/src/v1/team/team.module.ts +++ b/backend/src/v1/team/team.module.ts @@ -5,9 +5,15 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Team } from "./entities/team.entity"; import { CssModule } from "../../external_api/css/css.module"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { Officer } from "../officer/entities/officer.entity"; @Module({ - imports: [TypeOrmModule.forFeature([Team]), TypeOrmModule.forFeature([OfficerTeamXref]), CssModule], + imports: [ + TypeOrmModule.forFeature([Team]), + TypeOrmModule.forFeature([OfficerTeamXref]), + CssModule, + TypeOrmModule.forFeature([Officer]), + ], controllers: [TeamController], providers: [TeamService], exports: [TeamService], diff --git a/backend/src/v1/team/team.service.ts b/backend/src/v1/team/team.service.ts index df68aa68e..219c26178 100644 --- a/backend/src/v1/team/team.service.ts +++ b/backend/src/v1/team/team.service.ts @@ -1,20 +1,26 @@ import { Inject, Injectable, Logger } from "@nestjs/common"; import { Team } from "./entities/team.entity"; -import { Repository } from "typeorm"; +import { DataSource, Repository } from "typeorm"; import { InjectRepository } from "@nestjs/typeorm"; import { CssService } from "../../external_api/css/css.service"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; import { Role } from "../../enum/role.enum"; import { TeamUpdate } from "src/types/models/general/team-update"; +import { Officer } from "src/v1/officer/entities/officer.entity"; @Injectable() export class TeamService { private readonly logger = new Logger(TeamService.name); + constructor(private readonly dataSource: DataSource) {} + @InjectRepository(Team) - private teamRepository: Repository; + private readonly teamRepository: Repository; @InjectRepository(OfficerTeamXref) - private officerTeamXrefRepository: Repository; + private readonly officerTeamXrefRepository: Repository; + @InjectRepository(Officer) + private readonly officerRepository: Repository; + @Inject(CssService) private readonly cssService: CssService; @@ -88,6 +94,9 @@ export class TeamService { } } else { const teamGuid = await this.findByTeamCodeAndAgencyCode(teamCode, agencyCode); + //set user's office_guid to null because CEEB user doesn't have an office + await this.officerRepository.update({ officer_guid: officerGuid }, { office_guid: null }); + if (officerTeamXref) { const updateEnity = { team_guid: teamGuid, @@ -107,7 +116,7 @@ export class TeamService { create_user_id: adminIdirUsername, update_user_id: adminIdirUsername, }; - const newRecord = await this.officerTeamXrefRepository.create(newEntity); + const newRecord = this.officerTeamXrefRepository.create(newEntity); const saveResult = await this.officerTeamXrefRepository.save(newRecord); if (saveResult.officer_guid) { result.team = true; @@ -119,8 +128,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); - //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) { + //Remove existing roles that do not match with updated roles, but still keeps Admin role + if (roleItem.name !== Role.TEMPORARY_TEST_ADMIN && !rolesMatchWithUpdate) { await this.cssService.deleteUserRole(userIdir, roleItem.name); } } diff --git a/frontend/src/app/common/validation-multiselect.tsx b/frontend/src/app/common/validation-multiselect.tsx index 1b4714289..b1106ad90 100644 --- a/frontend/src/app/common/validation-multiselect.tsx +++ b/frontend/src/app/common/validation-multiselect.tsx @@ -12,6 +12,7 @@ interface ValidationMultiSelectProps { onChange: Function; errMsg: string; values?: Option[]; + isDisabled?: boolean; } export const ValidationMultiSelect: FC = ({ @@ -24,6 +25,7 @@ export const ValidationMultiSelect: FC = ({ onChange, errMsg, values, + isDisabled = false, }) => { const errClass = errMsg === "" ? "" : "error-message"; const calulatedClass = errMsg === "" ? "" : "error-border"; @@ -42,6 +44,7 @@ export const ValidationMultiSelect: FC = ({ defaultValue={defaultValue} value={values} isMulti + isDisabled={isDisabled} />
{errMsg}
diff --git a/frontend/src/app/components/containers/admin/user-management/add-user-search.tsx b/frontend/src/app/components/containers/admin/user-management/add-user-search.tsx index b5bbc6ac1..a59484daa 100644 --- a/frontend/src/app/components/containers/admin/user-management/add-user-search.tsx +++ b/frontend/src/app/components/containers/admin/user-management/add-user-search.tsx @@ -1,20 +1,11 @@ -import { Dispatch, FC, SetStateAction, useEffect, useState } from "react"; +import { Dispatch, FC, SetStateAction, useState } from "react"; import { Button } from "react-bootstrap"; -import { useAppDispatch, useAppSelector } from "@hooks/hooks"; -import { assignOfficerToOffice, selectOfficersDropdown } from "@store/reducers/officer"; -import { CompSelect } from "@components/common/comp-select"; +import { useAppDispatch } from "@hooks/hooks"; import Option from "@apptypes/app/option"; -import { fetchOfficeAssignments, selectOfficesForAssignmentDropdown, selectOffices } from "@store/reducers/office"; -import { ToastContainer } from "react-toastify"; -import { ToggleError, ToggleSuccess } from "@common/toast"; -import { clearNotification, selectNotification } from "@store/reducers/app"; -import { selectAgencyDropdown, selectTeamDropdown } from "@store/reducers/code-table"; -import { ROLE_OPTIONS } from "@constants/ceeb-roles"; -import { generateApiParameters, get, patch } from "@common/api"; +import { generateApiParameters, get } from "@common/api"; import config from "@/config"; -import { Officer } from "@apptypes/person/person"; -import { UUID } from "crypto"; -import { ValidationMultiSelect } from "@common/validation-multiselect"; +import { CssUser, Officer } from "@apptypes/person/person"; +import { CompInput } from "@/app/components/common/comp-input"; import "@assets/sass/user-management.scss"; interface AddUserSearchProps { @@ -31,6 +22,9 @@ interface AddUserSearchProps { handleAddNewUserDetails: () => void; handleCancel: () => void; handleAddNewUser: () => void; + goToEditView: () => void; + setIsInAddUserView: Dispatch>; + setNewUser: Dispatch>; } export const AddUserSearch: FC = ({ @@ -47,11 +41,45 @@ export const AddUserSearch: FC = ({ officerGuid, handleAddNewUserDetails, handleCancel, + goToEditView, + setIsInAddUserView, + setNewUser, }) => { - const handleOfficerChange = async (input: any) => { - setOfficerError(""); - if (input.value) { - setOfficer(input); + const UserStatus = { + notInKeyCloak: 0, + inNatCom: 1, + }; + + const dispatch = useAppDispatch(); + const [emailInput, setEmailInput] = useState(""); + const [userStatus, setUserStatus] = useState(); + + const getCssUserDetails = async (email: string): Promise => { + const parameters = generateApiParameters(`${config.API_BASE_URL}/v1/officer/find-by-email/${email}`); + const response = await get(dispatch, parameters); + return response; + }; + + const handleEmailChange = (input: any) => { + setEmailInput(input.trim()); + }; + + const handleSearch = async () => { + if (emailInput !== "") { + const userDetails = await getCssUserDetails(emailInput); + if (userDetails) { + if ((userDetails as Officer).officer_guid) { + setOfficer({ value: (userDetails as Officer).person_guid.person_guid, label: "" }); + setUserStatus(UserStatus.inNatCom); + } else if ((userDetails as CssUser).username) { + setNewUser(userDetails as CssUser); + setOfficer({ value: undefined, label: "" }); + setIsInAddUserView(true); + goToEditView(); + } + } else { + setUserStatus(UserStatus.notInKeyCloak); + } } }; @@ -66,20 +94,17 @@ export const AddUserSearch: FC = ({
-
Select User
+
Search user
- handleOfficerChange(evt)} - classNames={{ - menu: () => "top-layer-select", - }} - options={officers} - placeholder="Select" - enableValidation={true} - value={officer} - errorMessage={officerError} + handleEmailChange(evt.target.value)} />
@@ -94,11 +119,39 @@ export const AddUserSearch: FC = ({  
+ + {userStatus === UserStatus.inNatCom && ( + + )} + + {userStatus === UserStatus.notInKeyCloak && ( +
+

There are no results using your search criteria. The user might not exist in KeyCloak.

+

+ Click this link to add the user in KeyCloak first:{" "} + Common Hosted Single Sign-on (CSS) +

+
+ )} diff --git a/frontend/src/app/components/containers/admin/user-management/edit-user.tsx b/frontend/src/app/components/containers/admin/user-management/edit-user.tsx index bcfc1e587..43c3cbe78 100644 --- a/frontend/src/app/components/containers/admin/user-management/edit-user.tsx +++ b/frontend/src/app/components/containers/admin/user-management/edit-user.tsx @@ -1,12 +1,12 @@ import { FC, useEffect, useState } from "react"; import { Button } from "react-bootstrap"; import { useAppDispatch, useAppSelector } from "@hooks/hooks"; -import { assignOfficerToOffice, selectOfficerByPersonGuid } from "@store/reducers/officer"; +import { assignOfficerToOffice, createOfficer, getOfficers, selectOfficerByPersonGuid } from "@store/reducers/officer"; import { CompSelect } from "@components/common/comp-select"; import Option from "@apptypes/app/option"; import { fetchOfficeAssignments, selectOfficesForAssignmentDropdown, selectOffices } from "@store/reducers/office"; import { ToggleError, ToggleSuccess } from "@common/toast"; -import { clearNotification, selectNotification, userId } from "@store/reducers/app"; +import { clearNotification, openModal, selectNotification, userId } from "@store/reducers/app"; import { selectAgencyDropdown, selectTeamDropdown } from "@store/reducers/code-table"; import { CEEB_ROLE_OPTIONS, COS_ROLE_OPTIONS, ROLE_OPTIONS } from "@constants/ceeb-roles"; import { generateApiParameters, get, patch } from "@common/api"; @@ -14,15 +14,18 @@ import config from "@/config"; import { ValidationMultiSelect } from "@common/validation-multiselect"; import "@assets/sass/user-management.scss"; import { AgencyType } from "@/app/types/app/agency-types"; +import { CssUser, NewOfficer } from "@/app/types/person/person"; +import { TOGGLE_DEACTIVATE } from "@/app/types/modal/modal-types"; interface EditUserProps { officer: Option; isInAddUserView: boolean; + newUser: CssUser | null; handleCancel: () => void; goToSearchView: () => void; } -export const EditUser: FC = ({ officer, isInAddUserView, handleCancel, goToSearchView }) => { +export const EditUser: FC = ({ officer, isInAddUserView, newUser, handleCancel, goToSearchView }) => { const dispatch = useAppDispatch(); const officerData = useAppSelector(selectOfficerByPersonGuid(officer.value)); const officeAssignments = useAppSelector(selectOfficesForAssignmentDropdown); @@ -35,6 +38,10 @@ export const EditUser: FC = ({ officer, isInAddUserView, handleCa const [officerError, setOfficerError] = useState(""); const [officeError, setOfficeError] = useState(""); + const [lastName, setLastName] = useState(""); + const [firstName, setFirstName] = useState(""); + const [idir, setIdir] = useState(""); + const [email, setEmail] = useState(""); const [selectedAgency, setSelectedAgency] = useState