Skip to content

Commit 4eed808

Browse files
authored
Merge pull request #235 from mcagov/backoffice-dashboard-sorting
Backoffice dashboard sorting
2 parents ae9b061 + b4a1337 commit 4eed808

File tree

10 files changed

+244
-5
lines changed

10 files changed

+244
-5
lines changed

backoffice/src/Controllers/AccountController.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ public async Task<IActionResult> Index(DashboardView model)
3535
searchOptions.FilterByAssignedUser = true;
3636
searchOptions.ExcludeClosedDroits = true;
3737

38-
38+
searchOptions.OrderColumn = searchOptions.OrderColumn;
39+
searchOptions.OrderDescending = searchOptions.OrderDescending;
40+
3941
searchOptions.PageNumber = searchOptions.DroitsPageNumber;
4042
var droits = await _droitService.GetDroitsListViewAsync(searchOptions);
4143

backoffice/src/Helpers/ServiceHelper.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#region
22

3+
using System.Linq.Expressions;
4+
using Droits.Models.Entities;
35
using Droits.Models.ViewModels.ListViews;
46
using Microsoft.EntityFrameworkCore;
57

@@ -35,4 +37,23 @@ public static async Task<ListView<TView>> GetPagedResult<TView>(
3537
Items = pagedItems
3638
};
3739
}
40+
41+
public static Expression<Func<Droit, object>> GetOrderColumnExpression(SearchOptions searchOptions)
42+
{
43+
// Define a mapping between column name and expression
44+
var columnMap = new Dictionary<string, Expression<Func<Droit, object>>>()
45+
{
46+
{ "ReportedDate", d => d.ReportedDate },
47+
{ "Status", d => d.Status }
48+
};
49+
50+
// Try to get the expression for the given OrderColumn, default to "ReportedDate"
51+
if (!columnMap.TryGetValue(searchOptions.OrderColumn, out var orderColumnExpression))
52+
{
53+
// If the OrderColumn is not found, use ReportedDate as the default
54+
orderColumnExpression = columnMap["ReportedDate"];
55+
}
56+
57+
return orderColumnExpression;
58+
}
3859
}

backoffice/src/Models/ViewModels/ListViews/SearchOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ public class SearchOptions
99
public bool FilterByAssignedUser { get; set; }
1010
public bool ExcludeClosedDroits { get; set; }
1111
public bool SearchOpen { get; set; } = false;
12+
public string OrderColumn { get; set; } = "ReportedDate";
13+
public bool OrderDescending { get; set; } = true;
1214
}
1315

backoffice/src/Repositories/DroitRepository.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#region
22

3+
using System.Linq.Expressions;
34
using Droits.Data;
45
using Droits.Exceptions;
56
using Droits.Models.DTOs.Exports;
67
using Droits.Models.Entities;
78
using Droits.Models.FormModels;
9+
using Droits.Models.ViewModels.ListViews;
810
using Droits.Services;
911
using Microsoft.EntityFrameworkCore;
1012

@@ -16,6 +18,7 @@ public interface IDroitRepository
1618
{
1719
IQueryable<Droit> GetDroits();
1820
IQueryable<Droit> GetDroitsWithAssociations();
21+
IQueryable<Droit> GetOrderedDroitsWithAssociations(Expression<Func<Droit, object>> orderColumnExpression, bool orderDescending);
1922
Task<Droit> GetDroitWithAssociationsAsync(Guid id);
2023
Task<Droit> GetDroitAsync(Guid id);
2124
Task<Droit> GetDroitByPowerappsIdAsync(string powerappsId);
@@ -54,6 +57,20 @@ public IQueryable<Droit> GetDroitsWithAssociations()
5457
.Include(d => d.Salvor).AsNoTracking()
5558
.Include(d => d.WreckMaterials.OrderBy(wm => wm.Created)).AsNoTracking();
5659
}
60+
61+
public IQueryable<Droit> GetOrderedDroitsWithAssociations(Expression<Func<Droit, object>> orderColumnExpression, bool orderDescending)
62+
{
63+
var query = orderDescending
64+
? Context.Droits.OrderByDescending(orderColumnExpression).ThenByDescending(d => d.Id)
65+
: Context.Droits.OrderBy(orderColumnExpression).ThenByDescending(d => d.Id);
66+
67+
return query
68+
.Include(d => d.AssignedToUser).AsNoTracking()
69+
.Include(d => d.Letters).AsNoTracking()
70+
.Include(d => d.Wreck).AsNoTracking()
71+
.Include(d => d.Salvor).AsNoTracking()
72+
.Include(d => d.WreckMaterials.OrderBy(wm => wm.Created)).AsNoTracking();
73+
}
5774

5875

5976
public async Task<Droit> GetDroitWithAssociationsAsync(Guid id)

backoffice/src/Services/DroitService.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
#region
33

4+
using System.Linq.Expressions;
45
using AutoMapper;
56
using Droits.Data.Mappers.CsvMappers;
67
using Droits.Exceptions;
@@ -99,10 +100,13 @@ public async Task<string> GetNextDroitReference()
99100

