Skip to content

Commit

Permalink
Added ability to specify sort order for person/contact search results
Browse files Browse the repository at this point in the history
  • Loading branch information
hortha committed Sep 15, 2023
1 parent 9dcb346 commit 1833e72
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace TeachingRecordSystem.Core.Dqt.Queries;

public record GetContactsByDateOfBirthQuery(DateOnly DateOfBirth, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery<Contact[]>;
public record GetContactsByDateOfBirthQuery(DateOnly DateOfBirth, ContactSearchSortByOption SortBy, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery<Contact[]>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace TeachingRecordSystem.Core.Dqt.Queries;

public record GetContactsByNameQuery(string Name, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery<Contact[]>;
public record GetContactsByNameQuery(string Name, ContactSearchSortByOption SortBy, int MaxRecordCount, ColumnSet ColumnSet) : ICrmQuery<Contact[]>;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ public async Task<Contact[]> 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<Contact>()).ToArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,33 @@ public async Task<Contact[]> 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<Contact>()).ToArray();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@page "/persons"
@using TeachingRecordSystem.Core.Dqt.Models;
@model TeachingRecordSystem.SupportUi.Pages.Persons.IndexModel
@{
ViewBag.Title = "All records";
Expand All @@ -10,7 +11,7 @@
<div class="govuk-grid-column-two-thirds-from-desktop">
<div class="govuk-!-margin-bottom-6">
<form trs-action="l => l.Persons()" method="get" data-testid="search-form">
<div class="moj-search trs-search">
<div class="moj-search trs-search govuk-!-margin-bottom-4">
<govuk-input asp-for="Search"
input-class="moj-search__input"
label-class="moj-search__label govuk-!-font-weight-bold"
Expand All @@ -19,7 +20,16 @@
<govuk-input-hint class="moj-search__hint">Search by: Name, TRN or date of birth (DD/MM/YYYY)</govuk-input-hint>
</govuk-input>
<govuk-button class="moj-search__button" type="submit">Search</govuk-button>
</div>
</div>
<govuk-select asp-for="SortBy">
<govuk-select-label>Sort by</govuk-select-label>
<govuk-select-item value="@ContactSearchSortByOption.LastNameAscending">@ContactSearchSortByOption.LastNameAscending.GetDisplayName()</govuk-select-item>
<govuk-select-item value="@ContactSearchSortByOption.LastNameDescending">@ContactSearchSortByOption.LastNameDescending.GetDisplayName()</govuk-select-item>
<govuk-select-item value="@ContactSearchSortByOption.FirstNameAscending">@ContactSearchSortByOption.FirstNameAscending.GetDisplayName()</govuk-select-item>
<govuk-select-item value="@ContactSearchSortByOption.FirstNameDescending">@ContactSearchSortByOption.FirstNameDescending.GetDisplayName()</govuk-select-item>
<govuk-select-item value="@ContactSearchSortByOption.DateOfBirthAscending">@ContactSearchSortByOption.DateOfBirthAscending.GetDisplayName()</govuk-select-item>
<govuk-select-item value="@ContactSearchSortByOption.DateOfBirthDescending">@ContactSearchSortByOption.DateOfBirthDescending.GetDisplayName()</govuk-select-item>
</govuk-select>
</form>
</div>
</div>
Expand All @@ -33,12 +43,12 @@
<govuk-pagination>
@if (Model.PreviousPage.HasValue)
{
<govuk-pagination-previous asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.PreviousPage" />
<govuk-pagination-previous asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.PreviousPage" asp-route-sortby="@Model.SortBy" />
}

@if (Model.NextPage.HasValue)
{
<govuk-pagination-next asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.NextPage" />
<govuk-pagination-next asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.NextPage" asp-route-sortby="@Model.SortBy" />
}
</govuk-pagination>
}
Expand All @@ -64,7 +74,7 @@
@foreach (var personInfo in Model.SearchResults!)
{
<tr class="govuk-table__row" data-testid="[email protected]">
<td class="govuk-table__cell" data-testid="name"><a href="@LinkGenerator.PersonDetail(personInfo.PersonId, search: Model.Search, pageNumber: Model.PageNumber)" class="govuk-link">@personInfo.Name</a></td>
<td class="govuk-table__cell" data-testid="name"><a href="@LinkGenerator.PersonDetail(personInfo.PersonId, search: Model.Search, sortBy: Model.SortBy, pageNumber: Model.PageNumber)" class="govuk-link">@personInfo.Name</a></td>
<td class="govuk-table__cell" data-testid="date-of-birth">@(personInfo.DateOfBirth.HasValue ? personInfo.DateOfBirth.Value.ToString("dd/MM/yyyy") : "-")</td>
<td class="govuk-table__cell" data-testid="trn">@(!string.IsNullOrEmpty(personInfo.Trn) ? personInfo.Trn : "-")</td>
<td class="govuk-table__cell" data-testid="nino">@(!string.IsNullOrEmpty(personInfo.NationalInsuranceNumber) ? personInfo.NationalInsuranceNumber : "-")</td>
Expand All @@ -79,12 +89,12 @@
<govuk-pagination>
@if (Model.PreviousPage.HasValue)
{
<govuk-pagination-previous asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.PreviousPage" />
<govuk-pagination-previous asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.PreviousPage" asp-route-sortby="@Model.SortBy" />
}

@if (Model.NextPage.HasValue)
{
<govuk-pagination-next asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.NextPage" />
<govuk-pagination-next asp-page="Index" asp-route-search="@Model.Search" asp-route-pagenumber="@Model.NextPage" asp-route-sortby="@Model.SortBy" />
}
</govuk-pagination>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -72,15 +75,12 @@ private async Task<IActionResult> 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!))
{
Expand All @@ -92,7 +92,7 @@ private async Task<IActionResult> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
}

