Skip to content

Commit ddd9de9

Browse files
elielijah321AREMU, ElijahFrostyApeOneFrostyApeOne
authored
Feature/276382 filter on forms (#172)
* Pagination * update the project version and changelog version * send null pagesize if not defined * fix merge * Added the ability to search for applications by reference on the dashboard * Improved search * Added filters feature flag * Version bump --------- Co-authored-by: AREMU, Elijah <Elijah.AREMU@EDUCATION.GOV.UK> Co-authored-by: FrostyApeOne <Farshad.DASHTI@EDUCATION.GOV.UK> Co-authored-by: Farshad Dashti <78855469+FrostyApeOne@users.noreply.github.com>
1 parent 141c3e8 commit ddd9de9

12 files changed

Lines changed: 394 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,8 @@ All notable changes to this service will be documented in this file.
176176

177177
## [1.3.26]
178178
### Notes
179-
- Added Academy filtering feature flag on the endpoint
179+
- Added Academy filtering feature flag on the endpoint
180+
181+
## [1.3.27]
182+
### Notes
183+
- Added application search functionality

src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.46" />
10+
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.48-prerelease.14" />
1111
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.3.0" />
1212
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
1313
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />

src/DfE.ExternalApplications.Application/Options/DashboardOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ namespace DfE.ExternalApplications.Application.Options;
33
public class DashboardOptions
44
{
55
public int PageSize { get; set; } = 50;
6+
7+
/// <summary>
8+
/// When enabled, shows the application listing filter panel on the dashboard.
9+
/// </summary>
10+
public bool EnableApplicationFilters { get; set; }
611
}

src/DfE.ExternalApplications.Web/DfE.ExternalApplications.Web.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<UserSecretsId>8051c984-585b-4a5e-b6d7-833e5dd4afe7</UserSecretsId>
8-
<Version>1.3.26</Version>
9-
<InformationalVersion>1.3.26</InformationalVersion>
8+
<Version>1.3.27</Version>
9+
<InformationalVersion>1.3.27</InformationalVersion>
1010
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
1111
</PropertyGroup>
1212

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Globalization;
2+
using GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.Enums;
3+
4+
namespace DfE.ExternalApplications.Web.Models.Applications;
5+
6+
/// <summary>
7+
/// Search and filter values for the applications dashboard listing.
8+
/// </summary>
9+
public sealed class DashboardApplicationSearch
10+
{
11+
private const string DateQueryFormat = "yyyy-MM-dd";
12+
13+
public string? SearchReference { get; init; }
14+
15+
public string? DateStartedFromValue { get; init; }
16+
17+
public string? DateStartedToValue { get; init; }
18+
19+
public string? DateSubmittedFromValue { get; init; }
20+
21+
public string? DateSubmittedToValue { get; init; }
22+
23+
public ApplicationStatus? Status { get; init; }
24+
25+
public DateTime? DateStartedFrom => ParseDate(DateStartedFromValue);
26+
27+
public DateTime? DateStartedTo => ParseDate(DateStartedToValue);
28+
29+
public DateTime? DateSubmittedFrom => ParseDate(DateSubmittedFromValue);
30+
31+
public DateTime? DateSubmittedTo => ParseDate(DateSubmittedToValue);
32+
33+
public bool HasActiveFilters =>
34+
!string.IsNullOrWhiteSpace(SearchReference) ||
35+
!string.IsNullOrWhiteSpace(DateStartedFromValue) ||
36+
!string.IsNullOrWhiteSpace(DateStartedToValue) ||
37+
!string.IsNullOrWhiteSpace(DateSubmittedFromValue) ||
38+
!string.IsNullOrWhiteSpace(DateSubmittedToValue) ||
39+
Status.HasValue;
40+
41+
/// <summary>
42+
/// Builds a query string for pagination links, preserving active filters.
43+
/// </summary>
44+
public string BuildPaginationHref(int page)
45+
{
46+
var query = new List<string> { $"currentPage={page}" };
47+
48+
if (!string.IsNullOrWhiteSpace(SearchReference))
49+
query.Add($"searchReference={Uri.EscapeDataString(SearchReference)}");
50+
51+
if (!string.IsNullOrWhiteSpace(DateStartedFromValue))
52+
query.Add($"dateStartedFrom={Uri.EscapeDataString(DateStartedFromValue)}");
53+
54+
if (!string.IsNullOrWhiteSpace(DateStartedToValue))
55+
query.Add($"dateStartedTo={Uri.EscapeDataString(DateStartedToValue)}");
56+
57+
if (!string.IsNullOrWhiteSpace(DateSubmittedFromValue))
58+
query.Add($"dateSubmittedFrom={Uri.EscapeDataString(DateSubmittedFromValue)}");
59+
60+
if (!string.IsNullOrWhiteSpace(DateSubmittedToValue))
61+
query.Add($"dateSubmittedTo={Uri.EscapeDataString(DateSubmittedToValue)}");
62+
63+
if (Status.HasValue)
64+
query.Add($"status={(int)Status.Value}");
65+
66+
return "?" + string.Join("&", query);
67+
}
68+
69+
/// <summary>
70+
/// Parses HTML date input values (yyyy-MM-dd) for API calls.
71+
/// </summary>
72+
internal static DateTime? ParseDate(string? value) =>
73+
!string.IsNullOrWhiteSpace(value)
74+
&& DateTime.TryParseExact(
75+
value.Trim(),
76+
DateQueryFormat,
77+
CultureInfo.InvariantCulture,
78+
DateTimeStyles.None,
79+
out var parsed)
80+
? parsed.Date
81+
: null;
82+
}

src/DfE.ExternalApplications.Web/Pages/Applications/Dashboard.cshtml

Lines changed: 170 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
@{
1313
ViewData["Title"] = Configuration["Layout:ServiceName"];
14+
var showFiltersPanel = Model.FiltersEnabled && (Model.ShowFiltersPanel || !ViewData.ModelState.IsValid);
15+
}
16+
17+
@if (Model.FiltersEnabled)
18+
{
19+
<link rel="stylesheet" href="~/css/application-listing.css" asp-append-version="true" />
1420
}
1521

1622
<div class="govuk-grid-row">
@@ -32,14 +38,173 @@
3238
</div>
3339
</div>
3440
}
41+
else if (!ViewData.ModelState.IsValid)
42+
{
43+
<div class="govuk-error-summary" aria-labelledby="error-summary-title" role="alert" data-module="govuk-error-summary">
44+
<h2 class="govuk-error-summary__title" id="error-summary-title">
45+
There is a problem
46+
</h2>
47+
<div class="govuk-error-summary__body">
48+
<ul class="govuk-list govuk-error-summary__list">
49+
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
50+
{
51+
<li>@error.ErrorMessage</li>
52+
}
53+
</ul>
54+
</div>
55+
</div>
56+
}
3557

