Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
94de5ef
feat: feedback component and scss
Warren-Pitterson Nov 20, 2025
e903385
feat: renamed component to UserFeedback and added components to requi…
Warren-Pitterson Nov 20, 2025
a04cebd
chore: renamed imports from feedback to UserFeedback
Warren-Pitterson Nov 20, 2025
e515277
feat: unused css removed
Warren-Pitterson Nov 20, 2025
2e8f207
feat: input added to UI
Warren-Pitterson Nov 20, 2025
1b38ac3
feat: search page
Warren-Pitterson Nov 27, 2025
956e570
feat: search moved out of header to own component
Warren-Pitterson Nov 27, 2025
14c9550
feat: search function
Warren-Pitterson Nov 27, 2025
b58d7e1
feat: fetch updated with fetchExceptionsByNhsNumber endpoint
Warren-Pitterson Nov 27, 2025
17ff964
feat: ValidationExceptionsByNhsNumberResponse
Warren-Pitterson Nov 27, 2025
c948b98
feat: GetValidationExceptionsByNhsNumber function added
Warren-Pitterson Nov 27, 2025
a186816
feat: added showing item numbers
Warren-Pitterson Nov 27, 2025
498adb0
Merge branch 'main' into feat/DTOSS-10947-UI-Search
Warren-Pitterson Nov 27, 2025
a97fb36
feat: Header tidy and search button added
Warren-Pitterson Nov 27, 2025
96f2de2
feat: pagination fixed, reports added, page count made dynamic, respo…
Warren-Pitterson Dec 1, 2025
4b716e7
feat: added missing feedback component from report details page
Warren-Pitterson Dec 1, 2025
bdd12ed
feat: generate report logic
Warren-Pitterson Dec 2, 2025
3cb9521
feat: refactor service methods, and calling function, move pagination…
Warren-Pitterson Dec 2, 2025
0157b9d
feat: Error handling
Warren-Pitterson Dec 2, 2025
12834b1
feat: no results card
Warren-Pitterson Dec 2, 2025
c022e85
refactor: readonly props, refactor
Warren-Pitterson Dec 2, 2025
e1648d8
feat: No Result page added, Conditional display added to input Search…
Warren-Pitterson Dec 2, 2025
ee428fe
feat: error handling and redirect
Warren-Pitterson Dec 2, 2025
84ce999
Merge branch 'main' into feat/DTOSS-10947-UI-Search
Warren-Pitterson Dec 2, 2025
3b01ffc
chore: placeholder typo
Warren-Pitterson Dec 2, 2025
ac493eb
fix: updated test references
Warren-Pitterson Dec 2, 2025
a8495d4
feat: update tests and remove duplicate styling
Warren-Pitterson Dec 2, 2025
739d046
chore: sonarQube Issues
Warren-Pitterson Dec 2, 2025
ec7eb9b
feat: used standard component, updated scss
Warren-Pitterson Dec 4, 2025
daf3fda
feat: server side rendering on conditionalHeaderSearch Component
Warren-Pitterson Dec 5, 2025
910cf9e
chore: Sonaqube img fix
Warren-Pitterson Dec 5, 2025
2396f50
chore: styling
Warren-Pitterson Dec 5, 2025
e40f57e
feat: code tidy and refactor,
Warren-Pitterson Dec 9, 2025
1a18c64
refactor: GetExceptionsWithReportsByNhsNumber refactor, controller re…
Warren-Pitterson Dec 9, 2025
2fd0263
refactor: abstract methods
Warren-Pitterson Dec 9, 2025
799764b
test: unit tests added
Warren-Pitterson Dec 9, 2025
1add645
Merge branch 'main' into feat/DTOSS-10947-UI-Search
Warren-Pitterson Dec 9, 2025
a1bbbac
chore: encodeURIComponent
Warren-Pitterson Dec 9, 2025
f84bbfe
Merge branch 'feat/DTOSS-10947-UI-Search' of https://github.com/NHSDi…
Warren-Pitterson Dec 9, 2025
e173121
fix: removed wrong ExceptionCategory
Warren-Pitterson Dec 9, 2025
497e017
fix: type needed for all
Warren-Pitterson Dec 9, 2025
9dcac24
fix: NhsNumber logging removed
Warren-Pitterson Dec 11, 2025
4f7afd5
refactor: addressing PR comments, removed logging, changed return typ…
Warren-Pitterson Dec 11, 2025
b2d1f60
fix: mapping
Warren-Pitterson Dec 11, 2025
912b4da
fix: ISO string causing incorrect date to be parsed for some records
Warren-Pitterson Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Common;

