Skip to content

Commit 0bfee35

Browse files
feat: Add NHS number search functionality for exceptions and reports (#1777)
* feat: feedback component and scss * feat: renamed component to UserFeedback and added components to required pages * chore: renamed imports from feedback to UserFeedback * feat: unused css removed * feat: input added to UI * feat: search page * feat: search moved out of header to own component * feat: search function * feat: fetch updated with fetchExceptionsByNhsNumber endpoint * feat: ValidationExceptionsByNhsNumberResponse * feat: GetValidationExceptionsByNhsNumber function added * feat: added showing item numbers * feat: Header tidy and search button added * feat: pagination fixed, reports added, page count made dynamic, response object refactored * feat: added missing feedback component from report details page * feat: generate report logic * feat: refactor service methods, and calling function, move paginationResult DTO, front end structure change * feat: Error handling * feat: no results card * refactor: readonly props, refactor * feat: No Result page added, Conditional display added to input Search in header * feat: error handling and redirect * chore: placeholder typo * fix: updated test references * feat: update tests and remove duplicate styling * chore: sonarQube Issues * feat: used standard component, updated scss * feat: server side rendering on conditionalHeaderSearch Component * chore: Sonaqube img fix * chore: styling * feat: code tidy and refactor, * refactor: GetExceptionsWithReportsByNhsNumber refactor, controller reworked * refactor: abstract methods * test: unit tests added * chore: encodeURIComponent * fix: removed wrong ExceptionCategory * fix: type needed for all * fix: NhsNumber logging removed * refactor: addressing PR comments, removed logging, changed return type, refactor response object * fix: mapping * fix: ISO string causing incorrect date to be parsed for some records
1 parent 377bcf6 commit 0bfee35

File tree

17 files changed

+862
-93
lines changed

17 files changed

+862
-93
lines changed

application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Common;
22

33
using Microsoft.Azure.Functions.Worker.Http;
4+
using Model.Pagination;
45

56
public interface IPaginationService<T>
67
{

application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Common;
22

33
using Microsoft.Azure.Functions.Worker.Http;
4+
using Model.Pagination;
45

56
public class PaginationService<T> : IPaginationService<T>
67
{
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
namespace Data.Database;
22

3+
using System;
4+
using System.Linq.Expressions;
5+
using System.Threading.Tasks;
36
using Common;
47
using Model;
8+
using Model.DTO;
59
using Model.Enums;
610

711
public interface IValidationExceptionData
812
{
913
Task<bool> Create(ValidationException exception);
10-
Task<List<ValidationException>?> GetFilteredExceptions(ExceptionStatus? exceptionStatus, SortOrder? sortOrder, ExceptionCategory exceptionCategory);
14+
Task<List<ValidationException>> GetFilteredExceptions(ExceptionStatus? exceptionStatus, SortOrder? sortOrder, ExceptionCategory exceptionCategory);
1115
Task<ValidationException?> GetExceptionById(int exceptionId);
1216
Task<bool> RemoveOldException(string nhsNumber, string screeningName);
1317
Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptionId, string serviceNowId);
14-
Task<List<ValidationException>?> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory);
18+
Task<List<ValidationException>> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory);
19+
Task<List<ExceptionManagement>> GetByFilter(Expression<Func<ExceptionManagement, bool>> filter);
20+
Task<ValidationExceptionsByNhsNumberResponse> GetExceptionsWithReportsByNhsNumber(string nhsNumber);
1521
}

application/CohortManager/src/Functions/Shared/Data/Database/ValidationExceptionData.cs

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace Data.Database;
22

