diff --git a/AdminWebsite/AdminWebsite.IntegrationTests/packages.lock.json b/AdminWebsite/AdminWebsite.IntegrationTests/packages.lock.json index b9577ac4d..fe922575b 100644 --- a/AdminWebsite/AdminWebsite.IntegrationTests/packages.lock.json +++ b/AdminWebsite/AdminWebsite.IntegrationTests/packages.lock.json @@ -102,8 +102,8 @@ }, "BookingsApi.Client": { "type": "Transitive", - "resolved": "1.47.15", - "contentHash": "sd5lILWV3aiku/U/tFu16n7Fi9wAELGP0rVpLhaDFq3YzwxEL8JshbN0BUin3F/QZM3T8+DsnMKbMDcU449cqA==", + "resolved": "1.49.10", + "contentHash": "oXH1qyRZhkrbVbdZB7QsO8O0a/6SRCrB71Hb0leiRNUGr/xb0IYdqpaTr6imTeo6zimGxieMiu9uKMsnFpVA/w==", "dependencies": { "Microsoft.AspNetCore.Mvc.Core": "2.2.5" } @@ -2101,7 +2101,7 @@ "type": "Project", "dependencies": { "AspNetCore.HealthChecks.Uris": "[6.0.3, )", - "BookingsApi.Client": "[1.47.15, )", + "BookingsApi.Client": "[1.49.10, )", "FluentValidation.AspNetCore": "[10.4.0, )", "LaunchDarkly.ServerSdk": "[7.0.3, )", "MicroElements.Swashbuckle.FluentValidation": "[5.7.0, )", diff --git a/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/BookNewHearingTests.cs b/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/BookNewHearingTests.cs index ca32fde90..82129e0b5 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/BookNewHearingTests.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/BookNewHearingTests.cs @@ -242,10 +242,10 @@ public async Task Should_book_hearing_for_multi_day() } [Test] - public async Task Should_book_hearing_with_reference_data_flag_on() + public async Task Should_book_hearing_with_use_v2_api_flag_on() { // Arrange - _mocker.Mock().Setup(x => x.ReferenceDataToggle()).Returns(true); + _mocker.Mock().Setup(x => x.UseV2Api()).Returns(true); var bookingDetails = InitHearingForV2Test(); var bookingRequest = new BookHearingRequest @@ -259,7 +259,6 @@ public async Task Should_book_hearing_with_reference_data_flag_on() .WithParticipant("Individual", "fname2.lname2@hmcts.net") .WithParticipant("Individual", "fname3.lname3@hmcts.net") .WithParticipant("Judicial Office Holder", "fname4.lname4@hmcts.net") - .WithParticipant("Staff Member","staff.member@hmcts.net") .WithParticipant("Judge", "judge.fudge@hmcts.net"); _mocker.Mock().Setup(x => x.BookNewHearingWithCodeAsync(It.IsAny())) .ReturnsAsync(hearingDetailsResponse); @@ -415,65 +414,76 @@ private BookingDetailsRequest InitHearingForV2Test() { Participants = new List { - new () + new() { ContactEmail = "contact1@hmcts.net", HearingRoleCode = "APPL", DisplayName = "display name1", FirstName = "fname", MiddleNames = "", LastName = "lname1", Username = "username1@hmcts.net", OrganisationName = "", Representee = "", TelephoneNumber = "" }, - new () + new() { ContactEmail = "contact2@hmcts.net", HearingRoleCode = "APPL", DisplayName = "display name2", FirstName = "fname2", MiddleNames = "", LastName = "lname2", OrganisationName = "", Representee = "", TelephoneNumber = "", Username = "username2@hmcts.net" }, - new () + new() { ContactEmail = "contact3@hmcts.net", HearingRoleCode = "APPL", DisplayName = "display name3", FirstName = "fname3", MiddleNames = "", LastName = "lname3", OrganisationName = "", Representee = "", TelephoneNumber = "", Username = "username3@hmcts.net" }, - new () + new() { ContactEmail = "contact4@hmcts.net", HearingRoleCode = "PANL", DisplayName = "display name4", FirstName = "fname4", MiddleNames = "", LastName = "lname4", OrganisationName = "", Representee = "", TelephoneNumber = "", Username = "username4@hmcts.net" }, - new () + new() { ContactEmail = "contact5@hmcts.net", HearingRoleCode = "INTP", DisplayName = "display name2", FirstName = "fname5", MiddleNames = "", LastName = "lname5", OrganisationName = "", Representee = "", TelephoneNumber = "", Username = "username5@hmcts.net" + } + }, + JudiciaryParticipants = new List() + { + new() + { + DisplayName = "display name4", PersonalCode = "12345678", Role = "PanelMember" }, - new () + new() { - ContactEmail = "judge@hmcts.net", - HearingRoleCode = "JUDG", DisplayName = "Judge Fudge", - FirstName = "Jack", MiddleNames = "", LastName = "Fudge", - Username = "judge.fudge@hmcts.net", OrganisationName = "", Representee = "", - TelephoneNumber = "" + DisplayName = "Judge Fudge", PersonalCode = "12345678", Role = "Judge" } }, Endpoints = new List { - new () + new() {DisplayName = "displayname1", DefenceAdvocateContactEmail = "username1@hmcts.net"}, - new () + new() {DisplayName = "displayname2", DefenceAdvocateContactEmail = "fname2.lname2@hmcts.net"}, }, LinkedParticipants = new List { - new () { ParticipantContactEmail = "contact1@hmcts.net", LinkedParticipantContactEmail = "contact5@hmcts.net", Type = LinkedParticipantType.Interpreter }, - new () { ParticipantContactEmail = "contact5@hmcts.net", LinkedParticipantContactEmail = "contact1@hmcts.net", Type = LinkedParticipantType.Interpreter } + new() + { + ParticipantContactEmail = "contact1@hmcts.net", + LinkedParticipantContactEmail = "contact5@hmcts.net", Type = LinkedParticipantType.Interpreter + }, + new() + { + ParticipantContactEmail = "contact5@hmcts.net", + LinkedParticipantContactEmail = "contact1@hmcts.net", Type = LinkedParticipantType.Interpreter + } }, Cases = new List { - new () + new() { Name = "Case1", Number = "001", IsLeadCase = true } diff --git a/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/EditHearingTests.cs b/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/EditHearingTests.cs index ab5e6306d..5f05ac59d 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/EditHearingTests.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/EditHearingTests.cs @@ -21,6 +21,7 @@ using AdminWebsite.UnitTests.Helper; using BookingsApi.Client; using BookingsApi.Contract.V1.Requests; +using BookingsApi.Contract.V1.Requests.Enums; using BookingsApi.Contract.V1.Responses; using BookingsApi.Contract.V2.Enums; using BookingsApi.Contract.V2.Requests; @@ -32,6 +33,7 @@ using BookingStatus = BookingsApi.Contract.V1.Enums.BookingStatus; using CaseResponse = BookingsApi.Contract.V1.Responses.CaseResponse; using EndpointResponse = BookingsApi.Contract.V1.Responses.EndpointResponse; +using JudiciaryParticipantRequest = AdminWebsite.Contracts.Requests.JudiciaryParticipantRequest; using LinkedParticipantResponse = BookingsApi.Contract.V1.Responses.LinkedParticipantResponse; using LinkedParticipantType = BookingsApi.Contract.V1.Enums.LinkedParticipantType; @@ -358,6 +360,11 @@ public void Setup() Pin = "pin", Sip = "sip" } + }, + JudiciaryParticipants = new List() + { + new (){FullName = "Judge Fudge", FirstName = "John", LastName = "Doe", HearingRoleCode = JudiciaryParticipantHearingRoleCode.Judge, PersonalCode = "1234"}, + new (){FullName = "Jane Doe", FirstName = "Jane", LastName = "Doe", HearingRoleCode = JudiciaryParticipantHearingRoleCode.PanelMember, PersonalCode = "4567"} } }; } @@ -570,7 +577,7 @@ public async Task Should_return_updated_hearing() [Test] public async Task Should_return_updated_hearingV2() { - _featureToggle.Setup(e => e.ReferenceDataToggle()).Returns(true); + _featureToggle.Setup(e => e.UseV2Api()).Returns(true); var updatedHearing = _v2HearingDetailsResponse; _bookingsApiClient.SetupSequence(x => x.GetHearingDetailsByIdV2Async(It.IsAny())) .ReturnsAsync(updatedHearing) @@ -605,8 +612,19 @@ public async Task Should_return_updated_hearingV2() LinkedParticipantContactEmail = "interpreter@domain.net", Type = AdminWebsite.Contracts.Enums.LinkedParticipantType.Interpreter } - } + }, }); + _addNewParticipantRequest.JudiciaryParticipants = new List() + { + new() + { + PersonalCode = "4567", DisplayName = "Jane Doe 2", Role = JudiciaryParticipantHearingRoleCode.PanelMember.ToString() + }, + new() + { + PersonalCode = "5678", DisplayName = "New Judge Fudge", Role = JudiciaryParticipantHearingRoleCode.Judge.ToString() + } + }; var result = await _controller.EditHearing(_validId, _addNewParticipantRequest); var hearing = (AdminWebsite.Contracts.Responses.HearingDetailsResponse)((OkObjectResult)result.Result).Value; hearing.Id.Should().Be(updatedHearing.Id); @@ -614,6 +632,16 @@ public async Task Should_return_updated_hearingV2() It.Is(u => !u.Cases.IsNullOrEmpty())), Times.Once); + + _bookingsApiClient.Verify(x => x.RemoveJudiciaryParticipantFromHearingAsync(hearing.Id, "1234"), + Times.Once); + + _bookingsApiClient.Verify(x => x.UpdateJudiciaryParticipantAsync(hearing.Id, "4567", It.IsAny()), + Times.Once); + + _bookingsApiClient.Verify( + x => x.AddJudiciaryParticipantsToHearingAsync(hearing.Id, + It.IsAny>()), Times.Once); } [Test] diff --git a/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/GetHearingTests.cs b/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/GetHearingTests.cs index f9fcc58ec..36ede45dd 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/GetHearingTests.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Controllers/HearingsController/GetHearingTests.cs @@ -174,7 +174,7 @@ public async Task Should_return_ok_status_if_hearing_id_is_valid() public async Task Should_return_ok_status_if_hearing_id_is_validV2() { // Arrange - _featureToggle.Setup(e => e.ReferenceDataToggle()).Returns(true); + _featureToggle.Setup(e => e.UseV2Api()).Returns(true); _mocker.Mock().Setup(x => x.GetHearingDetailsByIdV2Async(It.IsAny())).ReturnsAsync(_vhExistingHearingV2); diff --git a/AdminWebsite/AdminWebsite.UnitTests/Controllers/JudiciaryAccountsControllerTest.cs b/AdminWebsite/AdminWebsite.UnitTests/Controllers/JudiciaryAccountsControllerTest.cs index b629e34af..cb0d776e3 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Controllers/JudiciaryAccountsControllerTest.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Controllers/JudiciaryAccountsControllerTest.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; -using System; using System.Collections.Generic; using System.Net; using System.Text.Encodings.Web; @@ -15,7 +14,6 @@ using BookingsApi.Client; using BookingsApi.Contract.V1.Requests; using BookingsApi.Contract.V1.Responses; -using System.Linq; using UserApi.Contract.Responses; namespace AdminWebsite.UnitTests.Controllers @@ -25,7 +23,7 @@ public class JudiciaryAccountsControllerTest private AdminWebsite.Controllers.JudiciaryAccountsController _controller; private Mock _bookingsApiClient; private Mock _userAccountService; - private List _judiciaryResponse; + private List _judiciaryResponse; private List _userResponses; [SetUp] @@ -41,64 +39,58 @@ public void Setup() _controller = new AdminWebsite.Controllers.JudiciaryAccountsController(_userAccountService.Object, JavaScriptEncoder.Default, _bookingsApiClient.Object, Options.Create(testSettings)); - _judiciaryResponse = new List + _judiciaryResponse = new List { - new PersonResponse + new() { - Id = Guid.NewGuid(), - ContactEmail = "", - FirstName = "Adam", - LastName = "Mann", - TelephoneNumber ="", - Title = "Ms", - MiddleNames = "No", - Username = "adoman@hmcts.net" + FirstName = "Adam", + LastName = "Mann", + Title = "Ms", + Email = "adoman@hmcts.net" } }; - + _userResponses = new List(); } [Test] public async Task Should_return_request_if_match_to_search_term() { - _judiciaryResponse.Add(new PersonResponse + _judiciaryResponse = new List { - Id = Guid.NewGuid(), - ContactEmail = "", - FirstName = "Jack", - LastName = "Mann", - TelephoneNumber = "", - Title = "Mr", - MiddleNames = "No", - Username = "jackman@judiciary.net" - }); + new() + { + FirstName = "Jack", + LastName = "Mann", + Title = "Mr", + Email = "jackman@judiciary.net" + } + }; _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) .ReturnsAsync(_judiciaryResponse); var searchTerm = "ado"; - var result = await _controller.PostJudiciaryPersonBySearchTermAsync(searchTerm); + var result = await _controller.SearchForJudiciaryPersonAsync(searchTerm); var okRequestResult = (OkObjectResult)result.Result; okRequestResult.StatusCode.Should().NotBeNull(); - var personRespList = (List)okRequestResult.Value; + var personRespList = (List)okRequestResult.Value; personRespList.Count.Should().Be(1); - personRespList[0].Username.Should().Be(_judiciaryResponse[1].Username); + personRespList[0].Email.Should().Be(_judiciaryResponse[0].Email); } [Test] public async Task Should_filter_out_judges_found_in_both_aad_and_database() { - _judiciaryResponse.Add(new PersonResponse + _judiciaryResponse.Add(new JudiciaryPersonResponse { - Id = Guid.NewGuid(), - ContactEmail = "", + Email = "jackman@judiciary.net", FirstName = "Jack", LastName = "Mann", - TelephoneNumber = "", + WorkPhone = "", Title = "Mr", - MiddleNames = "No", - Username = "jackman@judiciary.net" + FullName = "Mr Jack Mann", + PersonalCode = "12345678" }); _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) .ReturnsAsync(_judiciaryResponse); @@ -117,7 +109,7 @@ public async Task Should_filter_out_judges_found_in_both_aad_and_database() okRequestResult.StatusCode.Should().NotBeNull(); var personResponses = (List)okRequestResult.Value; personResponses.Count.Should().Be(1); - personResponses[0].Username.Should().Be(_judiciaryResponse[1].Username); + personResponses[0].ContactEmail.Should().Be(_judiciaryResponse[1].Email); } [Test] @@ -126,7 +118,7 @@ public async Task Should_pass_on_bad_request_from_bookings_api() _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.BadRequest)); - var response = await _controller.PostJudiciaryPersonBySearchTermAsync("term"); + var response = await _controller.SearchForJudiciaryPersonAsync("term"); response.Result.Should().BeOfType(); } @@ -135,7 +127,7 @@ public void Should_pass_on_exception_request_from_bookings_api() { _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.InternalServerError)); - Assert.ThrowsAsync(() => _controller.PostJudiciaryPersonBySearchTermAsync("term")); + Assert.ThrowsAsync(() => _controller.SearchForJudiciaryPersonAsync("term")); } [Test] @@ -145,37 +137,40 @@ public void Should_pass_on_exception_request_from_bookings_api() [TestCase(true, true)] public async Task Should_return_judiciary_and_courtroom_accounts_if_match_to_search_term(bool withJudiciary, bool withCourtroom) { - _judiciaryResponse.Add(new PersonResponse + _judiciaryResponse = new List { - Id = Guid.NewGuid(), - ContactEmail = "", - FirstName = "Jack", - LastName = "Mann", - TelephoneNumber = "", - Title = "Mr", - MiddleNames = "No", - Username = "jackman@judiciary.net" - }); + new() + { + Email = "jackman@judiciary.net", + FirstName = "Jack", + LastName = "Mann", + WorkPhone = "", + Title = "Mr", + FullName = "Mr Jack Mann", + PersonalCode = "12345678" + } + }; + _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) - .ReturnsAsync(withJudiciary ? _judiciaryResponse : new List()); + .ReturnsAsync(withJudiciary ? _judiciaryResponse : new List()); - var _courtRoomResponse = new List + var _courtRoomResponse = new List { - new JudgeResponse + new() { FirstName = "FirstName1", LastName = "FirstName2", Email = "judge.1@judiciary.net", ContactEmail = "judge1@personal.com" }, - new JudgeResponse + new() { FirstName = "FirstName3", LastName = "LastName3", Email = "judge.3@judiciary.net", ContactEmail = "judge3@personal.com" }, - new JudgeResponse + new() { FirstName = "FirstName2", LastName = "LastName2", @@ -184,16 +179,16 @@ public async Task Should_return_judiciary_and_courtroom_accounts_if_match_to_sea } }; - _userAccountService.Setup(x => x.SearchJudgesByEmail(It.IsAny())) - .ReturnsAsync(withCourtroom ? _courtRoomResponse : new List()); + _userAccountService.Setup(x => x.SearchEjudiciaryJudgesByEmailUserResponse(It.IsAny())) + .ReturnsAsync(withCourtroom ? _courtRoomResponse : new List()); var searchTerm = "judici"; - var result = await _controller.PostJudgesBySearchTermAsync(searchTerm); + var result = await _controller.PostJudiciaryPersonBySearchTermAsync(searchTerm); var okRequestResult = (OkObjectResult)result.Result; okRequestResult.StatusCode.Should().NotBeNull(); - var personRespList = (List)okRequestResult.Value; + var personRespList = (List)okRequestResult.Value; var expectedJudiciaryCount = withJudiciary ? _judiciaryResponse.Count : 0; var expectedCourtRoomCount = withCourtroom ? _courtRoomResponse.Count : 0; @@ -201,12 +196,7 @@ public async Task Should_return_judiciary_and_courtroom_accounts_if_match_to_sea var expectedTotal = expectedJudiciaryCount + expectedCourtRoomCount; personRespList.Count.Should().Be(expectedTotal); - if(!withJudiciary && withCourtroom) // Only courtroom is set up to test order - { - Assert.That(personRespList, Is.EquivalentTo(_courtRoomResponse)); - Assert.That(personRespList, Is.Not.EqualTo(_courtRoomResponse)); - Assert.That(personRespList, Is.EqualTo(_courtRoomResponse.OrderBy(x => x.Email))); - } + personRespList.Should().BeInAscendingOrder(x => x.ContactEmail); } [Test] @@ -214,8 +204,8 @@ public async Task Should_pass_on_bad_request_from_bookings_api_for_judge_account { _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.BadRequest)); - - var response = await _controller.PostJudgesBySearchTermAsync("term"); + + var response = await _controller.PostJudiciaryPersonBySearchTermAsync("term"); response.Result.Should().BeOfType(); } @@ -224,7 +214,114 @@ public void Should_pass_on_exception_request_from_bookings_api_for_judges_accoun { _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.InternalServerError)); - Assert.ThrowsAsync(() => _controller.PostJudgesBySearchTermAsync("term")); + Assert.ThrowsAsync(() => _controller.PostJudiciaryPersonBySearchTermAsync("term")); + } + + [Test] + public async Task Should_return_judiciary_person_list_by_email_search_term() + { + // Arrange + var term = "test"; + var encodedTerm = JavaScriptEncoder.Default.Encode(term); + var expectedJudiciaryPersonResponse = new List + { + new JudiciaryPersonResponse + { + FirstName = "John", + LastName = "Doe", + Email = "johndoe@hmcts.net" + } + }; + _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) + .ReturnsAsync(expectedJudiciaryPersonResponse); + _controller = new AdminWebsite.Controllers.JudiciaryAccountsController(_userAccountService.Object, + JavaScriptEncoder.Default, _bookingsApiClient.Object, Options.Create(new TestUserSecrets())); + + // Act + var result = await _controller.SearchForJudiciaryPersonAsync(term); + + // Assert + result.Result.Should().BeOfType(); + var okRequestResult = (OkObjectResult)result.Result; + okRequestResult.StatusCode.Should().NotBeNull(); + var personRespList = (List)okRequestResult.Value; + personRespList.Count.Should().Be(1); + personRespList[0].Email.Should().Be(expectedJudiciaryPersonResponse[0].Email); + _bookingsApiClient.Verify(x => x.PostJudiciaryPersonBySearchTermAsync(It.Is(y => y.Term == encodedTerm)), Times.Once); + } + + [Test] + public async Task Should_return_judiciary_person_list_by_email_search_term_and_filter_out_judges_found_in_both_aad_and_database() + { + // Arrange + var term = "test"; + var encodedTerm = JavaScriptEncoder.Default.Encode(term); + var expectedJudiciaryPersonResponse = new List + { + new() + { + FirstName = "John", + LastName = "Doe", + Email = "johndoe@hmcts.net" + } + }; + _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) + .ReturnsAsync(expectedJudiciaryPersonResponse); + _userResponses = new List() + { + new() + { + ContactEmail = "johndoe@hmcts.net", + } + }; + _userAccountService.Setup(x => x.SearchEjudiciaryJudgesByEmailUserResponse(It.IsAny())) + .ReturnsAsync(_userResponses); + _controller = new AdminWebsite.Controllers.JudiciaryAccountsController(_userAccountService.Object, + JavaScriptEncoder.Default, _bookingsApiClient.Object, Options.Create(new TestUserSecrets(){TestUsernameStem = "@hmcts.net"})); + + // Act + var result = await _controller.PostJudiciaryPersonBySearchTermAsync(term); + + // Assert + result.Result.Should().BeOfType(); + var okRequestResult = (OkObjectResult)result.Result; + okRequestResult.StatusCode.Should().NotBeNull(); + var personResponses = (List)okRequestResult.Value; + personResponses.Count.Should().Be(1); + _bookingsApiClient.Verify(x => x.PostJudiciaryPersonBySearchTermAsync(It.Is(y => y.Term == encodedTerm)), Times.Once); + } + + [Test] + public async Task Should_pass_on_bad_request_from_bookings_api_for_judiciary_person_list_by_email_search_term() + { + // Arrange + var term = "test"; + _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) + .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.BadRequest)); + _controller = new AdminWebsite.Controllers.JudiciaryAccountsController(_userAccountService.Object, + JavaScriptEncoder.Default, _bookingsApiClient.Object, Options.Create(new TestUserSecrets(){TestUsernameStem = "@hmcts.net"})); + + // Act + var response = await _controller.PostJudiciaryPersonBySearchTermAsync(term); + + // Assert + response.Result.Should().BeOfType(); + _bookingsApiClient.Verify(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny()), Times.Once); + } + + [Test] + public void Should_pass_on_exception_request_from_bookings_api_for_judiciary_person_list_by_email_search_term() + { + // Arrange + var term = "test"; + _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) + .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.InternalServerError)); + _controller = new AdminWebsite.Controllers.JudiciaryAccountsController(_userAccountService.Object, + JavaScriptEncoder.Default, _bookingsApiClient.Object, Options.Create(new TestUserSecrets(){TestUsernameStem = "@hmcts.net"})); + + // Act & Assert + Assert.ThrowsAsync(() => _controller.PostJudiciaryPersonBySearchTermAsync(term)); + _bookingsApiClient.Verify(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny()), Times.Once); } } } \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite.UnitTests/Controllers/PersonsControllerTest.cs b/AdminWebsite/AdminWebsite.UnitTests/Controllers/PersonsControllerTest.cs index fb1761506..c501eb8a5 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Controllers/PersonsControllerTest.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Controllers/PersonsControllerTest.cs @@ -139,7 +139,7 @@ public async Task Should_pass_on_bad_request_from_bookings_api() _userAccountService.Setup(x => x.GetJudgeUsers()).ReturnsAsync(new List()); _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) - .ReturnsAsync(new List()); + .ReturnsAsync(new List()); _bookingsApiClient.Setup(x => x.PostPersonBySearchTermAsync(It.IsAny())) .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.BadRequest)); @@ -154,7 +154,7 @@ public void Should_pass_on_exception_request_from_bookings_api() _userAccountService.Setup(x => x.GetJudgeUsers()).ReturnsAsync(new List()); _bookingsApiClient.Setup(x => x.PostJudiciaryPersonBySearchTermAsync(It.IsAny())) - .ReturnsAsync(new List()); + .ReturnsAsync(new List()); _bookingsApiClient.Setup(x => x.PostPersonBySearchTermAsync(It.IsAny())) .ThrowsAsync(ClientException.ForBookingsAPI(HttpStatusCode.InternalServerError)); diff --git a/AdminWebsite/AdminWebsite.UnitTests/Helper/HearingResponseV2Builder.cs b/AdminWebsite/AdminWebsite.UnitTests/Helper/HearingResponseV2Builder.cs index ce4c61291..2073c8df8 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Helper/HearingResponseV2Builder.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Helper/HearingResponseV2Builder.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using BookingsApi.Contract.V1.Requests; +using BookingsApi.Contract.V1.Requests.Enums; +using BookingsApi.Contract.V1.Responses; using BookingsApi.Contract.V2.Responses; using FizzWare.NBuilder; @@ -12,6 +15,7 @@ public static HearingDetailsResponseV2 Build() { return Builder.CreateNew() .With(x => x.Participants = new List()) + .With(x => x.JudiciaryParticipants = new List()) .With(x => x.Cases = new List { Builder.CreateNew().Build() }) .Build(); } @@ -27,16 +31,42 @@ public static HearingDetailsResponseV2 WithEndPoints(this HearingDetailsResponse public static HearingDetailsResponseV2 WithParticipant(this HearingDetailsResponseV2 hearingDetailsResponse, string userRoleName, string contactEmail =null) { - var participant = Builder.CreateNew() - .With(x => x.Id = Guid.NewGuid()) - .With(x => x.UserRoleName = userRoleName); - - if(!string.IsNullOrEmpty(contactEmail)) + var judicialRoles = new string[] {"Judicial Office Holder", "Panel Member"}; + if (userRoleName == "Judge") + { + var joh = Builder.CreateNew() + .With(x => x.PersonalCode = "12345678") + .With(x => x.HearingRoleCode = JudiciaryParticipantHearingRoleCode.Judge) + .With(x => x.Email = contactEmail) + .With(x => x.WorkPhone = "0123456789") + .Build(); + + hearingDetailsResponse.JudiciaryParticipants.Add(joh); + } + else if (judicialRoles.Contains(userRoleName)) { - participant.With(x => x.ContactEmail = contactEmail); + var joh = Builder.CreateNew() + .With(x => x.PersonalCode = "87654321") + .With(x => x.HearingRoleCode = JudiciaryParticipantHearingRoleCode.PanelMember) + .With(x => x.Email = contactEmail) + .With(x => x.WorkPhone = "0123456789") + .Build(); + hearingDetailsResponse.JudiciaryParticipants.Add(joh); + } + else + { + var participant = Builder.CreateNew() + .With(x => x.Id = Guid.NewGuid()) + .With(x => x.UserRoleName = userRoleName); + + if(!string.IsNullOrEmpty(contactEmail)) + { + participant.With(x => x.ContactEmail = contactEmail); + } + hearingDetailsResponse.Participants.Add(participant.Build()); } - hearingDetailsResponse.Participants.Add(participant.Build()); + return hearingDetailsResponse; } diff --git a/AdminWebsite/AdminWebsite.UnitTests/Mappers/JudgeResponseMapperTest.cs b/AdminWebsite/AdminWebsite.UnitTests/Mappers/JudgeResponseMapperTest.cs index bec410ad6..b68fb65bb 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/Mappers/JudgeResponseMapperTest.cs +++ b/AdminWebsite/AdminWebsite.UnitTests/Mappers/JudgeResponseMapperTest.cs @@ -1,4 +1,5 @@ -using AdminWebsite.Mappers; +using AdminWebsite.Contracts.Responses; +using AdminWebsite.Mappers; using BookingsApi.Contract.V1.Responses; using FluentAssertions; using NUnit.Framework; @@ -10,12 +11,11 @@ public class JudgeResponseMapperTest [Test] public void Should_map_person_response_to_judge_response() { - var personResponse = new PersonResponse + var personResponse = new JudgeResponse() { FirstName = "Sam", LastName = "Smith", - Title = "Mr", - Username = "email.sam@judiciary.net", + Email = "email.sam@judiciary.net", ContactEmail = "judge@personal.com" }; @@ -23,7 +23,7 @@ public void Should_map_person_response_to_judge_response() judgeResponse.FirstName.Should().Be(personResponse.FirstName); judgeResponse.LastName.Should().Be(personResponse.LastName); - judgeResponse.Email.Should().Be(personResponse.Username); + judgeResponse.Email.Should().Be(personResponse.Email); judgeResponse.ContactEmail.Should().Be(personResponse.ContactEmail); } } diff --git a/AdminWebsite/AdminWebsite.UnitTests/packages.lock.json b/AdminWebsite/AdminWebsite.UnitTests/packages.lock.json index 80488eb19..d09fa2d62 100644 --- a/AdminWebsite/AdminWebsite.UnitTests/packages.lock.json +++ b/AdminWebsite/AdminWebsite.UnitTests/packages.lock.json @@ -107,8 +107,8 @@ }, "BookingsApi.Client": { "type": "Transitive", - "resolved": "1.47.15", - "contentHash": "sd5lILWV3aiku/U/tFu16n7Fi9wAELGP0rVpLhaDFq3YzwxEL8JshbN0BUin3F/QZM3T8+DsnMKbMDcU449cqA==", + "resolved": "1.49.10", + "contentHash": "oXH1qyRZhkrbVbdZB7QsO8O0a/6SRCrB71Hb0leiRNUGr/xb0IYdqpaTr6imTeo6zimGxieMiu9uKMsnFpVA/w==", "dependencies": { "Microsoft.AspNetCore.Mvc.Core": "2.2.5" } @@ -1979,7 +1979,7 @@ "type": "Project", "dependencies": { "AspNetCore.HealthChecks.Uris": "[6.0.3, )", - "BookingsApi.Client": "[1.47.15, )", + "BookingsApi.Client": "[1.49.10, )", "FluentValidation.AspNetCore": "[10.4.0, )", "LaunchDarkly.ServerSdk": "[7.0.3, )", "MicroElements.Swashbuckle.FluentValidation": "[5.7.0, )", diff --git a/AdminWebsite/AdminWebsite/AdminWebsite.csproj b/AdminWebsite/AdminWebsite/AdminWebsite.csproj index 244af60a7..3b5b1c15c 100644 --- a/AdminWebsite/AdminWebsite/AdminWebsite.csproj +++ b/AdminWebsite/AdminWebsite/AdminWebsite.csproj @@ -32,7 +32,7 @@ - + @@ -74,12 +74,32 @@ + + + + + + + + + + + + + + + + + + <_ContentIncludedByDefault Remove="ClientApp\dist\assets\images\favicons\manifest.json" /> + + diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.spec.ts index b93c5e506..34f4b6b80 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.spec.ts @@ -3,7 +3,7 @@ import { fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/te import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { OidcSecurityService } from 'angular-auth-oidc-client'; -import { of, throwError } from 'rxjs'; +import { of } from 'rxjs'; import { AppComponent } from './app.component'; import { WindowLocation, WindowRef } from './security/window-ref'; import { ClientSettingsResponse } from './services/clients/api-client'; @@ -19,6 +19,7 @@ import { MockOidcSecurityService } from './testing/mocks/MockOidcSecurityService import { CancelPopupStubComponent } from './testing/stubs/cancel-popup-stub'; import { FooterStubComponent } from './testing/stubs/footer-stub'; import { SignOutPopupStubComponent } from './testing/stubs/sign-out-popup-stub'; +import { WaitPopupComponent } from './popups/wait-popup/wait-popup.component'; describe('AppComponent', () => { const router = { @@ -72,7 +73,8 @@ describe('AppComponent', () => { FooterStubComponent, SignOutPopupStubComponent, CancelPopupStubComponent, - UnsupportedBrowserComponent + UnsupportedBrowserComponent, + WaitPopupComponent ], providers: [ { provide: OidcSecurityService, useValue: mockOidcSecurityService }, @@ -177,7 +179,8 @@ describe('AppComponent - ConnectionService', () => { FooterStubComponent, SignOutPopupStubComponent, CancelPopupStubComponent, - UnsupportedBrowserComponent + UnsupportedBrowserComponent, + WaitPopupComponent ], providers: [ { provide: OidcSecurityService, useValue: mockOidcSecurityService }, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.ts index 3064586dd..7ba36eec0 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, HostListener, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; import { ConfigService } from './services/config.service'; import { PageTrackerService } from './services/page-tracker.service'; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.spec.ts index 746c4cabd..2df771e88 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.spec.ts @@ -291,7 +291,7 @@ describe('AddParticipantComponent', () => { 'mapParticipantHearingRoles' ]); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(false)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); participantServiceSpy.mapParticipantsRoles.and.returnValue(partyList); participantServiceSpy.mapParticipantHearingRoles.and.returnValue(mappedHearingRoles); bookingServiceSpy = jasmine.createSpyObj(['isEditMode', 'resetEditMode']); @@ -1078,7 +1078,7 @@ describe('AddParticipantComponent edit mode', () => { }).compileComponents(); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(false)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); const hearing = initExistHearingRequest(); videoHearingsServiceSpy.getParticipantRoles.and.returnValue(Promise.resolve(roleList)); @@ -1233,7 +1233,7 @@ describe('AddParticipantComponent edit mode', () => { })); it('shows single role list when reference data flag is on', fakeAsync(async () => { - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(true)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(true)); component.ngOnInit(); component.ngAfterViewInit(); @@ -1255,7 +1255,7 @@ describe('AddParticipantComponent edit mode', () => { })); it('gets participant roles by case type service id when reference data flag is off', fakeAsync(async () => { - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(false)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); component.ngOnInit(); component.ngAfterViewInit(); @@ -1499,7 +1499,7 @@ describe('AddParticipantComponent edit mode no participants added', () => { bookingServiceSpy.isEditMode.and.returnValue(true); bookingServiceSpy.getParticipantEmail.and.returnValue(''); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(false)); - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(false)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); TestBed.configureTestingModule({ imports: [SharedModule, RouterModule.forChild([]), BookingModule, PopupModule, TestingModule], @@ -1734,7 +1734,7 @@ describe('AddParticipantComponent set representer', () => { bookingServiceSpy.isEditMode.and.returnValue(true); bookingServiceSpy.getParticipantEmail.and.returnValue(''); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(false)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); const searchServiceStab = jasmine.createSpyObj(['participantSearch']); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.ts index 4c8be5fae..1792214a2 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/add-participant/add-participant.component.ts @@ -70,7 +70,7 @@ export class AddParticipantComponent extends AddParticipantBaseDirective impleme } ngOnInit() { - const referenceDataFlag$ = this.launchDarklyService.getFlag(FeatureFlags.referenceData).pipe(takeUntil(this.destroyed$)); + const referenceDataFlag$ = this.launchDarklyService.getFlag(FeatureFlags.useV2Api).pipe(takeUntil(this.destroyed$)); const ejudFeatureFlag$ = this.launchDarklyService.getFlag(FeatureFlags.eJudFeature).pipe(takeUntil(this.destroyed$)); combineLatest([referenceDataFlag$, ejudFeatureFlag$]).subscribe(([referenceDataFlag, ejudFeatureFlag]) => { @@ -684,7 +684,7 @@ export class AddParticipantComponent extends AddParticipantBaseDirective impleme } private checkParticipants(): boolean { - return this.hearing.participants && this.hearing.participants?.length > 0; + return this.hearing.participants?.length > 0 || this.hearing.judiciaryParticipants.length > 0; } get canNavigate() { @@ -767,7 +767,8 @@ export class AddParticipantComponent extends AddParticipantBaseDirective impleme email: this.constants.PleaseSelect, is_exist_person: false, is_judge: false, - is_courtroom_account: false + is_courtroom_account: false, + isJudiciaryMember: false }; this.interpreteeList.unshift(interpreteeModel); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.spec.ts index d2feaddd2..ca1b7df74 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.spec.ts @@ -105,7 +105,7 @@ describe('AssignJudgeComponent', () => { emailValidationServiceSpy.validateEmail.and.returnValue(true); emailValidationServiceSpy.hasCourtroomAccountPattern.and.returnValue(true); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.referenceData).and.returnValue(of(false)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); bookingServiseSpy = jasmine.createSpyObj('BookingService', ['resetEditMode', 'isEditMode', 'removeEditMode']); @@ -613,7 +613,7 @@ describe('AssignJudgeComponent', () => { const updatedJudgeDisplayName = 'UpdatedJudgeDisplayName'; videoHearingsServiceSpy.canAddJudge.and.returnValue(true); component.judge.display_name = updatedJudgeDisplayName; - component.referenceDataFeatureFlag = false; + component.useV2Api = false; }); it('should add judge account when none present', () => { @@ -625,7 +625,7 @@ describe('AssignJudgeComponent', () => { }); it('should add update judge when reference data flag is on', () => { - component.referenceDataFeatureFlag = true; + component.useV2Api = true; }); afterEach(() => { component.updateJudge(judge); @@ -636,7 +636,7 @@ describe('AssignJudgeComponent', () => { expect(component.courtAccountJudgeEmail).toEqual(judge.username); expect(component.judgeDisplayNameFld.value).toEqual(judge.display_name); expect(updatedJudges[0]).toBe(judge); - if (component.referenceDataFeatureFlag) { + if (component.useV2Api) { expect(updatedJudges[0].case_role_name).toBeNull(); expect(updatedJudges[0].hearing_role_code).toBe(Constants.HearingRoleCodes.Judge); } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.ts index 4f8114605..cc0653dc5 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/assign-judge/assign-judge.component.ts @@ -50,7 +50,7 @@ export class AssignJudgeComponent extends BookingBaseComponent implements OnInit isValidEmail = true; showStaffMemberFeature: boolean; ejudFeatureFlag = false; - referenceDataFeatureFlag = false; + useV2Api = false; destroyed$ = new Subject(); constructor( @@ -69,10 +69,10 @@ export class AssignJudgeComponent extends BookingBaseComponent implements OnInit ngOnInit() { this.launchDarklyService - .getFlag(FeatureFlags.referenceData) + .getFlag(FeatureFlags.useV2Api) .pipe(takeUntil(this.destroyed$)) .subscribe(enabled => { - this.referenceDataFeatureFlag = enabled; + this.useV2Api = enabled; }); this.launchDarklyService .getFlag(FeatureFlags.eJudFeature) @@ -103,8 +103,7 @@ export class AssignJudgeComponent extends BookingBaseComponent implements OnInit private checkForExistingRequest() { this.logger.debug(`${this.loggerPrefix} Checking for existing hearing`); this.hearing = this.hearingService.getCurrentRequest(); - this.isBookedHearing = - this.hearing && this.hearing.hearing_id !== undefined && this.hearing.hearing_id !== null && this.hearing.hearing_id.length > 0; + this.isBookedHearing = this.hearing?.hearing_id?.length > 0; this.isStaffMemberExisting = !!this.hearing?.participants.find(x => x.hearing_role_name === Constants.HearingRoles.StaffMember); this.otherInformationDetails = OtherInformationModel.init(this.hearing.other_information); } @@ -381,7 +380,7 @@ export class AssignJudgeComponent extends BookingBaseComponent implements OnInit if (this.hearingService.canAddJudge(judge.username)) { judge.is_judge = true; judge.case_role_name = 'Judge'; - if (this.referenceDataFeatureFlag) { + if (this.useV2Api) { judge.case_role_name = null; judge.hearing_role_code = Constants.HearingRoleCodes.Judge; } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking-routing.module.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking-routing.module.ts index b6a380a84..3ecfc535b 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking-routing.module.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking-routing.module.ts @@ -12,6 +12,7 @@ import { HearingScheduleComponent } from './hearing-schedule/hearing-schedule.co import { OtherInformationComponent } from './other-information/other-information.component'; import { SummaryComponent } from './summary/summary.component'; import { EndpointsComponent } from './endpoints/endpoints.component'; +import { AddJudicialOfficeHoldersComponent } from './judicial-office-holders/add-judicial-office-holders/add-judicial-office-holders.component'; export const routes: Routes = [ { path: 'book-hearing', component: CreateHearingComponent, canActivate: [AuthGuard, AdminGuard, LastMinuteAmendmentsGuard] }, @@ -22,6 +23,12 @@ export const routes: Routes = [ canActivate: [AuthGuard, AdminGuard, LastMinuteAmendmentsGuard], data: { exceptionToRuleCheck: true } }, + { + path: 'add-judicial-office-holders', + component: AddJudicialOfficeHoldersComponent, + canActivate: [AuthGuard, AdminGuard, LastMinuteAmendmentsGuard], + data: { exceptionToRuleCheck: true } + }, { path: 'add-participants', component: AddParticipantComponent, canActivate: [AuthGuard, AdminGuard] }, { path: 'video-access-points', component: EndpointsComponent, canActivate: [AuthGuard, AdminGuard] }, { path: 'other-information', component: OtherInformationComponent, canActivate: [AuthGuard, AdminGuard, LastMinuteAmendmentsGuard] }, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking.module.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking.module.ts index f71af3b36..760ee379f 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking.module.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/booking.module.ts @@ -17,11 +17,16 @@ import { EndpointsComponent } from './endpoints/endpoints.component'; import { ParticipantItemComponent, ParticipantListComponent } from './participant'; import { MultiDayHearingScheduleComponent } from './summary/multi-day-hearing-schedule'; import { DateErrorMessagesComponent } from './hearing-schedule/date-error-messages/date-error-messages'; +import { AddJudicialOfficeHoldersComponent } from './judicial-office-holders/add-judicial-office-holders/add-judicial-office-holders.component'; +import { SearchForJudicialMemberComponent } from './judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component'; +import { NgOptimizedImage } from '@angular/common'; export const Components: Type[] = [ CreateHearingComponent, DateErrorMessagesComponent, HearingScheduleComponent, + AddJudicialOfficeHoldersComponent, + SearchForJudicialMemberComponent, AssignJudgeComponent, AddParticipantComponent, AddStaffMemberComponent, @@ -38,7 +43,7 @@ export const Components: Type[] = [ ]; @NgModule({ - imports: [SharedModule, BookingRoutingModule, PopupModule], + imports: [SharedModule, BookingRoutingModule, PopupModule, NgOptimizedImage], declarations: Components, exports: Components }) diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.css b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.css index 9d92f435a..f9494d714 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.css +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.css @@ -1,48 +1,3 @@ -.vh-breadcrumbs-wrap { - margin-left: -65px !important; -} -.vh-breadcrumbs { - display: inline-block; - position: relative; - margin-bottom: 15px; - margin-left: 10px; - margin-top: 0; - padding-left: 15.655px; - list-style-type: none; - font-size: 18px; - font-family: 'nta', Arial, sans-serif; -} - -.vh-breadcrumbs:after { - content: ''; - display: block; - clear: both; -} -.vh-breadcrumbs:before { - content: ''; - display: block; - position: absolute; - top: 1px; - bottom: -1px; - left: -3.31px; - width: 7px; - height: 7px; - margin: auto 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - border: solid; - border-width: 1px 1px 0 0; - border-color: #6f777b; -} - -.vh-breadcrumbs:first-child:before { - content: none; - display: none; - margin-left: 0 !important; - padding-left: 0 !important; -} - .vh-active { color: #0666b4 !important; text-decoration: none; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.html index 848022ed1..e3ed78b72 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.html @@ -1,7 +1,8 @@ -
-
    -
  1. +
    +
      +
    1. { const videoHearingsServiceSpy = jasmine.createSpyObj([ 'validCurrentRequest', @@ -18,29 +19,41 @@ describe('BreadcrumbComponent', () => { url: '/hearing-schedule', ...jasmine.createSpyObj(['navigate']) } as jasmine.SpyObj; - beforeEach(async () => { + + beforeEach(() => { launchDarklyServiceSpy = jasmine.createSpyObj('LaunchDarklyService', ['getFlag']); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); component = new BreadcrumbComponent(router, videoHearingsServiceSpy, launchDarklyServiceSpy); component.breadcrumbItems = BreadcrumbItems.slice(); component.canNavigate = true; - await component.ngOnInit(); + component.ngOnInit(); + }); + + afterEach(() => { + component.ngOnDestroy(); }); + it('should create breadcrumb component', () => { expect(component).toBeTruthy(); }); - it('breadcrumb component should have predefine navigation items', () => { + + it('should have predefine navigation items', () => { expect(component.breadcrumbItems.length).toBeGreaterThan(0); }); - it('breadcrumb component currentItem should match the current route and be type BreadcrumbItemModel', () => { + + it('should match the current route and be type BreadcrumbItemModel', () => { expect(component.currentRouter).toEqual('/hearing-schedule'); expect(component.currentItem.Url).toEqual(component.currentRouter); + expect(component.currentItem instanceof BreadcrumbItemModel).toBeTruthy(); }); - it('breadcrumb component currentItem should have property Active set to true and property Value set to true', () => { + + it('should have property Active set to true and property Value set to true for currentItem', () => { expect(component.currentItem.Active).toBeTruthy(); expect(component.currentItem.Value).toBeTruthy(); }); - it('next items should have property Active set to false and property Value set to false', () => { + + it('should have property Active set to false and property Value set to false for next items', () => { for (const item of component.breadcrumbItems) { if (item.Url !== component.currentItem.Url && item.Id > component.currentItem.Id) { expect(item.Active).toBeFalsy(); @@ -48,7 +61,8 @@ describe('BreadcrumbComponent', () => { } } }); - it('previous items should have property Active set to true and property Value set to false', () => { + + it('should have property Active set to true and property Value set to false for previous items', () => { for (const item of component.breadcrumbItems) { if (item.Url !== component.currentItem.Url && item.Id < component.currentItem.Id) { expect(item.Active).toBeTruthy(); @@ -56,6 +70,7 @@ describe('BreadcrumbComponent', () => { } } }); + it('should not navigate to next route if canNavigate set to false', () => { component.canNavigate = false; const step = new BreadcrumbItemModel(2, false, 'Hearing schedule', '/hearing-schedule', false, false); @@ -63,15 +78,17 @@ describe('BreadcrumbComponent', () => { expect(router.navigate).toHaveBeenCalledTimes(0); expect(videoHearingsServiceSpy.validCurrentRequest).not.toHaveBeenCalled(); }); + it('should not navigate to next route if the difference between next item id and the current is greater than 1', () => { component.canNavigate = false; - const step = new BreadcrumbItemModel(2, false, 'Hearing schedule', '/assign-judge', false, false); + const step = BreadcrumbItems[BreadcrumbItems.length - 1]; component.clickBreadcrumbs(step); expect(router.navigate).toHaveBeenCalledTimes(0); expect(videoHearingsServiceSpy.validCurrentRequest).not.toHaveBeenCalled(); }); + it('should not navigate to next route if the next item with the given url is not found', () => { - component.canNavigate = false; + component.canNavigate = true; const step = new BreadcrumbItemModel(2, false, 'Hearing schedule', '/some-thing', false, false); component.clickBreadcrumbs(step); expect(router.navigate).toHaveBeenCalledTimes(0); @@ -82,7 +99,9 @@ describe('BreadcrumbComponent', () => { component.ngOnInit(); expect(component.breadcrumbItems.find(b => b.Url === PageUrls.AssignJudge).Name).toBe('Judge'); }); + it('should navigate to next route if canNavigate set to true and next item in correct order', () => { + component.canNavigate = true; const step = new BreadcrumbItemModel(2, false, 'Hearing schedule', '/assign-judge', false, false); component.clickBreadcrumbs(step); expect(router.navigate).toHaveBeenCalledWith(['/assign-judge']); @@ -94,176 +113,17 @@ describe('BreadcrumbComponent', () => { component.clickBreadcrumbs(step); expect(router.navigate).toHaveBeenCalledWith(['/assign-judge']); }); - describe('when other checks fail', () => { - const route = '/add-participants'; - let step: BreadcrumbItemModel; - beforeEach(() => { - step = new BreadcrumbItemModel(3, false, 'Hearing schedule', route, false, false); - videoHearingsServiceSpy.validCurrentRequest.calls.reset(); - router.navigate.calls.reset(); - }); - it('should not navigate when canNavigate set to true and is not validCurrentRequest', () => { - videoHearingsServiceSpy.validCurrentRequest.and.returnValue(false); - component.clickBreadcrumbs(step); - expect(router.navigate).not.toHaveBeenCalled(); - expect(videoHearingsServiceSpy.validCurrentRequest).toHaveBeenCalledTimes(1); - }); - it('should navigate to next route if canNavigate set to true and is validCurrentRequest', () => { - videoHearingsServiceSpy.validCurrentRequest.and.returnValue(true); - component.clickBreadcrumbs(step); - expect(router.navigate).toHaveBeenCalledWith([route]); - expect(videoHearingsServiceSpy.validCurrentRequest).toHaveBeenCalledTimes(1); - }); + + it('should set ejudFeatureFlag and addJudiciaryMemberFlag when ngOnInit is called', () => { + expect(component.ejudFeatureFlag).toBe(true); + expect(component.addJudiciaryMemberFlag).toBe(false); }); - describe('Set correct active', () => { - const defaultActive = undefined; - const breadCrumbId1 = 1; - const breadCrumbValue1 = true; - const breadCrumbName1 = 'BreadCrumbName1'; - const breadCrumbUrl1 = 'BreadCrumbUrl1'; - const breadCrumbLastMinuteAmendable1 = false; - const breadCrumb1 = new BreadcrumbItemModel( - breadCrumbId1, - breadCrumbValue1, - breadCrumbName1, - breadCrumbUrl1, - defaultActive, - breadCrumbLastMinuteAmendable1 - ); - const breadCrumbId2 = 2; - const breadCrumbValue2 = true; - const breadCrumbName2 = 'BreadCrumbName2'; - const breadCrumbUrl2 = 'BreadCrumbUrl2'; - const breadCrumbLastMinuteAmendable2 = true; - const breadCrumb2 = new BreadcrumbItemModel( - breadCrumbId2, - breadCrumbValue2, - breadCrumbName2, - breadCrumbUrl2, - defaultActive, - breadCrumbLastMinuteAmendable2 - ); - const breadCrumbId3 = 3; - const breadCrumbValue3 = true; - const breadCrumbName3 = 'Judge'; - const breadCrumbUrl3 = '/assign-judge'; - const breadCrumbLastMinuteAmendable3 = true; - const breadCrumb3 = new BreadcrumbItemModel( - breadCrumbId3, - breadCrumbValue3, - breadCrumbName3, - breadCrumbUrl3, - defaultActive, - breadCrumbLastMinuteAmendable3 - ); - const breadCrumbId4 = 4; - const breadCrumbValue4 = true; - const breadCrumbName4 = 'BreadCrumbName4'; - const breadCrumbUrl4 = 'BreadCrumbUrl4'; - const breadCrumbLastMinuteAmendable4 = true; - const breadCrumb4 = new BreadcrumbItemModel( - breadCrumbId4, - breadCrumbValue4, - breadCrumbName4, - breadCrumbUrl4, - defaultActive, - breadCrumbLastMinuteAmendable4 - ); - const breadCrumbId5 = 5; - const breadCrumbValue5 = true; - const breadCrumbName5 = 'BreadCrumbName5'; - const breadCrumbUrl5 = 'BreadCrumbUrl5'; - const breadCrumbLastMinuteAmendable5 = false; - const breadCrumb5 = new BreadcrumbItemModel( - breadCrumbId5, - breadCrumbValue5, - breadCrumbName5, - breadCrumbUrl5, - defaultActive, - breadCrumbLastMinuteAmendable5 - ); - const breadCrumbs = [breadCrumb1, breadCrumb2, breadCrumb3, breadCrumb4, breadCrumb5]; - beforeAll(() => { - BreadcrumbItems.splice(0, BreadcrumbItems.length); - BreadcrumbItems.push(...breadCrumbs); - }); - it('if currentRouter does not match any breadcrumbs, all breadcrumbs.active should not change', () => { - // @ts-ignore: force this readonly property value for testing. - router.url = 'NoMatches'; - component.ngOnInit(); - component.breadcrumbItems.map(item => { - expect(item.Active).toBe(defaultActive); - }); - }); - describe('currentRouter matches a breadcrumb', () => { - const activeIndex = 2; - beforeEach(() => { - // @ts-ignore: force this readonly property value for testing. - router.url = breadCrumbs[activeIndex].Url; - }); - it('ensure all test cases are covered', () => { - expect(breadCrumbs.some(breadCrumb => breadCrumb.Id < breadCrumbs[activeIndex].Id && breadCrumb.LastMinuteAmendable)).toBe( - true - ); - expect(breadCrumbs.some(breadCrumb => breadCrumb.Id < breadCrumbs[activeIndex].Id && !breadCrumb.LastMinuteAmendable)).toBe( - true - ); - expect(breadCrumbs.some(breadCrumb => breadCrumb.Id > breadCrumbs[activeIndex].Id && breadCrumb.LastMinuteAmendable)).toBe( - true - ); - expect(breadCrumbs.some(breadCrumb => breadCrumb.Id > breadCrumbs[activeIndex].Id && !breadCrumb.LastMinuteAmendable)).toBe( - true - ); - }); - describe('when not last minute amendment', () => { - describe('conference closed', () => { - beforeEach(() => { - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(true); - }); - it('only ids before current router should be active', () => {}); - }); - describe('hearing not about to start', () => { - beforeEach(() => { - videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(false); - }); - it('only ids before current router should be active', () => {}); - }); - describe('conference close adn hearing not about to start', () => { - beforeEach(() => { - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(true); - videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(false); - }); - it('only ids before current router should be active', () => {}); - }); - afterEach(async () => { - await component.ngOnInit(); - for (let i = 0; i < breadCrumbs.length; i++) { - expect(breadCrumbs[i].Active).toBe(breadCrumbs[i].Id <= breadCrumbs[activeIndex].Id); - } - }); - }); - describe('when last minute amendment', () => { - beforeEach(() => { - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); - videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(true); - }); - it('only ids before current router and marked as lastMinuteAmendable should be active', async () => { - await component.ngOnInit(); - for (let i = 0; i < breadCrumbs.length; i++) { - const currentBreadCrumb = breadCrumbs[i]; - expect(currentBreadCrumb.Active).toBe( - currentBreadCrumb.Id <= breadCrumbs[activeIndex].Id && currentBreadCrumb.LastMinuteAmendable - ); - } - }); - it('when ejud feature flag is off, assign judge should NOT be marked as active', async () => { - launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(false)); - await component.ngOnInit(); - const assignJudgeCrumb = breadCrumbs.find(e => e.Url === '/assign-judge'); - expect(assignJudgeCrumb.Active).toBe(false); - }); - }); - }); + it('should unsubscribe from subscriptions when ngOnDestroy is called', () => { + spyOn(component.destroyed$, 'next'); + spyOn(component.destroyed$, 'complete'); + component.ngOnDestroy(); + expect(component.destroyed$.next).toHaveBeenCalled(); + expect(component.destroyed$.complete).toHaveBeenCalled(); }); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.ts index 4e360549a..d56577b6c 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumb.component.ts @@ -4,8 +4,9 @@ import { BreadcrumbItems } from './breadcrumbItems'; import { BreadcrumbItemModel } from './breadcrumbItem.model'; import { VideoHearingsService } from '../../services/video-hearings.service'; import { takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { Subject, combineLatest } from 'rxjs'; import { FeatureFlags, LaunchDarklyService } from 'src/app/services/launch-darkly.service'; +import { PageUrls } from 'src/app/shared/page-url.constants'; @Component({ selector: 'app-breadcrumb', templateUrl: './breadcrumb.component.html', @@ -18,22 +19,29 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { @Input() canNavigate: boolean; ejudFeatureFlag = false; + addJudiciaryMemberFlag = false; destroyed$ = new Subject(); - constructor(private router: Router, private videoHearingsService: VideoHearingsService, private featureService: LaunchDarklyService) { - this.breadcrumbItems = JSON.parse(JSON.stringify(BreadcrumbItems)); - } + constructor(private router: Router, private videoHearingsService: VideoHearingsService, private featureService: LaunchDarklyService) {} - async ngOnInit() { + ngOnInit() { this.currentRouter = this.router.url; - this.featureService - .getFlag(FeatureFlags.eJudFeature) - .pipe(takeUntil(this.destroyed$)) - .subscribe(result => { - this.ejudFeatureFlag = result; - }); + const ejudFeatureFlag$ = this.featureService.getFlag(FeatureFlags.eJudFeature).pipe(takeUntil(this.destroyed$)); + const addJudicalMembers$ = this.featureService.getFlag(FeatureFlags.useV2Api).pipe(takeUntil(this.destroyed$)); + + combineLatest([ejudFeatureFlag$, addJudicalMembers$]).subscribe(([ejudFeatureFlag, addJudiciaryMemberFlag]) => { + this.ejudFeatureFlag = ejudFeatureFlag; + this.addJudiciaryMemberFlag = addJudiciaryMemberFlag; - this.initBreadcrumb(); + let tempBreadcrumbModel: BreadcrumbItemModel[]; + if (this.addJudiciaryMemberFlag) { + tempBreadcrumbModel = BreadcrumbItems.filter(x => x.Url !== PageUrls.AssignJudge); + } else { + tempBreadcrumbModel = BreadcrumbItems.filter(x => x.Url !== PageUrls.AddJudicialOfficeHolders); + } + this.breadcrumbItems = tempBreadcrumbModel; + this.initBreadcrumb(); + }); } ngOnDestroy(): void { diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumbItems.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumbItems.ts index 89f3bda28..061433b93 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumbItems.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/breadcrumb/breadcrumbItems.ts @@ -1,60 +1,13 @@ +import { PageUrls } from 'src/app/shared/page-url.constants'; import { BreadcrumbItemModel } from './breadcrumbItem.model'; export const BreadcrumbItems: BreadcrumbItemModel[] = [ - { - Id: 1, - Value: true, - Name: 'Hearing details', - Url: '/book-hearing', - Active: true, - LastMinuteAmendable: false - }, - { - Id: 2, - Value: false, - Name: 'Hearing schedule', - Url: '/hearing-schedule', - Active: false, - LastMinuteAmendable: false - }, - { - Id: 3, - Value: false, - Name: 'Judge', - Url: '/assign-judge', - Active: false, - LastMinuteAmendable: true - }, - { - Id: 4, - Value: false, - Name: 'Participants', - Url: '/add-participants', - Active: false, - LastMinuteAmendable: true - }, - { - Id: 5, - Value: false, - Name: 'Video access points', - Url: '/video-access-points', - Active: false, - LastMinuteAmendable: true - }, - { - Id: 6, - Value: false, - Name: 'Other information', - Url: '/other-information', - Active: false, - LastMinuteAmendable: false - }, - { - Id: 7, - Value: false, - Name: 'Summary', - Url: '/summary', - Active: false, - LastMinuteAmendable: true - } + new BreadcrumbItemModel(1, true, 'Hearing details', PageUrls.CreateHearing, true, false), + new BreadcrumbItemModel(2, false, 'Hearing schedule', PageUrls.HearingSchedule, false, false), + new BreadcrumbItemModel(3, false, 'Judge', PageUrls.AssignJudge, false, true), + new BreadcrumbItemModel(4, false, 'Judicial Office Holder(s)', PageUrls.AddJudicialOfficeHolders, false, true), + new BreadcrumbItemModel(5, false, 'Participants', PageUrls.AddParticipants, false, true), + new BreadcrumbItemModel(6, false, 'Video access points', PageUrls.Endpoints, false, true), + new BreadcrumbItemModel(7, false, 'Other information', PageUrls.OtherInformation, false, false), + new BreadcrumbItemModel(8, false, 'Summary', PageUrls.Summary, false, true) ]; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/create-hearing/create-hearing.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/create-hearing/create-hearing.component.html index 549a1e647..81805494b 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/create-hearing/create-hearing.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/create-hearing/create-hearing.component.html @@ -82,14 +82,13 @@

      Please complete class="govuk-input govuk-!-width-one-half" type="text" formControlName="caseType" - [value]="this.hearing.case_type" />
      Please select a case type

    -
    +
    +
      +
    • + {{ result.email }} +
    • +
    +
    + +
    + + +
    + + + +

    + No results found +

    + +
    + + + Warning + No existing user account found + +
    diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.scss b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.scss new file mode 100644 index 000000000..ccabf3256 --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.scss @@ -0,0 +1,65 @@ +@import 'govuk-frontend/govuk/base'; + +.search-result-list { + margin-top: 3px; + width: 115%; + list-style: none; + border: 1px solid; + font-size: 18px; + text-decoration: none; +} + +.search-result-row { + color: govuk-colour('black'); + text-decoration: none; + margin-left: -30px; + + &:hover { + background-color: govuk-colour('light-blue'); + color: govuk-colour('dark-blue'); + cursor: pointer; + } + + &:focus { + background-color: govuk-colour('light-blue'); + color: govuk-colour('dark-blue'); + } +} + +.vh-li-email { + margin-top: 3px; + min-width: fit-content; + width: auto; + list-style: none; + border: 1px solid; + font-size: 18px; + text-decoration: none; +} + +.vh-a-email { + &:hover { + background-color: govuk-colour('dark-blue'); + color: govuk-colour('white') !important; + cursor: pointer; + } + + &:focus { + background-color: govuk-colour('dark-blue'); + color: govuk-colour('white') !important; + } +} + +.vk-showlist-m30 { + margin-left: -30px; + padding-right: 10px; + + &:hover { + background-color: govuk-colour('dark-blue'); + color: govuk-colour('white') !important; + } + + &:focus { + background-color: govuk-colour('dark-blue'); + color: govuk-colour('white') !important; + } +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.spec.ts new file mode 100644 index 000000000..90b20b514 --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.spec.ts @@ -0,0 +1,219 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ReactiveFormsModule, Validators } from '@angular/forms'; +import { of } from 'rxjs'; +import { JudicialService } from '../../services/judicial.service'; +import { JudiciaryPerson } from 'src/app/services/clients/api-client'; +import { SearchForJudicialMemberComponent } from './search-for-judicial-member.component'; +import { JudicialMemberDto } from '../models/add-judicial-member.model'; + +describe('SearchForJudicialMemberComponent', () => { + let component: SearchForJudicialMemberComponent; + let fixture: ComponentFixture; + let judicialServiceSpy: jasmine.SpyObj; + + beforeEach(async () => { + judicialServiceSpy = jasmine.createSpyObj('JudicialService', ['getJudicialUsers']); + judicialServiceSpy.getJudicialUsers.and.returnValue(of([])); + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + declarations: [SearchForJudicialMemberComponent], + providers: [{ provide: JudicialService, useValue: judicialServiceSpy }] + }).compileComponents(); + + fixture = TestBed.createComponent(SearchForJudicialMemberComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('existingJudicialMember', () => { + it('should set form values and disable judiciaryEmail control when existingJudicialMember is set', () => { + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + judicialMember.displayName = 'Test User display name'; + judicialMember.roleCode = 'Judge'; + component.existingJudicialMember = judicialMember; + expect(component.form.controls.judiciaryEmail.value).toBe(judicialMember.email); + expect(component.form.controls.displayName.value).toBe(judicialMember.displayName); + expect(component.form.controls.judiciaryEmail.disabled).toBeTrue(); + }); + + it('should reset form values and enable judiciaryEmail control when existingJudicialMember is not set', () => { + component.existingJudicialMember = null; + expect(component.form.controls.judiciaryEmail.value).toBe(''); + expect(component.form.controls.displayName.value).toBe(''); + expect(component.form.controls.judiciaryEmail.enabled).toBeTrue(); + }); + }); + + describe('searchForJudicialMember', () => { + it('should call judicialService.getJudicialUsers with the correct email and set searchResult and showResult', () => { + const email = 'test@test.com'; + const searchResult: JudiciaryPerson[] = [ + new JudiciaryPerson({ + email: 'test@test.com', + full_name: 'Test User', + title: 'Mr', + first_name: 'Test', + last_name: 'User', + personal_code: '1234', + work_phone: '1234567890' + }) + ]; + judicialServiceSpy.getJudicialUsers.and.returnValue(of(searchResult)); + + component.form.controls.judiciaryEmail.setValue(email); + component.searchForJudicialMember(); + + expect(judicialServiceSpy.getJudicialUsers).toHaveBeenCalledWith(email); + expect(component.searchResult).toEqual(searchResult); + expect(component.showResult).toBeTrue(); + expect(component.form.controls.displayName.hasValidator(Validators.required)).toBeTrue(); + }); + + it('should call judicialService.getJudicialUsers with the correct email and ignore existing judiciary memebrs and showResult', () => { + const email = 'test@test.com'; + const judiciaryPerson = new JudiciaryPerson({ + email: 'test@test.com', + full_name: 'Test User', + title: 'Mr', + first_name: 'Test', + last_name: 'User', + personal_code: '1234', + work_phone: '1234567890' + }); + const searchResult: JudiciaryPerson[] = [judiciaryPerson]; + judicialServiceSpy.getJudicialUsers.and.returnValue(of(searchResult)); + component.existingJudicialMembers = [ + new JudicialMemberDto( + judiciaryPerson.first_name, + judiciaryPerson.last_name, + judiciaryPerson.full_name, + judiciaryPerson.email, + judiciaryPerson.work_phone, + judiciaryPerson.personal_code + ) + ]; + + component.form.controls.judiciaryEmail.setValue(email); + component.searchForJudicialMember(); + + expect(judicialServiceSpy.getJudicialUsers).toHaveBeenCalledWith(email); + expect(component.searchResult).toEqual([]); + expect(component.showResult).toBeTrue(); + expect(component.form.controls.displayName.hasValidator(Validators.required)).toBeTrue(); + }); + }); + + describe('selectJudicialMember', () => { + it('should set form values and emit judicialMemberSelected event with the correct values', () => { + const judicialMember: JudiciaryPerson = new JudiciaryPerson({ + email: 'test@test.com', + full_name: 'Test User', + title: 'Mr', + first_name: 'Test', + last_name: 'User', + personal_code: '1234', + work_phone: '1234567890' + }); + const expectedJudicialMember = new JudicialMemberDto( + judicialMember.first_name, + judicialMember.last_name, + judicialMember.full_name, + judicialMember.email, + judicialMember.work_phone, + judicialMember.personal_code + ); + spyOn(component.judicialMemberSelected, 'emit'); + + component.selectJudicialMember(judicialMember); + + expect(component.form.value.judiciaryEmail).toBe(judicialMember.email); + expect(component.form.value.displayName).toBe(judicialMember.full_name); + expect(component.showResult).toBeFalse(); + }); + }); + + describe('confirmJudiciaryMemberWithDisplayName', () => { + it('should set judicialMember displayName and emit judicialMemberSelected event with the correct values', () => { + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + const displayName = 'Test User'; + spyOn(component.judicialMemberSelected, 'emit'); + + component['judicialMember'] = judicialMember; + component.form.controls.displayName.setValue(displayName); + component.confirmJudiciaryMemberWithDisplayName(); + + expect(component['judicialMember'].displayName).toBe(displayName); + expect(component.judicialMemberSelected.emit).toHaveBeenCalledWith(judicialMember); + expect(component.form.value.judiciaryEmail).toBe(''); + expect(component.form.value.displayName).toBe(''); + expect(component.form.controls.displayName.hasValidator(Validators.required)).toBeFalse(); + }); + }); + + describe('createForm', () => { + it('should create a form with the correct controls and validators', () => { + component.createForm(); + + expect(component.form.controls.judiciaryEmail).toBeDefined(); + expect(component.form.controls.judiciaryEmail.value).toBe(''); + expect(component.form.controls.judiciaryEmail.hasValidator(Validators.required)).toBeTrue(); + + expect(component.form.controls.displayName).toBeDefined(); + expect(component.form.controls.displayName.value).toBe(''); + expect(component.form.controls.displayName.hasValidator(Validators.required)).toBeFalse(); + }); + + it('should remove displayName required validator and reset form when judiciaryEmail is empty', fakeAsync(() => { + spyOn(component.form.controls.displayName, 'removeValidators'); + spyOn(component.form, 'reset'); + + component.form.controls.judiciaryEmail.setValue(''); + component.form.controls.judiciaryEmail.updateValueAndValidity(); + + tick(component.NotificationDelayTime); + + expect(component.showResult).toBeFalse(); + expect(component.form.controls.displayName.removeValidators).toHaveBeenCalledWith(Validators.required); + expect(component.form.reset).toHaveBeenCalledWith({ + judiciaryEmail: '', + displayName: '' + }); + })); + + it('should not search for judicial member when judiciaryEmail is invalid', () => { + component.form.controls.judiciaryEmail.setValue('te'); + component.form.controls.judiciaryEmail.updateValueAndValidity(); + + expect(component.showResult).toBeFalse(); + expect(judicialServiceSpy.getJudicialUsers).not.toHaveBeenCalled(); + }); + + it('should not search for judicial member when in edit mode', () => { + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + judicialMember.displayName = 'Test User display name'; + judicialMember.roleCode = 'Judge'; + component.existingJudicialMember = judicialMember; + + component.form.controls.judiciaryEmail.setValue('test@test.com'); + component.form.controls.judiciaryEmail.updateValueAndValidity(); + + expect(component.showResult).toBeFalse(); + expect(judicialServiceSpy.getJudicialUsers).not.toHaveBeenCalled(); + }); + + it('should search for judicial member when judiciaryEmail is valid and not in edit mode', fakeAsync(() => { + component.form.controls.judiciaryEmail.setValue('test@test.com'); + component.form.controls.judiciaryEmail.updateValueAndValidity(); + + tick(component.NotificationDelayTime); + + expect(component.showResult).toBeTrue(); + expect(judicialServiceSpy.getJudicialUsers).toHaveBeenCalledWith('test@test.com'); + })); + }); +}); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.ts new file mode 100644 index 000000000..cfc2ed141 --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/judicial-office-holders/search-for-judicial-member/search-for-judicial-member.component.ts @@ -0,0 +1,120 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { JudicialService } from '../../services/judicial.service'; +import { JudiciaryPerson } from 'src/app/services/clients/api-client'; +import { debounceTime, tap } from 'rxjs'; +import { JudicialMemberDto } from '../models/add-judicial-member.model'; + +@Component({ + selector: 'app-search-for-judicial-member', + templateUrl: './search-for-judicial-member.component.html', + styleUrls: ['./search-for-judicial-member.component.scss'] +}) +export class SearchForJudicialMemberComponent { + readonly NotificationDelayTime = 1200; + + form: FormGroup; + searchResult: JudiciaryPerson[] = []; + showResult = false; + + @Input() saveButtonText = 'Save'; + @Input() existingJudicialMembers: JudicialMemberDto[] = []; + @Input() set existingJudicialMember(judicialMember: JudicialMemberDto) { + if (judicialMember) { + this.form.setValue( + { judiciaryEmail: judicialMember.email, displayName: judicialMember.displayName }, + { emitEvent: false, onlySelf: true } + ); + this.judicialMember = judicialMember; + this.form.controls.judiciaryEmail.disable(); + this.editMode = true; + } else { + this.editMode = false; + this.form.controls.judiciaryEmail.enable(); + } + } + + @Output() judicialMemberSelected = new EventEmitter(); + + judicialMember: JudicialMemberDto; + private editMode = false; + constructor(private judiciaryService: JudicialService) { + this.createForm(); + } + + createForm() { + this.form = new FormGroup({ + judiciaryEmail: new FormControl('', [Validators.required, Validators.minLength(3)]), + displayName: new FormControl('') + }); + + this.form.controls.judiciaryEmail.valueChanges + .pipe( + tap(() => { + this.form.controls.displayName.removeValidators(Validators.required); + this.form.controls.displayName.updateValueAndValidity({ emitEvent: false }); + }), + debounceTime(this.NotificationDelayTime) + ) + .subscribe(newJudiciaryEmail => { + if (newJudiciaryEmail === '') { + this.showResult = false; + this.form.reset({ + judiciaryEmail: '', + displayName: '' + }); + } + + if (this.form.controls.judiciaryEmail.invalid) { + return; + } + if (this.editMode) { + return; + } + this.searchForJudicialMember(); + }); + } + + searchForJudicialMember() { + this.judiciaryService.getJudicialUsers(this.form.value.judiciaryEmail).subscribe(result => { + // exclude existing judicial members from search results + result = result.filter(x => !this.existingJudicialMembers.find(y => y.personalCode === x.personal_code)); + this.searchResult = result; + this.showResult = true; + this.form.controls.displayName.addValidators(Validators.required); + this.form.controls.displayName.updateValueAndValidity({ emitEvent: false }); + }); + } + + selectJudicialMember(judicialMember: JudiciaryPerson) { + this.form.setValue( + { judiciaryEmail: judicialMember.email, displayName: judicialMember.full_name }, + { emitEvent: false, onlySelf: true } + ); + this.judicialMember = new JudicialMemberDto( + judicialMember.first_name, + judicialMember.last_name, + judicialMember.full_name, + judicialMember.email, + judicialMember.work_phone, + judicialMember.personal_code + ); + + this.showResult = false; + } + + confirmJudiciaryMemberWithDisplayName() { + this.judicialMember.displayName = this.form.controls.displayName.value; + this.judicialMemberSelected.emit(this.judicialMember); + this.form.reset({ + judiciaryEmail: '', + displayName: '' + }); + this.form.controls.displayName.removeValidators(Validators.required); + } +} + +interface SearchForJudicialMemberForm { + judiciaryEmail: FormControl; + displayName: FormControl; +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/other-information/other-information.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/other-information/other-information.component.spec.ts index ccf838acc..bbb596639 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/other-information/other-information.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/other-information/other-information.component.spec.ts @@ -15,6 +15,7 @@ import { of } from 'rxjs'; import { CaseModel } from 'src/app/common/model/case.model'; import { HearingModel } from 'src/app/common/model/hearing.model'; import { LaunchDarklyService, FeatureFlags } from 'src/app/services/launch-darkly.service'; +import { BreadcrumbStubComponent } from 'src/app/testing/stubs/breadcrumb-stub'; function initHearingRequest(): HearingModel { const participants: ParticipantModel[] = []; @@ -60,6 +61,7 @@ describe('OtherInformationComponent', () => { let fixture: ComponentFixture; launchDarklyServiceSpy = jasmine.createSpyObj('LaunchDarklyService', ['getFlag']); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); + launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); videoHearingsServiceSpy = jasmine.createSpyObj('VideoHearingsService', [ 'getCurrentRequest', 'cancelRequest', @@ -76,14 +78,15 @@ describe('OtherInformationComponent', () => { { provide: Router, useValue: routerSpy }, { provide: VideoHearingsService, useValue: videoHearingsServiceSpy }, { provide: Logger, useValue: loggerSpy }, - { provide: LaunchDarklyService, useValue: launchDarklyServiceSpy } + { provide: LaunchDarklyService, useValue: launchDarklyServiceSpy }, + { provide: BreadcrumbComponent, useClass: BreadcrumbStubComponent } ], declarations: [ OtherInformationComponent, - BreadcrumbComponent, CancelPopupStubComponent, ConfirmationPopupStubComponent, - DiscardConfirmPopupComponent + DiscardConfirmPopupComponent, + BreadcrumbStubComponent ] }).compileComponents(); const hearingRequest = initHearingRequest(); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.html index f4b3c1c73..5e852ad6f 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.html @@ -21,7 +21,12 @@
@@ -54,6 +59,7 @@
{{ participant.title }} {{ participant.first_name }} {{ participant.last_name }}
+
{{ participant.display_name }}
@@ -97,7 +103,12 @@
-
+ diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.scss b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.scss index 7edab4b18..3013b0f4a 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.scss +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.scss @@ -33,8 +33,6 @@ } &__email { color: #505a5f; - font-size: 1.1875rem; - line-height: 1.31578947; } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.spec.ts index f48d75a02..677836976 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.spec.ts @@ -7,6 +7,8 @@ import { Logger } from 'src/app/services/logger'; import { ParticipantItemComponent } from './participant-item.component'; import { VideoHearingsService } from 'src/app/services/video-hearings.service'; import { Constants } from 'src/app/common/constants'; +import { ParticipantModel } from 'src/app/common/model/participant.model'; +import { PageUrls } from 'src/app/shared/page-url.constants'; const router = { navigate: jasmine.createSpy('navigate'), @@ -26,7 +28,8 @@ describe('ParticipantItemComponent', () => { const participant: any = { title: 'Mrs', - first_name: 'Sam' + first_name: 'Sam', + isJudiciaryMember: false }; beforeEach(waitForAsync(() => { @@ -68,15 +71,47 @@ describe('ParticipantItemComponent', () => { it('should edit participant details', () => { component.isSummaryPage = true; - component.editParticipant({ email: 'email@hmcts.net', is_exist_person: false, is_judge: false }); + component.participant = { representee: 'rep', is_judge: false, is_exist_person: false, isJudiciaryMember: false }; + const pat: ParticipantModel = { + email: 'email@hmcts.net', + is_exist_person: false, + is_judge: false, + isJudiciaryMember: false + }; + component.editParticipant(pat); fixture.detectChanges(); expect(bookingServiceSpy.setEditMode).toHaveBeenCalled(); - expect(bookingServiceSpy.setEditMode).toHaveBeenCalledWith(); - expect(router.navigate).toHaveBeenCalled(); + expect(bookingServiceSpy.setParticipantEmail).toHaveBeenCalledWith(pat.email); + expect(router.navigate).toHaveBeenCalledWith([PageUrls.AddParticipants]); + }); + + it('should edit judicial office holder details', () => { + component.isSummaryPage = true; + component.participant = { representee: 'rep', is_judge: true, is_exist_person: false, isJudiciaryMember: true }; + const pat: ParticipantModel = { email: 'email@hmcts.net', is_exist_person: false, is_judge: true, isJudiciaryMember: true }; + component.editParticipant(pat); + fixture.detectChanges(); + expect(bookingServiceSpy.setEditMode).toHaveBeenCalled(); + expect(bookingServiceSpy.setParticipantEmail).toHaveBeenCalledWith(pat.email); + expect(router.navigate).toHaveBeenCalledWith([PageUrls.AddJudicialOfficeHolders]); + }); + + it('should emit edit event for non-summary page', () => { + component.isSummaryPage = false; + const pat: ParticipantModel = { + email: 'email@hmcts.net', + is_exist_person: false, + is_judge: false, + isJudiciaryMember: false + }; + spyOn(component.edit, 'emit'); + component.editParticipant(pat); + fixture.detectChanges(); + expect(component.edit.emit).toHaveBeenCalledWith(pat); }); it('should return true if participant has a representative', () => { - component.participant = { representee: 'rep', is_judge: false, is_exist_person: false }; + component.participant = { representee: 'rep', is_judge: false, is_exist_person: false, isJudiciaryMember: false }; fixture.detectChanges(); expect(component.isRepresentative).toBeTruthy(); }); @@ -104,35 +139,53 @@ describe('ParticipantItemComponent', () => { }); it('should return false if participant`s case role is None', () => { - component.participant = { case_role_name: 'None', is_judge: true, is_exist_person: false }; + component.participant = { case_role_name: 'None', is_judge: true, is_exist_person: false, isJudiciaryMember: false }; fixture.detectChanges(); expect(component.hasCaseRole).toBeFalsy(); }); it('should return true if participant is an observer', () => { - component.participant = { hearing_role_name: 'Observer', is_judge: true, is_exist_person: false }; + component.participant = { hearing_role_name: 'Observer', is_judge: true, is_exist_person: false, isJudiciaryMember: false }; fixture.detectChanges(); expect(component.isObserverOrPanelMember).toBeTruthy(); }); it('should return true if participant is a panel member', () => { - component.participant = { hearing_role_name: 'Panel Member', is_judge: true, is_exist_person: false }; + component.participant = { hearing_role_name: 'Panel Member', is_judge: true, is_exist_person: false, isJudiciaryMember: false }; fixture.detectChanges(); expect(component.isObserverOrPanelMember).toBeTruthy(); }); it('should return true if participant has a case role and is not a Panel Member', () => { - component.participant = { hearing_role_name: 'Judge', case_role_name: 'Judge', is_judge: true, is_exist_person: false }; + component.participant = { + hearing_role_name: 'Judge', + case_role_name: 'Judge', + is_judge: true, + is_exist_person: false, + isJudiciaryMember: false + }; fixture.detectChanges(); expect(component.displayCaseRole).toBeTruthy(); }); it('should get judge email', () => { - component.participant = { hearing_role_name: 'Judge', case_role_name: 'Judge', is_judge: true, is_exist_person: false }; + component.participant = { + hearing_role_name: 'Judge', + case_role_name: 'Judge', + is_judge: true, + is_exist_person: false, + isJudiciaryMember: false + }; const email = component.getJudgeEmail(); expect(email).toBe('James.Doe@hmcts.net'); }); it('should get judge phone', () => { - component.participant = { hearing_role_name: 'Judge', case_role_name: 'Judge', is_judge: true, is_exist_person: false }; + component.participant = { + hearing_role_name: 'Judge', + case_role_name: 'Judge', + is_judge: true, + is_exist_person: false, + isJudiciaryMember: false + }; const phone = component.getJudgePhone(component.participant); expect(phone).toBe('123456789'); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.ts index eede2b39e..6143404c6 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/item/participant-item.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { ParticipantModel } from 'src/app/common/model/participant.model'; import { BookingService } from 'src/app/services/booking.service'; @@ -8,13 +8,14 @@ import { OtherInformationModel } from '../../../common/model/other-information.m import { HearingModel } from '../../../common/model/hearing.model'; import { VideoHearingsService } from 'src/app/services/video-hearings.service'; import { Constants } from 'src/app/common/constants'; +import { HearingRoleCodes } from '../../../common/model/hearing-roles.model'; @Component({ selector: 'app-participant-item', templateUrl: './participant-item.component.html', styleUrls: ['./participant-item.component.scss'] }) -export class ParticipantItemComponent { +export class ParticipantItemComponent implements OnInit { private readonly loggerPrefix = '[ParticipantList - Item] -'; @Input() participant: ParticipantModel; @@ -26,6 +27,8 @@ export class ParticipantItemComponent { @Output() remove = new EventEmitter(); staffMemberRole = Constants.HearingRoles.StaffMember; + showParticipantActions: boolean; + showJudicaryActions: boolean; constructor( private bookingService: BookingService, @@ -34,16 +37,28 @@ export class ParticipantItemComponent { private videoHearingsService: VideoHearingsService ) {} + ngOnInit(): void { + this.showParticipantActions = this.router.url.includes(PageUrls.AddParticipants) || this.router.url.includes(PageUrls.Summary); + this.showJudicaryActions = + this.router.url.includes(PageUrls.AddJudicialOfficeHolders) || this.router.url.includes(PageUrls.Summary); + } + getJudgeUser(participant: ParticipantModel): string { return participant.username; } getJudgeEmail(): string { + if (this.participant.isJudiciaryMember) { + return null; // username and email are the same, no need to show it twice + } const otherInformation = OtherInformationModel.init(this.hearing.other_information); return otherInformation.JudgeEmail; } getJudgePhone(participant: ParticipantModel): string { + if (this.participant.isJudiciaryMember) { + return this.participant.phone; // ejud data does not have phone number + } const otherInformation = OtherInformationModel.init(this.hearing.other_information); return otherInformation.JudgePhone ?? participant.phone; } @@ -55,10 +70,16 @@ export class ParticipantItemComponent { editParticipant(participant: ParticipantModel) { this.editJudge(); - if (this.isSummaryPage) { + if (this.isSummaryPage && !this.participant.isJudiciaryMember) { this.bookingService.setParticipantEmail(participant.email); this.logger.debug(`${this.loggerPrefix} Navigating back to participants to edit`, { participant: participant.email }); this.router.navigate([PageUrls.AddParticipants]); + } else if (this.isSummaryPage && this.participant.isJudiciaryMember) { + this.logger.debug(`${this.loggerPrefix} Navigating back to judicial office holders to edit`, { + participant: participant.email + }); + this.bookingService.setParticipantEmail(participant.email); + this.router.navigate([PageUrls.AddJudicialOfficeHolders]); } else { this.edit.emit(participant); } @@ -86,7 +107,10 @@ export class ParticipantItemComponent { } get isObserverOrPanelMember() { - return ['Observer', 'Panel Member'].includes(this.participant?.hearing_role_name); + return ( + ['Observer', 'Panel Member'].includes(this.participant?.hearing_role_name) || + [HearingRoleCodes.Observer, 'PanelMember'].includes(this.participant?.hearing_role_code) + ); } get displayCaseRole() { @@ -94,7 +118,7 @@ export class ParticipantItemComponent { } get isInterpreter() { - return this.participant.hearing_role_name === 'Interpreter'; + return this.participant.hearing_role_name === 'Interpreter' || this.participant.hearing_role_code === HearingRoleCodes.Interpreter; } get isInterpretee() { diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.html index a4af6af61..2243b0c15 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.html @@ -1,5 +1,15 @@

{{ isEditMode ? 'Participants' : 'Participants added' }}

+ ('Logger', ['error', 'debug', 'warn']); const router = { @@ -24,8 +26,16 @@ describe('ParticipantListComponent', () => { const pat1 = new ParticipantModel(); pat1.title = 'Mrs'; pat1.first_name = 'Sam'; + pat1.display_name = 'Sam'; pat1.addedDuringHearing = false; - const participants: any[] = [pat1, pat1]; + pat1.hearing_role_code = HearingRoleCodes.Applicant; + const pat2 = new ParticipantModel(); + pat2.title = 'Mr'; + pat2.first_name = 'John'; + pat2.display_name = 'Doe'; + pat2.addedDuringHearing = false; + pat2.hearing_role_code = HearingRoleCodes.Applicant; + const participants: any[] = [pat1, pat2]; beforeEach(waitForAsync(() => { videoHearingsServiceSpy = jasmine.createSpyObj(['isConferenceClosed', 'isHearingAboutToStart']); @@ -48,54 +58,147 @@ describe('ParticipantListComponent', () => { fixture.detectChanges(); }); + describe('ngDoCheck - sorting participants on change', () => { + it('should call sortParticipants when participant list changes', () => { + const sortSpy = spyOn(component, 'sortParticipants'); + component.hearing.participants = [{ display_name: 'B' }, { display_name: 'A' }, { display_name: 'C' }]; + component.sortedParticipants = [{ display_name: 'A' }, { display_name: 'B' }]; + component.ngDoCheck(); + expect(sortSpy).toHaveBeenCalled(); + }); + + it('should not call sortParticipants when participant list does not change', () => { + const sortSpy = spyOn(component, 'sortParticipants'); + component.hearing.participants = [{ display_name: 'A' }, { display_name: 'B' }]; + component.sortedParticipants = [{ display_name: 'A' }, { display_name: 'B' }]; + component.ngDoCheck(); + expect(sortSpy).not.toHaveBeenCalled(); + }); + + it('should call sortJudiciaryMembers when judiciary participant list changes', () => { + const johJudge = new JudicialMemberDto('Test', 'User', 'Test User', 'testjudge@test.com', '1234567890', '1234'); + johJudge.roleCode = 'Judge'; + johJudge.displayName = 'Judge Test User'; + + const johPm1 = new JudicialMemberDto('Test PM 1', 'User PM 1', 'Test User PM 1', 'testpm1@test.com', '1234567890', '2345'); + johPm1.displayName = 'Test User 1'; + johPm1.roleCode = 'PanelMember'; + const johPm2 = new JudicialMemberDto('Test PM 2', 'User PM 2', 'Test User PM 2', 'testpm2test.com', '123456098', '3456'); + johPm2.displayName = 'Test User 2'; + johPm2.roleCode = 'PanelMember'; + component.hearing.judiciaryParticipants = [johPm2, johJudge, johPm1]; + component.sortedJudiciaryMembers = []; + component.ngDoCheck(); + expect(component.sortedJudiciaryMembers[0].hearing_role_code).toEqual('Judge'); + expect(component.sortedJudiciaryMembers[1].hearing_role_code).toEqual('PanelMember'); + expect(component.sortedJudiciaryMembers[1].display_name).toEqual(johPm1.displayName); + expect(component.sortedJudiciaryMembers[2].hearing_role_code).toEqual('PanelMember'); + expect(component.sortedJudiciaryMembers[2].display_name).toEqual(johPm2.displayName); + }); + }); + it('should create participants list component', () => { expect(component).toBeTruthy(); }); - it('should display participants', done => { - component.hearing.participants = participants; + it('should display participants', fakeAsync(() => { + component.sortedParticipants = []; + component.ngOnInit(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const elementArray = debugElement.queryAll(By.css('app-participant-item')); - expect(elementArray.length).toBeGreaterThan(0); - expect(elementArray.length).toBe(2); - done(); + + tick(); + component.hearing.participants = participants; + fixture.detectChanges(); + tick(); + const elementArray = debugElement.queryAll(By.css('app-participant-item')); + expect(elementArray.length).toBeGreaterThan(0); + expect(elementArray.length).toBe(2); + })); + + describe('Edit rules', () => { + it('should emit on remove', () => { + spyOn(component.$selectedForRemove, 'emit'); + component.removeParticipant({ email: 'email@hmcts.net', is_exist_person: false, is_judge: false }); + expect(component.$selectedForRemove.emit).toHaveBeenCalled(); + }); + it('should not be able to edit participant if canEdit is false', () => { + component.canEdit = false; + expect(component.canEditParticipant(pat1)).toBe(false); + }); + it('should not be able to edit participant if canEdit is true and hearing is closed', () => { + component.canEdit = true; + videoHearingsServiceSpy.isConferenceClosed.and.returnValue(true); + expect(component.canEditParticipant(pat1)).toBe(false); + }); + it('should not be able to edit participant if canEdit is true, hearing is open, hearing is about to start and addedDuringHearing is false', () => { + component.canEdit = true; + videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); + videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(true); + pat1.addedDuringHearing = false; + expect(component.canEditParticipant(pat1)).toBe(false); + }); + it('should be able to edit participant if canEdit is true, hearing is open and about to start & addedDuringHearing is true', () => { + component.canEdit = true; + videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); + videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(true); + pat1.addedDuringHearing = true; + expect(component.canEditParticipant(pat1)).toBe(true); + }); + it('should be able to edit participant if canEdit is true, hearing is open and hearing is not about to start', () => { + component.canEdit = true; + videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); + videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(false); + expect(component.canEditParticipant(pat1)).toBe(true); }); }); - it('should emit on remove', () => { - spyOn(component.$selectedForRemove, 'emit'); - component.removeParticipant({ email: 'email@hmcts.net', is_exist_person: false, is_judge: false }); - expect(component.$selectedForRemove.emit).toHaveBeenCalled(); - }); - it('should not be able to edit participant if canEdit is false', () => { - component.canEdit = false; - expect(component.canEditParticipant(pat1)).toBe(false); - }); - it('should not be able to edit participant if canEdit is true and hearing is closed', () => { - component.canEdit = true; - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(true); - expect(component.canEditParticipant(pat1)).toBe(false); - }); - it('should not be able to edit participant if canEdit is true, hearing is open, hearing is about to start and addedDuringHearing is false', () => { - component.canEdit = true; - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); - videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(true); - pat1.addedDuringHearing = false; - expect(component.canEditParticipant(pat1)).toBe(false); - }); - it('should be able to edit participant if canEdit is true, hearing is open and about to start & addedDuringHearing is true', () => { - component.canEdit = true; - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); - videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(true); - pat1.addedDuringHearing = true; - expect(component.canEditParticipant(pat1)).toBe(true); - }); - it('should be able to edit participant if canEdit is true, hearing is open and hearing is not about to start', () => { - component.canEdit = true; - videoHearingsServiceSpy.isConferenceClosed.and.returnValue(false); - videoHearingsServiceSpy.isHearingAboutToStart.and.returnValue(false); - expect(component.canEditParticipant(pat1)).toBe(true); + + describe('sortJudiciaryMembers', () => { + let judge: JudicialMemberDto; + let panelMember: JudicialMemberDto; + + beforeEach(() => { + judge = new JudicialMemberDto('Judge', 'Fudge', 'Judge Fudge', 'judge@test.com', '1234567890', '1234'); + judge.roleCode = 'Judge'; + judge.displayName = 'Judge Fudge'; + + panelMember = new JudicialMemberDto('John', 'Doe', 'John Doe', 'pm@test.com', '2345678901', '2345'); + panelMember.roleCode = 'PanelMember'; + panelMember.displayName = 'PM Doe'; + }); + + it('should not sort if hearing.judiciaryParticipants is not defined', () => { + component.hearing.judiciaryParticipants = undefined; + component.sortJudiciaryMembers(); + expect(component.sortedJudiciaryMembers).toEqual([]); + }); + + it('should sort judiciary members with Judge at the beginning', () => { + component.hearing.judiciaryParticipants = [panelMember, judge]; + component.sortJudiciaryMembers(); + expect(component.sortedJudiciaryMembers[0].hearing_role_code).toEqual('Judge'); + expect(component.sortedJudiciaryMembers[1].hearing_role_code).toEqual('PanelMember'); + }); + + it('should sort judiciary members with Judge at the beginning even if Judge is last in the original list', () => { + component.hearing.judiciaryParticipants = [judge, panelMember]; + component.sortJudiciaryMembers(); + expect(component.sortedJudiciaryMembers[0].hearing_role_code).toEqual('Judge'); + expect(component.sortedJudiciaryMembers[1].hearing_role_code).toEqual('PanelMember'); + }); + + it('should not change the order if all judiciary members are Judges', () => { + component.hearing.judiciaryParticipants = [judge, judge]; + component.sortJudiciaryMembers(); + expect(component.sortedJudiciaryMembers[0].hearing_role_code).toEqual('Judge'); + expect(component.sortedJudiciaryMembers[1].hearing_role_code).toEqual('Judge'); + }); + + it('should not change the order if there are no Judges', () => { + component.hearing.judiciaryParticipants = [panelMember, panelMember]; + component.sortJudiciaryMembers(); + expect(component.sortedJudiciaryMembers[0].hearing_role_code).toEqual('PanelMember'); + expect(component.sortedJudiciaryMembers[1].hearing_role_code).toEqual('PanelMember'); + }); }); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.ts index 41a2df743..03de9cc9d 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/participant/list/participant-list.component.ts @@ -5,6 +5,7 @@ import { LinkedParticipantType } from 'src/app/services/clients/api-client'; import { Logger } from 'src/app/services/logger'; import { VideoHearingsService } from 'src/app/services/video-hearings.service'; import { HearingModel } from '../../../common/model/hearing.model'; +import { HearingRoleCodes } from '../../../common/model/hearing-roles.model'; @Component({ selector: 'app-participant-list', @@ -14,6 +15,7 @@ import { HearingModel } from '../../../common/model/hearing.model'; export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { @Input() hearing: HearingModel; sortedParticipants: ParticipantModel[] = []; + sortedJudiciaryMembers: ParticipantModel[] = []; $selectedForEdit = new EventEmitter(); $selectedForRemove = new EventEmitter(); @@ -26,14 +28,50 @@ export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { constructor(private logger: Logger, private videoHearingsService: VideoHearingsService) {} ngDoCheck(): void { - const containsNewParticipants = - !this.hearing?.participants?.every(hearingParticipant => this.sortedParticipants.includes(hearingParticipant)) ?? false; - const containsRemovedParticipants = - !this.sortedParticipants?.every(sortedParticipant => this.hearing.participants.includes(sortedParticipant)) ?? false; - - if (containsNewParticipants || containsRemovedParticipants) { + const participantsLocal = [...(this.hearing?.participants || [])].sort(this.sortByDisplayName()); + const sortedParticipantslocal = [...(this.sortedParticipants || [])].sort(this.sortByDisplayName()); + const hasParticipantListChanged = JSON.stringify(participantsLocal) !== JSON.stringify(sortedParticipantslocal); + if (hasParticipantListChanged) { this.sortParticipants(); } + + const judicialMembersLocal = + this.hearing?.judiciaryParticipants + ?.map(j => ({ email: j.email, displayName: j.displayName, role: j.roleCode })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)) ?? []; + const sortedJudicialMembersLocal = + this.sortedJudiciaryMembers + ?.map(j => ({ email: j.email, displayName: j.display_name, role: j.hearing_role_code })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)) ?? []; + + const judiciaryEmailListChanged = JSON.stringify(judicialMembersLocal) !== JSON.stringify(sortedJudicialMembersLocal); + if (judiciaryEmailListChanged) { + this.sortJudiciaryMembers(); + } + } + + sortJudiciaryMembers() { + if (!this.hearing.judiciaryParticipants) { + return; + } + + const judicialJudge = [this.hearing.judiciaryParticipants.filter(j => j.roleCode === 'Judge')][0]?.map(h => + ParticipantModel.fromJudicialMember(h, true) + ); + const judicialPanelMembers = this.getJudicialPanelMembers(); + + const sortedJohList = [...judicialJudge, ...judicialPanelMembers]; + + sortedJohList.sort((a, b) => { + if (a.hearing_role_code.includes('Judge') && !b.hearing_role_code.includes('Judge')) { + return -1; + } else if (!a.hearing_role_code.includes('Judge') && b.hearing_role_code.includes('Judge')) { + return 1; + } else { + return 0; + } + }); + this.sortedJudiciaryMembers = sortedJohList; } ngOnChanges() { @@ -61,13 +99,14 @@ export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { } sortParticipants() { - if (!this.hearing.participants) { + if (!this.hearing.participants && !this.hearing.judiciaryParticipants) { return; } const judges = this.getJudges(); const staffMembers = this.getStaffMembers(); const panelMembers = this.getPanelMembers(); const observers = this.getObservers(); + const others = this.getOthers(staffMembers, panelMembers, observers); const sortedList = [...judges, ...panelMembers, ...staffMembers, ...others, ...observers]; @@ -76,11 +115,23 @@ export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { this.sortedParticipants = sortedList; } + private sortByDisplayName() { + return (a: ParticipantModel, b: ParticipantModel) => { + if (a.display_name < b.display_name) { + return -1; + } + if (a.display_name > b.display_name) { + return 1; + } + return 0; + }; + } + private compareByPartyThenByFirstName() { - return (a, b) => { + return (a: ParticipantModel, b: ParticipantModel) => { const swapIndices = a > b ? 1 : 0; - const partyA = a.case_role_name === Constants.None ? a.hearing_role_name : a.case_role_name; - const partyB = b.case_role_name === Constants.None ? b.hearing_role_name : b.case_role_name; + const partyA = a.case_role_name === Constants.None ? a.hearing_role_name ?? a.hearing_role_code : a.case_role_name; + const partyB = b.case_role_name === Constants.None ? b.hearing_role_name ?? b.hearing_role_code : b.case_role_name; if (partyA === partyB) { return a.first_name < b.first_name ? -1 : swapIndices; } @@ -96,7 +147,8 @@ export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { !staffMembers.includes(participant) && !panelMembers.includes(participant) && !observers.includes(participant) && - participant.hearing_role_name !== Constants.HearingRoles.Interpreter + (!participant.hearing_role_name || participant.hearing_role_name !== Constants.HearingRoles.Interpreter) && + (!participant.hearing_role_code || participant.hearing_role_code !== HearingRoleCodes.Interpreter) ) .sort(this.compareByPartyThenByFirstName()); } @@ -111,6 +163,14 @@ export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { .sort(this.compareByPartyThenByFirstName()); } + private getJudicialPanelMembers(): ParticipantModel[] { + console.log(this.hearing.judiciaryParticipants); + return this.hearing.judiciaryParticipants + .filter(j => j.roleCode === 'PanelMember') + .sort((a, b) => a.displayName.localeCompare(b.displayName)) + .map(h => ParticipantModel.fromJudicialMember(h, false)); + } + private getPanelMembers() { return this.hearing.participants .filter(participant => @@ -134,7 +194,9 @@ export class ParticipantListComponent implements OnInit, OnChanges, DoCheck { private insertInterpreters(sortedList: ParticipantModel[]) { this.clearInterpreteeList(); const interpreters = this.hearing.participants.filter( - participant => participant.hearing_role_name === Constants.HearingRoles.Interpreter + participant => + participant.hearing_role_name === Constants.HearingRoles.Interpreter || + participant.hearing_role_code === HearingRoleCodes.Interpreter ); interpreters.forEach(interpreterParticipant => { let interpretee: ParticipantModel; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/services/judicial.service.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/services/judicial.service.spec.ts new file mode 100644 index 000000000..beb6da4c0 --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/services/judicial.service.spec.ts @@ -0,0 +1,29 @@ +import { JudicialService } from './judicial.service'; +import { of } from 'rxjs'; +import { BHClient, JudiciaryPerson } from 'src/app/services/clients/api-client'; + +describe('JudicialService', () => { + let service: JudicialService; + let bhClientSpy: jasmine.SpyObj; + + beforeEach(() => { + bhClientSpy = jasmine.createSpyObj('BHClient', ['searchForJudiciaryPerson']); + service = new JudicialService(bhClientSpy); + }); + + describe('getJudicialUsers', () => { + it('should return an array of JudiciaryPerson objects with work_phone set to "01234567890"', () => { + const searchText = 'test'; + const expectedJudicialUsers: JudiciaryPerson[] = [ + new JudiciaryPerson({ personal_code: '1', full_name: 'John Doe', work_phone: '01234567890' }), + new JudiciaryPerson({ personal_code: '2', full_name: 'Jane Doe', work_phone: '01234567890' }) + ]; + bhClientSpy.searchForJudiciaryPerson.and.returnValue(of(expectedJudicialUsers)); + + service.getJudicialUsers(searchText).subscribe(judicialUsers => { + expect(judicialUsers).toEqual(expectedJudicialUsers); + expect(bhClientSpy.searchForJudiciaryPerson).toHaveBeenCalledWith(searchText); + }); + }); + }); +}); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/services/judicial.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/services/judicial.service.ts new file mode 100644 index 000000000..2980ff35e --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/services/judicial.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { BHClient, JudiciaryPerson } from 'src/app/services/clients/api-client'; + +@Injectable({ + providedIn: 'root' +}) +export class JudicialService { + constructor(private bhClient: BHClient) {} + + getJudicialUsers(searchText: string): Observable { + return this.bhClient.searchForJudiciaryPerson(searchText); + } +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.spec.ts index 2b20251db..19fb0f018 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.spec.ts @@ -36,6 +36,7 @@ import { SummaryComponent } from './summary.component'; import { ResponseTestData } from 'src/app/testing/data/response-test-data'; import { BookingStatusService } from 'src/app/services/booking-status-service'; import { FeatureFlags, LaunchDarklyService } from 'src/app/services/launch-darkly.service'; +import { TruncatableTextComponent } from 'src/app/shared/truncatable-text/truncatable-text.component'; function initExistingHearingRequest(): HearingModel { const pat1 = new ParticipantModel(); @@ -122,6 +123,7 @@ videoHearingsServiceSpy = jasmine.createSpyObj('VideoHeari ]); const launchDarklyServiceSpy = jasmine.createSpyObj('LaunchDarklyService', ['getFlag']); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); +launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.useV2Api).and.returnValue(of(false)); const bookingStatusService = new BookingStatusService(videoHearingsServiceSpy); describe('SummaryComponent with valid request', () => { @@ -161,7 +163,8 @@ describe('SummaryComponent with valid request', () => { WaitPopupComponent, SaveFailedPopupComponent, LongDatetimePipe, - RemoveInterpreterPopupComponent + RemoveInterpreterPopupComponent, + TruncatableTextComponent ], imports: [RouterTestingModule] }).compileComponents(); @@ -618,7 +621,8 @@ describe('SummaryComponent with invalid request', () => { WaitPopupComponent, SaveFailedPopupComponent, LongDatetimePipe, - RemoveInterpreterPopupComponent + RemoveInterpreterPopupComponent, + TruncatableTextComponent ] }).compileComponents(); })); @@ -676,7 +680,8 @@ describe('SummaryComponent with existing request', () => { RemovePopupComponent, WaitPopupComponent, SaveFailedPopupComponent, - LongDatetimePipe + LongDatetimePipe, + TruncatableTextComponent ] }).compileComponents(); })); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.ts index 8ff114e3d..a34e6dddf 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/booking/summary/summary.component.ts @@ -24,7 +24,7 @@ import { PageUrls } from '../../shared/page-url.constants'; import { ParticipantListComponent } from '../participant'; import { ParticipantService } from '../services/participant.service'; import { OtherInformationModel } from '../../common/model/other-information.model'; -import { first } from 'rxjs/operators'; +import { finalize, first } from 'rxjs/operators'; import { BookingStatusService } from 'src/app/services/booking-status-service'; import { FeatureFlags, LaunchDarklyService } from 'src/app/services/launch-darkly.service'; @@ -73,6 +73,7 @@ export class SummaryComponent implements OnInit, OnDestroy { @ViewChild(RemoveInterpreterPopupComponent) removeInterpreterPopupComponent: RemoveInterpreterPopupComponent; judgeAssigned: boolean; ejudFeatureFlag = false; + useApiV2 = false; saveFailedMessages: string[]; constructor( @@ -95,6 +96,24 @@ export class SummaryComponent implements OnInit, OnDestroy { this.ejudFeatureFlag = result; }) ); + + this.$subscriptions.push( + this.featureService + .getFlag(FeatureFlags.eJudFeature) + .pipe(first()) + .subscribe(result => { + this.ejudFeatureFlag = result; + }) + ); + + this.$subscriptions.push( + this.featureService + .getFlag(FeatureFlags.useV2Api) + .pipe(first()) + .subscribe(result => { + this.useApiV2 = result; + }) + ); } ngOnInit() { @@ -115,7 +134,9 @@ export class SummaryComponent implements OnInit, OnDestroy { }) ); } - this.judgeAssigned = this.hearing.participants.filter(e => e.is_judge).length > 0; + this.judgeAssigned = + this.hearing.participants.filter(e => e.is_judge).length > 0 || + this.hearing.judiciaryParticipants.some(e => e.roleCode === 'Judge'); } private checkForExistingRequest() { @@ -141,17 +162,26 @@ export class SummaryComponent implements OnInit, OnDestroy { private confirmRemoveParticipant() { const participant = this.hearing.participants.find(x => x.email.toLowerCase() === this.selectedParticipantEmail.toLowerCase()); - const title = participant?.title ? `${participant.title}` : ''; - this.removerFullName = participant ? `${title} ${participant.first_name} ${participant.last_name}` : ''; - - const isInterpretee = - (participant.linked_participants && - participant.linked_participants.length > 0 && - participant.hearing_role_name.toLowerCase() !== HearingRoles.INTERPRETER) || - this.hearing.participants.some(p => p.interpreterFor === participant.email); - if (isInterpretee) { - this.showConfirmRemoveInterpretee = true; - } else { + + if (participant) { + const title = participant?.title ? `${participant.title}` : ''; + this.removerFullName = participant ? `${title} ${participant.first_name} ${participant.last_name}` : ''; + + const isInterpretee = + (participant.linked_participants && + participant.linked_participants.length > 0 && + participant.hearing_role_name.toLowerCase() !== HearingRoles.INTERPRETER) || + this.hearing.participants.some(p => p.interpreterFor === participant.email); + if (isInterpretee) { + this.showConfirmRemoveInterpretee = true; + } else { + this.showConfirmationRemoveParticipant = true; + } + } + + const judicalParticipant = this.hearing.judiciaryParticipants.findIndex(x => x.email === this.selectedParticipantEmail); + if (judicalParticipant > -1) { + this.removerFullName = this.hearing.judiciaryParticipants[judicalParticipant].fullName; this.showConfirmationRemoveParticipant = true; } } @@ -182,10 +212,17 @@ export class SummaryComponent implements OnInit, OnDestroy { this.hearing.participants.splice(indexOfParticipant, 1); this.removeLinkedParticipant(this.selectedParticipantEmail); this.hearing = { ...this.hearing }; - this.hearingService.updateHearingRequest(this.hearing); - this.hearingService.setBookingHasChanged(true); - this.bookingService.removeParticipantEmail(); } + + const judicalParticipant = this.hearing.judiciaryParticipants.findIndex(x => x.email === this.selectedParticipantEmail); + if (judicalParticipant > -1) { + this.hearing.judiciaryParticipants.splice(judicalParticipant, 1); + this.hearing = { ...this.hearing }; + } + + this.hearingService.updateHearingRequest(this.hearing); + this.hearingService.setBookingHasChanged(true); + this.bookingService.removeParticipantEmail(); } private retrieveHearingSummary() { @@ -359,6 +396,9 @@ export class SummaryComponent implements OnInit, OnDestroy { this.$subscriptions.push( this.hearingService.updateHearing(this.hearing).subscribe({ next: (hearingDetailsResponse: HearingDetailsResponse) => { + const noJudgePrior = + this.hearing.status === BookingStatus.BookedWithoutJudge || + this.hearing.status === BookingStatus.ConfirmedWithoutJudge; this.showWaitSaving = false; this.hearingService.setBookingHasChanged(false); this.logger.info(`${this.loggerPrefix} Updated booking. Navigating to booking details.`, { @@ -371,7 +411,20 @@ export class SummaryComponent implements OnInit, OnDestroy { return; } sessionStorage.setItem(this.newHearingSessionKey, hearingDetailsResponse.id); - this.router.navigate([PageUrls.BookingConfirmation]); + if (this.judgeAssigned && noJudgePrior) { + this.showWaitSaving = true; + this.bookingStatusService + .pollForStatus(hearingDetailsResponse.id) + .pipe( + finalize(() => { + this.showWaitSaving = false; + this.router.navigate([PageUrls.BookingConfirmation]); + }) + ) + .subscribe(); + } else { + this.router.navigate([PageUrls.BookingConfirmation]); + } }, error: error => { this.logger.error(`${this.loggerPrefix} Failed to update hearing with ID: ${this.hearing.hearing_id}.`, error, { @@ -470,6 +523,10 @@ export class SummaryComponent implements OnInit, OnDestroy { } navToAddJudge() { - this.router.navigate([PageUrls.AssignJudge]); + if (this.useApiV2) { + this.router.navigate([PageUrls.AddJudicialOfficeHolders]); + } else { + this.router.navigate([PageUrls.AssignJudge]); + } } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.html index aa5d3204a..699ed1b70 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.html @@ -27,6 +27,7 @@

@@ -76,7 +77,7 @@

- +
diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.spec.ts index 9745f02a4..ff9481dbb 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.spec.ts @@ -26,6 +26,7 @@ import { VideoHearingsService } from '../../services/video-hearings.service'; import { PageUrls } from '../../shared/page-url.constants'; import { BookingDetailsComponent } from './booking-details.component'; import { BookingStatusService } from 'src/app/services/booking-status-service'; +import { HearingRoleCodes } from '../../common/model/hearing-roles.model'; let component: BookingDetailsComponent; let videoHearingServiceSpy: jasmine.SpyObj; @@ -75,6 +76,7 @@ export class BookingDetailsTestData { 'email1@hmcts.net', 'Applicant', 'Representative', + HearingRoleCodes.Representative, 'Alan Brake', '', 'ABC Solicitors', @@ -94,6 +96,7 @@ export class BookingDetailsTestData { 'email2@hmcts.net', 'Applicant', 'Litigant in person', + HearingRoleCodes.Applicant, 'Roy Bark', '', 'ABC Solicitors', @@ -113,6 +116,7 @@ export class BookingDetailsTestData { 'email3@hmcts.net', 'Respondent', 'Litigant in person', + HearingRoleCodes.Respondent, 'Fill', '', 'ABC Solicitors', diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.ts index 0077e6efa..54c65f7a0 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-details/booking-details.component.ts @@ -6,6 +6,7 @@ import { ReturnUrlService } from 'src/app/services/return-url.service'; import { BookingsDetailsModel } from '../../common/model/bookings-list.model'; import { HearingModel } from '../../common/model/hearing.model'; import { ParticipantDetailsModel } from '../../common/model/participant-details.model'; +import { JudiciaryParticipantDetailsModel } from 'src/app/common/model/judiciary-participant-details.model'; import { BookingDetailsService } from '../../services/booking-details.service'; import { BookingService } from '../../services/booking.service'; import { BookingPersistService } from '../../services/bookings-persist.service'; @@ -34,6 +35,7 @@ export class BookingDetailsComponent implements OnInit, OnDestroy { booking: HearingModel; participants: Array = []; judges: Array = []; + judicialMembers: Array = []; isVhOfficerAdmin = false; showCancelBooking: boolean; showConfirming: boolean; @@ -137,6 +139,7 @@ export class BookingDetailsComponent implements OnInit, OnDestroy { const participants_and_judges = this.bookingDetailsService.mapBookingParticipants(hearingResponse); this.participants = participants_and_judges.participants; this.judges = participants_and_judges.judges; + this.judicialMembers = participants_and_judges.judicialMembers; this.hearing.Endpoints = this.bookingDetailsService.mapBookingEndpoints(hearingResponse); this.videoHearingService .getAllocatedCsoForHearing(hearingResponse.id) @@ -342,6 +345,6 @@ CY: ${this.conferencePhoneNumberWelsh} (ID: ${this.telephoneConferenceId})`; } get judgeExists(): boolean { - return this.judges.length > 0; + return this.judges.length > 0 || this.judicialMembers.some(j => j.isJudge); } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.html index bb08d23e1..6df2ee85b 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.html @@ -8,6 +8,14 @@

Participants

class="participant-detail judge-detail" > + + { let component: BookingParticipantListComponent; @@ -24,7 +16,7 @@ describe('BookingParticipantListComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [BookingParticipantListComponent, ParticipantDetailsMockComponent], + declarations: [BookingParticipantListComponent, ParticipantDetailsComponent], imports: [RouterTestingModule] }).compileComponents(); })); @@ -51,6 +43,7 @@ describe('BookingParticipantListComponent', () => { 'email1@hmcts.net', 'Respondent', 'Litigant in person', + HearingRoleCodes.Respondent, 'Alan Brake', '', 'ABC Solicitors', @@ -87,6 +80,7 @@ describe('BookingParticipantListComponent', () => { 'email1@hmcts.net', 'Judge', 'Judge', + null, 'Alan Brake', '', 'ABC Solicitors', @@ -119,6 +113,7 @@ describe('BookingParticipantListComponent', () => { participantsArray.push({ FirstName: p.FirstName, isJudge: p.isJudge ?? false, + HearingRoleCode: p.HearingRoleCode, HearingRoleName: p.HearingRoleName, CaseRoleName: p.CaseRoleName, LinkedParticipants: p.LinkedParticipants ?? null, @@ -218,4 +213,47 @@ describe('BookingParticipantListComponent', () => { } done(); }); + + it('should sort judiciary participants and members', () => { + const jp1 = new JudiciaryParticipantDetailsModel( + 'Mrs', + 'Alan', + 'Brake', + 'Judge', + 'email.p1@hmcts.net', + 'email1@hmcts.net', + 'Judge', + 'Judge', + 'Alan Brake' + ); + const jp2 = new JudiciaryParticipantDetailsModel( + 'Mr', + 'John', + 'Doe', + 'Winger', + 'email.p2@hmcts.net', + 'email2@hmcts.net', + 'Winger', + 'PanelMember', + 'John Doe' + ); + const jp3 = new JudiciaryParticipantDetailsModel( + 'Ms', + 'Jane', + 'Doe', + 'Panel Member', + 'email.p3@hmcts.net', + 'email3@hmcts.net', + 'Panel Member', + 'PanelMember', + 'Jane Doe' + ); + + component.judiciaryParticipants = [jp1, jp2, jp3]; + + expect(component.sortedJudiciaryMembers.length).toEqual(3); + expect(component.sortedJudiciaryMembers[0]).toEqual(jp1); + expect(component.sortedJudiciaryMembers[1]).toEqual(jp3); + expect(component.sortedJudiciaryMembers[2]).toEqual(jp2); + }); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.ts index 284abdef9..ea49de937 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/booking-participant-list/booking-participant-list.component.ts @@ -1,7 +1,9 @@ import { Component, Input } from '@angular/core'; import { ParticipantDetailsModel } from '../../common/model/participant-details.model'; +import { JudiciaryParticipantDetailsModel } from 'src/app/common/model/judiciary-participant-details.model'; import { BookingsDetailsModel } from '../../common/model/bookings-list.model'; import { Constants } from '../../common/constants'; +import {} from 'src/app/common/model/participant.model'; @Component({ selector: 'app-booking-participant-list', @@ -10,12 +12,21 @@ import { Constants } from '../../common/constants'; }) export class BookingParticipantListComponent { private _participants: Array = []; + private _judiciaryParticipants: Array = []; sortedParticipants: ParticipantDetailsModel[] = []; + sortedJudiciaryMembers: JudiciaryParticipantDetailsModel[] = []; @Input() set participants(participants: Array) { this._participants = participants; this.sortParticipants(); + this.sortJudiciaryMembers(); + } + @Input() + set judiciaryParticipants(judiciaryParticipants: Array) { + this._judiciaryParticipants = judiciaryParticipants; + this.sortParticipants(); + this.sortJudiciaryMembers(); } @Input() hearing: BookingsDetailsModel; @@ -24,8 +35,6 @@ export class BookingParticipantListComponent { @Input() vh_officer_admin: boolean; - constructor() {} - get participants(): Array { let indexItem = 0; this._participants.forEach(x => { @@ -75,6 +84,28 @@ export class BookingParticipantListComponent { this.sortedParticipants = sorted; } + sortJudiciaryMembers() { + if (!this._judiciaryParticipants) { + return; + } + + const judicialJudge = [this._judiciaryParticipants.filter(j => j.roleCode === 'Judge')][0]; + const judicialPanelMembers = this._judiciaryParticipants.filter(j => j.roleCode === 'PanelMember'); + + const sortedJohList = [...judicialJudge, ...judicialPanelMembers]; + + sortedJohList.sort((a, b) => { + if (a.roleCode.includes('Judge') && !b.roleCode.includes('Judge')) { + return -1; + } else if (!a.roleCode.includes('Judge') && b.roleCode.includes('Judge')) { + return 1; + } else { + return a.firstName.localeCompare(b.firstName); + } + }); + this.sortedJudiciaryMembers = sortedJohList; + } + private insertInterpreters(interpreters: ParticipantDetailsModel[], sorted: ParticipantDetailsModel[]) { interpreters.forEach(interpreterParticipant => { let interpretee: ParticipantDetailsModel; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list.module.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list.module.ts index b562f935a..86e95f3ff 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list.module.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list.module.ts @@ -13,6 +13,7 @@ import { CopyConferencePhoneComponent } from './copy-conference-phone/copy-confe import { CopyJoinLinkComponent } from './copy-join-link/copy-join-link.component'; import { NgSelectModule } from '@ng-select/ng-select'; import { BookingStatusComponent } from './booking-status/booking-status.component'; +import { JudicialParticipantDetailsComponent } from './participant-details/judicial-participant-details.component'; @NgModule({ imports: [SharedModule, BookingsListRoutingModule, PopupModule, MomentModule, NgSelectModule], @@ -25,7 +26,8 @@ import { BookingStatusComponent } from './booking-status/booking-status.componen CopySipComponent, CopyConferencePhoneComponent, CopyJoinLinkComponent, - BookingStatusComponent + BookingStatusComponent, + JudicialParticipantDetailsComponent ], providers: [], exports: [ diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.html index 8dd192dc5..8dcebabb6 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.html @@ -55,7 +55,7 @@

Search bookings

/>
- +
diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.spec.ts index d2001d5f0..9f00b0d75 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/bookings-list/bookings-list.component.spec.ts @@ -38,6 +38,7 @@ import { CaseTypesMenuComponent } from '../../shared/menus/case-types-menu/case- import { VenuesMenuComponent } from '../../shared/menus/venues-menu/venues-menu.component'; import { JusticeUsersService } from 'src/app/services/justice-users.service'; import { JusticeUserMenuStubComponent } from 'src/app/testing/stubs/dropdown-menu/justice-user-menu-stub.component'; +import { BookingStatusComponent } from '../booking-status/booking-status.component'; let component: BookingsListComponent; let bookingPersistService: BookingPersistService; @@ -602,7 +603,8 @@ describe('BookingsListComponent', () => { LongDatetimePipe, JusticeUserMenuStubComponent, CaseTypesMenuComponent, - VenuesMenuComponent + VenuesMenuComponent, + BookingStatusComponent ], imports: [HttpClientModule, MomentModule, ReactiveFormsModule, NgSelectModule], providers: [ @@ -616,6 +618,7 @@ describe('BookingsListComponent', () => { { provide: LaunchDarklyService, useValue: launchDarklyServiceSpy }, { provide: ReferenceDataService, useValue: referenceDataServiceSpy }, { provide: JusticeUsersService, useValue: justiceUserServiceSpy }, + { provide: JusticeUsersMenuComponent, useClass: JusticeUserMenuStubComponent }, DatePipe ] }).compileComponents(); @@ -624,6 +627,7 @@ describe('BookingsListComponent', () => { component = fixture.componentInstance; bookingPersistService = TestBed.inject(BookingPersistService); returnUrlService = TestBed.inject(ReturnUrlService); + component.csoMenu = TestBed.inject(JusticeUsersMenuComponent); fixture.detectChanges(); })); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.html index af5e6ac4b..f4337edff 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.html @@ -1,10 +1,10 @@

Booking details

-
+
Created by:
-
+
{{ hearing?.CreatedBy }} @@ -16,10 +16,10 @@

Booki

-
+
Last edited by:
-
+
{{ hearing?.LastEditBy }} @@ -31,10 +31,10 @@

Booki

-
+
Confirmed by:
-
+
{{ hearing?.ConfirmedBy }} @@ -50,10 +50,10 @@

Booki class="govuk-grid-row" *ngIf="hearing?.AllocatedTo && hearing?.AllocatedTo.length && vhoWorkAllocationFeature" > -
+
Allocated to:
-
+
{{ hearing?.AllocatedTo }} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.spec.ts index c2dd04994..9400ea538 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/hearing-details/hearing-details.component.spec.ts @@ -85,7 +85,6 @@ describe('HearingDetailsComponent', () => { const phoneDetails = '11111 (ID: 1234567)'; fixture.whenStable().then(() => { fixture.detectChanges(); - console.log(component.hearing); const divElementRole = debugElement.queryAll(By.css('#hearing-name')); expect(divElementRole.length).toBeGreaterThan(0); expect(divElementRole.length).toBe(1); @@ -109,6 +108,7 @@ describe('HearingDetailsComponent', () => { 'contact_email', 'case_role_name', 'hearing_role_name', + 'hearing_role_code', 'display_name', 'middle_names', 'organisation', @@ -135,6 +135,7 @@ describe('HearingDetailsComponent', () => { 'contact_email', 'case_role_name', 'hearing_role_name', + 'hearing_role_code', 'display_name', 'middle_names', 'organisation', diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.html new file mode 100644 index 000000000..5449f759b --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.html @@ -0,0 +1,55 @@ +
+
+
+ HM Courts & Tribunals Service crest +
+
+ {{ participant.displayName }} +
Judge
+
+
+ +
+
+
+
+
+ + {{ participant.fullName }} + +
+
{{ participant.displayName }}
+
Panel Member
+
+
+
+ +
+
+
Email
+
+
+ {{ participant.email }} +
+
+
+
+
Telephone
+
+
+ {{ participant.telephone }} +
+ +
TBC
+
+
+
+
+
+
diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.scss b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.scss new file mode 100644 index 000000000..7570a80b8 --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.scss @@ -0,0 +1,70 @@ +:host { + display: block; + line-height: 1.3em; +} + +.container { + padding: 1em 0; + + .judge-row { + display: flex; + align-items: center; + + > div:first-child { + flex: 0 0 auto; + } + + > div { + padding-left: 15px; + flex: 1 1 auto; + } + + > div:last-child { + padding-right: 15px; + } + } + + .participant-row { + display: flex; + flex-direction: column; + + > div { + padding-left: 15px; + flex: 1 1 auto; + } + + &__detail { + display: flex; + + > div:first-child { + padding-left: 0; + } + + > div { + flex: 0 0 auto; + padding-left: 15px; + } + } + } + + .contact { + padding-left: 15px; + margin-top: 0.75em; + padding-top: 0.75em; + border-top: 1px solid rgba(0, 0, 0, 0.06); + &-label { + font-weight: bold; + } + } + + .wrap { + word-break: break-all; + overflow-wrap: break-word; + } + + .light { + color: #505a5f; + font-size: 1.1875rem; + line-height: 1.31578947; + } +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.spec.ts new file mode 100644 index 000000000..41b986c3f --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.spec.ts @@ -0,0 +1,192 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { JudicialParticipantDetailsComponent } from './judicial-participant-details.component'; +import { JudiciaryParticipantDetailsModel } from 'src/app/common/model/judiciary-participant-details.model'; +import { BookingDetailsTestData } from '../booking-details/booking-details.component.spec'; +import { By } from '@angular/platform-browser'; + +describe('JudicialParticipantDetailsComponent', () => { + let component: JudicialParticipantDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [JudicialParticipantDetailsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(JudicialParticipantDetailsComponent); + component = fixture.componentInstance; + }); + + describe('Participant is a Judge', () => { + it('should display a judge without contact details', () => { + const participant = new JudiciaryParticipantDetailsModel( + 'Mr', + 'John', + 'Doe', + 'John Doe', + 'john@doe.com', + '1234567890', + 'A1234', + 'Judge', + 'Judge John' + ); + const hearing = new BookingDetailsTestData().getBookingsDetailsModel(); + + component.participant = participant; + component.hearing = hearing; + component.vh_officer_admin = false; + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + + const imgElement = fixture.debugElement.nativeElement.querySelector('img[src="/assets/images/govuk-crest.png"]'); + expect(imgElement).toBeTruthy(); + + const nameElement = fixture.debugElement.query(By.css('#judge-name')); + expect(nameElement.nativeElement.textContent).toContain(participant.displayName); + + const contactElement = fixture.debugElement.query(By.css('contact')); + expect(contactElement).toBeNull(); + }); + + it('should display a judge with contact details', () => { + const participant = new JudiciaryParticipantDetailsModel( + 'Mr', + 'John', + 'Doe', + 'John Doe', + 'john@doe.com', + '1234567890', + 'A1234', + 'Judge', + 'Judge John' + ); + const hearing = new BookingDetailsTestData().getBookingsDetailsModel(); + + component.participant = participant; + component.hearing = hearing; + component.vh_officer_admin = true; + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + + const imgElement = fixture.debugElement.nativeElement.querySelector('img[src="/assets/images/govuk-crest.png"]'); + expect(imgElement).toBeTruthy(); + + const nameElement = fixture.debugElement.query(By.css('#judge-name')); + expect(nameElement.nativeElement.textContent).toContain(participant.displayName); + + const emailDiv = fixture.debugElement.nativeElement.querySelector(`#participant-${participant.personalCode}-email`); + expect(emailDiv.textContent).toContain(component.participant.email); + + const phoneDiv = fixture.debugElement.nativeElement.querySelector(`#participant-${participant.personalCode}-phone`); + expect(phoneDiv.textContent).toContain(component.participant.telephone); + }); + + it('should display TBC when no phone number is provided', () => { + const participant = new JudiciaryParticipantDetailsModel( + 'Mr', + 'John', + 'Doe', + 'John Doe', + 'john@doe.com', + null, + 'A1234', + 'Judge', + 'Judge John' + ); + const hearing = new BookingDetailsTestData().getBookingsDetailsModel(); + + component.participant = participant; + component.hearing = hearing; + component.vh_officer_admin = true; + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + + const imgElement = fixture.debugElement.nativeElement.querySelector('img[src="/assets/images/govuk-crest.png"]'); + expect(imgElement).toBeTruthy(); + + const nameElement = fixture.debugElement.query(By.css('#judge-name')); + expect(nameElement.nativeElement.textContent).toContain(participant.displayName); + + const emailDiv = fixture.debugElement.nativeElement.querySelector(`#participant-${participant.personalCode}-email`); + expect(emailDiv.textContent).toContain(component.participant.email); + + const phoneDiv = fixture.debugElement.nativeElement.querySelector(`#participant-${participant.personalCode}-phone`); + expect(phoneDiv.textContent).toContain('TBC'); + }); + }); + + describe('Participant is a Panel Member', () => { + it('should display a panel member without contact details', () => { + const participant = new JudiciaryParticipantDetailsModel( + 'Mr', + 'John', + 'Doe', + 'John Doe', + 'john@doe.com', + '1234567890', + 'A1234', + 'PanelMember', + 'PM John' + ); + const hearing = new BookingDetailsTestData().getBookingsDetailsModel(); + + component.participant = participant; + component.hearing = hearing; + component.vh_officer_admin = false; + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + + const imgElement = fixture.debugElement.nativeElement.querySelector('img[src="/assets/images/govuk-crest.png"]'); + expect(imgElement).toBeNull(); + + const nameElement = fixture.debugElement.query(By.css('#judge-name')); + expect(nameElement).toBeNull(); + + const contactElement = fixture.debugElement.query(By.css('contact')); + expect(contactElement).toBeNull(); + }); + + it('should display a panel member with contact details', () => { + const participant = new JudiciaryParticipantDetailsModel( + 'Mr', + 'John', + 'Doe', + 'John Doe', + 'john@doe.com', + '1234567890', + 'A1234', + 'PanelMember', + 'PM John' + ); + const hearing = new BookingDetailsTestData().getBookingsDetailsModel(); + + component.participant = participant; + component.hearing = hearing; + component.vh_officer_admin = true; + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + + const imgElement = fixture.debugElement.nativeElement.querySelector('img[src="/assets/images/govuk-crest.png"]'); + expect(imgElement).toBeNull(); + + const nameElement = fixture.debugElement.query(By.css('#judge-name')); + expect(nameElement).toBeNull(); + + const emailDiv = fixture.debugElement.nativeElement.querySelector(`#participant-${participant.personalCode}-email`); + expect(emailDiv.textContent).toContain(component.participant.email); + + const phoneDiv = fixture.debugElement.nativeElement.querySelector(`#participant-${participant.personalCode}-phone`); + expect(phoneDiv.textContent).toContain(component.participant.telephone); + }); + }); +}); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.ts new file mode 100644 index 000000000..bfb304f32 --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/judicial-participant-details.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { JudiciaryParticipantDetailsModel } from 'src/app/common/model/judiciary-participant-details.model'; +import { BookingsDetailsModel } from '../../common/model/bookings-list.model'; + +@Component({ + selector: 'app-judicial-participant-details', + templateUrl: './judicial-participant-details.component.html', + styleUrls: ['./judicial-participant-details.component.scss'] +}) +export class JudicialParticipantDetailsComponent { + @Input() + participant: JudiciaryParticipantDetailsModel; + @Input() + hearing: BookingsDetailsModel; + @Input() + vh_officer_admin: boolean; +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.html index 0c639603d..7917d4022 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.html @@ -25,6 +25,7 @@ {{ participant?.fullName }} +
{{ participant?.DisplayName }}
@@ -68,8 +69,8 @@
-
Email
-
+
Email
+
{{ judgeEmail }} @@ -87,14 +88,14 @@
-
Username
-
+
Username
+
{{ participant?.UserName }}
-
Telephone
-
+
Telephone
+
{{ judgePhone }} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.spec.ts index f989f94ec..1f42beb06 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/bookings-list/participant-details/participant-details.component.spec.ts @@ -5,7 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ParticipantDetailsModel } from '../../common/model/participant-details.model'; import { ParticipantDetailsComponent } from './participant-details.component'; import { BookingsDetailsModel } from '../../common/model/bookings-list.model'; -import { HearingRoles } from '../../common/model/hearing-roles.model'; +import { HearingRoleCodes, HearingRoles } from '../../common/model/hearing-roles.model'; describe('ParticipantDetailsComponent', () => { let component: ParticipantDetailsComponent; @@ -60,6 +60,7 @@ describe('ParticipantDetailsComponent', () => { 'email@hmcts.net', 'Respondent', 'Respondent LIP', + HearingRoleCodes.Respondent, 'Alan Brake', '', 'ABC Solicitors', @@ -105,6 +106,7 @@ describe('ParticipantDetailsComponent', () => { 'email@hmcts.net', 'Respondent', 'Judge', + null, 'Judge', '', 'ABC Solicitors', diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing-roles.model.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing-roles.model.ts index 2aaf63bf1..cfa5f6d53 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing-roles.model.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing-roles.model.ts @@ -24,3 +24,14 @@ export enum HearingRoles { WINGER = 'winger', WITNESS = 'witness' } + +export class HearingRoleCodes { + public static readonly Applicant: string = 'APPL'; + public static readonly Intermediary: string = 'INTE'; + public static readonly Interpreter: string = 'INTP'; + public static readonly Representative: string = 'RPTT'; + public static readonly Respondent: string = 'RESP'; + public static readonly StaffMember: string = 'STAF'; + public static readonly WelfareRepresentative: string = 'WERP'; + public static readonly Observer: string = 'OBSV'; +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing.model.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing.model.ts index ec48efdf5..af51c28d0 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing.model.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/hearing.model.ts @@ -2,6 +2,7 @@ import { CaseModel } from './case.model'; import { ParticipantModel } from './participant.model'; import { EndpointModel } from './endpoint.model'; import { LinkedParticipantModel } from './linked-participant.model'; +import { JudicialMemberDto } from 'src/app/booking/judicial-office-holders/models/add-judicial-member.model'; export class HearingModel { constructor() { @@ -12,6 +13,7 @@ export class HearingModel { this.linked_participants = []; this.hearing_dates = []; this.hearing_id = ''; + this.judiciaryParticipants = []; } hearing_id?: string; scheduled_date_time?: Date; @@ -19,6 +21,7 @@ export class HearingModel { hearing_type_id?: number; cases?: CaseModel[]; participants?: ParticipantModel[]; + judiciaryParticipants?: JudicialMemberDto[]; created_by?: string; case_type?: string; case_type_service_id?: string; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/judiciary-participant-details.model.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/judiciary-participant-details.model.ts new file mode 100644 index 000000000..1a172359c --- /dev/null +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/judiciary-participant-details.model.ts @@ -0,0 +1,19 @@ +import { JudicaryRoleCode } from 'src/app/booking/judicial-office-holders/models/add-judicial-member.model'; + +export class JudiciaryParticipantDetailsModel { + constructor( + public title: string, + public firstName: string, + public lastName: string, + public fullName: string, + public email: string, + public telephone: string, + public personalCode: string, + public roleCode: JudicaryRoleCode, + public displayName: string + ) {} + + get isJudge(): boolean { + return this.roleCode === 'Judge'; + } +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.spec.ts index 0f4efe851..6806d8056 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.spec.ts @@ -1,5 +1,6 @@ import { ParticipantDetailsModel } from './participant-details.model'; import { ParticipantModel } from './participant.model'; +import { HearingRoleCodes } from './hearing-roles.model'; describe('participant details model', () => { it('should get full Name', () => { @@ -13,6 +14,7 @@ describe('participant details model', () => { 'contact_email', 'case_role_name', 'hearing_role_name', + 'hearings_role_code', 'display_name', 'middle_names', 'organisation', @@ -37,6 +39,7 @@ describe('participant details model', () => { 'contact_email', 'case_role_name', 'hearing_role_name', + 'hearings_role_code', 'display_name', 'middle_names', 'organisation', @@ -61,6 +64,7 @@ describe('participant details model', () => { 'contact_email', 'case_role_name', 'hearing_role_name', + 'hearings_role_code', 'display_name', 'middle_names', 'organisation', @@ -85,6 +89,7 @@ describe('participant details model', () => { 'contact_email', 'case_role_name', 'hearing_role_name', + 'hearings_role_code', 'display_name', 'middle_names', 'organisation', @@ -107,6 +112,7 @@ describe('participant details model', () => { 'Judiciaryemail', 'case_role_name', 'hearing_role_name', + 'hearings_role_code', 'display_name', 'middle_names', 'organisation', @@ -129,6 +135,7 @@ describe('participant details model', () => { 'contact_email', 'case_role_name', 'Representative', + HearingRoleCodes.Representative, 'display_name', 'middle_names', 'organisation', @@ -153,6 +160,7 @@ describe('participant details model', () => { 'contact_email', 'case_role_name', 'Individual', + HearingRoleCodes.Applicant, 'display_name', 'middle_names', 'organisation', @@ -176,6 +184,7 @@ describe('participant details model', () => { 'contact_email', 'none', 'Individual', + HearingRoleCodes.Applicant, 'display_name', 'middle_names', 'organisation', @@ -198,6 +207,7 @@ describe('participant details model', () => { 'contact_email', 'Staff Member', 'Individual', + HearingRoleCodes.StaffMember, 'display_name', 'middle_names', 'organisation', @@ -220,6 +230,7 @@ describe('participant details model', () => { 'contact_email', 'observer', 'Individual', + HearingRoleCodes.Observer, 'display_name', 'middle_names', 'organisation', @@ -242,6 +253,7 @@ describe('participant details model', () => { 'contact_email', 'Representative', 'Individual', + HearingRoleCodes.Representative, 'display_name', 'middle_names', 'organisation', @@ -264,6 +276,7 @@ describe('participant details model', () => { 'contact_email', 'Citizen', 'Interpreter', + HearingRoleCodes.Interpreter, 'display_name', 'middle_names', 'organisation', @@ -286,6 +299,7 @@ describe('participant details model', () => { 'contact_email', 'Citizen', 'Interpreter', + HearingRoleCodes.Interpreter, 'display_name', 'middle_names', 'organisation', @@ -308,6 +322,7 @@ describe('participant details model', () => { 'contact_email', null, 'Interpreter', + HearingRoleCodes.Interpreter, 'display_name', 'middle_names', 'organisation', diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.ts index 738e04b50..808f2b0c0 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant-details.model.ts @@ -1,5 +1,5 @@ import { CaseRoles } from './case-roles'; -import { HearingRoles } from './hearing-roles.model'; +import { HearingRoleCodes, HearingRoles } from './hearing-roles.model'; import { LinkedParticipant } from '../../services/clients/api-client'; export class ParticipantDetailsModel { @@ -13,6 +13,7 @@ export class ParticipantDetailsModel { email: string, caseRoleName: string, hearingRoleName: string, + hearingRoleCode: string, displayName: string, middleNames: string, organisation: string, @@ -23,15 +24,16 @@ export class ParticipantDetailsModel { linkedParticipants: LinkedParticipant[] ) { this.ParticipantId = participantId; - this.FirstName = firstName == null ? '' : firstName; - this.LastName = lastName == null ? '' : lastName; - this.Title = title == null ? '' : title; + this.FirstName = firstName ?? ''; + this.LastName = lastName ?? ''; + this.Title = title ?? ''; this.UserRoleName = role; this.UserName = userName; this.Flag = false; this.Email = email; this.CaseRoleName = caseRoleName; this.HearingRoleName = hearingRoleName; + this.HearingRoleCode = hearingRoleCode; this.DisplayName = displayName; this.MiddleNames = middleNames; this.Representee = representee; @@ -51,6 +53,7 @@ export class ParticipantDetailsModel { Email: string; CaseRoleName: string; HearingRoleName: string; + HearingRoleCode: string; DisplayName: string; MiddleNames: string; Representee: string; @@ -91,7 +94,10 @@ export class ParticipantDetailsModel { } get isInterpreter(): boolean { - return this.HearingRoleName && this.HearingRoleName.toLowerCase().trim() === HearingRoles.INTERPRETER; + return ( + (this.HearingRoleName && this.HearingRoleName.toLowerCase().trim() === HearingRoles.INTERPRETER) || + (this.HearingRoleCode && this.HearingRoleCode === HearingRoleCodes.Interpreter) + ); } get isJudge(): boolean { @@ -100,9 +106,11 @@ export class ParticipantDetailsModel { get isRepOrInterpreter(): boolean { return ( - this.HearingRoleName && - (this.HearingRoleName.toLowerCase().trim() === HearingRoles.INTERPRETER || - this.HearingRoleName.toLowerCase().trim() === HearingRoles.REPRESENTATIVE) + (this.HearingRoleName && + (this.HearingRoleName.toLowerCase().trim() === HearingRoles.INTERPRETER || + this.HearingRoleName.toLowerCase().trim() === HearingRoles.REPRESENTATIVE)) || + (this.HearingRoleCode && + (this.HearingRoleCode === HearingRoleCodes.Interpreter || this.HearingRoleCode === HearingRoleCodes.Representative)) ); } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant.model.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant.model.ts index 3aa4c51f6..7cfe76bff 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant.model.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/common/model/participant.model.ts @@ -1,5 +1,6 @@ import { JudgeAccountType, JudgeResponse, PersonResponse } from 'src/app/services/clients/api-client'; import { LinkedParticipantModel } from './linked-participant.model'; +import { JudicialMemberDto } from 'src/app/booking/judicial-office-holders/models/add-judicial-member.model'; export class ParticipantModel { id?: string; @@ -27,6 +28,7 @@ export class ParticipantModel { addedDuringHearing?: boolean; is_staff_member?: boolean; contact_email?: string; + isJudiciaryMember?: boolean; constructor(init?: Partial) { Object.assign(this, init); @@ -39,7 +41,8 @@ export class ParticipantModel { email: person.contact_email ?? person.username, phone: person.telephone_number, representee: '', - company: person.organisation + company: person.organisation, + isJudiciaryMember: false } : null; } @@ -50,7 +53,8 @@ export class ParticipantModel { ...judge, email: judge.contact_email ?? judge.email, username: judge.email, - is_courtroom_account: judge.account_type === JudgeAccountType.Courtroom + is_courtroom_account: judge.account_type === JudgeAccountType.Courtroom, + isJudiciaryMember: false } : null; } @@ -58,4 +62,24 @@ export class ParticipantModel { static IsEmailEjud(email: string): boolean { return email?.toLowerCase().includes('judiciary') ?? false; } + + static fromJudicialMember(judicialMember: JudicialMemberDto, isJudge = false) { + const hearingRoleName = isJudge ? 'Judge' : 'Panel Member'; + const userRoleName = isJudge ? 'Judge' : 'PanelMember'; + const hearingRoleCode = isJudge ? 'Judge' : 'PanelMember'; + return new ParticipantModel({ + first_name: judicialMember.firstName, + last_name: judicialMember.lastName, + hearing_role_name: hearingRoleName, + username: judicialMember.email, + email: judicialMember.email, + is_exist_person: true, + user_role_name: userRoleName, + isJudiciaryMember: true, + hearing_role_code: hearingRoleCode, + phone: judicialMember.telephone, + display_name: judicialMember.displayName, + is_judge: isJudge + }); + } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/dashboard/unallocated-hearings/unallocated-hearings.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/dashboard/unallocated-hearings/unallocated-hearings.component.spec.ts index 65e32fc09..5931855e5 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/dashboard/unallocated-hearings/unallocated-hearings.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/dashboard/unallocated-hearings/unallocated-hearings.component.spec.ts @@ -10,6 +10,7 @@ import { import { of, throwError } from 'rxjs'; import { Logger } from '../../services/logger'; import { UserIdentityService } from '../../services/user-identity.service'; +import { RouterTestingModule } from '@angular/router/testing'; describe('UnallocatedHearingsComponent', () => { let component: UnallocatedHearingsComponent; @@ -49,6 +50,7 @@ describe('UnallocatedHearingsComponent', () => { userIdentityServiceSpy.getUserInformation.and.returnValue(of(new UserProfileResponse({ is_vh_team_leader: true }))); await TestBed.configureTestingModule({ + imports: [RouterTestingModule], declarations: [UnallocatedHearingsComponent], providers: [ { provide: BHClient, useValue: bHClientSpy }, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/justice-user-form/justice-user-form.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/justice-user-form/justice-user-form.component.spec.ts index 543930a0b..c58134d1b 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/justice-user-form/justice-user-form.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/justice-user-form/justice-user-form.component.spec.ts @@ -10,6 +10,7 @@ import { MockLogger } from 'src/app/shared/testing/mock-logger'; import { JusticeUserFormComponent } from './justice-user-form.component'; import { LaunchDarklyService, FeatureFlags } from 'src/app/services/launch-darkly.service'; +import { ReactiveFormsModule } from '@angular/forms'; describe('JusticeUserFormComponent', () => { const justiceUsersServiceSpy = jasmine.createSpyObj('JusticeUsersService', [ @@ -34,6 +35,7 @@ describe('JusticeUserFormComponent', () => { launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.dom1Integration).and.returnValue(of(false)); await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], declarations: [JusticeUserFormComponent], providers: [ { provide: Logger, useValue: new MockLogger() }, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.html index f72b86dcf..a410eeb86 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.html @@ -1,3 +1,4 @@ +

Manage team

diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.spec.ts index eb46b1286..142d73875 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.spec.ts @@ -10,9 +10,10 @@ import { Component } from '@angular/core'; import { newGuid } from '@microsoft/applicationinsights-core-js'; import { MockLogger } from 'src/app/shared/testing/mock-logger'; import { Constants } from 'src/app/common/constants'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { JusticeUserFormMode } from '../justice-user-form/justice-user-form.component'; +import { JusticeUserFormComponent, JusticeUserFormMode } from '../justice-user-form/justice-user-form.component'; import { RolesToDisplayPipe } from '../../shared/pipes/roles-to-display.pipe'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TooltipDirective } from 'src/app/shared/directives/tooltip.directive'; @Component({ selector: 'app-justice-user-form', template: '' }) export class JusticeUserFormStubComponent {} @@ -33,13 +34,15 @@ describe('ManageTeamComponent', () => { justiceUsersServiceSpy.filteredUsers$ = filteredUsers$; await TestBed.configureTestingModule({ - declarations: [ManageTeamComponent, JusticeUserFormStubComponent, RolesToDisplayPipe], + declarations: [ManageTeamComponent, JusticeUserFormStubComponent, RolesToDisplayPipe, TooltipDirective], providers: [ FormBuilder, HttpClient, HttpHandler, + TooltipDirective, { provide: Logger, useValue: new MockLogger() }, - { provide: JusticeUsersService, useValue: justiceUsersServiceSpy } + { provide: JusticeUsersService, useValue: justiceUsersServiceSpy }, + { provide: JusticeUserFormComponent, useClass: JusticeUserFormStubComponent } ], imports: [ReactiveFormsModule, FontAwesomeModule] }).compileComponents(); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.ts index 8f38864f4..a35d5b76c 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/manage-team/manage-team/manage-team.component.ts @@ -30,6 +30,7 @@ export class ManageTeamComponent implements OnInit, OnDestroy { userToDelete: JusticeUserResponse; displayRestoreUserPopup = false; userToRestore: JusticeUserResponse; + isAnErrorMessage = false; message$ = new BehaviorSubject(null); users$: Observable; @@ -57,6 +58,7 @@ export class ManageTeamComponent implements OnInit, OnDestroy { ngOnInit() { this.form.controls.inputSearch.valueChanges.subscribe(() => this.displayAddButton$.next(false)); + this.isAnErrorMessage$.pipe(takeUntil(this.destroyed$)).subscribe(isAnErrorMessage => (this.isAnErrorMessage = isAnErrorMessage)); this.users$ = this.justiceUserService.filteredUsers$.pipe( takeUntil(this.destroyed$), diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/app-insights-logger.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/app-insights-logger.service.ts index da2723582..9a48050aa 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/app-insights-logger.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/app-insights-logger.service.ts @@ -19,6 +19,14 @@ export class AppInsightsLogger implements LogAdapter { this.appInsights.loadAppInsights(); this.oidcService.userData$.subscribe(ud => { this.appInsights.addTelemetryInitializer((envelope: ITelemetryItem) => { + const remoteDepedencyType = 'RemoteDependencyData'; + if (envelope.baseType === remoteDepedencyType && (envelope.baseData.name as string)) { + const name = envelope.baseData.name as string; + if (name.startsWith('HEAD /assets/images/favicons/favicon.ico?')) { + // ignore favicon requests used to poll for availability + return false; + } + } envelope.tags['ai.cloud.role'] = 'vh-admin-web'; envelope.tags['ai.user.id'] = ud.userData.preferred_username.toLowerCase(); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.spec.ts index 076de5d8e..92ed650cc 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.spec.ts @@ -65,6 +65,8 @@ export class ResponseTestData { endpoint1.id = '022f5e0c-696d-43cf-6fe0-08d846dbdb21'; response.endpoints = []; response.endpoints.push(endpoint1); + + response.judiciary_participants = []; return response; } } @@ -212,6 +214,8 @@ describe('booking details service', () => { response.participants.push(par2); response.participants.push(par3); + response.judiciary_participants = []; + const model = service.mapBookingParticipants(response); expect(model).toBeTruthy(); expect(model.participants[0].Interpretee).toBe(''); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.ts index 178d69174..1fd27317b 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/booking-details.service.ts @@ -1,9 +1,11 @@ import { Injectable } from '@angular/core'; import { BookingsDetailsModel } from '../common/model/bookings-list.model'; import { EndpointModel } from '../common/model/endpoint.model'; -import { HearingRoles } from '../common/model/hearing-roles.model'; +import { HearingRoleCodes, HearingRoles } from '../common/model/hearing-roles.model'; import { ParticipantDetailsModel } from '../common/model/participant-details.model'; +import { JudiciaryParticipantDetailsModel } from '../common/model/judiciary-participant-details.model'; import { HearingDetailsResponse, ParticipantResponse } from './clients/api-client'; +import { JudicaryRoleCode } from '../booking/judicial-office-holders/models/add-judicial-member.model'; @Injectable({ providedIn: 'root' }) export class BookingDetailsService { @@ -43,6 +45,24 @@ export class BookingDetailsService { mapBookingParticipants(hearingResponse: HearingDetailsResponse) { const participants: Array = []; const judges: Array = []; + const judicialMembers: Array = []; + + const mappedJohs = hearingResponse.judiciary_participants.map( + j => + new JudiciaryParticipantDetailsModel( + j.title, + j.first_name, + j.last_name, + j.full_name, + j.email, + j.work_phone, + j.personal_code, + j.role_code.toString() as JudicaryRoleCode, + j.display_name + ) + ); + judicialMembers.push(...mappedJohs); + if (hearingResponse.participants && hearingResponse.participants.length > 0) { hearingResponse.participants.forEach(p => { const model = new ParticipantDetailsModel( @@ -55,6 +75,7 @@ export class BookingDetailsService { p.contact_email, p.case_role_name, p.hearing_role_name, + p.hearing_role_code, p.display_name, p.middle_names, p.organisation, @@ -64,7 +85,6 @@ export class BookingDetailsService { this.isInterpretee(p), p.linked_participants ); - // model.Interpretee = this.getInterpretee(hearingResponse, p); if (p.user_role_name === this.JUDGE) { judges.push(model); } else { @@ -73,7 +93,7 @@ export class BookingDetailsService { }); } - return { judges: judges, participants: participants }; + return { judges: judges, participants: participants, judicialMembers: judicialMembers }; } mapBookingEndpoints(hearingResponse: HearingDetailsResponse): EndpointModel[] { @@ -95,11 +115,10 @@ export class BookingDetailsService { private getInterpretee(hearingResponse: HearingDetailsResponse, participant: ParticipantResponse): string { let interpreteeDisplayName = ''; - if ( - participant.hearing_role_name.toLowerCase().trim() === HearingRoles.INTERPRETER && - participant.linked_participants && - participant.linked_participants.length > 0 - ) { + const isInterpreter = + participant.hearing_role_name?.toLowerCase()?.trim() === HearingRoles.INTERPRETER || + participant.hearing_role_code === HearingRoleCodes.Interpreter; + if (isInterpreter && participant.linked_participants && participant.linked_participants.length > 0) { const interpreteeId = participant.linked_participants[0].linked_id; const interpretee = hearingResponse.participants.find(p => p.id === interpreteeId); interpreteeDisplayName = interpretee?.display_name; @@ -108,10 +127,9 @@ export class BookingDetailsService { } private isInterpretee(participant: ParticipantResponse): boolean { - return ( - participant.hearing_role_name.toLowerCase().trim() !== HearingRoles.INTERPRETER && - participant.linked_participants && - participant.linked_participants.length > 0 - ); + const isInterpreter = + participant.hearing_role_name?.toLowerCase()?.trim() === HearingRoles.INTERPRETER || + participant.hearing_role_code === HearingRoleCodes.Interpreter; + return !isInterpreter && participant.linked_participants && participant.linked_participants.length > 0; } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/clients/api-client.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/clients/api-client.ts index 1e384c13c..35ff9ba1d 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/clients/api-client.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/clients/api-client.ts @@ -1868,12 +1868,12 @@ export class BHClient extends ApiClientBase { } /** - * Find judges and court rooms accounts list by email search term. + * Find judiciary person list by email search term. * @param body (optional) The email address search term. * @return Success */ - postJudgesBySearchTerm(body: string | undefined): Observable { - let url_ = this.baseUrl + '/api/judiciary/judges'; + postJudiciaryPersonBySearchTerm(body: string | undefined): Observable { + let url_ = this.baseUrl + '/api/judiciary'; url_ = url_.replace(/[?&]$/, ''); const content_ = JSON.stringify(body); @@ -1896,23 +1896,23 @@ export class BHClient extends ApiClientBase { ) .pipe( _observableMergeMap((response_: any) => { - return this.processPostJudgesBySearchTerm(response_); + return this.processPostJudiciaryPersonBySearchTerm(response_); }) ) .pipe( _observableCatch((response_: any) => { if (response_ instanceof HttpResponseBase) { try { - return this.processPostJudgesBySearchTerm(response_ as any); + return this.processPostJudiciaryPersonBySearchTerm(response_ as any); } catch (e) { - return _observableThrow(e) as any as Observable; + return _observableThrow(e) as any as Observable; } - } else return _observableThrow(response_) as any as Observable; + } else return _observableThrow(response_) as any as Observable; }) ); } - protected processPostJudgesBySearchTerm(response: HttpResponseBase): Observable { + protected processPostJudiciaryPersonBySearchTerm(response: HttpResponseBase): Observable { const status = response.status; const responseBlob = response instanceof HttpResponse @@ -1943,7 +1943,7 @@ export class BHClient extends ApiClientBase { let resultData200 = _responseText === '' ? null : JSON.parse(_responseText, this.jsonParseReviver); if (Array.isArray(resultData200)) { result200 = [] as any; - for (let item of resultData200) result200!.push(JudgeResponse.fromJS(item)); + for (let item of resultData200) result200!.push(PersonResponse.fromJS(item)); } else { result200 = null; } @@ -1980,8 +1980,8 @@ export class BHClient extends ApiClientBase { * @param body (optional) The email address search term. * @return Success */ - postJudiciaryPersonBySearchTerm(body: string | undefined): Observable { - let url_ = this.baseUrl + '/api/judiciary'; + searchForJudiciaryPerson(body: string | undefined): Observable { + let url_ = this.baseUrl + '/api/judiciary/search'; url_ = url_.replace(/[?&]$/, ''); const content_ = JSON.stringify(body); @@ -2004,23 +2004,23 @@ export class BHClient extends ApiClientBase { ) .pipe( _observableMergeMap((response_: any) => { - return this.processPostJudiciaryPersonBySearchTerm(response_); + return this.processSearchForJudiciaryPerson(response_); }) ) .pipe( _observableCatch((response_: any) => { if (response_ instanceof HttpResponseBase) { try { - return this.processPostJudiciaryPersonBySearchTerm(response_ as any); + return this.processSearchForJudiciaryPerson(response_ as any); } catch (e) { - return _observableThrow(e) as any as Observable; + return _observableThrow(e) as any as Observable; } - } else return _observableThrow(response_) as any as Observable; + } else return _observableThrow(response_) as any as Observable; }) ); } - protected processPostJudiciaryPersonBySearchTerm(response: HttpResponseBase): Observable { + protected processSearchForJudiciaryPerson(response: HttpResponseBase): Observable { const status = response.status; const responseBlob = response instanceof HttpResponse @@ -2051,7 +2051,7 @@ export class BHClient extends ApiClientBase { let resultData200 = _responseText === '' ? null : JSON.parse(_responseText, this.jsonParseReviver); if (Array.isArray(resultData200)) { result200 = [] as any; - for (let item of resultData200) result200!.push(PersonResponse.fromJS(item)); + for (let item of resultData200) result200!.push(JudiciaryPerson.fromJS(item)); } else { result200 = null; } @@ -5244,6 +5244,7 @@ export class BookingDetailsRequest implements IBookingDetailsRequest { hearing_type_code?: string | undefined; cases?: CaseRequest[] | undefined; participants?: ParticipantRequest[] | undefined; + judiciary_participants?: JudiciaryParticipantRequest[] | undefined; hearing_room_name?: string | undefined; other_information?: string | undefined; created_by?: string | undefined; @@ -5281,6 +5282,11 @@ export class BookingDetailsRequest implements IBookingDetailsRequest { this.participants = [] as any; for (let item of _data['participants']) this.participants!.push(ParticipantRequest.fromJS(item)); } + if (Array.isArray(_data['judiciary_participants'])) { + this.judiciary_participants = [] as any; + for (let item of _data['judiciary_participants']) + this.judiciary_participants!.push(JudiciaryParticipantRequest.fromJS(item)); + } this.hearing_room_name = _data['hearing_room_name']; this.other_information = _data['other_information']; this.created_by = _data['created_by']; @@ -5322,6 +5328,10 @@ export class BookingDetailsRequest implements IBookingDetailsRequest { data['participants'] = []; for (let item of this.participants) data['participants'].push(item.toJSON()); } + if (Array.isArray(this.judiciary_participants)) { + data['judiciary_participants'] = []; + for (let item of this.judiciary_participants) data['judiciary_participants'].push(item.toJSON()); + } data['hearing_room_name'] = this.hearing_room_name; data['other_information'] = this.other_information; data['created_by'] = this.created_by; @@ -5350,6 +5360,7 @@ export interface IBookingDetailsRequest { hearing_type_code?: string | undefined; cases?: CaseRequest[] | undefined; participants?: ParticipantRequest[] | undefined; + judiciary_participants?: JudiciaryParticipantRequest[] | undefined; hearing_room_name?: string | undefined; other_information?: string | undefined; created_by?: string | undefined; @@ -5534,6 +5545,49 @@ export interface IEndpointRequest { defence_advocate_contact_email?: string | undefined; } +export class JudiciaryParticipantRequest implements IJudiciaryParticipantRequest { + personal_code?: string | undefined; + role?: string | undefined; + display_name?: string | undefined; + + constructor(data?: IJudiciaryParticipantRequest) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.personal_code = _data['personal_code']; + this.role = _data['role']; + this.display_name = _data['display_name']; + } + } + + static fromJS(data: any): JudiciaryParticipantRequest { + data = typeof data === 'object' ? data : {}; + let result = new JudiciaryParticipantRequest(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data['personal_code'] = this.personal_code; + data['role'] = this.role; + data['display_name'] = this.display_name; + return data; + } +} + +export interface IJudiciaryParticipantRequest { + personal_code?: string | undefined; + role?: string | undefined; + display_name?: string | undefined; +} + export class LinkedParticipantRequest implements ILinkedParticipantRequest { participant_contact_email?: string | undefined; linked_participant_contact_email?: string | undefined; @@ -6275,6 +6329,7 @@ export class HearingDetailsResponse implements IHearingDetailsResponse { participants?: ParticipantResponse[] | undefined; /** V1 only */ telephone_participants?: TelephoneParticipantResponse[] | undefined; + judiciary_participants?: JudiciaryParticipantResponse[] | undefined; hearing_room_name?: string | undefined; other_information?: string | undefined; created_date?: Date; @@ -6321,6 +6376,11 @@ export class HearingDetailsResponse implements IHearingDetailsResponse { for (let item of _data['telephone_participants']) this.telephone_participants!.push(TelephoneParticipantResponse.fromJS(item)); } + if (Array.isArray(_data['judiciary_participants'])) { + this.judiciary_participants = [] as any; + for (let item of _data['judiciary_participants']) + this.judiciary_participants!.push(JudiciaryParticipantResponse.fromJS(item)); + } this.hearing_room_name = _data['hearing_room_name']; this.other_information = _data['other_information']; this.created_date = _data['created_date'] ? new Date(_data['created_date'].toString()) : undefined; @@ -6370,6 +6430,10 @@ export class HearingDetailsResponse implements IHearingDetailsResponse { data['telephone_participants'] = []; for (let item of this.telephone_participants) data['telephone_participants'].push(item.toJSON()); } + if (Array.isArray(this.judiciary_participants)) { + data['judiciary_participants'] = []; + for (let item of this.judiciary_participants) data['judiciary_participants'].push(item.toJSON()); + } data['hearing_room_name'] = this.hearing_room_name; data['other_information'] = this.other_information; data['created_date'] = this.created_date ? this.created_date.toISOString() : undefined; @@ -6410,6 +6474,7 @@ export interface IHearingDetailsResponse { participants?: ParticipantResponse[] | undefined; /** V1 only */ telephone_participants?: TelephoneParticipantResponse[] | undefined; + judiciary_participants?: JudiciaryParticipantResponse[] | undefined; hearing_room_name?: string | undefined; other_information?: string | undefined; created_date?: Date; @@ -6598,6 +6663,164 @@ export interface IJudgeResponse { account_type?: JudgeAccountType; } +export class JudiciaryParticipantResponse implements IJudiciaryParticipantResponse { + /** Judiciary person's Title. */ + title?: string | undefined; + /** Judiciary person's first name. */ + first_name?: string | undefined; + /** Judiciary person's last name. */ + last_name?: string | undefined; + /** Judiciary person's full name. */ + full_name?: string | undefined; + /** Judiciary person's contact email */ + email?: string | undefined; + /** Judiciary person's work phone */ + work_phone?: string | undefined; + /** The Judiciary person's unique personal code */ + personal_code?: string | undefined; + /** The Judiciary person's role code (Judge or Panel Member) */ + role_code?: string | undefined; + /** The judiciary person's display name */ + display_name?: string | undefined; + + constructor(data?: IJudiciaryParticipantResponse) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.title = _data['title']; + this.first_name = _data['first_name']; + this.last_name = _data['last_name']; + this.full_name = _data['full_name']; + this.email = _data['email']; + this.work_phone = _data['work_phone']; + this.personal_code = _data['personal_code']; + this.role_code = _data['role_code']; + this.display_name = _data['display_name']; + } + } + + static fromJS(data: any): JudiciaryParticipantResponse { + data = typeof data === 'object' ? data : {}; + let result = new JudiciaryParticipantResponse(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data['title'] = this.title; + data['first_name'] = this.first_name; + data['last_name'] = this.last_name; + data['full_name'] = this.full_name; + data['email'] = this.email; + data['work_phone'] = this.work_phone; + data['personal_code'] = this.personal_code; + data['role_code'] = this.role_code; + data['display_name'] = this.display_name; + return data; + } +} + +export interface IJudiciaryParticipantResponse { + /** Judiciary person's Title. */ + title?: string | undefined; + /** Judiciary person's first name. */ + first_name?: string | undefined; + /** Judiciary person's last name. */ + last_name?: string | undefined; + /** Judiciary person's full name. */ + full_name?: string | undefined; + /** Judiciary person's contact email */ + email?: string | undefined; + /** Judiciary person's work phone */ + work_phone?: string | undefined; + /** The Judiciary person's unique personal code */ + personal_code?: string | undefined; + /** The Judiciary person's role code (Judge or Panel Member) */ + role_code?: string | undefined; + /** The judiciary person's display name */ + display_name?: string | undefined; +} + +export class JudiciaryPerson implements IJudiciaryPerson { + /** Judiciary person's Title. */ + title?: string | undefined; + /** Judiciary person's first name. */ + first_name?: string | undefined; + /** Judiciary person's last name. */ + last_name?: string | undefined; + /** Judiciary person's full name. */ + full_name?: string | undefined; + /** Judiciary person's contact email */ + email?: string | undefined; + /** Judiciary person's work phone */ + work_phone?: string | undefined; + /** The Judiciary person's unique personal code */ + personal_code?: string | undefined; + + constructor(data?: IJudiciaryPerson) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.title = _data['title']; + this.first_name = _data['first_name']; + this.last_name = _data['last_name']; + this.full_name = _data['full_name']; + this.email = _data['email']; + this.work_phone = _data['work_phone']; + this.personal_code = _data['personal_code']; + } + } + + static fromJS(data: any): JudiciaryPerson { + data = typeof data === 'object' ? data : {}; + let result = new JudiciaryPerson(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data['title'] = this.title; + data['first_name'] = this.first_name; + data['last_name'] = this.last_name; + data['full_name'] = this.full_name; + data['email'] = this.email; + data['work_phone'] = this.work_phone; + data['personal_code'] = this.personal_code; + return data; + } +} + +export interface IJudiciaryPerson { + /** Judiciary person's Title. */ + title?: string | undefined; + /** Judiciary person's first name. */ + first_name?: string | undefined; + /** Judiciary person's last name. */ + last_name?: string | undefined; + /** Judiciary person's full name. */ + full_name?: string | undefined; + /** Judiciary person's contact email */ + email?: string | undefined; + /** Judiciary person's work phone */ + work_phone?: string | undefined; + /** The Judiciary person's unique personal code */ + personal_code?: string | undefined; +} + export class LinkedParticipantResponse implements ILinkedParticipantResponse { linked_id?: string; type?: LinkedParticipantType; @@ -7178,6 +7401,8 @@ export class EditHearingRequest implements IEditHearingRequest { case!: EditCaseRequest; /** List of participants in hearing */ participants!: EditParticipantRequest[] | undefined; + /** List of judiciary participants in hearing */ + judiciary_participants?: JudiciaryParticipantRequest[] | undefined; telephone_participants?: EditTelephoneParticipantRequest[] | undefined; /** Any other information about the hearing */ other_information?: string | undefined; @@ -7209,6 +7434,11 @@ export class EditHearingRequest implements IEditHearingRequest { this.participants = [] as any; for (let item of _data['participants']) this.participants!.push(EditParticipantRequest.fromJS(item)); } + if (Array.isArray(_data['judiciary_participants'])) { + this.judiciary_participants = [] as any; + for (let item of _data['judiciary_participants']) + this.judiciary_participants!.push(JudiciaryParticipantRequest.fromJS(item)); + } if (Array.isArray(_data['telephone_participants'])) { this.telephone_participants = [] as any; for (let item of _data['telephone_participants']) @@ -7242,6 +7472,10 @@ export class EditHearingRequest implements IEditHearingRequest { data['participants'] = []; for (let item of this.participants) data['participants'].push(item.toJSON()); } + if (Array.isArray(this.judiciary_participants)) { + data['judiciary_participants'] = []; + for (let item of this.judiciary_participants) data['judiciary_participants'].push(item.toJSON()); + } if (Array.isArray(this.telephone_participants)) { data['telephone_participants'] = []; for (let item of this.telephone_participants) data['telephone_participants'].push(item.toJSON()); @@ -7271,6 +7505,8 @@ export interface IEditHearingRequest { case: EditCaseRequest; /** List of participants in hearing */ participants: EditParticipantRequest[] | undefined; + /** List of judiciary participants in hearing */ + judiciary_participants?: JudiciaryParticipantRequest[] | undefined; telephone_participants?: EditTelephoneParticipantRequest[] | undefined; /** Any other information about the hearing */ other_information?: string | undefined; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/launch-darkly.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/launch-darkly.service.ts index 891ebf922..ad6587b80 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/launch-darkly.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/launch-darkly.service.ts @@ -10,7 +10,8 @@ export const FeatureFlags = { eJudFeature: 'ejud-feature', dom1Integration: 'dom1', hrsIntegration: 'hrs-integration', - referenceData: 'reference-data' + referenceData: 'reference-data', + useV2Api: 'use-bookings-api-v2' }; @Injectable({ diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/recording-guard.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/recording-guard.service.ts index d1428c693..539cf3719 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/recording-guard.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/recording-guard.service.ts @@ -7,12 +7,16 @@ import { ParticipantModel } from '../common/model/participant.model'; export class RecordingGuardService { excludedCaseTypes: string[] = ['Court of Appeal Criminal Division', 'Crime Crown Court']; mandatoryRecordingRoles: string[] = ['Interpreter']; + mandatoryRecordingRoleCodes: string[] = ['INTP']; switchOffRecording(caseType: string): boolean { return this.excludedCaseTypes.indexOf(caseType) > -1; } mandatoryRecordingForHearingRole(participants: ParticipantModel[]) { - return participants.some(pat => this.mandatoryRecordingRoles.includes(pat.hearing_role_name.trim())); + return ( + participants.some(pat => this.mandatoryRecordingRoles.includes(pat.hearing_role_name?.trim())) || + participants.some(pat => this.mandatoryRecordingRoleCodes.includes(pat.hearing_role_code?.trim())) + ); } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.spec.ts index feb116b53..37ba2896e 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.spec.ts @@ -136,15 +136,15 @@ describe('SearchService', () => { beforeEach(() => { clientApiSpy = jasmine.createSpyObj('BHClient', [ 'postPersonBySearchTerm', + 'searchForJudiciaryPerson', 'postJudiciaryPersonBySearchTerm', - 'postJudgesBySearchTerm', 'getStaffMembersBySearchTerm' ]); clientApiSpy.postPersonBySearchTerm.and.returnValue(of(personList)); clientApiSpy.getStaffMembersBySearchTerm.and.returnValue(of(staffMemberList)); - clientApiSpy.postJudiciaryPersonBySearchTerm.and.returnValue(of(judiciaryPersonList)); - clientApiSpy.postJudgesBySearchTerm.and.returnValue(of(judgeList)); + clientApiSpy.searchForJudiciaryPerson.and.returnValue(of(judiciaryPersonList)); + clientApiSpy.postJudiciaryPersonBySearchTerm.and.returnValue(of(judgeList)); launchDarklyServiceSpy.getFlag.withArgs(FeatureFlags.eJudFeature).and.returnValue(of(true)); @@ -166,8 +166,8 @@ describe('SearchService', () => { beforeEach(() => { clientApiSpy = jasmine.createSpyObj('BHClient', [ 'postPersonBySearchTerm', + 'searchForJudiciaryPerson', 'postJudiciaryPersonBySearchTerm', - 'postJudgesBySearchTerm', 'getStaffMembersBySearchTerm' ]); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.ts index 05c79cdbf..9ad11800e 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/search.service.ts @@ -94,7 +94,7 @@ export class SearchService { } searchJudiciaryEntries(term): Observable> { - return this.bhClient.postJudiciaryPersonBySearchTerm(term); + return this.bhClient.searchForJudiciaryPerson(term); } searchStaffMemberAccounts(term): Observable> { @@ -102,6 +102,6 @@ export class SearchService { } searchJudgeAccounts(term): Observable> { - return this.bhClient.postJudgesBySearchTerm(term); + return this.bhClient.postJudiciaryPersonBySearchTerm(term); } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.spec.ts index 642626bf6..6843ab886 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.spec.ts @@ -12,7 +12,8 @@ import { LinkedParticipantResponse, BookingStatus, AllocatedCsoResponse, - JusticeUserResponse + JusticeUserResponse, + JudiciaryParticipantResponse } from './clients/api-client'; import { HearingModel } from '../common/model/hearing.model'; import { CaseModel } from '../common/model/case.model'; @@ -20,6 +21,7 @@ import { ParticipantModel } from '../common/model/participant.model'; import { lastValueFrom, of } from 'rxjs'; import { EndpointModel } from '../common/model/endpoint.model'; import { LinkedParticipantModel, LinkedParticipantType } from '../common/model/linked-participant.model'; +import { JudicialMemberDto } from '../booking/judicial-office-holders/models/add-judicial-member.model'; describe('Video hearing service', () => { let service: VideoHearingsService; @@ -200,6 +202,19 @@ describe('Video hearing service', () => { model.other_information = 'note'; model.cases = [caseModel]; model.participants = []; + model.judiciary_participants = [ + new JudiciaryParticipantResponse({ + title: 'Mr', + first_name: 'Dan', + last_name: 'Smith', + display_name: 'Judge Dan Smith', + email: 'joh@judge.com', + full_name: 'Dan Smith', + personal_code: '1234', + work_phone: '123123123', + role_code: 'Judge' + }) + ]; model.audio_recording_required = true; const request = service.mapHearingDetailsResponseToHearingModel(model); @@ -214,6 +229,8 @@ describe('Video hearing service', () => { expect(request.scheduled_date_time).toEqual(new Date(date)); expect(request.scheduled_duration).toBe(30); expect(request.audio_recording_required).toBeTruthy(); + expect(request.judiciaryParticipants[0]).toBeTruthy(); + expect(request.judiciaryParticipants[0].displayName).toBe('Judge Dan Smith'); }); it('should map ParticipantResponse to ParticipantModel', () => { @@ -617,4 +634,132 @@ describe('Video hearing service', () => { await service.getHearingRoles(); expect(clientApiSpy.getHearingRoles).toHaveBeenCalled(); }); + + describe('addJudiciaryJudge', () => { + it('should add a new judge when none exists', () => { + // Arrange + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + spyOn(service['modelHearing'].judiciaryParticipants, 'findIndex').and.returnValue(-1); + + // Act + service.addJudiciaryJudge(judicialMember); + + // Assert + expect(service['modelHearing'].judiciaryParticipants.length).toBe(1); + expect(service['modelHearing'].judiciaryParticipants[0]).toBe(judicialMember); + }); + + it('should replace an existing judge', () => { + // Arrange + const newJudge = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + const existingJudge = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '5678'); + service['modelHearing'].judiciaryParticipants.push(existingJudge); + spyOn(service['modelHearing'].judiciaryParticipants, 'findIndex').and.returnValue(0); + + // Act + service.addJudiciaryJudge(newJudge); + + // Assert + expect(service['modelHearing'].judiciaryParticipants.length).toBe(1); + expect(service['modelHearing'].judiciaryParticipants[0]).toBe(newJudge); + }); + }); + + describe('removeJudiciaryJudge', () => { + it('should remove judge from judiciary participants', () => { + // Arrange + const judge = new JudicialMemberDto('Test', 'User', 'Test User', 'test1@test.com', '1234567890', '1234'); + judge.roleCode = 'Judge'; + const nonJudge = new JudicialMemberDto('Test', 'User', 'Test User', 'test2@test.com', '1234567890', '5678'); + nonJudge.roleCode = 'PanelMember'; + service['modelHearing'].judiciaryParticipants = [judge, nonJudge]; + + // Act + service.removeJudiciaryJudge(); + + // Assert + expect(service['modelHearing'].judiciaryParticipants.length).toBe(1); + expect(service['modelHearing'].judiciaryParticipants[0]).toBe(nonJudge); + }); + + it('should not remove anything if judge is not present', () => { + // Arrange + const nonJudge1 = new JudicialMemberDto('Test', 'User', 'Test User', 'test1@test.com', '1234567890', '1234'); + nonJudge1.roleCode = 'PanelMember'; + const nonJudge2 = new JudicialMemberDto('Test', 'User', 'Test User', 'test2@test.com', '1234567890', '5678'); + nonJudge2.roleCode = 'PanelMember'; + service['modelHearing'].judiciaryParticipants = [nonJudge1, nonJudge2]; + + // Act + service.removeJudiciaryJudge(); + + // Assert + expect(service['modelHearing'].judiciaryParticipants.length).toBe(2); + expect(service['modelHearing'].judiciaryParticipants[0]).toBe(nonJudge1); + expect(service['modelHearing'].judiciaryParticipants[1]).toBe(nonJudge2); + }); + }); + + describe('addJudiciaryPanelMember', () => { + it('should add a new judiciary panel member', () => { + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + + service.addJudiciaryPanelMember(judicialMember); + + expect(service['modelHearing'].judiciaryParticipants.length).toBe(1); + expect(service['modelHearing'].judiciaryParticipants[0]).toEqual(judicialMember); + }); + + it('should update an existing judiciary panel member', () => { + const judicialMember1 = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + judicialMember1.displayName = 'Test User 1'; + const judicialMember2 = new JudicialMemberDto('Test', 'User', 'Test User', 'test@test.com', '1234567890', '1234'); + judicialMember2.displayName = 'Test User 2'; + + service.addJudiciaryPanelMember(judicialMember1); + service.addJudiciaryPanelMember(judicialMember2); + + expect(service['modelHearing'].judiciaryParticipants.length).toBe(1); + expect(service['modelHearing'].judiciaryParticipants[0]).toEqual(judicialMember2); + }); + }); + + describe('removeJudiciaryParticipant', () => { + it('should remove judiciary participant from modelHearing', () => { + // Arrange + const participantEmail = 'test@example.com'; + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', participantEmail, '1234567890', '1234'); + service['modelHearing'].judiciaryParticipants = [judicialMember]; + + // Act + service.removeJudiciaryParticipant(participantEmail); + + // Assert + expect(service['modelHearing'].judiciaryParticipants).not.toContain(judicialMember); + }); + + it('should not remove judiciary participant if email does not match', () => { + // Arrange + const participantEmail = 'test@example.com'; + const judicialMember = new JudicialMemberDto('Test', 'User', 'Test User', participantEmail, '1234567890', '1234'); + service['modelHearing'].judiciaryParticipants = [judicialMember]; + + // Act + service.removeJudiciaryParticipant('other@example.com'); + + // Assert + expect(service['modelHearing'].judiciaryParticipants).toContain(judicialMember); + }); + + it('should not remove judiciary participant if modelHearing is undefined', () => { + // Arrange + service['modelHearing'] = undefined; + + // Act + service.removeJudiciaryParticipant('test@example.com'); + + // Assert + expect(service['modelHearing']).toBeUndefined(); + }); + }); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.ts index 1264a1f7b..3dc4af6c5 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/video-hearings.service.ts @@ -26,7 +26,9 @@ import { LinkedParticipant, BookingStatus, AllocatedCsoResponse, - HearingRoleResponse + HearingRoleResponse, + JudiciaryParticipantRequest, + JudiciaryParticipantResponse } from './clients/api-client'; import { HearingModel } from '../common/model/hearing.model'; import { CaseModel } from '../common/model/case.model'; @@ -35,6 +37,7 @@ import { EndpointModel } from '../common/model/endpoint.model'; import { LinkedParticipantModel } from '../common/model/linked-participant.model'; import { Constants } from '../common/constants'; import * as moment from 'moment'; +import { JudicialMemberDto } from '../booking/judicial-office-holders/models/add-judicial-member.model'; @Injectable({ providedIn: 'root' @@ -214,6 +217,7 @@ export class VideoHearingsService { hearing.scheduled_date_time = new Date(booking.scheduled_date_time); hearing.scheduled_duration = booking.scheduled_duration; hearing.participants = this.mapParticipantModelToEditParticipantRequest(booking.participants); + hearing.judiciary_participants = this.mapJudicialMemberDtoToJudiciaryParticipantRequest(booking.judiciaryParticipants); hearing.audio_recording_required = booking.audio_recording_required; hearing.endpoints = this.mapEndpointModelToEditEndpointRequest(booking.endpoints); return hearing; @@ -296,6 +300,8 @@ export class VideoHearingsService { newHearingRequest.audio_recording_required = newRequest.audio_recording_required; newHearingRequest.endpoints = this.mapEndpoints(newRequest.endpoints); newHearingRequest.linked_participants = this.mapLinkedParticipants(newRequest.linked_participants); + newHearingRequest.judiciary_participants = this.mapJudicialMemberDtoToJudiciaryParticipantRequest(newRequest.judiciaryParticipants); + return newHearingRequest; } @@ -320,6 +326,9 @@ export class VideoHearingsService { hearing.status = response.status; hearing.audio_recording_required = response.audio_recording_required; hearing.endpoints = this.mapEndpointResponseToEndpointModel(response.endpoints, response.participants); + hearing.judiciaryParticipants = response.judiciary_participants.map(judiciaryParticipant => + JudicialMemberDto.fromJudiciaryParticipantResponse(judiciaryParticipant) + ); hearing.isConfirmed = Boolean(response.confirmed_date); return hearing; } @@ -352,6 +361,17 @@ export class VideoHearingsService { return cases; } + mapJudicialMemberDtoToJudiciaryParticipantRequest(judicialMemberDtos: JudicialMemberDto[]): JudiciaryParticipantRequest[] { + return judicialMemberDtos.map(judicialMemberDto => { + const judiciaryParticipantRequest: JudiciaryParticipantRequest = new JudiciaryParticipantRequest({ + personal_code: judicialMemberDto.personalCode, + display_name: judicialMemberDto.displayName, + role: judicialMemberDto.roleCode + }); + return judiciaryParticipantRequest; + }); + } + mapParticipants(newRequest: ParticipantModel[]): ParticipantRequest[] { const participants: ParticipantRequest[] = []; let participant: ParticipantRequest; @@ -524,4 +544,42 @@ export class VideoHearingsService { getAllocatedCsoForHearing(hearingId: string): Observable { return this.bhClient.getAllocationForHearing(hearingId); } + + addJudiciaryJudge(judicialMember: JudicialMemberDto) { + const judgeIndex = this.modelHearing.judiciaryParticipants.findIndex(holder => holder.roleCode === 'Judge'); + + if (judgeIndex !== -1) { + // Judge exists, replace or add entry + this.modelHearing.judiciaryParticipants[judgeIndex] = judicialMember; + } else { + // Judge does not exist, add entry + this.modelHearing.judiciaryParticipants.push(judicialMember); + } + } + + removeJudiciaryJudge() { + const judgeIndex = this.modelHearing.judiciaryParticipants.findIndex(holder => holder.roleCode === 'Judge'); + if (judgeIndex !== -1) { + this.modelHearing.judiciaryParticipants.splice(judgeIndex, 1); + } + } + + addJudiciaryPanelMember(judicialMember: JudicialMemberDto) { + const panelMemberIndex = this.modelHearing.judiciaryParticipants.findIndex( + holder => holder.personalCode === judicialMember.personalCode + ); + if (panelMemberIndex !== -1) { + this.modelHearing.judiciaryParticipants[panelMemberIndex] = judicialMember; + } else { + this.modelHearing.judiciaryParticipants.push(judicialMember); + } + } + + removeJudiciaryParticipant(participantEmail: string) { + const index = + this.modelHearing?.judiciaryParticipants?.findIndex(judicialMember => judicialMember.email === participantEmail) ?? -1; + if (index !== -1) { + this.modelHearing.judiciaryParticipants.splice(index, 1); + } + } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/case-types-menu/case-types-menu.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/case-types-menu/case-types-menu.component.spec.ts index 50dea6f16..f875a3f67 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/case-types-menu/case-types-menu.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/case-types-menu/case-types-menu.component.spec.ts @@ -2,12 +2,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CaseTypesMenuComponent } from './case-types-menu.component'; import { HttpClient, HttpHandler } from '@angular/common/http'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MockLogger } from '../../testing/mock-logger'; import { Logger } from '../../../services/logger'; import { VideoHearingsService } from '../../../services/video-hearings.service'; import { of, throwError } from 'rxjs'; import { HearingTypeResponse } from '../../../services/clients/api-client'; +import { NgSelectModule } from '@ng-select/ng-select'; describe('CaseTypesMenuComponent', () => { let component: CaseTypesMenuComponent; @@ -19,6 +20,7 @@ describe('CaseTypesMenuComponent', () => { videoHearingServiceSpy = jasmine.createSpyObj('VideoHearingsService', ['getHearingTypes']); videoHearingServiceSpy.getHearingTypes.and.returnValue(of([new HearingTypeResponse({ group: caseType })])); await TestBed.configureTestingModule({ + imports: [NgSelectModule, ReactiveFormsModule], declarations: [CaseTypesMenuComponent], providers: [ HttpClient, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts index e478371fe..81b51fcbc 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts @@ -1,12 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { JusticeUsersMenuComponent } from './justice-users-menu.component'; import { HttpClient, HttpHandler } from '@angular/common/http'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MockLogger } from '../../testing/mock-logger'; import { Logger } from '../../../services/logger'; import { JusticeUsersService } from 'src/app/services/justice-users.service'; import { BehaviorSubject } from 'rxjs'; import { JusticeUserResponse } from '../../../services/clients/api-client'; +import { NgSelectModule } from '@ng-select/ng-select'; describe('JusticeUsersMenuComponent', () => { let component: JusticeUsersMenuComponent; @@ -24,6 +25,7 @@ describe('JusticeUsersMenuComponent', () => { justiceUsersServiceSpy.allUsers$ = new BehaviorSubject(users); await TestBed.configureTestingModule({ declarations: [JusticeUsersMenuComponent], + imports: [NgSelectModule, ReactiveFormsModule], providers: [ HttpClient, HttpHandler, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/venues-menu/venues-menu.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/venues-menu/venues-menu.component.spec.ts index f9cc74301..ad66e9d01 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/venues-menu/venues-menu.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/venues-menu/venues-menu.component.spec.ts @@ -1,12 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { VenuesMenuComponent } from './venues-menu.component'; import { HttpClient, HttpHandler } from '@angular/common/http'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MockLogger } from '../../testing/mock-logger'; import { Logger } from '../../../services/logger'; import { HearingVenueResponse } from '../../../services/clients/api-client'; import { of, throwError } from 'rxjs'; import { ReferenceDataService } from '../../../services/reference-data.service'; +import { NgSelectModule } from '@ng-select/ng-select'; describe('VenuesMenuComponent', () => { let component: VenuesMenuComponent; @@ -18,6 +19,7 @@ describe('VenuesMenuComponent', () => { refDataServiceSpy.getCourts.and.returnValue(of([new HearingVenueResponse()])); await TestBed.configureTestingModule({ + imports: [NgSelectModule, ReactiveFormsModule], declarations: [VenuesMenuComponent], providers: [ HttpClient, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/page-url.constants.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/page-url.constants.ts index 6f8e97a17..b7416e83f 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/page-url.constants.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/page-url.constants.ts @@ -6,6 +6,7 @@ export const PageUrls = { CreateHearing: '/book-hearing', HearingSchedule: '/hearing-schedule', AssignJudge: '/assign-judge', + AddJudicialOfficeHolders: '/add-judicial-office-holders', AddParticipants: '/add-participants', Endpoints: '/video-access-points', OtherInformation: '/other-information', diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/truncatable-text/truncatable-text.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/truncatable-text/truncatable-text.component.spec.ts index 87f1d7820..099f076ae 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/truncatable-text/truncatable-text.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/truncatable-text/truncatable-text.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TruncatableTextComponent } from './truncatable-text.component'; +import { TooltipDirective } from '../directives/tooltip.directive'; describe('TruncatableTextComponent', () => { let component: TruncatableTextComponent; @@ -7,7 +8,8 @@ describe('TruncatableTextComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [TruncatableTextComponent] + declarations: [TruncatableTextComponent, TooltipDirective], + providers: [TooltipDirective] }).compileComponents(); fixture = TestBed.createComponent(TruncatableTextComponent); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/testing/stubs/participant-list-stub.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/testing/stubs/participant-list-stub.ts index fb4569411..09b200391 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/testing/stubs/participant-list-stub.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/testing/stubs/participant-list-stub.ts @@ -4,6 +4,9 @@ import { HearingModel } from '../../common/model/hearing.model'; @Component({ selector: 'app-participant-list', template: '
' }) export class ParticipantsListStubComponent { + @Input() isSummaryPage = false; + @Input() canEdit = false; + @Input() hearing: HearingModel; participants: ParticipantRequest[]; diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/allocate-hearings/allocate-hearings.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/allocate-hearings/allocate-hearings.component.spec.ts index 10b894a30..e7c71abd1 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/allocate-hearings/allocate-hearings.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/allocate-hearings/allocate-hearings.component.spec.ts @@ -18,6 +18,7 @@ import { VideoHearingsService } from 'src/app/services/video-hearings.service'; import { SharedModule } from '../../shared/shared.module'; import { Logger } from 'src/app/services/logger'; import { SelectComponent, SelectOption } from 'src/app/shared/select'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; describe('AllocateHearingsComponent', () => { let component: AllocateHearingsComponent; @@ -76,7 +77,7 @@ describe('AllocateHearingsComponent', () => { { provide: VideoHearingsService, useValue: hearingServiceMock }, { provide: Logger, useValue: loggerMock } ], - imports: [SharedModule] + imports: [SharedModule, FontAwesomeModule] }).compileComponents(); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-search/vho-search.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-search/vho-search.component.spec.ts index fedbf5d3b..985bd3fb7 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-search/vho-search.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-search/vho-search.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin import { VhoSearchComponent } from './vho-search.component'; import { VhoNonAvailabilityWorkHoursResponse, VhoWorkHoursResponse } from '../../../services/clients/api-client'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { Logger } from '../../../services/logger'; import { HoursType } from '../../../common/model/hours-type'; import { VideoHearingsService } from '../../../services/video-hearings.service'; @@ -27,6 +27,7 @@ describe('VhoSearchComponent', () => { ]); logger = jasmine.createSpyObj('Logger', ['debug']); await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], declarations: [VhoSearchComponent], providers: [ FormBuilder, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-work-hours-non-availability-table/vho-work-hours-non-availability-table.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-work-hours-non-availability-table/vho-work-hours-non-availability-table.component.spec.ts index 09e127748..e94bb0f3d 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-work-hours-non-availability-table/vho-work-hours-non-availability-table.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/edit-work-hours/vho-work-hours-non-availability-table/vho-work-hours-non-availability-table.component.spec.ts @@ -7,9 +7,10 @@ import { DatePipe } from '@angular/common'; import { ValidationFailure, VhoWorkHoursNonAvailabilityTableComponent } from './vho-work-hours-non-availability-table.component'; import { EditVhoNonAvailabilityWorkHoursModel } from '../edit-non-work-hours-model'; import { By } from '@angular/platform-browser'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { VideoHearingsService } from '../../../services/video-hearings.service'; import { MockWorkAllocationValues } from '../../../testing/data/work-allocation-test-data'; +import { FontAwesomeTestingModule } from '@fortawesome/angular-fontawesome/testing'; describe('VhoNonAvailabilityWorkHoursTableComponent', () => { let component: VhoWorkHoursNonAvailabilityTableComponent; @@ -26,6 +27,7 @@ describe('VhoNonAvailabilityWorkHoursTableComponent', () => { bHClientSpy.deleteNonAvailabilityWorkHours.and.returnValue(of(undefined)); loggerSpy = jasmine.createSpyObj('Logger', ['info', 'error']); await TestBed.configureTestingModule({ + imports: [FontAwesomeTestingModule, ReactiveFormsModule, FormsModule], providers: [ { provide: Logger, useValue: loggerSpy }, { provide: BHClient, useValue: bHClientSpy }, diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/styles.scss b/AdminWebsite/AdminWebsite/ClientApp/src/styles.scss index cc15ba459..a87228af1 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/styles.scss +++ b/AdminWebsite/AdminWebsite/ClientApp/src/styles.scss @@ -90,3 +90,12 @@ body { filter: opacity(50%); } } + +.keep-right { + float: right; +} + +.vh-horizontal-align { + display: flex; + align-items: center; +} diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/tslint.json b/AdminWebsite/AdminWebsite/ClientApp/src/tslint.json index 68e1466fa..435337719 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/tslint.json +++ b/AdminWebsite/AdminWebsite/ClientApp/src/tslint.json @@ -5,6 +5,7 @@ }, "rules": { "directive-selector": [true, "attribute", "app", "camelCase"], - "component-selector": [true, "element", "app", "kebab-case"] + "component-selector": [true, "element", "app", "kebab-case"], + "no-unused-expression": true } } diff --git a/AdminWebsite/AdminWebsite/Configuration/FeatureToggles.cs b/AdminWebsite/AdminWebsite/Configuration/FeatureToggles.cs index a5e4e8dee..3e50b2225 100644 --- a/AdminWebsite/AdminWebsite/Configuration/FeatureToggles.cs +++ b/AdminWebsite/AdminWebsite/Configuration/FeatureToggles.cs @@ -11,6 +11,7 @@ public interface IFeatureToggles public bool BookAndConfirmToggle(); public bool Dom1Enabled(); public bool ReferenceDataToggle(); + public bool UseV2Api(); public bool EJudEnabled(); } @@ -22,6 +23,7 @@ public class FeatureToggles : IFeatureToggles private const string BookAndConfirmToggleKey = "Book_and_Confirm"; private const string Dom1EnabledToggleKey = "dom1"; private const string ReferenceDataToggleKey = "reference-data"; + private const string UseV2ApiToggleKey = "use-bookings-api-v2"; private const string EJudFeatureToggleKey = "ejud-feature"; public FeatureToggles(string sdkKey, string environmentName) @@ -52,6 +54,11 @@ public bool EJudEnabled() return GetBoolValueWithKey(EJudFeatureToggleKey); } + public bool UseV2Api() + { + return GetBoolValueWithKey(UseV2ApiToggleKey); + } + private bool GetBoolValueWithKey(string key) { if (!_ldClient.Initialized) @@ -61,5 +68,6 @@ private bool GetBoolValueWithKey(string key) return _ldClient.BoolVariation(key, _context); } + } } \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite/Contracts/Requests/BookingDetailsRequest.cs b/AdminWebsite/AdminWebsite/Contracts/Requests/BookingDetailsRequest.cs index ef37f0453..fe5264547 100644 --- a/AdminWebsite/AdminWebsite/Contracts/Requests/BookingDetailsRequest.cs +++ b/AdminWebsite/AdminWebsite/Contracts/Requests/BookingDetailsRequest.cs @@ -16,6 +16,7 @@ public class BookingDetailsRequest public string HearingTypeCode { get; set; } public List Cases { get; set; } public List Participants { get; set; } + public List JudiciaryParticipants { get; set; } public string HearingRoomName { get; set; } public string OtherInformation { get; set; } public string CreatedBy { get; set; } diff --git a/AdminWebsite/AdminWebsite/Contracts/Requests/JudiciaryParticipantRequest.cs b/AdminWebsite/AdminWebsite/Contracts/Requests/JudiciaryParticipantRequest.cs new file mode 100644 index 000000000..cdf8ae2c7 --- /dev/null +++ b/AdminWebsite/AdminWebsite/Contracts/Requests/JudiciaryParticipantRequest.cs @@ -0,0 +1,8 @@ +namespace AdminWebsite.Contracts.Requests; + +public class JudiciaryParticipantRequest +{ + public string PersonalCode { get; set; } + public string Role { get; set; } + public string DisplayName { get; set; } +} diff --git a/AdminWebsite/AdminWebsite/Contracts/Requests/ParticipantRequest.cs b/AdminWebsite/AdminWebsite/Contracts/Requests/ParticipantRequest.cs index 51be19562..c30039845 100644 --- a/AdminWebsite/AdminWebsite/Contracts/Requests/ParticipantRequest.cs +++ b/AdminWebsite/AdminWebsite/Contracts/Requests/ParticipantRequest.cs @@ -19,4 +19,4 @@ public class ParticipantRequest public string HearingRoleCode { get; set; } public string Representee { get; set; } public string OrganisationName { get; set; } -} +} \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite/Contracts/Responses/HearingDetailsResponse.cs b/AdminWebsite/AdminWebsite/Contracts/Responses/HearingDetailsResponse.cs index f8f262326..f7e8fbd12 100644 --- a/AdminWebsite/AdminWebsite/Contracts/Responses/HearingDetailsResponse.cs +++ b/AdminWebsite/AdminWebsite/Contracts/Responses/HearingDetailsResponse.cs @@ -39,6 +39,9 @@ public class HearingDetailsResponse /// V1 only /// public List TelephoneParticipants { get; set; } + + public List JudiciaryParticipants { get; set; } + public string HearingRoomName { get; set; } public string OtherInformation { get; set; } public DateTime CreatedDate { get; set; } diff --git a/AdminWebsite/AdminWebsite/Contracts/Responses/JudiciaryParticipantResponse.cs b/AdminWebsite/AdminWebsite/Contracts/Responses/JudiciaryParticipantResponse.cs new file mode 100644 index 000000000..da2cf0588 --- /dev/null +++ b/AdminWebsite/AdminWebsite/Contracts/Responses/JudiciaryParticipantResponse.cs @@ -0,0 +1,49 @@ +namespace AdminWebsite.Contracts.Responses; + +public class JudiciaryParticipantResponse +{ + /// + /// Judiciary person's Title. + /// + public string Title { get; set; } + + /// + /// Judiciary person's first name. + /// + public string FirstName { get; set; } + + /// + /// Judiciary person's last name. + /// + public string LastName { get; set; } + + /// + /// Judiciary person's full name. + /// + public string FullName { get; set; } + + /// + /// Judiciary person's contact email + /// + public string Email { get; set; } + + /// + /// Judiciary person's work phone + /// + public string WorkPhone { get; set; } + + /// + /// The Judiciary person's unique personal code + /// + public string PersonalCode { get; set; } + + /// + /// The Judiciary person's role code (Judge or Panel Member) + /// + public string RoleCode { get; set; } + + /// + /// The judiciary person's display name + /// + public string DisplayName { get; set; } +} \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite/Contracts/Responses/JudiciaryPerson.cs b/AdminWebsite/AdminWebsite/Contracts/Responses/JudiciaryPerson.cs new file mode 100644 index 000000000..f94c280f8 --- /dev/null +++ b/AdminWebsite/AdminWebsite/Contracts/Responses/JudiciaryPerson.cs @@ -0,0 +1,39 @@ +namespace AdminWebsite.Contracts.Responses; + +public class JudiciaryPerson +{ + /// + /// Judiciary person's Title. + /// + public string Title { get; set; } + + /// + /// Judiciary person's first name. + /// + public string FirstName { get; set; } + + /// + /// Judiciary person's last name. + /// + public string LastName { get; set; } + + /// + /// Judiciary person's full name. + /// + public string FullName { get; set; } + + /// + /// Judiciary person's contact email + /// + public string Email { get; set; } + + /// + /// Judiciary person's work phone + /// + public string WorkPhone { get; set; } + + /// + /// The Judiciary person's unique personal code + /// + public string PersonalCode { get; set; } +} \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite/Controllers/HearingsController.cs b/AdminWebsite/AdminWebsite/Controllers/HearingsController.cs index cd5dda99e..bb902c589 100644 --- a/AdminWebsite/AdminWebsite/Controllers/HearingsController.cs +++ b/AdminWebsite/AdminWebsite/Controllers/HearingsController.cs @@ -16,6 +16,7 @@ using BookingsApi.Client; using BookingsApi.Contract.Interfaces.Requests; using BookingsApi.Contract.V1.Requests; +using BookingsApi.Contract.V1.Requests.Enums; using BookingsApi.Contract.V2.Requests; using FluentValidation; using Microsoft.AspNetCore.Mvc; @@ -118,8 +119,8 @@ private async Task BookNewHearing(BookingDetailsRequest newBookingReques Guid hearingId; _logger.LogInformation("BookNewHearing - Attempting to send booking request to Booking API"); - - if (_featureToggles.ReferenceDataToggle()) + + if (_featureToggles.UseV2Api()) { var newBookingRequestV2 = newBookingRequest.MapToV2(); @@ -308,7 +309,7 @@ public async Task> EditHearing(Guid hearing private async Task GetHearing(Guid hearingId) { - if (_featureToggles.ReferenceDataToggle()) + if (_featureToggles.UseV2Api()) { var responseV2 = await _bookingsApiClient.GetHearingDetailsByIdV2Async(hearingId); return responseV2.Map(); @@ -320,7 +321,7 @@ private async Task GetHearing(Guid hearingId) private async Task MapHearingToUpdate(Guid hearingId) { - if (_featureToggles.ReferenceDataToggle()) + if (_featureToggles.UseV2Api()) { var updatedHearing2 = await _bookingsApiClient.GetHearingDetailsByIdV2Async(hearingId); return updatedHearing2.Map(); @@ -333,18 +334,20 @@ private async Task MapHearingToUpdate(Guid hearingId) private async Task UpdateHearing(EditHearingRequest request, Guid hearingId, HearingDetailsResponse originalHearing) { //Save hearing details - if (_featureToggles.ReferenceDataToggle()) + if (_featureToggles.UseV2Api()) { var updateHearingRequestV2 = HearingUpdateRequestMapper.MapToV2(request, _userIdentity.GetUserIdentityName()); await _bookingsApiClient.UpdateHearingDetails2Async(hearingId, updateHearingRequestV2); await UpdateParticipantsV2(hearingId, request, originalHearing); - - return; + await UpdateJudiciaryParticipants(hearingId, request, originalHearing); + } + else + { + var updateHearingRequestV1 = + HearingUpdateRequestMapper.MapToV1(request, _userIdentity.GetUserIdentityName()); + await _bookingsApiClient.UpdateHearingDetailsAsync(hearingId, updateHearingRequestV1); + await UpdateParticipantsV1(hearingId, request, originalHearing); } - - var updateHearingRequestV1 = HearingUpdateRequestMapper.MapToV1(request, _userIdentity.GetUserIdentityName()); - await _bookingsApiClient.UpdateHearingDetailsAsync(hearingId, updateHearingRequestV1); - await UpdateParticipantsV1(hearingId, request, originalHearing); } private async Task UpdateParticipantsV1(Guid hearingId, EditHearingRequest request, HearingDetailsResponse originalHearing) @@ -397,6 +400,50 @@ private static List GetRemovedParticipantIds(EditHearingRequest request, H .Select(x => x.Id).ToList(); } + private async Task UpdateJudiciaryParticipants(Guid hearingId, EditHearingRequest request, + HearingDetailsResponse originalHearing) + { + // keep the order of removal first. this will allow admin web to change judiciary judges post booking + var removedJohs = originalHearing.JudiciaryParticipants.Where(ojp => + request.JudiciaryParticipants.TrueForAll(jp => jp.PersonalCode != ojp.PersonalCode)).ToList(); + foreach (var removedJoh in removedJohs) + { + await _bookingsApiClient.RemoveJudiciaryParticipantFromHearingAsync(hearingId, removedJoh.PersonalCode); + } + + var newJohs = request.JudiciaryParticipants.Where(jp => + !originalHearing.JudiciaryParticipants.Exists(ojp => ojp.PersonalCode == jp.PersonalCode)).ToList(); + + var newJohRequest = newJohs.Select(jp => + { + var roleCode = Enum.Parse(jp.Role); + return new BookingsApi.Contract.V1.Requests.JudiciaryParticipantRequest() + { + DisplayName = jp.DisplayName, + PersonalCode = jp.PersonalCode, + HearingRoleCode = roleCode + }; + }).ToList(); + if (newJohRequest.Any()) + { + await _bookingsApiClient.AddJudiciaryParticipantsToHearingAsync(hearingId, newJohRequest); + } + + // get existing judiciary participants based on the personal code being present in the original hearing + var existingJohs = request.JudiciaryParticipants.Where(jp => + originalHearing.JudiciaryParticipants.Exists(ojp => ojp.PersonalCode == jp.PersonalCode)).ToList(); + + foreach (var joh in existingJohs) + { + var roleCode = Enum.Parse(joh.Role); + await _bookingsApiClient.UpdateJudiciaryParticipantAsync(hearingId, joh.PersonalCode, + new UpdateJudiciaryParticipantRequest() + { + DisplayName = joh.DisplayName, HearingRoleCode = roleCode + }); + } + } + private static List ExtractLinkedParticipants( EditHearingRequest request, HearingDetailsResponse originalHearing, @@ -490,7 +537,7 @@ public async Task GetHearingById(Guid hearingId) try { HearingDetailsResponse hearingResponse; - if (_featureToggles.ReferenceDataToggle()) + if (_featureToggles.UseV2Api()) { var response = await _bookingsApiClient.GetHearingDetailsByIdV2Async(hearingId); hearingResponse = response.Map(); diff --git a/AdminWebsite/AdminWebsite/Controllers/JudiciaryAccountsController.cs b/AdminWebsite/AdminWebsite/Controllers/JudiciaryAccountsController.cs index 83d170106..4c4642470 100644 --- a/AdminWebsite/AdminWebsite/Controllers/JudiciaryAccountsController.cs +++ b/AdminWebsite/AdminWebsite/Controllers/JudiciaryAccountsController.cs @@ -1,6 +1,5 @@ using AdminWebsite.Configuration; using AdminWebsite.Contracts.Responses; -using AdminWebsite.Mappers; using AdminWebsite.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -10,6 +9,7 @@ using System.Net; using System.Text.Encodings.Web; using System.Threading.Tasks; +using AdminWebsite.Mappers; using BookingsApi.Client; using BookingsApi.Contract.V1.Requests; using BookingsApi.Contract.V1.Responses; @@ -27,8 +27,8 @@ public class JudiciaryAccountsController : ControllerBase private readonly TestUserSecrets _testSettings; public JudiciaryAccountsController(IUserAccountService userAccountService, JavaScriptEncoder encoder, - IBookingsApiClient bookingsApiClient, IOptions testSettings) - { + IBookingsApiClient bookingsApiClient, IOptions testSettings) + { _userAccountService = userAccountService; _encoder = encoder; _bookingsApiClient = bookingsApiClient; @@ -36,32 +36,33 @@ public JudiciaryAccountsController(IUserAccountService userAccountService, JavaS } /// - /// Find judges and court rooms accounts list by email search term. + /// Find judiciary person list by email search term. /// /// The email address search term. - /// The list of judges - [HttpPost("judges", Name = "PostJudgesBySearchTerm")] - [SwaggerOperation(OperationId = "PostJudgesBySearchTerm")] - [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] + /// The list of judiciary person + [HttpPost(Name = "PostJudiciaryPersonBySearchTerm")] + [SwaggerOperation(OperationId = "PostJudiciaryPersonBySearchTerm")] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task>> PostJudgesBySearchTermAsync([FromBody] string term) + public async Task>> PostJudiciaryPersonBySearchTermAsync([FromBody] string term) { try { term = _encoder.Encode(term); var searchTerm = new SearchTermRequest(term); - - var courtRoomJudgesTask = _userAccountService.SearchJudgesByEmail(searchTerm.Term); + var courtRoomJudgesTask = _userAccountService.SearchEjudiciaryJudgesByEmailUserResponse(searchTerm.Term); var eJudiciaryJudgesTask = GetEjudiciaryJudgesBySearchTermAsync(searchTerm); await Task.WhenAll(courtRoomJudgesTask, eJudiciaryJudgesTask); - var courtRoomJudges = await courtRoomJudgesTask; - var eJudiciaryJudges = (await eJudiciaryJudgesTask).Select(x => JudgeResponseMapper.MapTo(x)); - + var eJudiciaryJudges = (await eJudiciaryJudgesTask) + .Where(p => !p.Username.Contains(_testSettings.TestUsernameStem)).ToList(); + var courtRoomJudges = (await courtRoomJudgesTask) + .Where(x => !eJudiciaryJudges.Select(e => e.Username).Contains(x.ContactEmail)) + .Select(x => UserResponseMapper.MapFrom(x)); + var allJudges = courtRoomJudges.Concat(eJudiciaryJudges) - .OrderBy(x => x.Email).Take(20).ToList(); - + .OrderBy(x => x.ContactEmail).Take(20).ToList(); return Ok(allJudges); } catch (BookingsApiException e) @@ -70,43 +71,30 @@ public async Task>> PostJudgesBySearchTermAsyn { return BadRequest(e.Response); } - throw; } } - /// /// Find judiciary person list by email search term. /// /// The email address search term. /// The list of judiciary person - [HttpPost(Name = "PostJudiciaryPersonBySearchTerm")] - [SwaggerOperation(OperationId = "PostJudiciaryPersonBySearchTerm")] - [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] + [HttpPost("search",Name = "SearchForJudiciaryPerson")] + [SwaggerOperation(OperationId = "SearchForJudiciaryPerson")] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] - public async Task>> PostJudiciaryPersonBySearchTermAsync([FromBody] string term) + public async Task>> SearchForJudiciaryPersonAsync([FromBody] string term) { try { term = _encoder.Encode(term); var searchTerm = new SearchTermRequest(term); - - var courtRoomJudgesTask = _userAccountService.SearchEjudiciaryJudgesByEmailUserResponse(searchTerm.Term); - var eJudiciaryJudgesTask = GetEjudiciaryJudgesBySearchTermAsync(searchTerm); - - await Task.WhenAll(courtRoomJudgesTask, eJudiciaryJudgesTask); - - var eJudiciaryJudges = (await eJudiciaryJudgesTask) - .Where(p => !p.Username.Contains(_testSettings.TestUsernameStem)).ToList(); - var courtRoomJudges = (await courtRoomJudgesTask) - .Where(x => !eJudiciaryJudges.Select(e => e.Username).Contains(x.ContactEmail)) - .Select(x => UserResponseMapper.MapFrom(x)); - var allJudges = courtRoomJudges.Concat(eJudiciaryJudges) - .OrderBy(x => x.ContactEmail).Take(20).ToList(); - - return Ok(allJudges); + var eJudiciaryJudges = (await _bookingsApiClient.PostJudiciaryPersonBySearchTermAsync(searchTerm)).ToList(); + var allJudges = eJudiciaryJudges.OrderBy(x => x.Email).ToList(); + var mapped = allJudges.Select(x => x.MapToAdminWebResponse()).ToList(); + return Ok(mapped); } catch (BookingsApiException e) { @@ -118,12 +106,11 @@ public async Task>> PostJudiciaryPersonBySear throw; } } - - private async Task> GetEjudiciaryJudgesBySearchTermAsync(SearchTermRequest term) + + private async Task> GetEjudiciaryJudgesBySearchTermAsync(SearchTermRequest term) { - var personsResponse = (await _bookingsApiClient.PostJudiciaryPersonBySearchTermAsync(term)).ToList(); - - return personsResponse; + var judiciaryPersonResponses = (await _bookingsApiClient.PostJudiciaryPersonBySearchTermAsync(term)).ToList(); + return judiciaryPersonResponses.Select(x => x.MapToPersonResponse()).ToList(); } } } diff --git a/AdminWebsite/AdminWebsite/Mappers/BookingDetailsRequestMapper.cs b/AdminWebsite/AdminWebsite/Mappers/BookingDetailsRequestMapper.cs index 05a583920..966b77c5b 100644 --- a/AdminWebsite/AdminWebsite/Mappers/BookingDetailsRequestMapper.cs +++ b/AdminWebsite/AdminWebsite/Mappers/BookingDetailsRequestMapper.cs @@ -1,5 +1,7 @@ +using System; using System.Linq; using AdminWebsite.Contracts.Requests; +using BookingsApi.Contract.V1.Requests.Enums; using V1 = BookingsApi.Contract.V1.Requests; using V2 = BookingsApi.Contract.V2.Requests; @@ -60,6 +62,12 @@ public static V2.BookNewHearingRequestV2 MapToV2(this BookingDetailsRequest book Participants = bookingDetails.Participants? .Select(p => p.MapToV2()) .ToList(), + JudiciaryParticipants = bookingDetails.JudiciaryParticipants.Select(jp => new V1.JudiciaryParticipantRequest + { + DisplayName = jp.DisplayName, + HearingRoleCode = Enum.Parse(jp.Role, ignoreCase:true), + PersonalCode = jp.PersonalCode + }).ToList(), HearingRoomName = bookingDetails.HearingRoomName, OtherInformation = bookingDetails.OtherInformation, CreatedBy = bookingDetails.CreatedBy, diff --git a/AdminWebsite/AdminWebsite/Mappers/HearingDetailsResponseMapper.cs b/AdminWebsite/AdminWebsite/Mappers/HearingDetailsResponseMapper.cs index d2d6679c1..2fd55c77e 100644 --- a/AdminWebsite/AdminWebsite/Mappers/HearingDetailsResponseMapper.cs +++ b/AdminWebsite/AdminWebsite/Mappers/HearingDetailsResponseMapper.cs @@ -72,6 +72,7 @@ public static HearingDetailsResponse Map(this V2.HearingDetailsResponseV2 hearin AudioRecordingRequired = hearingDetails.AudioRecordingRequired, CancelReason = hearingDetails.CancelReason, Endpoints = hearingDetails.Endpoints.Select(e => e.Map()).ToList(), + JudiciaryParticipants = hearingDetails.JudiciaryParticipants?.Select(j => j.Map()).ToList(), GroupId = hearingDetails.GroupId }; } diff --git a/AdminWebsite/AdminWebsite/Mappers/JudgeResponseMapper.cs b/AdminWebsite/AdminWebsite/Mappers/JudgeResponseMapper.cs index c8315c757..776c852d3 100644 --- a/AdminWebsite/AdminWebsite/Mappers/JudgeResponseMapper.cs +++ b/AdminWebsite/AdminWebsite/Mappers/JudgeResponseMapper.cs @@ -6,13 +6,13 @@ namespace AdminWebsite.Mappers { public static class JudgeResponseMapper { - public static JudgeResponse MapTo(PersonResponse personResponse) + public static JudgeResponse MapTo(JudgeResponse personResponse) { return new JudgeResponse { FirstName = personResponse.FirstName, LastName = personResponse.LastName, - Email = personResponse.Username, + Email = personResponse.Email, AccountType = JudgeAccountType.Judiciary, ContactEmail = personResponse.ContactEmail }; diff --git a/AdminWebsite/AdminWebsite/Mappers/JudiciaryParticipantResponseMapper.cs b/AdminWebsite/AdminWebsite/Mappers/JudiciaryParticipantResponseMapper.cs new file mode 100644 index 000000000..d3d94e398 --- /dev/null +++ b/AdminWebsite/AdminWebsite/Mappers/JudiciaryParticipantResponseMapper.cs @@ -0,0 +1,23 @@ +using AdminWebsite.Contracts.Responses; + +namespace AdminWebsite.Mappers +{ + public static class JudiciaryParticipantResponseMapper + { + public static JudiciaryParticipantResponse Map(this BookingsApi.Contract.V1.Responses.JudiciaryParticipantResponse judiciaryParticipantResponse) + { + return new JudiciaryParticipantResponse() + { + Email = judiciaryParticipantResponse.Email, + Title = judiciaryParticipantResponse.Title, + FirstName = judiciaryParticipantResponse.FirstName, + LastName = judiciaryParticipantResponse.LastName, + FullName = judiciaryParticipantResponse.FullName, + PersonalCode = judiciaryParticipantResponse.PersonalCode, + RoleCode = judiciaryParticipantResponse.HearingRoleCode.ToString(), + WorkPhone = judiciaryParticipantResponse.WorkPhone, + DisplayName = judiciaryParticipantResponse.DisplayName + }; + } + } +} \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite/Mappers/JudiciaryPersonResponseMapper.cs b/AdminWebsite/AdminWebsite/Mappers/JudiciaryPersonResponseMapper.cs new file mode 100644 index 000000000..d56fea518 --- /dev/null +++ b/AdminWebsite/AdminWebsite/Mappers/JudiciaryPersonResponseMapper.cs @@ -0,0 +1,34 @@ +using AdminWebsite.Contracts.Responses; +using BookingsApi.Contract.V1.Responses; + +namespace AdminWebsite.Mappers +{ + public static class JudiciaryPersonResponseMapper + { + public static JudiciaryPerson MapToAdminWebResponse(this BookingsApi.Contract.V1.Responses.JudiciaryPersonResponse judiciaryPersonResponse) + { + return new JudiciaryPerson() + { + Email = judiciaryPersonResponse.Email, + Title = judiciaryPersonResponse.Title, + FirstName = judiciaryPersonResponse.FirstName, + LastName = judiciaryPersonResponse.LastName, + FullName = judiciaryPersonResponse.FullName, + PersonalCode = judiciaryPersonResponse.PersonalCode, + WorkPhone = judiciaryPersonResponse.WorkPhone + }; + } + + public static PersonResponse MapToPersonResponse(this BookingsApi.Contract.V1.Responses.JudiciaryPersonResponse judiciaryPersonResponse) + { + return new PersonResponse() + { + Title = judiciaryPersonResponse.Title, + FirstName = judiciaryPersonResponse.FirstName, + LastName = judiciaryPersonResponse.LastName, + ContactEmail = judiciaryPersonResponse.Email, + Username = judiciaryPersonResponse.Email + }; + } + } +} \ No newline at end of file diff --git a/AdminWebsite/AdminWebsite/Mappers/ParticipantResponseMapper.cs b/AdminWebsite/AdminWebsite/Mappers/ParticipantResponseMapper.cs index 2c754fc2d..421daf968 100644 --- a/AdminWebsite/AdminWebsite/Mappers/ParticipantResponseMapper.cs +++ b/AdminWebsite/AdminWebsite/Mappers/ParticipantResponseMapper.cs @@ -45,6 +45,7 @@ public static List Map(this List MiddleNames = p.MiddleNames, LastName = p.LastName, ContactEmail = p.ContactEmail, + Username = p.Username, TelephoneNumber = p.TelephoneNumber, Organisation = p.Organisation, Representee = p.Representee, diff --git a/AdminWebsite/AdminWebsite/Mappers/UpdateParticipantRequestMapper.cs b/AdminWebsite/AdminWebsite/Mappers/UpdateParticipantRequestMapper.cs index 03f748723..f9c998130 100644 --- a/AdminWebsite/AdminWebsite/Mappers/UpdateParticipantRequestMapper.cs +++ b/AdminWebsite/AdminWebsite/Mappers/UpdateParticipantRequestMapper.cs @@ -17,7 +17,7 @@ public static UpdateParticipantRequest MapTo(EditParticipantRequest participant) TelephoneNumber = participant.TelephoneNumber, Representee = participant.Representee, ParticipantId = participant.Id ?? Guid.Empty, - ContactEmail = participant.ContactEmail + ContactEmail = participant.ContactEmail }; return updateParticipantRequest; } @@ -31,7 +31,10 @@ public static UpdateParticipantRequestV2 MapToV2(EditParticipantRequest particip OrganisationName = participant.OrganisationName, TelephoneNumber = participant.TelephoneNumber, Representee = participant.Representee, - ParticipantId = participant.Id ?? Guid.Empty + ParticipantId = participant.Id ?? Guid.Empty, + FirstName = participant.FirstName, + LastName = participant.LastName, + MiddleNames = participant.MiddleNames }; return updateParticipantRequest; } diff --git a/AdminWebsite/AdminWebsite/Models/EditHearingRequest.cs b/AdminWebsite/AdminWebsite/Models/EditHearingRequest.cs index 720fbdeb7..8fccbaef4 100644 --- a/AdminWebsite/AdminWebsite/Models/EditHearingRequest.cs +++ b/AdminWebsite/AdminWebsite/Models/EditHearingRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AdminWebsite.Contracts.Requests; namespace AdminWebsite.Models { @@ -12,6 +13,7 @@ public EditHearingRequest() { Participants = new List(); Endpoints = new List(); + JudiciaryParticipants = new List(); } /// @@ -48,6 +50,11 @@ public EditHearingRequest() /// List of participants in hearing /// public List Participants { get; set; } + + /// + /// List of judiciary participants in hearing + /// + public List JudiciaryParticipants { get; set; } public List TelephoneParticipants { get; set; } diff --git a/AdminWebsite/AdminWebsite/packages.lock.json b/AdminWebsite/AdminWebsite/packages.lock.json index 4d3a40915..a825bcc20 100644 --- a/AdminWebsite/AdminWebsite/packages.lock.json +++ b/AdminWebsite/AdminWebsite/packages.lock.json @@ -14,9 +14,9 @@ }, "BookingsApi.Client": { "type": "Direct", - "requested": "[1.47.15, )", - "resolved": "1.47.15", - "contentHash": "sd5lILWV3aiku/U/tFu16n7Fi9wAELGP0rVpLhaDFq3YzwxEL8JshbN0BUin3F/QZM3T8+DsnMKbMDcU449cqA==", + "requested": "[1.49.10, )", + "resolved": "1.49.10", + "contentHash": "oXH1qyRZhkrbVbdZB7QsO8O0a/6SRCrB71Hb0leiRNUGr/xb0IYdqpaTr6imTeo6zimGxieMiu9uKMsnFpVA/w==", "dependencies": { "Microsoft.AspNetCore.Mvc.Core": "2.2.5" } diff --git a/charts/vh-admin-web/values.dev.template.yaml b/charts/vh-admin-web/values.dev.template.yaml index c653d0aa1..8cc08891d 100644 --- a/charts/vh-admin-web/values.dev.template.yaml +++ b/charts/vh-admin-web/values.dev.template.yaml @@ -8,4 +8,4 @@ java: AZUREAD__POSTLOGOUTREDIRECTURI: https://${SERVICE_FQDN}/logout AZUREAD__REDIRECTURI: https://${SERVICE_FQDN}/home DOM1__POSTLOGOUTREDIRECTURI: https://${SERVICE_FQDN}/logout - DOM1__REDIRECTURI: https://${SERVICE_FQDN}/home \ No newline at end of file + DOM1__REDIRECTURI: https://${SERVICE_FQDN}/home diff --git a/tests/Dockerfile b/tests/Dockerfile index 3c4e861d1..43d6137d4 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -4,12 +4,16 @@ WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium -# Install NodeJS and chromium -RUN apt-get -y update \ - && apt-get install -y curl \ - && curl -sL https://deb.nodesource.com/setup_18.x | bash - \ - && apt-get install -y nodejs \ - && apt-get clean +RUN apt-get update +RUN apt-get install -y ca-certificates curl gnupg +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + +ENV NODE_MAJOR=18 +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + +RUN apt-get update +RUN apt-get install nodejs -y RUN apt-get update \ && apt-get install -y chromium \