using Microsoft.Azure.Functions.Worker.Http;
using Model.Pagination;

public interface IPaginationService<T>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Common;

using Microsoft.Azure.Functions.Worker.Http;
using Model.Pagination;

public class PaginationService<T> : IPaginationService<T>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
namespace Data.Database;

using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Common;
using Model;
using Model.DTO;
using Model.Enums;

public interface IValidationExceptionData
{
Task<bool> Create(ValidationException exception);
Task<List<ValidationException>?> GetFilteredExceptions(ExceptionStatus? exceptionStatus, SortOrder? sortOrder, ExceptionCategory exceptionCategory);
Task<List<ValidationException>> GetFilteredExceptions(ExceptionStatus? exceptionStatus, SortOrder? sortOrder, ExceptionCategory exceptionCategory);
Task<ValidationException?> GetExceptionById(int exceptionId);
Task<bool> RemoveOldException(string nhsNumber, string screeningName);
Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptionId, string serviceNowId);
Task<List<ValidationException>?> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory);
Task<List<ValidationException>> GetReportExceptions(DateTime? reportDate, ExceptionCategory exceptionCategory);
Task<List<ExceptionManagement>> GetByFilter(Expression<Func<ExceptionManagement, bool>> filter);
Task<ValidationExceptionsByNhsNumberResponse> GetExceptionsWithReportsByNhsNumber(string nhsNumber);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace Data.Database;

using System;
using System.Data;
using System.Linq.Expressions;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
Expand All @@ -17,17 +18,17 @@ public class ValidationExceptionData : IValidationExceptionData
{
private readonly ILogger<ValidationExceptionData> _logger;
private readonly IDataServiceClient<ExceptionManagement> _validationExceptionDataServiceClient;

public ValidationExceptionData(
ILogger<ValidationExceptionData> logger,
IDataServiceClient<ExceptionManagement> validationExceptionDataServiceClient,
IDataServiceClient<ParticipantDemographic> demographicDataServiceClient
IDataServiceClient<ExceptionManagement> validationExceptionDataServiceClient
)
{
_logger = logger;
_validationExceptionDataServiceClient = validationExceptionDataServiceClient;
}

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


public async Task<bool> Create(ValidationException exception)
{
var exceptionToUpdate = new ExceptionManagement().FromValidationException(exception);
return await _validationExceptionDataServiceClient.Add(exceptionToUpdate);
}

public async Task<bool> RemoveOldException(string nhsNumber, string screeningName)
{
var exceptions = await GetExceptionRecords(nhsNumber, screeningName);
Expand All @@ -63,7 +64,6 @@ public async Task<bool> RemoveOldException(string nhsNumber, string screeningNam
return false;
}

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

Expand Down Expand Up @@ -117,7 +117,7 @@ public async Task<ServiceResponseModel> UpdateExceptionServiceNowId(int exceptio
}
}

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

var results = filteredExceptions.Select(GetValidationExceptionWithDetails);
return results.Where(x => x != null).ToList()!;
return MapToValidationExceptions(filteredExceptions);
}

public async Task<List<ExceptionManagement>> GetByFilter(Expression<Func<ExceptionManagement, bool>> filter)
{
var result = await _validationExceptionDataServiceClient.GetByFilter(filter) ?? Enumerable.Empty<ExceptionManagement>();
return result.ToList();
}

public async Task<ValidationExceptionsByNhsNumberResponse> GetExceptionsWithReportsByNhsNumber(string nhsNumber)
{
var validationExceptions = await GetValidationExceptionsByNhsNumber(nhsNumber);

if (validationExceptions.Count == 0)
{
return new ValidationExceptionsByNhsNumberResponse
{
Exceptions = [],
Reports = [],
NhsNumber = nhsNumber
};
}

var reports = GenerateExceptionReports(validationExceptions);
return new ValidationExceptionsByNhsNumberResponse
{
Exceptions = validationExceptions,
Reports = reports,
NhsNumber = nhsNumber
};
}

private List<ValidationException> MapToValidationExceptions(IEnumerable<ExceptionManagement> exceptions)
{
return exceptions.Select(GetValidationExceptionWithDetails).Where(x => x != null).ToList()!;
}

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

private async Task<List<ExceptionManagement>?> GetExceptionRecords(string nhsNumber, string screeningName)
{

var exceptions = await _validationExceptionDataServiceClient.GetByFilter(x => x.NhsNumber == nhsNumber && x.ScreeningName == screeningName);
return exceptions?.ToList();

}