100101
public async Task<DroitListView> GetDroitsListViewAsync(SearchOptions searchOptions)
101102
{
102-
var query = searchOptions.IncludeAssociations
103-
? _repo.GetDroitsWithAssociations()
103+
var orderColumnExpression = ServiceHelper.GetOrderColumnExpression(searchOptions);
104+
105+
IQueryable<Droit> query =
106+
searchOptions.IncludeAssociations
107+
? _repo.GetOrderedDroitsWithAssociations(orderColumnExpression, searchOptions.OrderDescending)
104108
: _repo.GetDroits();
105-
109+
106110
if ( searchOptions.FilterByAssignedUser )
107111
{
108112
var currentUserId = _accountService.GetCurrentUserId();

backoffice/src/Views/Account/Index.cshtml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
<div class="js-dashboard-search d-none">
2525
@Html.EditorFor(f => f.DashboardSearchForm.DroitsPageNumber, new { htmlAttributes = new { @class = "js-droit-page-number-field"} })
2626
@Html.EditorFor(f => f.DashboardSearchForm.LettersPageNumber, new { htmlAttributes = new { @class = "js-letter-page-number-field"} })
27+
@Html.EditorFor(f => f.DashboardSearchForm.OrderDescending, new { htmlAttributes = new { @class = "js-order-descending-field"} })
28+
@Html.EditorFor(f => f.DashboardSearchForm.OrderColumn, new { htmlAttributes = new { @class = "js-order-column-field"} })
2729
</div>
2830
</form>
2931

@@ -35,7 +37,7 @@
3537
<div class="card-body">
3638
@if (Model.Droits.TotalCount > 0)
3739
{
38-
<partial name="Droit/_DroitListTable" model="@Model.Droits"/>
40+
<partial name="Account/_AccountDroitListTable" model="@Model.Droits"/>
3941
<div class="mt-4 pagination-container" data-pagination-input-selector=".js-droit-page-number-field">
4042
@await Html.PartialAsync("_PaginationPartial", Model.Droits)
4143
</div>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
@model DroitListView
2+
3+
@{
4+
var anyVerifiedWrecks = Model.Items.Any(item => ((DroitView)item)?.Wreck?.Name != null);
5+
}
6+
<div class="table-responsive">
7+
<table class="table table-striped">
8+
<thead>
9+
<tr>
10+
<th scope="col">Reference</th>
11+
12+
@if (Model.IncludeAssociations)
13+
{
14+
<th scope="col">Salvor</th>
15+
@if (Model.AnyVerifiedWrecks)
16+
{
17+
<th scope="col" class="text-nowrap">Verified Wreck</th>
18+
19+
}
20+
}
21+
<th scope="col" class="text-nowrap">Reported Wreck</th>
22+
<th scope="col">Triage</th>
23+
@if (Model.IncludeAssociations)
24+
{
25+
<th scope="col">Items</th>
26+
}
27+
<th scope="col">RoW</th>
28+
<th role="button" scope="col">
29+
<a class="sort-link" data-sort-col="Status">Status</a>
30+
</th>
31+
<th role="button" scope="col">
32+
<a class="sort-link" data-sort-col="ReportedDate">Reported</a>
33+
</th>
34+
<th scope="col">Actions</th>
35+
</tr>
36+
</thead>
37+
<tbody>
38+
@foreach (DroitView droit in Model.Items)
39+
{
40+
<tr>
41+
<td class="py-3 text-uppercase">
42+
<a target="_blank" asp-controller="Droit" asp-action="View" asp-route-id="@droit.Id">@droit.Reference</a>
43+
</td>
44+
@if (Model.IncludeAssociations)
45+
{
46+
<td class="py-3">
47+
@{
48+
if (droit?.Salvor?.Name != null)
49+
{
50+
if (droit?.Salvor?.Name != null)
51+
{
52+
@Html.ActionLink(droit?.Salvor?.Name, "View", "Salvor", new { id = droit?.Salvor?.Id }, new { target="_blank" })
53+
}
54+
}
55+
else
56+
{
57+
<span>Unknown Salvor</span>
58+
}
59+
}
60+
</td>
61+
@if (Model.AnyVerifiedWrecks)
62+
{
63+
<td class="py-3">
64+
@{
65+
if (droit?.Wreck?.Name != null)
66+
{
67+
@Html.ActionLink(droit?.Wreck?.Name, "View", "Wreck", new { id = droit?.Wreck?.Id }, new { target="_blank" })
68+
}
69+
}
70+
</td>
71+
}
72+
}
73+
<td class="py-3 text-uppercase">@droit?.ReportedWreckInfo.ReportedWreckName</td>
74+
<td class="py-3 text-uppercase">
75+
<span class="badge rounded-pill @(droit?.TriageNumber >0 ? "bg-primary" : "bg-dark")">@droit?.TriageNumber</span>
76+
</td>
77+
@if (Model.IncludeAssociations)
78+
{
79+
<td class="py-3">
80+
<span class="badge rounded-pill bg-primary">@droit?.WreckMaterials.Count</span>
81+
</td>
82+
}
83+
<td class="py-3">
84+
<span class="text-uppercase">@droit?.AssignedUser</span>
85+
</td>
86+
<td class="py-3">
87+
<span class="badge rounded-pill status-@droit?.Status">@droit?.Status.GetDisplayName()</span>
88+
</td>
89+
<td class="py-3">@droit?.ReportedDate.ToString("dd/MM/yyyy")</td>
90+
<td class="py-3">
91+
<div class="btn-group">
92+
<a asp-action="Edit" asp-controller="Droit" asp-route-id="@droit?.Id" class="btn btn-primary">Edit</a>
93+
<a asp-action="View" asp-controller="Droit" asp-route-id="@droit?.Id" class="btn btn-primary">View</a>
94+
</div>
95+
</td>
96+
</tr>
97+
}
98+
</tbody>
99+
</table>
100+
</div>

backoffice/src/wwwroot/js/searchForms.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,38 @@ function initializeSearchForm(formClass, toggleButtonClass) {
1212
const searchForm = document.querySelector(formClass);
1313
searchForm.classList.toggle('d-none');
1414
})});
15+
16+
const sortButtons = document.querySelectorAll(".sort-link");
17+
18+
if (sortButtons.length > 0) {
19+
const orderColumnField = document.querySelector(".js-order-column-field");
20+
const orderDescendingField = document.querySelector(".js-order-descending-field");
21+
const sortArrow = document.createElement("span");
22+
sortArrow.className = "sort-arrow";
23+
sortArrow.textContent = orderDescendingField.checked ? "\u2193" : "\u2191";
24+
25+
sortButtons.forEach((button) => {
26+
const currentButtonDataField = button.getAttribute("data-sort-col");
27+
28+
if (orderColumnField.value === currentButtonDataField) {
29+
button.appendChild(sortArrow);
30+
}
31+
32+
button.addEventListener('click', (ev) => {
33+
ev.preventDefault();
34+
35+
if (orderColumnField.value === currentButtonDataField) {
36+
orderDescendingField.checked = !orderDescendingField.checked;
37+
} else {
38+
orderDescendingField.checked = true;
39+
}
40+
41+
orderColumnField.value = currentButtonDataField;
42+
43+
orderColumnField.closest("form").submit();
44+
})
45+
})
46+
}
1547

