Skip to content

Commit

Permalink
Merge pull request #235 from mcagov/backoffice-dashboard-sorting
Browse files Browse the repository at this point in the history
Backoffice dashboard sorting
  • Loading branch information
bnjn-mt authored Oct 23, 2024
2 parents ae9b061 + b4a1337 commit 4eed808
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 5 deletions.
4 changes: 3 additions & 1 deletion backoffice/src/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public async Task<IActionResult> Index(DashboardView model)
searchOptions.FilterByAssignedUser = true;
searchOptions.ExcludeClosedDroits = true;


searchOptions.OrderColumn = searchOptions.OrderColumn;
searchOptions.OrderDescending = searchOptions.OrderDescending;

searchOptions.PageNumber = searchOptions.DroitsPageNumber;
var droits = await _droitService.GetDroitsListViewAsync(searchOptions);

Expand Down
21 changes: 21 additions & 0 deletions backoffice/src/Helpers/ServiceHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#region

using System.Linq.Expressions;
using Droits.Models.Entities;
using Droits.Models.ViewModels.ListViews;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -35,4 +37,23 @@ public static async Task<ListView<TView>> GetPagedResult<TView>(
Items = pagedItems
};
}

public static Expression<Func<Droit, object>> GetOrderColumnExpression(SearchOptions searchOptions)
{
// Define a mapping between column name and expression
var columnMap = new Dictionary<string, Expression<Func<Droit, object>>>()
{
{ "ReportedDate", d => d.ReportedDate },
{ "Status", d => d.Status }
};

// Try to get the expression for the given OrderColumn, default to "ReportedDate"
if (!columnMap.TryGetValue(searchOptions.OrderColumn, out var orderColumnExpression))
{
// If the OrderColumn is not found, use ReportedDate as the default
orderColumnExpression = columnMap["ReportedDate"];
}

return orderColumnExpression;
}
}
2 changes: 2 additions & 0 deletions backoffice/src/Models/ViewModels/ListViews/SearchOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public class SearchOptions
public bool FilterByAssignedUser { get; set; }
public bool ExcludeClosedDroits { get; set; }
public bool SearchOpen { get; set; } = false;
public string OrderColumn { get; set; } = "ReportedDate";
public bool OrderDescending { get; set; } = true;
}

17 changes: 17 additions & 0 deletions backoffice/src/Repositories/DroitRepository.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#region

using System.Linq.Expressions;
using Droits.Data;
using Droits.Exceptions;
using Droits.Models.DTOs.Exports;
using Droits.Models.Entities;
using Droits.Models.FormModels;
using Droits.Models.ViewModels.ListViews;
using Droits.Services;
using Microsoft.EntityFrameworkCore;

Expand All @@ -16,6 +18,7 @@ public interface IDroitRepository
{
IQueryable<Droit> GetDroits();
IQueryable<Droit> GetDroitsWithAssociations();
IQueryable<Droit> GetOrderedDroitsWithAssociations(Expression<Func<Droit, object>> orderColumnExpression, bool orderDescending);
Task<Droit> GetDroitWithAssociationsAsync(Guid id);
Task<Droit> GetDroitAsync(Guid id);
Task<Droit> GetDroitByPowerappsIdAsync(string powerappsId);
Expand Down Expand Up @@ -54,6 +57,20 @@ public IQueryable<Droit> GetDroitsWithAssociations()
.Include(d => d.Salvor).AsNoTracking()
.Include(d => d.WreckMaterials.OrderBy(wm => wm.Created)).AsNoTracking();
}

public IQueryable<Droit> GetOrderedDroitsWithAssociations(Expression<Func<Droit, object>> orderColumnExpression, bool orderDescending)
{
var query = orderDescending
? Context.Droits.OrderByDescending(orderColumnExpression).ThenByDescending(d => d.Id)
: Context.Droits.OrderBy(orderColumnExpression).ThenByDescending(d => d.Id);

return query
.Include(d => d.AssignedToUser).AsNoTracking()
.Include(d => d.Letters).AsNoTracking()
.Include(d => d.Wreck).AsNoTracking()
.Include(d => d.Salvor).AsNoTracking()
.Include(d => d.WreckMaterials.OrderBy(wm => wm.Created)).AsNoTracking();
}


public async Task<Droit> GetDroitWithAssociationsAsync(Guid id)
Expand Down
10 changes: 7 additions & 3 deletions backoffice/src/Services/DroitService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