private static string DateToString(DateTime? datetime)
Expand Down Expand Up @@ -311,4 +342,39 @@ private static List<ValidationException> SortExceptions(SortOrder? sortOrder, IE
? [.. filteredList.OrderBy(dateProperty)]
: [.. filteredList.OrderByDescending(dateProperty)];
}



private async Task<List<ValidationException>> GetValidationExceptionsByNhsNumber(string nhsNumber)
{
var exceptions = await _validationExceptionDataServiceClient.GetByFilter(x => x.NhsNumber == nhsNumber);
if (exceptions == null || !exceptions.Any())
{
return [];
}

return [.. exceptions
.Select(GetValidationExceptionWithDetails)
.Where(x => x != null)
.Cast<ValidationException>()
.OrderByDescending(x => x.DateCreated)];
}

private static List<ValidationExceptionReport> GenerateExceptionReports(List<ValidationException> validationExceptions)
{
return [.. validationExceptions
.Where(x => x.Category.HasValue && (x.Category.Value == 12 || x.Category.Value == 13))
.GroupBy(x => new
{
Date = x.DateCreated?.Date ?? DateTime.Now.Date,
Category = x.Category
})
.Select(g => new ValidationExceptionReport
{
ReportDate = g.Key.Date,
Category = g.Key.Category,
ExceptionCount = g.Count()
})
.OrderByDescending(r => r.ReportDate)];
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace Common;
namespace Model.Pagination;

public class PaginationResult<T>
{
public IEnumerable<T> Items { get; set; }
public required IEnumerable<T> Items { get; set; }
public bool IsFirstPage { get; set; }
public bool HasNextPage { get; set; }
public bool HasPreviousPage { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Model.DTO;

using Model.Pagination;

/// <summary>
/// Response model for NHS number search containing paginated exceptions and associated reports
/// </summary>
public class ValidationExceptionsByNhsNumberResponse
{
public string NhsNumber { get; set; } = string.Empty;
public List<ValidationException> Exceptions { get; set; } = [];
public List<ValidationExceptionReport> Reports { get; set; } = [];
public PaginationResult<ValidationException> PaginatedExceptions { get; set; } = new() { Items = [] };
}

public class ValidationExceptionReport
{
public DateTime ReportDate { get; set; }
public int? Category { get; set; }
public int ExceptionCount { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ namespace NHS.CohortManager.ScreeningDataServices;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Model;
using Model.DTO;
using Model.Enums;
using Model.Pagination;

/// <summary>
/// Azure Function for retrieving and managing validation exceptions.
Expand Down Expand Up @@ -129,4 +131,53 @@ public async Task<HttpResponseData> UpdateExceptionServiceNowId([HttpTrigger(Aut
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req);
}
}

/// <summary>
/// Retrieves validation exceptions and reports for a specific NHS number.
/// </summary>
[Function(nameof(GetValidationExceptionsByNhsNumber))]
public async Task<HttpResponseData> GetValidationExceptionsByNhsNumber([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
{
var nhsNumber = req.Query["nhsNumber"];
var page = _httpParserHelper.GetQueryParameterAsInt(req, "page", 1);
var pageSize = _httpParserHelper.GetQueryParameterAsInt(req, "pageSize", 10);

if (string.IsNullOrWhiteSpace(nhsNumber))
{
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req, "NHS number is required.");
}

var cleanedNhsNumber = nhsNumber.Replace(" ", "");
if (!ValidationHelper.ValidateNHSNumber(cleanedNhsNumber))
{
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req, "Invalid NHS number format.");
}

try
{
var result = await _validationData.GetExceptionsWithReportsByNhsNumber(cleanedNhsNumber);

if (result.Exceptions.Count == 0)
{
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
}

var paginatedExceptions = _paginationService.GetPaginatedResult(result.Exceptions.AsQueryable(), page, pageSize);
var headers = _paginationService.AddNavigationHeaders(req, paginatedExceptions);

var response = new ValidationExceptionsByNhsNumberResponse
{
NhsNumber = result.NhsNumber,
PaginatedExceptions = paginatedExceptions,
Reports = result.Reports
};

return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, req, JsonSerializer.Serialize(response), headers);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving validation exceptions for provided NHS number");
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
interface ConditionalHeaderSearchProps {
readonly children: React.ReactNode;
readonly pathname: string;
}

export function ConditionalHeaderSearch({
children,
pathname,
}: ConditionalHeaderSearchProps) {
const isNoResultsPage = pathname === "/exceptions/noResults";

// Don't render the header search if we're on the no results page
if (isNoResultsPage) {
return null;
}

return <>{children}</>;
}
Loading
Loading