3658
<h2 class="govuk-heading-l govuk-!-margin-bottom-3">
3759
@AppTerminology.PluralCapitalised in progress
3860
</h2>
3961

62+
@if (Model.FiltersEnabled)
63+
{
64+
<details class="govuk-details application-listing__filters-container" data-module="govuk-details" @(showFiltersPanel ? "open" : null)>
65+
<summary class="govuk-details__summary application-listing__filters-summary">
66+
<span class="govuk-details__summary-text govuk-button govuk-button--secondary application-listing__filters-button" data-testid="filter-applications-button">
67+
Filter @AppTerminology.Plural
68+
</span>
69+
</summary>
70+
<div class="govuk-details__text application-listing__filters-details">
71+
<form method="get" action="/applications/dashboard">
72+
<div class="govuk-grid-row">
73+
<div class="govuk-grid-column-one-quarter">
74+
<div class="govuk-form-group">
75+
<h2 class="govuk-label-wrapper">
76+
<label class="govuk-label govuk-label--m" for="search-reference">
77+
Reference number
78+
</label>
79+
</h2>
80+
<div id="search-reference-hint" class="govuk-hint">
81+
Enter all or part of a reference number
82+
</div>
83+
<input class="govuk-input" id="search-reference" name="searchReference"
84+
type="search" value="@Model.SearchReference" autocomplete="off"
85+
aria-describedby="search-reference-hint" data-testid="search-reference">
86+
</div>
87+
</div>
88+
89+
<div class="govuk-grid-column-one-quarter">
90+
<div class="govuk-form-group">
91+
<label class="govuk-label govuk-label--m" for="search-status">
92+
Status
93+
</label>
94+
<select class="govuk-select" id="search-status" name="status" data-testid="search-status">
95+
@if (Model.Status is null)
96+
{
97+
<option value="" selected>All statuses</option>
98+
}
99+
else
100+
{
101+
<option value="">All statuses</option>
102+
}
103+
@if (Model.Status == ApplicationStatus.InProgress)
104+
{
105+
<option value="@((int)ApplicationStatus.InProgress)" selected>In progress</option>
106+
}
107+
else
108+
{
109+
<option value="@((int)ApplicationStatus.InProgress)">In progress</option>
110+
}
111+
@if (Model.Status == ApplicationStatus.Submitted)
112+
{
113+
<option value="@((int)ApplicationStatus.Submitted)" selected>Submitted</option>
114+
}
115+
else
116+
{
117+
<option value="@((int)ApplicationStatus.Submitted)">Submitted</option>
118+
}
119+
</select>
120+
</div>
121+
</div>
122+
123+
<div class="govuk-grid-column-one-quarter">
124+
<div class="govuk-form-group @(ViewData.ModelState[nameof(Model.DateStartedTo)]?.Errors.Count > 0 ? "govuk-form-group--error" : null)">
125+
<fieldset class="govuk-fieldset" aria-describedby="date-started-hint">
126+
<legend class="govuk-fieldset__legend govuk-fieldset__legend--m">
127+
Date started
128+
</legend>
129+
<div id="date-started-hint" class="govuk-hint">For example, 01 03 2024</div>
130+
@if (ViewData.ModelState[nameof(Model.DateStartedTo)]?.Errors.Count > 0)
131+
{
132+
<span class="govuk-error-message">
133+
<span class="govuk-visually-hidden">Error:</span>
134+
@ViewData.ModelState[nameof(Model.DateStartedTo)]!.Errors[0].ErrorMessage
135+
</span>
136+
}
137+
<label class="govuk-label" for="date-started-from">From</label>
138+
<input class="govuk-input govuk-!-margin-bottom-2 @(ViewData.ModelState[nameof(Model.DateStartedTo)]?.Errors.Count > 0 ? "govuk-input--error" : null)"
139+
id="date-started-from" name="dateStartedFrom" type="date"
140+
value="@Model.DateStartedFrom" data-testid="date-started-from">
141+
<label class="govuk-label" for="date-started-to">To</label>
142+
<input class="govuk-input @(ViewData.ModelState[nameof(Model.DateStartedTo)]?.Errors.Count > 0 ? "govuk-input--error" : null)"
143+
id="date-started-to" name="dateStartedTo" type="date"
144+
value="@Model.DateStartedTo" data-testid="date-started-to">
145+
</fieldset>
146+
</div>
147+
</div>
148+
149+
<div class="govuk-grid-column-one-quarter">
150+
<div class="govuk-form-group @(ViewData.ModelState[nameof(Model.DateSubmittedTo)]?.Errors.Count > 0 ? "govuk-form-group--error" : null)">
151+
<fieldset class="govuk-fieldset" aria-describedby="date-submitted-hint">
152+
<legend class="govuk-fieldset__legend govuk-fieldset__legend--m">
153+
Date submitted
154+
</legend>
155+
<div id="date-submitted-hint" class="govuk-hint">For example, 01 03 2024</div>
156+
@if (ViewData.ModelState[nameof(Model.DateSubmittedTo)]?.Errors.Count > 0)
157+
{
158+
<span class="govuk-error-message">
159+
<span class="govuk-visually-hidden">Error:</span>
160+
@ViewData.ModelState[nameof(Model.DateSubmittedTo)]!.Errors[0].ErrorMessage
161+
</span>
162+
}
163+
<label class="govuk-label" for="date-submitted-from">From</label>
164+
<input class="govuk-input govuk-!-margin-bottom-2 @(ViewData.ModelState[nameof(Model.DateSubmittedTo)]?.Errors.Count > 0 ? "govuk-input--error" : null)"
165+
id="date-submitted-from" name="dateSubmittedFrom" type="date"
166+
value="@Model.DateSubmittedFrom" data-testid="date-submitted-from">
167+
<label class="govuk-label" for="date-submitted-to">To</label>
168+
<input class="govuk-input @(ViewData.ModelState[nameof(Model.DateSubmittedTo)]?.Errors.Count > 0 ? "govuk-input--error" : null)"
169+
id="date-submitted-to" name="dateSubmittedTo" type="date"
170+
value="@Model.DateSubmittedTo" data-testid="date-submitted-to">
171+
</fieldset>
172+
</div>
173+
</div>
174+
</div>
175+
176+
<div class="govuk-grid-row">
177+
<div class="govuk-grid-column-full">
178+
<div class="govuk-button-group">
179+
<button class="govuk-button govuk-button--secondary" type="submit" data-module="govuk-button" data-testid="apply-filters">
180+
Apply filters
181+
</button>
182+
@if (Model.IsSearchActive)
183+
{
184+
<a class="govuk-link" href="/applications/dashboard" data-testid="clear-filters">Clear filters</a>
185+
}
186+
</div>
187+
</div>
188+
</div>
189+
</form>
190+
</div>
191+
</details>
192+
}
193+
194+
<div class="govuk-!-margin-bottom-6"></div>
195+
40196
@if (!Model.HasError && !Model.Applications.Any())
41197
{
42-
<p class="govuk-body">You have no @AppTerminology.Plural in progress.</p>
198+
<p class="govuk-body">
199+
@if (Model.IsSearchActive)
200+
{
201+
<span>No @AppTerminology.Plural found matching your filters.</span>
202+
}
203+
else
204+
{
205+
<span>You have no @AppTerminology.Plural in progress.</span>
206+
}
207+
</p>
43208
}
44209
else if (!Model.HasError)
45210
{
@@ -93,7 +258,7 @@
93258
@if (Model.CurrentPage > 1)
94259
{
95260
<div class="govuk-pagination__prev">
96-
<a class="govuk-link govuk-pagination__link" href="?currentPage=@(Model.CurrentPage - 1)" rel="prev">
261+
<a class="govuk-link govuk-pagination__link" href="@Model.BuildPaginationHref(Model.CurrentPage - 1)" rel="prev">
97262
<svg class="govuk-pagination__icon govuk-pagination__icon--prev" xmlns="http://www.w3.org/2000/svg" height="13" width="15" focusable="false" aria-hidden="true" viewBox="0 0 15 13">
98263
<path d="m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z" />
99264
</svg>
@@ -107,13 +272,13 @@
107272
if (i == Model.CurrentPage)
108273
{
109274
<li class="govuk-pagination__item govuk-pagination__item--current">
110-
<a class="govuk-link govuk-pagination__link" href="?currentPage=@i" aria-label="Page @i" aria-current="page">@i</a>
275+
<a class="govuk-link govuk-pagination__link" href="@Model.BuildPaginationHref(i)" aria-label="Page @i" aria-current="page">@i</a>
111276
</li>
112277
}
113278
else if (i == 1 || i == Model.TotalPages || Math.Abs(i - Model.CurrentPage) <= 1)
114279
{
115280
<li class="govuk-pagination__item">
116-
<a class="govuk-link govuk-pagination__link" href="?currentPage=@i" aria-label="Page @i">@i</a>
281+
<a class="govuk-link govuk-pagination__link" href="@Model.BuildPaginationHref(i)" aria-label="Page @i">@i</a>
117282
</li>
118283
}
119284
else if (Math.Abs(i - Model.CurrentPage) == 2)
@@ -125,7 +290,7 @@
125290
@if (Model.CurrentPage < Model.TotalPages)
126291
{
127292
<div class="govuk-pagination__next">
128-
<a class="govuk-link govuk-pagination__link" href="?currentPage=@(Model.CurrentPage + 1)" rel="next">
293+
<a class="govuk-link govuk-pagination__link" href="@Model.BuildPaginationHref(Model.CurrentPage + 1)" rel="next">
129294
<span class="govuk-pagination__link-title">Next<span class="govuk-visually-hidden"> page</span></span>
130295
<svg class="govuk-pagination__icon govuk-pagination__icon--next" xmlns="http://www.w3.org/2000/svg" height="13" width="15" focusable="false" aria-hidden="true" viewBox="0 0 15 13">
131296
<path d="m8.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7246z" />

0 commit comments

Comments
 (0)