#region

using System.Linq.Expressions;
using AutoMapper;
using Droits.Data.Mappers.CsvMappers;
using Droits.Exceptions;
Expand Down Expand Up @@ -99,10 +100,13 @@ public async Task<string> GetNextDroitReference()

public async Task<DroitListView> GetDroitsListViewAsync(SearchOptions searchOptions)
{
var query = searchOptions.IncludeAssociations
? _repo.GetDroitsWithAssociations()
var orderColumnExpression = ServiceHelper.GetOrderColumnExpression(searchOptions);

IQueryable<Droit> query =
searchOptions.IncludeAssociations
? _repo.GetOrderedDroitsWithAssociations(orderColumnExpression, searchOptions.OrderDescending)
: _repo.GetDroits();

if ( searchOptions.FilterByAssignedUser )
{
var currentUserId = _accountService.GetCurrentUserId();
Expand Down
4 changes: 3 additions & 1 deletion backoffice/src/Views/Account/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<div class="js-dashboard-search d-none">
@Html.EditorFor(f => f.DashboardSearchForm.DroitsPageNumber, new { htmlAttributes = new { @class = "js-droit-page-number-field"} })
@Html.EditorFor(f => f.DashboardSearchForm.LettersPageNumber, new { htmlAttributes = new { @class = "js-letter-page-number-field"} })
@Html.EditorFor(f => f.DashboardSearchForm.OrderDescending, new { htmlAttributes = new { @class = "js-order-descending-field"} })
@Html.EditorFor(f => f.DashboardSearchForm.OrderColumn, new { htmlAttributes = new { @class = "js-order-column-field"} })
</div>
</form>

Expand All @@ -35,7 +37,7 @@
<div class="card-body">
@if (Model.Droits.TotalCount > 0)
{
<partial name="Droit/_DroitListTable" model="@Model.Droits"/>
<partial name="Account/_AccountDroitListTable" model="@Model.Droits"/>
<div class="mt-4 pagination-container" data-pagination-input-selector=".js-droit-page-number-field">
@await Html.PartialAsync("_PaginationPartial", Model.Droits)
</div>
Expand Down
100 changes: 100 additions & 0 deletions backoffice/src/Views/Shared/Account/_AccountDroitListTable.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
@model DroitListView

@{
var anyVerifiedWrecks = Model.Items.Any(item => ((DroitView)item)?.Wreck?.Name != null);
}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Reference</th>

@if (Model.IncludeAssociations)
{
<th scope="col">Salvor</th>
@if (Model.AnyVerifiedWrecks)
{
<th scope="col" class="text-nowrap">Verified Wreck</th>

}
}
<th scope="col" class="text-nowrap">Reported Wreck</th>
<th scope="col">Triage</th>
@if (Model.IncludeAssociations)
{
<th scope="col">Items</th>
}
<th scope="col">RoW</th>
<th role="button" scope="col">
<a class="sort-link" data-sort-col="Status">Status</a>
</th>
<th role="button" scope="col">
<a class="sort-link" data-sort-col="ReportedDate">Reported</a>
</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@foreach (DroitView droit in Model.Items)
{
<tr>
<td class="py-3 text-uppercase">
<a target="_blank" asp-controller="Droit" asp-action="View" asp-route-id="@droit.Id">@droit.Reference</a>
</td>
@if (Model.IncludeAssociations)
{
<td class="py-3">
@{
if (droit?.Salvor?.Name != null)
{
if (droit?.Salvor?.Name != null)
{
@Html.ActionLink(droit?.Salvor?.Name, "View", "Salvor", new { id = droit?.Salvor?.Id }, new { target="_blank" })
}
}
else
{
<span>Unknown Salvor</span>
}
}
</td>
@if (Model.AnyVerifiedWrecks)
{
<td class="py-3">
@{
if (droit?.Wreck?.Name != null)
{
@Html.ActionLink(droit?.Wreck?.Name, "View", "Wreck", new { id = droit?.Wreck?.Id }, new { target="_blank" })
}
}
</td>
}
}
<td class="py-3 text-uppercase">@droit?.ReportedWreckInfo.ReportedWreckName</td>
<td class="py-3 text-uppercase">
<span class="badge rounded-pill @(droit?.TriageNumber >0 ? "bg-primary" : "bg-dark")">@droit?.TriageNumber</span>
</td>
@if (Model.IncludeAssociations)
{
<td class="py-3">
<span class="badge rounded-pill bg-primary">@droit?.WreckMaterials.Count</span>
</td>
}
<td class="py-3">
<span class="text-uppercase">@droit?.AssignedUser</span>
</td>
<td class="py-3">
<span class="badge rounded-pill status-@droit?.Status">@droit?.Status.GetDisplayName()</span>
</td>
<td class="py-3">@droit?.ReportedDate.ToString("dd/MM/yyyy")</td>
<td class="py-3">
<div class="btn-group">
<a asp-action="Edit" asp-controller="Droit" asp-route-id="@droit?.Id" class="btn btn-primary">Edit</a>
<a asp-action="View" asp-controller="Droit" asp-route-id="@droit?.Id" class="btn btn-primary">View</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
32 changes: 32 additions & 0 deletions backoffice/src/wwwroot/js/searchForms.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,38 @@ function initializeSearchForm(formClass, toggleButtonClass) {
const searchForm = document.querySelector(formClass);
searchForm.classList.toggle('d-none');
})});

const sortButtons = document.querySelectorAll(".sort-link");

if (sortButtons.length > 0) {
const orderColumnField = document.querySelector(".js-order-column-field");
const orderDescendingField = document.querySelector(".js-order-descending-field");
const sortArrow = document.createElement("span");
sortArrow.className = "sort-arrow";
sortArrow.textContent = orderDescendingField.checked ? "\u2193" : "\u2191";

sortButtons.forEach((button) => {
const currentButtonDataField = button.getAttribute("data-sort-col");

if (orderColumnField.value === currentButtonDataField) {
button.appendChild(sortArrow);
}

button.addEventListener('click', (ev) => {
ev.preventDefault();

if (orderColumnField.value === currentButtonDataField) {
orderDescendingField.checked = !orderDescendingField.checked;
} else {
orderDescendingField.checked = true;
}

orderColumnField.value = currentButtonDataField;

orderColumnField.closest("form").submit();
})
})
}

const paginationButtons = document.querySelectorAll(".js-page-link");
paginationButtons.forEach((button) => {
Expand Down
8 changes: 8 additions & 0 deletions backoffice/src/wwwroot/scss/site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ div.form-sections {
font-family: var(--bs-body-font-family) !important;
}

.sort-link {
text-decoration: none;
}

.sort-arrow {
margin-left: 0.25rem;
}

.status-Received {@extend .bg-success;}
.status-AcknowledgementLetterSent {@extend .bg-warning;}
.status-InitialResearch {@extend .bg-info;}
Expand Down
51 changes: 51 additions & 0 deletions backoffice/tests/UnitTests/Helpers/ServiceHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Linq.Expressions;
using Droits.Helpers;
using Droits.Models.Entities;
using Droits.Models.ViewModels.ListViews;

namespace Droits.Tests.UnitTests.Helpers;

public class ServiceHelperTests
{
[Fact]
public void GetOrderColumnExpression_ShouldReturnReportedDate_WhenOrderColumnIsInvalid()
{
// Arrange
var searchOptions = new SearchOptions { OrderColumn = "InvalidColumn" };
var expectedExpression = (Expression<Func<Droit, object>>)(d => d.ReportedDate);

// Act
var result = ServiceHelper.GetOrderColumnExpression(searchOptions);

// Assert
Assert.Equal(expectedExpression.ToString(), result.ToString());
}

[Fact]
public void GetOrderColumnExpression_ShouldReturnStatus_WhenOrderColumnIsReportedDate()
{
// Arrange
var searchOptions = new SearchOptions { OrderColumn = "ReportedDate" };
var expectedExpression = (Expression<Func<Droit, object>>)(d => d.ReportedDate);

// Act
var result = ServiceHelper.GetOrderColumnExpression(searchOptions);

// Assert
Assert.Equal(expectedExpression.ToString(), result.ToString());
}

[Fact]
public void GetOrderColumnExpression_ShouldReturnStatus_WhenOrderColumnIsStatus()
{
// Arrange
var searchOptions = new SearchOptions { OrderColumn = "Status" };
var expectedExpression = (Expression<Func<Droit, object>>)(d => d.Status);

// Act
var result = ServiceHelper.GetOrderColumnExpression(searchOptions);

// Assert
Assert.Equal(expectedExpression.ToString(), result.ToString());
}
}

0 comments on commit 4eed808

Please sign in to comment.