33
using System;
44
using System.Data;
5+
using System.Linq.Expressions;
56
using System.Net;
67
using System.Text.Json;
78
using System.Threading.Tasks;
@@ -17,17 +18,17 @@ public class ValidationExceptionData : IValidationExceptionData
1718
{
1819
private readonly ILogger<ValidationExceptionData> _logger;
1920
private readonly IDataServiceClient<ExceptionManagement> _validationExceptionDataServiceClient;
21+
2022
public ValidationExceptionData(
2123
ILogger<ValidationExceptionData> logger,
22-
IDataServiceClient<ExceptionManagement> validationExceptionDataServiceClient,
23-
IDataServiceClient<ParticipantDemographic> demographicDataServiceClient
24+
IDataServiceClient<ExceptionManagement> validationExceptionDataServiceClient
2425
)
2526
{
2627
_logger = logger;
2728
_validationExceptionDataServiceClient = validationExceptionDataServiceClient;
2829
}
2930

30-
public async Task<List<ValidationException>?> GetFilteredExceptions(ExceptionStatus? exceptionStatus, SortOrder? sortOrder, ExceptionCategory exceptionCategory)
31+
public async Task<List<ValidationException>> GetFilteredExceptions(ExceptionStatus? exceptionStatus, SortOrder? sortOrder, ExceptionCategory exceptionCategory)
3132
{
3233
var category = (int)exceptionCategory;
3334
var exceptions = await _validationExceptionDataServiceClient.GetByFilter(x => x.Category != null && x.Category.Value == category);
@@ -49,12 +50,12 @@ IDataServiceClient<ParticipantDemographic> demographicDataServiceClient
4950
return GetValidationExceptionWithDetails(exception);
5051
}
5152

52-
5353
public async Task<bool> Create(ValidationException exception)
5454
{
5555
var exceptionToUpdate = new ExceptionManagement().FromValidationException(exception);
5656
return await _validationExceptionDataServiceClient.Add(exceptionToUpdate);
5757
}
58+
5859
public async Task<bool> RemoveOldException(string nhsNumber, string screeningName)
5960
{
6061
var exceptions = await GetExceptionRecords(nhsNumber, screeningName);
@@ -63,7 +64,6 @@ public async Task<bool> RemoveOldException(string nhsNumber, string screeningNam
6364
return false;
6465
}
6566

66-
// we only need to get the last unresolved exception for the nhs number and screening service
6767
var validationExceptionToUpdate = exceptions.Where(x => DateToString(x.DateResolved) == "9999-12-31")
6868
.OrderByDescending(x => x.DateCreated).FirstOrDefault();
6969

@@ -117,7 +117,7 @@ public async Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptio
117117
}
118118
}
119119

