From 77f051304eea8814ab3249693dff567d10f86ae0 Mon Sep 17 00:00:00 2001 From: Andrew Horth Date: Thu, 14 Sep 2023 17:45:22 +0100 Subject: [PATCH] Added ability to specify sort order for person/contact search results --- .../Dqt/Models/ContactSearchSortByOption.cs | 19 +++++ .../Queries/GetContactsByDateOfBirthQuery.cs | 2 +- .../Dqt/Queries/GetContactsByNameQuery.cs | 2 +- .../GetContactsByDateOfBirthHandler.cs | 23 ++++++ .../QueryHandlers/GetContactsByNameHandler.cs | 26 ++++++- .../Pages/Persons/Index.cshtml | 24 ++++-- .../Pages/Persons/Index.cshtml.cs | 12 +-- .../Pages/Persons/PersonDetail/Index.cshtml | 8 +- .../Persons/PersonDetail/Index.cshtml.cs | 3 + .../TrsLinkGenerator.cs | 8 +- .../GetContactsByDateOfBirthTests.cs | 73 +++++++++++++++++- .../QueryTests/GetContactsByNameTests.cs | 76 +++++++++++++++++-- 12 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/ContactSearchSortByOption.cs diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/ContactSearchSortByOption.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/ContactSearchSortByOption.cs new file mode 100644 index 0000000000..d31e28ed40 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/ContactSearchSortByOption.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace TeachingRecordSystem.Core.Dqt.Models; + +public enum ContactSearchSortByOption +{ + [Display(Name = "Last name (A-Z)")] + LastNameAscending, + [Display(Name = "Last name (Z-A)")] + LastNameDescending, + [Display(Name = "First name (A-Z)")] + FirstNameAscending, + [Display(Name = "First name (Z-A)")] + FirstNameDescending, + [Display(Name = "Date of birth (ascending)")] + DateOfBirthAscending, + [Display(Name = "Date of birth (descending)")] + DateOfBirthDescending +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByDateOfBirthQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByDateOfBirthQuery.cs index c88df54ff1..d1cf1fd706 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByDateOfBirthQuery.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByDateOfBirthQuery.cs @@ -2,4 +2,4 @@ namespace TeachingRecordSystem.Core.Dqt.Queries; -public record GetContactsByDateOfBirthQuery(DateOnly DateOfBirth, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery; +public record GetContactsByDateOfBirthQuery(DateOnly DateOfBirth, ContactSearchSortByOption SortBy, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByNameQuery.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByNameQuery.cs index e7f9725255..a165709063 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByNameQuery.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Queries/GetContactsByNameQuery.cs @@ -2,4 +2,4 @@ namespace TeachingRecordSystem.Core.Dqt.Queries; -public record GetContactsByNameQuery(string Name, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery; +public record GetContactsByNameQuery(string Name, ContactSearchSortByOption SortBy, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByDateOfBirthHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByDateOfBirthHandler.cs index 7c13c08b52..8f03be99cd 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByDateOfBirthHandler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByDateOfBirthHandler.cs @@ -16,6 +16,29 @@ public async Task Execute(GetContactsByDateOfBirthQuery query, IOrgan }; queryByAttribute.AddAttributeValue(Contact.Fields.StateCode, (int)ContactState.Active); queryByAttribute.AddAttributeValue(Contact.Fields.BirthDate, query.DateOfBirth.ToDateTime()); + switch (query.SortBy) + { + case ContactSearchSortByOption.LastNameAscending: + queryByAttribute.AddOrder(Contact.Fields.LastName, OrderType.Ascending); + break; + case ContactSearchSortByOption.LastNameDescending: + queryByAttribute.AddOrder(Contact.Fields.LastName, OrderType.Descending); + break; + case ContactSearchSortByOption.FirstNameAscending: + queryByAttribute.AddOrder(Contact.Fields.FirstName, OrderType.Ascending); + break; + case ContactSearchSortByOption.FirstNameDescending: + queryByAttribute.AddOrder(Contact.Fields.FirstName, OrderType.Descending); + break; + case ContactSearchSortByOption.DateOfBirthAscending: + queryByAttribute.AddOrder(Contact.Fields.BirthDate, OrderType.Ascending); + break; + case ContactSearchSortByOption.DateOfBirthDescending: + queryByAttribute.AddOrder(Contact.Fields.BirthDate, OrderType.Descending); + break; + default: + break; + } var response = await organizationService.RetrieveMultipleAsync(queryByAttribute); return response.Entities.Select(e => e.ToEntity()).ToArray(); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByNameHandler.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByNameHandler.cs index 91dc23206c..d336527a76 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByNameHandler.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/QueryHandlers/GetContactsByNameHandler.cs @@ -18,9 +18,33 @@ public async Task Execute(GetContactsByNameQuery query, IOrganization { ColumnSet = query.ColumnSet, Criteria = filter, - TopCount = query.MaxRecordCount + TopCount = query.MaxRecordCount, }; + switch (query.SortBy) + { + case ContactSearchSortByOption.LastNameAscending: + queryExpression.AddOrder(Contact.Fields.LastName, OrderType.Ascending); + break; + case ContactSearchSortByOption.LastNameDescending: + queryExpression.AddOrder(Contact.Fields.LastName, OrderType.Descending); + break; + case ContactSearchSortByOption.FirstNameAscending: + queryExpression.AddOrder(Contact.Fields.FirstName, OrderType.Ascending); + break; + case ContactSearchSortByOption.FirstNameDescending: + queryExpression.AddOrder(Contact.Fields.FirstName, OrderType.Descending); + break; + case ContactSearchSortByOption.DateOfBirthAscending: + queryExpression.AddOrder(Contact.Fields.BirthDate, OrderType.Ascending); + break; + case ContactSearchSortByOption.DateOfBirthDescending: + queryExpression.AddOrder(Contact.Fields.BirthDate, OrderType.Descending); + break; + default: + break; + } + var response = await organizationService.RetrieveMultipleAsync(queryExpression); return response.Entities.Select(e => e.ToEntity()).ToArray(); } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml index 8961141acc..166932c84a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml @@ -1,4 +1,5 @@ @page "/persons" +@using TeachingRecordSystem.Core.Dqt.Models; @model TeachingRecordSystem.SupportUi.Pages.Persons.IndexModel @{ ViewBag.Title = "All records"; @@ -10,7 +11,7 @@
- + + Sort by + @ContactSearchSortByOption.LastNameAscending.GetDisplayName() + @ContactSearchSortByOption.LastNameDescending.GetDisplayName() + @ContactSearchSortByOption.FirstNameAscending.GetDisplayName() + @ContactSearchSortByOption.FirstNameDescending.GetDisplayName() + @ContactSearchSortByOption.DateOfBirthAscending.GetDisplayName() + @ContactSearchSortByOption.DateOfBirthDescending.GetDisplayName() +
@@ -33,12 +43,12 @@ @if (Model.PreviousPage.HasValue) { - + } @if (Model.NextPage.HasValue) { - + } } @@ -64,7 +74,7 @@ @foreach (var personInfo in Model.SearchResults!) { - @personInfo.Name + @personInfo.Name @(personInfo.DateOfBirth.HasValue ? personInfo.DateOfBirth.Value.ToString("dd/MM/yyyy") : "-") @(!string.IsNullOrEmpty(personInfo.Trn) ? personInfo.Trn : "-") @(!string.IsNullOrEmpty(personInfo.NationalInsuranceNumber) ? personInfo.NationalInsuranceNumber : "-") @@ -79,12 +89,12 @@ @if (Model.PreviousPage.HasValue) { - + } @if (Model.NextPage.HasValue) { - + } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs index 2e25eec96c..7f996caa8d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs @@ -31,6 +31,9 @@ public IndexModel( [Display(Name = "Search")] public string? Search { get; set; } + [BindProperty(SupportsGet = true)] + public ContactSearchSortByOption SortBy { get; set; } + [BindProperty(SupportsGet = true)] public int? PageNumber { get; set; } @@ -72,15 +75,12 @@ private async Task PerformSearch() Contact.Fields.dfeta_StatedFirstName, Contact.Fields.dfeta_StatedMiddleName, Contact.Fields.dfeta_StatedLastName, - Contact.Fields.EMailAddress1, - Contact.Fields.MobilePhone, - Contact.Fields.dfeta_NINumber, - Contact.Fields.dfeta_ActiveSanctions); + Contact.Fields.dfeta_NINumber); // Check if the search string is a date of birth, TRN or one or more names if (DateOnly.TryParse(Search, out var dateOfBirth)) { - contacts = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByDateOfBirthQuery(dateOfBirth, MaxSearchResultCount, columnSet)); + contacts = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByDateOfBirthQuery(dateOfBirth, SortBy, MaxSearchResultCount, columnSet)); } else if (TrnRegex().IsMatch(Search!)) { @@ -92,7 +92,7 @@ private async Task PerformSearch() } else { - contacts = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByNameQuery(Search!, MaxSearchResultCount, columnSet)); + contacts = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByNameQuery(Search!, SortBy, MaxSearchResultCount, columnSet)); } TotalKnownPages = Math.Max((int)Math.Ceiling((decimal)contacts!.Length / PageSize), 1); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml index 40528ab1c9..984202b15a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml @@ -5,7 +5,7 @@ } @section BeforeContent { - + }

@ViewBag.Title

@@ -16,15 +16,15 @@ diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml.cs index e4d56e205d..bd8883772c 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Index.cshtml.cs @@ -24,6 +24,9 @@ public IndexModel(ICrmQueryDispatcher crmQueryDispatcher) [FromQuery] public int? PageNumber { get; set; } + [FromQuery] + public ContactSearchSortByOption SortBy { get; set; } + [FromQuery] public PersonSubNavigationTab? SelectedTab { get; set; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs index 35487799d9..675cba5155 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs @@ -1,3 +1,4 @@ +using TeachingRecordSystem.Core.Dqt.Models; using static TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.IndexModel; namespace TeachingRecordSystem.SupportUi; @@ -27,10 +28,11 @@ public TrsLinkGenerator(LinkGenerator linkGenerator) public string RejectCase(string ticketNumber) => GetRequiredPathByPage("/Cases/EditCase/Reject", routeValues: new { ticketNumber }); - public string Persons(string? search = null, int? pageNumber = null) => GetRequiredPathByPage("/Persons/Index", routeValues: new { search, pageNumber }); + public string Persons(string? search = null, ContactSearchSortByOption? sortBy = null, int? pageNumber = null) => + GetRequiredPathByPage("/Persons/Index", routeValues: new { search, sortBy, pageNumber }); - public string PersonDetail(Guid personId, PersonSubNavigationTab? selectedTab = null, string? search = null, int? pageNumber = null) => - GetRequiredPathByPage("/Persons/PersonDetail/Index", routeValues: new { personId, selectedTab, search, pageNumber }); + public string PersonDetail(Guid personId, PersonSubNavigationTab? selectedTab = null, string? search = null, ContactSearchSortByOption? sortBy = null, int? pageNumber = null) => + GetRequiredPathByPage("/Persons/PersonDetail/Index", routeValues: new { personId, selectedTab, search, sortBy, pageNumber }); public string Users() => GetRequiredPathByPage("/Users/Index"); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByDateOfBirthTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByDateOfBirthTests.cs index dcae18c4e7..46316ef692 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByDateOfBirthTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByDateOfBirthTests.cs @@ -17,22 +17,89 @@ public GetContactsByDateOfBirthTests(CrmClientFixture crmClientFixture) public async Task DisposeAsync() => await _dataScope.DisposeAsync(); - [Fact] - public async Task ReturnsMatchingContactsFromCrm() + public static TheoryData GetContactSearchSortScenarioData() + { + return new TheoryData + { + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.LastNameAscending, + Selector = (Contact c) => c.LastName, + IsAscending = true + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.LastNameDescending, + Selector = (Contact c) => c.LastName, + IsAscending = false + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.FirstNameAscending, + Selector = (Contact c) => c.FirstName, + IsAscending = true + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.FirstNameDescending, + Selector = (Contact c) => c.FirstName, + IsAscending = false + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.DateOfBirthAscending, + Selector = (Contact c) => c.BirthDate is null ? string.Empty : c.BirthDate.Value.ToString("yyyy-MM-dd"), + IsAscending = true + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.DateOfBirthDescending, + Selector = (Contact c) => c.BirthDate is null ? string.Empty : c.BirthDate.Value.ToString("yyyy-MM-dd"), + IsAscending = false + } + }; + } + + [Theory] + [MemberData(nameof(GetContactSearchSortScenarioData))] + public async Task ReturnsMatchingContactsFromCrmInExpectedSortOrder(ContactSearchSortScenarioData testScenarioData) { // Arrange var dateOfBirth = new DateOnly(1990, 1, 1); var maxRecordCount = 3; + var columnSet = new ColumnSet( + Contact.Fields.dfeta_TRN, + Contact.Fields.BirthDate, + Contact.Fields.FirstName, + Contact.Fields.MiddleName, + Contact.Fields.LastName, + Contact.Fields.FullName); + var person1 = await _dataScope.TestData.CreatePerson(b => b.WithDateOfBirth(dateOfBirth)); var person2 = await _dataScope.TestData.CreatePerson(b => b.WithDateOfBirth(dateOfBirth)); var person3 = await _dataScope.TestData.CreatePerson(b => b.WithDateOfBirth(dateOfBirth)); // Act - var results = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByDateOfBirthQuery(dateOfBirth, maxRecordCount, new ColumnSet())); + var results = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByDateOfBirthQuery(dateOfBirth, testScenarioData.SortBy, maxRecordCount, columnSet)); // Assert Assert.NotNull(results); Assert.Equal(maxRecordCount, results.Length); + if (testScenarioData.IsAscending) + { + Assert.Equal(results.Select(testScenarioData.Selector).OrderBy(x => x).ToArray(), results.Select(testScenarioData.Selector).ToArray()); + } + else + { + Assert.Equal(results.Select(testScenarioData.Selector).OrderByDescending(x => x).ToArray(), results.Select(testScenarioData.Selector).ToArray()); + } + } + + public class ContactSearchSortScenarioData + { + public required ContactSearchSortByOption SortBy { get; init; } + public required Func Selector { get; init; } + public required bool IsAscending { get; init; } } } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByNameTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByNameTests.cs index 519d6a3db4..1c5bcdf89d 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByNameTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Dqt.Tests/QueryTests/GetContactsByNameTests.cs @@ -17,18 +17,84 @@ public GetContactsByNameTests(CrmClientFixture crmClientFixture) public async Task DisposeAsync() => await _dataScope.DisposeAsync(); - [Fact] - public async Task ReturnsMatchingContactsFromCrm() + public static TheoryData GetContactSearchSortScenarioData() + { + return new TheoryData + { + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.LastNameAscending, + Selector = (Contact c) => c.LastName, + IsAscending = true + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.LastNameDescending, + Selector = (Contact c) => c.LastName, + IsAscending = false + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.FirstNameAscending, + Selector = (Contact c) => c.FirstName, + IsAscending = true + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.FirstNameDescending, + Selector = (Contact c) => c.FirstName, + IsAscending = false + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.DateOfBirthAscending, + Selector = (Contact c) => c.BirthDate is null ? string.Empty : c.BirthDate.Value.ToString("yyyy-MM-dd"), + IsAscending = true + }, + new ContactSearchSortScenarioData + { + SortBy = ContactSearchSortByOption.DateOfBirthDescending, + Selector = (Contact c) => c.BirthDate is null ? string.Empty : c.BirthDate.Value.ToString("yyyy-MM-dd"), + IsAscending = false + } + }; + } + + [Theory] + [MemberData(nameof(GetContactSearchSortScenarioData))] + public async Task ReturnsMatchingContactsFromCrmInExpectedSortOrder(ContactSearchSortScenarioData testScenarioData) { // Arrange - var name = "smith"; - var maxRecordCount = 4; // pretty safe bet there will always be at least 4 smiths in the dev CRM database + var name = "andrew"; + var maxRecordCount = 4; // pretty safe bet there will always be at least 4 andrew names in the dev CRM database + var columnSet = new ColumnSet( + Contact.Fields.dfeta_TRN, + Contact.Fields.BirthDate, + Contact.Fields.FirstName, + Contact.Fields.MiddleName, + Contact.Fields.LastName, + Contact.Fields.FullName); // Act - var results = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByNameQuery(name, maxRecordCount, new ColumnSet())); + var results = await _crmQueryDispatcher.ExecuteQuery(new GetContactsByNameQuery(name, testScenarioData.SortBy, maxRecordCount, columnSet)); // Assert Assert.NotNull(results); Assert.Equal(maxRecordCount, results.Length); + if (testScenarioData.IsAscending) + { + Assert.Equal(results.Select(testScenarioData.Selector).OrderBy(x => x).ToArray(), results.Select(testScenarioData.Selector).ToArray()); + } + else + { + Assert.Equal(results.Select(testScenarioData.Selector).OrderByDescending(x => x).ToArray(), results.Select(testScenarioData.Selector).ToArray()); + } + } + + public class ContactSearchSortScenarioData + { + public required ContactSearchSortByOption SortBy { get; init; } + public required Func Selector { get; init; } + public required bool IsAscending { get; init; } } }