1648
const paginationButtons = document.querySelectorAll(".js-page-link");
1749
paginationButtons.forEach((button) => {

backoffice/src/wwwroot/scss/site.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ div.form-sections {
113113
font-family: var(--bs-body-font-family) !important;
114114
}
115115

116+
.sort-link {
117+
text-decoration: none;
118+
}
119+
120+
.sort-arrow {
121+
margin-left: 0.25rem;
122+
}
123+
116124
.status-Received {@extend .bg-success;}
117125
.status-AcknowledgementLetterSent {@extend .bg-warning;}
118126
.status-InitialResearch {@extend .bg-info;}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Linq.Expressions;
2+
using Droits.Helpers;
3+
using Droits.Models.Entities;
4+
using Droits.Models.ViewModels.ListViews;
5+
6+
namespace Droits.Tests.UnitTests.Helpers;
7+
8+
public class ServiceHelperTests
9+
{
10+
[Fact]
11+
public void GetOrderColumnExpression_ShouldReturnReportedDate_WhenOrderColumnIsInvalid()
12+
{
13+
// Arrange
14+
var searchOptions = new SearchOptions { OrderColumn = "InvalidColumn" };
15+
var expectedExpression = (Expression<Func<Droit, object>>)(d => d.ReportedDate);
16+
17+
// Act
18+
var result = ServiceHelper.GetOrderColumnExpression(searchOptions);
19+
20+
// Assert
21+
Assert.Equal(expectedExpression.ToString(), result.ToString());
22+
}
23+
24+
[Fact]
25+
public void GetOrderColumnExpression_ShouldReturnStatus_WhenOrderColumnIsReportedDate()
26+
{
27+
// Arrange
28+
var searchOptions = new SearchOptions { OrderColumn = "ReportedDate" };
29+
var expectedExpression = (Expression<Func<Droit, object>>)(d => d.ReportedDate);
30+
31+
// Act
32+
var result = ServiceHelper.GetOrderColumnExpression(searchOptions);
33+
34+
// Assert
35+
Assert.Equal(expectedExpression.ToString(), result.ToString());
36+
}
37+
38+
[Fact]
39+
public void GetOrderColumnExpression_ShouldReturnStatus_WhenOrderColumnIsStatus()
40+
{
41+
// Arrange
42+
var searchOptions = new SearchOptions { OrderColumn = "Status" };
43+
var expectedExpression = (Expression<Func<Droit, object>>)(d => d.Status);
44+
45+
// Act
46+
var result = ServiceHelper.GetOrderColumnExpression(searchOptions);
47+
48+
// Assert
49+
Assert.Equal(expectedExpression.ToString(), result.ToString());
50+
}
51+
}

0 commit comments

Comments
 (0)