120-
public async Task<List<ValidationException>?> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory)
120+
public async Task<List<ValidationException>> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory)
121121
{
122122
if (exceptionCategory is not (ExceptionCategory.Confusion or ExceptionCategory.Superseded or ExceptionCategory.NBO))
123123
{
@@ -129,8 +129,41 @@ public async Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptio
129129
if (filteredExceptions == null || !filteredExceptions.Any())
130130
return [];
131131

132-
var results = filteredExceptions.Select(GetValidationExceptionWithDetails);
133-
return results.Where(x => x != null).ToList()!;
132+
return MapToValidationExceptions(filteredExceptions);
133+
}
134+
135+
public async Task<List<ExceptionManagement>> GetByFilter(Expression<Func<ExceptionManagement, bool>> filter)
136+
{
137+
var result = await _validationExceptionDataServiceClient.GetByFilter(filter) ?? Enumerable.Empty<ExceptionManagement>();
138+
return result.ToList();
139+
}
140+
141+
public async Task<ValidationExceptionsByNhsNumberResponse> GetExceptionsWithReportsByNhsNumber(string nhsNumber)
142+
{
143+
var validationExceptions = await GetValidationExceptionsByNhsNumber(nhsNumber);
144+
145+
if (validationExceptions.Count == 0)
146+
{
147+
return new ValidationExceptionsByNhsNumberResponse
148+
{
149+
Exceptions = [],
150+
Reports = [],
151+
NhsNumber = nhsNumber
152+
};
153+
}
154+
155+
var reports = GenerateExceptionReports(validationExceptions);
156+
return new ValidationExceptionsByNhsNumberResponse
157+
{
158+
Exceptions = validationExceptions,
159+
Reports = reports,
160+
NhsNumber = nhsNumber
161+
};
162+
}
163+
164+
private List<ValidationException> MapToValidationExceptions(IEnumerable<ExceptionManagement> exceptions)
165+
{
166+
return exceptions.Select(GetValidationExceptionWithDetails).Where(x => x != null).ToList()!;
134167
}
135168

136169
private ServiceResponseModel CreateSuccessResponse(string message) => CreateResponse(true, HttpStatusCode.OK, message);
@@ -183,7 +216,7 @@ public async Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptio
183216
NhsNumber = long.TryParse(errorRecordData.NhsNumber, out long nhsNumber) ? nhsNumber : 0,
184217
GivenName = errorRecordData.FirstName,
185218
FamilyName = errorRecordData.FamilyName,
186-
DateOfBirth = MappingUtilities.FormatDateTime(MappingUtilities.ParseDates(errorRecordData.DateOfBirth)),
219+
DateOfBirth = MappingUtilities.FormatDateTime(MappingUtilities.ParseDates(errorRecordData.DateOfBirth ?? string.Empty)),
187220
SupersededByNhsNumber = long.TryParse(errorRecordData.SupersededByNhsNumber, out long superseded) ? superseded : null,
188221
Gender = errorRecordData.Gender,
189222
AddressLine1 = errorRecordData.AddressLine1,
@@ -277,10 +310,8 @@ private ServiceResponseModel CreateResponse(bool success, HttpStatusCode statusC
277310

278311
private async Task<List<ExceptionManagement>?> GetExceptionRecords(string nhsNumber, string screeningName)
279312
{
280-
281313
var exceptions = await _validationExceptionDataServiceClient.GetByFilter(x => x.NhsNumber == nhsNumber && x.ScreeningName == screeningName);
282314
return exceptions?.ToList();
283-
284315
}
285316

286317
private static string DateToString(DateTime? datetime)
@@ -311,4 +342,39 @@ private static List<ValidationException> SortExceptions(SortOrder? sortOrder, IE
311342
? [.. filteredList.OrderBy(dateProperty)]
312343
: [.. filteredList.OrderByDescending(dateProperty)];
313344
}
345+
346+
347+
348+
private async Task<List<ValidationException>> GetValidationExceptionsByNhsNumber(string nhsNumber)
349+
{
350+
var exceptions = await _validationExceptionDataServiceClient.GetByFilter(x => x.NhsNumber == nhsNumber);
351+
if (exceptions == null || !exceptions.Any())
352+
{
353+
return [];
354+
}
355+
356+
return [.. exceptions
357+
.Select(GetValidationExceptionWithDetails)
358+
.Where(x => x != null)
359+
.Cast<ValidationException>()
360+
.OrderByDescending(x => x.DateCreated)];
361+
}
362+
363+
private static List<ValidationExceptionReport> GenerateExceptionReports(List<ValidationException> validationExceptions)
364+
{
365+
return [.. validationExceptions
366+
.Where(x => x.Category.HasValue && (x.Category.Value == 12 || x.Category.Value == 13))
367+
.GroupBy(x => new
368+
{
369+
Date = x.DateCreated?.Date ?? DateTime.Now.Date,
370+
Category = x.Category
371+
})
372+
.Select(g => new ValidationExceptionReport
373+
{
374+
ReportDate = g.Key.Date,
375+
Category = g.Key.Category,
376+
ExceptionCount = g.Count()
377+
})
378+
.OrderByDescending(r => r.ReportDate)];
379+
}
314380
}

application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationResult.cs renamed to application/CohortManager/src/Functions/Shared/Model/DTO/PaginationResult.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
namespace Common;
1+
namespace Model.Pagination;
22

33
public class PaginationResult<T>
44
{
5-
public IEnumerable<T> Items { get; set; }
5+
public required IEnumerable<T> Items { get; set; }
66
public bool IsFirstPage { get; set; }
77
public bool HasNextPage { get; set; }
88
public bool HasPreviousPage { get; set; }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Model.DTO;
2+
3+
using Model.Pagination;
4+
5+
/// <summary>
6+
/// Response model for NHS number search containing paginated exceptions and associated reports
7+
/// </summary>
8+
public class ValidationExceptionsByNhsNumberResponse
9+
{
10+
public string NhsNumber { get; set; } = string.Empty;
11+
public List<ValidationException> Exceptions { get; set; } = [];
12+
public List<ValidationExceptionReport> Reports { get; set; } = [];
13+
public PaginationResult<ValidationException> PaginatedExceptions { get; set; } = new() { Items = [] };
14+
}
15+
16+
public class ValidationExceptionReport
17+
{
18+
public DateTime ReportDate { get; set; }
19+
public int? Category { get; set; }
20+
public int ExceptionCount { get; set; }
21+
}

application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ namespace NHS.CohortManager.ScreeningDataServices;
1010
using Microsoft.Azure.Functions.Worker.Http;
1111
using Microsoft.Extensions.Logging;
1212
using Model;
13+
using Model.DTO;
1314
using Model.Enums;
15+
using Model.Pagination;
1416

1517
/// <summary>
1618
/// Azure Function for retrieving and managing validation exceptions.
@@ -129,4 +131,53 @@ public async Task<HttpResponseData> UpdateExceptionServiceNowId([HttpTrigger(Aut
129131
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req);
130132
}
131133
}
134+
135+
/// <summary>
136+
/// Retrieves validation exceptions and reports for a specific NHS number.
137+
/// </summary>
138+
[Function(nameof(GetValidationExceptionsByNhsNumber))]
139+
public async Task<HttpResponseData> GetValidationExceptionsByNhsNumber([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
140+
{
141+
var nhsNumber = req.Query["nhsNumber"];
142+
var page = _httpParserHelper.GetQueryParameterAsInt(req, "page", 1);
143+
var pageSize = _httpParserHelper.GetQueryParameterAsInt(req, "pageSize", 10);
144+
145+
if (string.IsNullOrWhiteSpace(nhsNumber))
146+
{
147+
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req, "NHS number is required.");
148+
}
149+
150+
var cleanedNhsNumber = nhsNumber.Replace(" ", "");
151+
if (!ValidationHelper.ValidateNHSNumber(cleanedNhsNumber))
152+
{
153+
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req, "Invalid NHS number format.");
154+
}
155+
156+
try
157+
{
158+
var result = await _validationData.GetExceptionsWithReportsByNhsNumber(cleanedNhsNumber);
159+
160+
if (result.Exceptions.Count == 0)
161+
{
162+
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
163+
}
164+
165+
var paginatedExceptions = _paginationService.GetPaginatedResult(result.Exceptions.AsQueryable(), page, pageSize);
166+
var headers = _paginationService.AddNavigationHeaders(req, paginatedExceptions);
167+
168+
var response = new ValidationExceptionsByNhsNumberResponse
169+
{
170+
NhsNumber = result.NhsNumber,
171+
PaginatedExceptions = paginatedExceptions,
172+
Reports = result.Reports
173+
};
174+
175+
return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, req, JsonSerializer.Serialize(response), headers);
176+
}
177+
catch (Exception ex)
178+
{
179+
_logger.LogError(ex, "Error retrieving validation exceptions for provided NHS number");
180+
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req);
181+
}
182+
}
132183
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
interface ConditionalHeaderSearchProps {
2+
readonly children: React.ReactNode;
3+
readonly pathname: string;
4+
}
5+
6+
export function ConditionalHeaderSearch({
7+
children,
8+
pathname,
9+
}: ConditionalHeaderSearchProps) {
10+
const isNoResultsPage = pathname === "/exceptions/noResults";
11+
12+
// Don't render the header search if we're on the no results page
13+
if (isNoResultsPage) {
14+
return null;
15+
}
16+
17+
return <>{children}</>;
18+
}

0 commit comments

Comments
 (0)