@section BeforeContent {
<govuk-back-link href="@LinkGenerator.Persons(Model.Search, Model.PageNumber)" />
<govuk-back-link href="@LinkGenerator.Persons(Model.Search, Model.SortBy, Model.PageNumber)" />
}

<h1 class="govuk-heading-l" data-testid="page-title">@ViewBag.Title</h1>
Expand All @@ -16,15 +16,15 @@
<nav class="moj-sub-navigation" aria-label="Sub navigation">
<ul class="moj-sub-navigation__list">
<li class="moj-sub-navigation__item">
<a class="moj-sub-navigation__link" aria-current="@(Model.SelectedTab == IndexModel.PersonSubNavigationTab.General ? "page" : null)" href="@LinkGenerator.PersonDetail(Model.PersonId, IndexModel.PersonSubNavigationTab.General, Model.Search, Model.PageNumber)">General</a>
<a class="moj-sub-navigation__link" aria-current="@(Model.SelectedTab == IndexModel.PersonSubNavigationTab.General ? "page" : null)" href="@LinkGenerator.PersonDetail(Model.PersonId, IndexModel.PersonSubNavigationTab.General, Model.Search, Model.SortBy, Model.PageNumber)">General</a>
</li>

<li class="moj-sub-navigation__item">
<a class="moj-sub-navigation__link" aria-current="@(Model.SelectedTab == IndexModel.PersonSubNavigationTab.Alerts ? "page" : null)" href="@LinkGenerator.PersonDetail(Model.PersonId, IndexModel.PersonSubNavigationTab.Alerts, Model.Search, Model.PageNumber)">Alerts</a>
<a class="moj-sub-navigation__link" aria-current="@(Model.SelectedTab == IndexModel.PersonSubNavigationTab.Alerts ? "page" : null)" href="@LinkGenerator.PersonDetail(Model.PersonId, IndexModel.PersonSubNavigationTab.Alerts, Model.Search, Model.SortBy, Model.PageNumber)">Alerts</a>
</li>

<li class="moj-sub-navigation__item">
<a class="moj-sub-navigation__link" aria-current="@(Model.SelectedTab == IndexModel.PersonSubNavigationTab.ChangeLog ? "page" : null)" href="@LinkGenerator.PersonDetail(Model.PersonId, IndexModel.PersonSubNavigationTab.ChangeLog, Model.Search, Model.PageNumber)">Change log</a>
<a class="moj-sub-navigation__link" aria-current="@(Model.SelectedTab == IndexModel.PersonSubNavigationTab.ChangeLog ? "page" : null)" href="@LinkGenerator.PersonDetail(Model.PersonId, IndexModel.PersonSubNavigationTab.ChangeLog, Model.Search, Model.SortBy, Model.PageNumber)">Change log</a>
</li>
</ul>
</nav>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using TeachingRecordSystem.Core.Dqt.Models;
using static TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.IndexModel;

namespace TeachingRecordSystem.SupportUi;
Expand Down Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,89 @@ public GetContactsByDateOfBirthTests(CrmClientFixture crmClientFixture)

public async Task DisposeAsync() => await _dataScope.DisposeAsync();

[Fact]
public async Task ReturnsMatchingContactsFromCrm()
public static TheoryData<ContactSearchSortScenarioData> GetContactSearchSortScenarioData()
{
return new TheoryData<ContactSearchSortScenarioData>
{
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<Contact, string> Selector { get; init; }
public required bool IsAscending { get; init; }
}
}
Loading

0 comments on commit 1833e72

Please sign in to comment.