From f4935325d06fd993a50f5be3df858309fae79197 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:42:03 +0100 Subject: [PATCH 01/16] Remove Candidate and Candidate Identifier entities --- .../Interfaces/IApplicationDbContext.cs | 3 - .../Features/Candidates/DTOs/CandidateDto.cs | 32 ++++------ .../Search/CandidateSearchQueryHandler.cs | 29 --------- .../Commands/CreateParticipant.cs | 12 ++-- src/Domain/Entities/Candidates/Candidate.cs | 47 --------------- .../Entities/Participants/Participant.cs | 14 ++--- .../ValueObjects/CandidateIdentifier.cs | 36 ----------- .../Constants/Database/DatabaseSchema.cs | 2 - .../Persistence/ApplicationDbContext.cs | 2 - .../CandidatesEntityTypeConfiguration.cs | 60 ------------------- 10 files changed, 23 insertions(+), 214 deletions(-) delete mode 100644 src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryHandler.cs delete mode 100644 src/Domain/Entities/Candidates/Candidate.cs delete mode 100644 src/Domain/ValueObjects/CandidateIdentifier.cs delete mode 100644 src/Infrastructure/Persistence/Configurations/CandidatesEntityTypeConfiguration.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index f5569737..f399b7b8 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,5 +1,4 @@ using Cfo.Cats.Domain.Entities.Administration; -using Cfo.Cats.Domain.Entities.Candidates; using Cfo.Cats.Domain.Entities.Documents; using Cfo.Cats.Domain.Entities.Participants; using Cfo.Cats.Domain.Identity; @@ -23,8 +22,6 @@ public interface IApplicationDbContext public DbSet KeyValues { get; } - public DbSet Candidates { get; } - public DbSet ParticipantEnrolmentHistories { get; } public DbSet Users { get; } diff --git a/src/Application/Features/Candidates/DTOs/CandidateDto.cs b/src/Application/Features/Candidates/DTOs/CandidateDto.cs index 3e3607b6..bc547163 100644 --- a/src/Application/Features/Candidates/DTOs/CandidateDto.cs +++ b/src/Application/Features/Candidates/DTOs/CandidateDto.cs @@ -1,5 +1,4 @@ -using Cfo.Cats.Domain.Entities.Candidates; -using Cfo.Cats.Domain.Entities.Participants; +using System.Text.Json.Serialization; namespace Cfo.Cats.Application.Features.Candidates.DTOs; @@ -28,7 +27,15 @@ public class CandidateDto /// /// A collection of identifiers from external systems. /// - public string[] ExternalIdentifiers { get; set; } + public string[] ExternalIdentifiers { get; set; } = []; + + + /// + /// Indicates whether the candidate is marked as active in the data source(s). + /// + public bool IsActive { get; set; } + + public string Gender { get; set; } /// /// The location CATS thinks the user is registered at @@ -38,23 +45,4 @@ public class CandidateDto public EnrolmentStatus? EnrolmentStatus { get; set; } public string? ReferralSource { get; set; } - - private class Mapping : Profile - { - public Mapping() - { - CreateMap(MemberList.None) - .ForMember(candidateDto => candidateDto.Identifier, - options => options.MapFrom(candidate => candidate.Id)) - .ForMember(candidateDto => candidateDto.FirstName, - options => options.MapFrom(candidate => candidate.FirstName)) - .ForMember(candidateDto => candidateDto.LastName, - options => options.MapFrom(candidate => candidate.LastName)) - .ForMember(candidateDto => candidateDto.CurrentLocation, - options => options.MapFrom(candidate => candidate.CurrentLocation.Name)) - .ForMember(candidateDto => candidateDto.ExternalIdentifiers, - options => options.MapFrom(candidate => candidate.Identifiers.Select(p => p.IdentifierValue).ToArray())); - - } - } } diff --git a/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryHandler.cs b/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryHandler.cs deleted file mode 100644 index 4a2bc754..00000000 --- a/src/Application/Features/Candidates/Queries/Search/CandidateSearchQueryHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Cfo.Cats.Application.Features.Candidates.Caching; -using Cfo.Cats.Application.Features.Candidates.DTOs; - -namespace Cfo.Cats.Application.Features.Candidates.Queries.Search; - -public class CandidateSearchQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext dbContext; - private readonly IMapper mapper; - - public CandidateSearchQueryHandler(IApplicationDbContext dbContext, IMapper mapper) - { - this.dbContext = dbContext; - this.mapper = mapper; - } - - public async Task> Handle(CandidateSearchQuery request, CancellationToken cancellationToken) - { - var query = from c in dbContext.Candidates - where - c.LastName == request.LastName && c.DateOfBirth == request.DateOfBirth - || c.Identifiers.Any(i => i.IdentifierValue == request.ExternalIdentifier) - select c; - - - return await query.ProjectTo(mapper.ConfigurationProvider).ToArrayAsync(cancellationToken); - - } -} \ No newline at end of file diff --git a/src/Application/Features/Participants/Commands/CreateParticipant.cs b/src/Application/Features/Participants/Commands/CreateParticipant.cs index a6dd4fc7..9a6b4735 100644 --- a/src/Application/Features/Participants/Commands/CreateParticipant.cs +++ b/src/Application/Features/Participants/Commands/CreateParticipant.cs @@ -1,7 +1,7 @@ using Cfo.Cats.Application.Common.Security; +using Cfo.Cats.Application.Features.Candidates.DTOs; using Cfo.Cats.Application.Features.Participants.Caching; using Cfo.Cats.Application.SecurityConstants; -using Cfo.Cats.Domain.Entities.Candidates; using Cfo.Cats.Domain.Entities.Participants; namespace Cfo.Cats.Application.Features.Participants.Commands; @@ -14,14 +14,16 @@ public class Command: ICacheInvalidatorRequest> /// /// The CATS identifier /// - public string? Identifier { get; set; } + public string? Identifier => Candidate.Identifier; + + public required CandidateDto Candidate { get; set; } public string? ReferralSource { get; set; } public string? ReferralComments { get; set; } public UserProfile? CurrentUser { get; set; } - + public string CacheKey => ParticipantCacheKey.GetCacheKey($"{this}"); public CancellationTokenSource? SharedExpiryTokenSource @@ -38,8 +40,8 @@ public class Handler(IApplicationDbContext dbContext, ICurrentUserService curren { public async Task> Handle(Command request, CancellationToken cancellationToken) { - Candidate candidate = await dbContext.Candidates.FirstAsync(c => c.Id == request.Identifier, cancellationToken); - Participant participant = Participant.CreateFrom(candidate, request.ReferralSource!, request.ReferralComments); + var candidate = request.Candidate; + Participant participant = Participant.CreateFrom(candidate.Identifier, candidate.FirstName, candidate.LastName, candidate.DateOfBirth, request.ReferralSource!, request.ReferralComments); participant.AssignTo(currentUserService.UserId); dbContext.Participants.Add(participant); diff --git a/src/Domain/Entities/Candidates/Candidate.cs b/src/Domain/Entities/Candidates/Candidate.cs deleted file mode 100644 index 33c8fcca..00000000 --- a/src/Domain/Entities/Candidates/Candidate.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; -using Cfo.Cats.Domain.Common.Entities; -using Cfo.Cats.Domain.Entities.Administration; -using Cfo.Cats.Domain.Entities.Participants; -using Cfo.Cats.Domain.ValueObjects; - -namespace Cfo.Cats.Domain.Entities.Candidates; - -public class Candidate : BaseAuditableEntity -{ - private Candidate() - { - } - - private List _identifiers = new(); - - public string FirstName { get; private set; } - - public string? MiddleName { get; private set; } - public string LastName { get; private set; } - public DateTime DateOfBirth { get; private set; } - public int CurrentLocationId { get; private set; } - - public Location CurrentLocation { get; private set; } - - public IReadOnlyCollection Identifiers => _identifiers.AsReadOnly(); - - public void AddIdentifier(CandidateIdentifier identifier) - { - if (_identifiers.Any(id => id.Equals(identifier))) - { - throw new InvalidOperationException("The identifier already exists for this candidate."); - } - - _identifiers.Add(identifier); - } - - public void RemoveIdentifier(CandidateIdentifier identifier) - { - if (!_identifiers.Remove(identifier)) - { - throw new InvalidOperationException("The identifier does not exist for this candidate."); - } - } - -} - diff --git a/src/Domain/Entities/Participants/Participant.cs b/src/Domain/Entities/Participants/Participant.cs index 13fc5eb5..69b9e008 100644 --- a/src/Domain/Entities/Participants/Participant.cs +++ b/src/Domain/Entities/Participants/Participant.cs @@ -8,8 +8,6 @@ using Cfo.Cats.Domain.Common.Enums; using Cfo.Cats.Domain.Common.Exceptions; using Cfo.Cats.Domain.Entities.Administration; -using Cfo.Cats.Domain.Entities.Candidates; -using Cfo.Cats.Domain.Entities.Documents; using Cfo.Cats.Domain.Events; namespace Cfo.Cats.Domain.Entities.Participants; @@ -29,19 +27,19 @@ private Participant() } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public static Participant CreateFrom(Candidate candidate, string referralSource, string? referralComments) + public static Participant CreateFrom(string id, string firstName, string lastName, DateTime dateOfBirth, string referralSource, string? referralComments) { Participant p = new Participant { ConsentStatus = ConsentStatus.PendingStatus, EnrolmentStatus = EnrolmentStatus.PendingStatus, - Id = candidate.Id, - DateOfBirth = DateOnly.FromDateTime(candidate.DateOfBirth), - FirstName = candidate.FirstName, - LastName = candidate.LastName, + Id = id, + DateOfBirth = DateOnly.FromDateTime(dateOfBirth), + FirstName = firstName, + LastName = lastName, ReferralSource = referralSource, ReferralComments = referralComments, - _currentLocationId = candidate.CurrentLocationId + _currentLocationId = 1 }; p.AddDomainEvent(new ParticipantCreatedDomainEvent(p)); diff --git a/src/Domain/ValueObjects/CandidateIdentifier.cs b/src/Domain/ValueObjects/CandidateIdentifier.cs deleted file mode 100644 index 9db85714..00000000 --- a/src/Domain/ValueObjects/CandidateIdentifier.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Cfo.Cats.Domain.ValueObjects; - -public class CandidateIdentifier : ValueObject -{ - public string IdentifierType { get; } - public string IdentifierValue { get; } - - // required for EF Core -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - private CandidateIdentifier() - { - } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - - public CandidateIdentifier(string identifierType, string identifierValue) - { - if (string.IsNullOrEmpty(identifierType)) - { - throw new ArgumentException("Identifier Type cannot be null or empty"); - } - - if (string.IsNullOrEmpty(identifierValue)) - { - throw new ArgumentException("Identifier Value cannot be null or empty"); - } - - IdentifierType = identifierType; - IdentifierValue = identifierValue; - } - - protected override IEnumerable GetEqualityComponents() - { - yield return IdentifierType; - yield return IdentifierValue; - } -} \ No newline at end of file diff --git a/src/Infrastructure/Constants/Database/DatabaseSchema.cs b/src/Infrastructure/Constants/Database/DatabaseSchema.cs index e3432f6c..b8a135bb 100644 --- a/src/Infrastructure/Constants/Database/DatabaseSchema.cs +++ b/src/Infrastructure/Constants/Database/DatabaseSchema.cs @@ -14,8 +14,6 @@ public static class Tables public const string ApplicationUserToken = nameof(ApplicationUserToken); public const string ApplicationUserRole = nameof(ApplicationUserRole); public const string AuditTrail = nameof(AuditTrail); - public const string Candidate = nameof(Candidate); - public const string CandidateIdentifier = nameof(CandidateIdentifier); public const string Consent = nameof(Consent); public const string Contract = nameof(Contract); public const string Document = nameof(Document); diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index c221babb..b1f80ce9 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,7 +1,6 @@ using System.Reflection; using Cfo.Cats.Domain.Common.Contracts; using Cfo.Cats.Domain.Entities.Administration; -using Cfo.Cats.Domain.Entities.Candidates; using Cfo.Cats.Domain.Entities.Documents; using Cfo.Cats.Domain.Entities.Participants; using Cfo.Cats.Domain.Identity; @@ -36,7 +35,6 @@ public ApplicationDbContext(DbContextOptions options) public DbSet Documents => Set(); public DbSet Participants => Set(); public DbSet KeyValues => Set(); - public DbSet Candidates => Set(); public DbSet ParticipantEnrolmentHistories => Set(); public DbSet Locations => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/CandidatesEntityTypeConfiguration.cs b/src/Infrastructure/Persistence/Configurations/CandidatesEntityTypeConfiguration.cs deleted file mode 100644 index 680d10fb..00000000 --- a/src/Infrastructure/Persistence/Configurations/CandidatesEntityTypeConfiguration.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Cfo.Cats.Domain.Entities.Candidates; -using Cfo.Cats.Infrastructure.Constants.Database; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Cfo.Cats.Infrastructure.Persistence.Configurations; - -public class CandidatesEntityTypeConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable(DatabaseSchema.Tables.Candidate); - - builder.HasKey(c => c.Id); - - builder.Property(c => c.Id) - .HasMaxLength(9) - .ValueGeneratedNever(); - - builder.Property(p => p.FirstName) - .IsRequired() - .HasMaxLength(50); - - builder.Property(p => p.MiddleName) - .IsRequired(false) - .HasMaxLength(50); - - builder.Property(p => p.LastName) - .IsRequired() - .HasMaxLength(50); - - builder.Property(p => p.DateOfBirth) - .IsRequired(); - - builder.HasOne(p => p.CurrentLocation) - .WithMany() - .HasForeignKey(p => p.CurrentLocationId); - - builder.OwnsMany(c => c.Identifiers, a => { - a.WithOwner() - .HasForeignKey("CandidateId"); - - a.ToTable(DatabaseSchema.Tables.CandidateIdentifier); - - a.Property("Id"); - a.HasKey("Id"); - - a.Property(i => i.IdentifierValue) - .IsRequired() - .HasMaxLength(20); - - a.Property(i => i.IdentifierType) - .IsRequired() - .HasMaxLength(20); - - - }); - } - - -} From edea8a283cb20e8613dd01f515585720e68d4240 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:43:41 +0100 Subject: [PATCH 02/16] Candidate DOB is required, ensure non-null values --- .../Features/Candidates/CandidateFluentValidator.cs | 4 ++-- src/Application/Features/Candidates/DTOs/CandidateDto.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Application/Features/Candidates/CandidateFluentValidator.cs b/src/Application/Features/Candidates/CandidateFluentValidator.cs index ac0b950e..d5240bb3 100644 --- a/src/Application/Features/Candidates/CandidateFluentValidator.cs +++ b/src/Application/Features/Candidates/CandidateFluentValidator.cs @@ -39,10 +39,10 @@ public CandidateFluentValidator() return result.Errors.Select(e => e.ErrorMessage); }; - private bool BeValidAge(DateTime? date) + private bool BeValidAge(DateTime date) { var minimumAge = DateTime.Now.Date.AddYears(-18); - return date is not null && (minimumAge < date) == false; + return (minimumAge < date) == false; } } \ No newline at end of file diff --git a/src/Application/Features/Candidates/DTOs/CandidateDto.cs b/src/Application/Features/Candidates/DTOs/CandidateDto.cs index bc547163..9d20b5aa 100644 --- a/src/Application/Features/Candidates/DTOs/CandidateDto.cs +++ b/src/Application/Features/Candidates/DTOs/CandidateDto.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Cfo.Cats.Application.Features.Candidates.DTOs; @@ -22,7 +22,7 @@ public class CandidateDto /// /// The candidates date of birth /// - public DateTime? DateOfBirth { get; set; } + public DateTime DateOfBirth { get; set; } /// /// A collection of identifiers from external systems. From 249d4da7e62a9b1803e775e946e55a1c3e8d6899 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:44:46 +0100 Subject: [PATCH 03/16] Candidate Service --- src/Server.UI/DependencyInjection.cs | 8 ++++++ .../Services/Candidate/CandidateService.cs | 26 +++++++++++++++++++ src/Server.UI/appsettings.json | 6 ++++- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/Server.UI/Services/Candidate/CandidateService.cs diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index f20e136a..5f53e373 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -21,6 +21,8 @@ using Toolbelt.Blazor.Extensions.DependencyInjection; using ActualLab.Fusion.Extensions; using Cfo.Cats.Server.UI.Middlewares; +using Cfo.Cats.Server.UI.Services.Candidate; +using Cfo.Cats.Infrastructure; namespace Cfo.Cats.Server.UI; @@ -104,6 +106,12 @@ public static IServiceCollection AddServerUi(this IServiceCollection services, I return service; }); + services.AddHttpClient((provider, client) => + { + client.DefaultRequestHeaders.Add("X-API-KEY", config.GetRequiredValue("DMS:ApiKey")); + client.BaseAddress = new Uri(config.GetRequiredValue("DMS:ApplicationUrl")); + }); + services.Configure(options => { options.ForwardedHeaders = diff --git a/src/Server.UI/Services/Candidate/CandidateService.cs b/src/Server.UI/Services/Candidate/CandidateService.cs new file mode 100644 index 00000000..f9a11b0f --- /dev/null +++ b/src/Server.UI/Services/Candidate/CandidateService.cs @@ -0,0 +1,26 @@ +using Cfo.Cats.Application.Features.Candidates.DTOs; +using Cfo.Cats.Application.Features.Candidates.Queries.Search; + +namespace Cfo.Cats.Server.UI.Services.Candidate; + +public class CandidateService(HttpClient client) +{ + public async Task?> SearchAsync(CandidateSearchQuery searchQuery) + { + var queryParams = new[] + { + $"identifier={searchQuery.ExternalIdentifier}", + $"lastName={searchQuery.LastName}", + $"dateOfBirth={searchQuery.DateOfBirth.Value.ToString("yyyy-MM-dd")}" + }; + + return await client.GetFromJsonAsync>($"/search?{string.Join('&', queryParams)}"); + } + + public async Task GetByUpciAsync(string upci) + { + return await client.GetFromJsonAsync($"clustering/{upci}/Aggregate"); + } +} + +public record SearchResult(string Upci, int Precedence); \ No newline at end of file diff --git a/src/Server.UI/appsettings.json b/src/Server.UI/appsettings.json index 5d268847..90748869 100644 --- a/src/Server.UI/appsettings.json +++ b/src/Server.UI/appsettings.json @@ -51,9 +51,13 @@ }, "AllowlistOptions": { "AllowedIPs": [ - + ] }, + "DMS": { + "ApplicationUrl": "", + "ApiKey": "" + }, "AWS": { "Region": "", "AccessKey": "", From 5928d9a041488f7ffa8061134e54c8fb93c9ad6c Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:45:47 +0100 Subject: [PATCH 04/16] Cleanup unused candidate/search pages --- .../CandidateSearch/CandidateSearch.razor | 14 --- .../CandidateSearch/CandidateSearch.razor.cs | 99 ------------------- .../Enrolments/Search/CandidateFinder.razor | 76 -------------- .../Enrolments/Search/CandidateSearch.razor | 42 -------- .../Search/CandidateSearch.razor.cs | 55 ----------- .../Enrolments/Search/CandidateView.razor | 49 --------- src/Server.UI/Server.UI.csproj | 66 ++++++------- 7 files changed, 31 insertions(+), 370 deletions(-) delete mode 100644 src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor delete mode 100644 src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor.cs delete mode 100644 src/Server.UI/Pages/Enrolments/Search/CandidateFinder.razor delete mode 100644 src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor delete mode 100644 src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor.cs delete mode 100644 src/Server.UI/Pages/Enrolments/Search/CandidateView.razor diff --git a/src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor b/src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor deleted file mode 100644 index 1be897a5..00000000 --- a/src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor +++ /dev/null @@ -1,14 +0,0 @@ -@page "/pages/enrolments" -@page "/pages/enrolments/search" - - - - - Find a person to enrol - - - - - - - diff --git a/src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor.cs b/src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor.cs deleted file mode 100644 index 62552873..00000000 --- a/src/Server.UI/Pages/Enrolments/CandidateSearch/CandidateSearch.razor.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Cfo.Cats.Application.Features.Candidates.DTOs; -using Cfo.Cats.Application.Features.Participants.Commands; -using Cfo.Cats.Server.UI.Pages.Enrolments.Search; - -namespace Cfo.Cats.Server.UI.Pages.Enrolments.CandidateSearch; -public partial class CandidateSearch -{ - public List Reasons { get; set; } = []; - - private CandidateDto? Candidate { get; set; } - - public void ClearErrors() => Reasons = []; - - public async Task CandidateFound(CandidateDto candidate) - { - Candidate = candidate; - - if (Candidate is null) - { - return; - } - - var participant = await Mediator.Send(new CreateParticipant.Command() { Identifier = ""}); - - //await CaseService!.AddDummyCase(Candidate.Identifier); - Snackbar.Add("Enrolment started!", Severity.Success); - } - - public void OnUpdate(List reasons) - { - Reasons = reasons.ToList(); - - if (reasons is []) - { - selectedType = typeof(CandidateFinder); - } - else - { - selectedType = typeof(Error); - } - StateHasChanged(); - } - - public void NavigateToEnrolment(string participantId) - => Navigation.NavigateTo($"/Enrolments/{participantId}"); - - private Type selectedType = typeof(CandidateFinder); - - - private Dictionary Components => - new() - { - { - nameof(CandidateFinder), - new ComponentMetaData - { - Name = nameof(CandidateFinder), - Parameters = new() - { - { - "OnCandidateFound", - EventCallback.Factory.Create(this, CandidateFound) - }, - { - "OnUpdate", - EventCallback.Factory.Create>(this, OnUpdate) - } - } - } - }, - { - nameof(Success), - new ComponentMetaData - { - Name = nameof(Success), - Parameters = new() - { - { "ParticipantId", Candidate?.Identifier ?? string.Empty }, - { "Successes", Reasons.OfType().ToArray() } - } - } - }, - { - nameof(Error), - new ComponentMetaData - { - Name = nameof(Error), - Parameters = new() { { "Errors", Reasons.ToArray() } } - } - } - }; - - - public class ComponentMetaData - { - public required string Name { get; set; } - public Dictionary Parameters { get; set; } = []; - } -} diff --git a/src/Server.UI/Pages/Enrolments/Search/CandidateFinder.razor b/src/Server.UI/Pages/Enrolments/Search/CandidateFinder.razor deleted file mode 100644 index 5bad0bf5..00000000 --- a/src/Server.UI/Pages/Enrolments/Search/CandidateFinder.razor +++ /dev/null @@ -1,76 +0,0 @@ -@using System.Collections.Immutable -@using Cfo.Cats.Application.Features.Candidates.DTOs -@using MudExtensions - - - - - - - - - - - Search - - - - @if (MatchFound) - { - - - - } - - -@code { - public bool IsLoading { get; set; } = false; - - [Parameter] - public EventCallback OnCandidateFound { get; set; } - - [Parameter] - public EventCallback> OnUpdate { get; set; } - - private CandidateSearch? search; - - private CandidateDto[]? response; - - public bool MatchFound => response is { Length: 1 }; - - public async Task Search() - { - IsLoading = true; - - search?.form?.Validate(); - - if(search?.form?.IsValid is false) - { - IsLoading = false; - return; - } - - var result = await search?.Submit()!; - - response ??= result; - - //todo: handle too many or not enough - - StateHasChanged(); - search?.form?.Validate(); - - IsLoading = false; - } - - public async Task Enrol() - { - await OnUpdate.InvokeAsync([]); - await OnCandidateFound.InvokeAsync(response!.First()); - } - - private void Reset() - { - response = null; - } - -} diff --git a/src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor b/src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor deleted file mode 100644 index 216bc717..00000000 --- a/src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor +++ /dev/null @@ -1,42 +0,0 @@ -@using MudBlazor.Extensions - - - - @* - if/else: not ideal, but is essential here for the two-stage validation. - the first stage relies on the form-level validation, and performs a fluent validation on all inputs. - the second stage uses field-level validation, ensuring the candidate that is shown matches the input criteria. - if/else is necessary here as you cannot conditionally render the [Validation] attribute as it nukes validation. - *@ - @if (HasResult) - { - - - - - - - - - - - - - } - else - { - - - - - - - - - - - - - } - - \ No newline at end of file diff --git a/src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor.cs b/src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor.cs deleted file mode 100644 index c6619dd7..00000000 --- a/src/Server.UI/Pages/Enrolments/Search/CandidateSearch.razor.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Cfo.Cats.Application.Common.Security; -using Cfo.Cats.Application.Features.Candidates; -using Cfo.Cats.Application.Features.Candidates.DTOs; -using Cfo.Cats.Application.Features.Candidates.Queries.Search; -using Faker; - -namespace Cfo.Cats.Server.UI.Pages.Enrolments.Search; - -public partial class CandidateSearch : ComponentBase -{ - - [CascadingParameter] - public Result? CandidateResult { get; set; } - - [CascadingParameter] - private UserProfile? UserProfile { get; set; } - - public bool HasResult => CandidateResult != null; - - public MudForm? form { get; set; } - - CandidateFluentValidator candidateValidator = new(); - - public CandidateDto Criteria { get; set; } = new() - { - Identifier = Identification.UkNationalInsuranceNumber(), - FirstName = "John", - LastName = "Doe", - DateOfBirth = DateTime.Now.AddYears(-18).AddDays(-1), - }; - - - public async Task Submit() - { - CandidateSearchQuery query = new CandidateSearchQuery() - { - CurrentUser = UserProfile!, - FirstName = Criteria.FirstName, - LastName = Criteria.LastName, - ExternalIdentifier = Criteria.Identifier, - DateOfBirth = Criteria.DateOfBirth!.Value - }; - - - var result = await Mediator.Send(query); // CandidateService.Find(Criteria.Identifier, Criteria.FirstName, Criteria.LastName, Criteria.DateOfBirth); - return result.ToArray(); - } - - public Func ValidateValue(string? value) => (model) => - { - var valid = value?.Equals(model, StringComparison.OrdinalIgnoreCase) ?? false; - return valid ? null : "A partial match was found"; - }; - -} diff --git a/src/Server.UI/Pages/Enrolments/Search/CandidateView.razor b/src/Server.UI/Pages/Enrolments/Search/CandidateView.razor deleted file mode 100644 index f855e2a1..00000000 --- a/src/Server.UI/Pages/Enrolments/Search/CandidateView.razor +++ /dev/null @@ -1,49 +0,0 @@ - -@* - - We couldn't find an exact match. Did you intend to search for this candidate? - -*@ - - - - - - JD - - - - John Doe (NMS01234) - - 01/01/1998 - - - - - - - - HMP Risley (Prison) - - - - - - Select Person - Cancel - - - - - -@code { - public bool Confirmation { get; set; } - - [Parameter] - public EventCallback OnCancel { get; set; } - - [Parameter] - public EventCallback OnEnrol { get; set; } - - -} \ No newline at end of file diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index 06a23afe..9968de61 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -15,9 +15,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,33 +27,33 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + ResXFileCodeGenerator Dashboard.Designer.cs - + @@ -61,36 +61,32 @@ True Dashboard.resx - + - + - + - <_ContentIncludedByDefault Remove="Pages\Assessment\Assessment.razor"/> - <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentComponents\AssessmentCheckbox.razor"/> - <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentComponents\AssessmentQuestionBase.razor"/> - <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentPathway.razor"/> - <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentQuestions\AssessmentMultipleChoiceQuestion.razor"/> - <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentQuestions\AssessmentToggleQuestion.razor"/> - <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentResultStep.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\CandidateSearch\CandidateSearch.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\CandidateSearch\Error.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\CandidateSearch\Success.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\Consent.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\Demographics.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\EnrolmentContainer.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\Location.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\RightToWork.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Enrolment.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Search\CandidateFinder.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Search\CandidateSearch.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\Search\CandidateView.razor"/> - <_ContentIncludedByDefault Remove="Pages\Enrolments\_Imports.razor"/> + <_ContentIncludedByDefault Remove="Pages\Assessment\Assessment.razor" /> + <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentComponents\AssessmentCheckbox.razor" /> + <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentComponents\AssessmentQuestionBase.razor" /> + <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentPathway.razor" /> + <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentQuestions\AssessmentMultipleChoiceQuestion.razor" /> + <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentQuestions\AssessmentToggleQuestion.razor" /> + <_ContentIncludedByDefault Remove="Pages\Assessment\AssessmentResultStep.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\CandidateSearch\Error.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\CandidateSearch\Success.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\Consent.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\Demographics.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\EnrolmentContainer.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\Location.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\Components\RightToWork.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\Enrolment.razor" /> + <_ContentIncludedByDefault Remove="Pages\Enrolments\_Imports.razor" /> From a44e910d334df404421ef1302ed300b1ec364d99 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:46:45 +0100 Subject: [PATCH 05/16] Amend behaviour tests for new DTO configuration --- .../Common/Behaviours/RequestLoggerTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs b/test/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs index 68576116..8b26cdc5 100644 --- a/test/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs +++ b/test/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Cfo.Cats.Application.Common.Interfaces; using Cfo.Cats.Application.Common.Interfaces.Identity; +using Cfo.Cats.Application.Features.Candidates.DTOs; using Cfo.Cats.Application.Features.Participants.Commands; using Cfo.Cats.Application.Pipeline.PreProcessors; using Microsoft.Extensions.Logging; @@ -23,7 +24,7 @@ public async Task ShouldCallGetUserNameAsyncOnceIfAuthenticated() currentUserService.Setup(x => x.UserId).Returns("Administrator"); var requestLogger = new LoggingPreProcessor(logger.Object, currentUserService.Object); await requestLogger.Process( - new CreateParticipant.Command { Identifier = "aABBB" }, + new CreateParticipant.Command { Candidate = new CandidateDto { Identifier = "aABBB" } }, new CancellationToken()); currentUserService.Verify(i => i.UserName, Times.Once); } @@ -33,7 +34,7 @@ public async Task ShouldNotCallGetUserNameAsyncOnceIfUnauthenticated() { var requestLogger = new LoggingPreProcessor(logger.Object, currentUserService.Object); await requestLogger.Process( - new CreateParticipant.Command { Identifier = "aABBB" } , + new CreateParticipant.Command { Candidate = new CandidateDto { Identifier = "aABBB" } }, new CancellationToken()); identityService.Verify(i => i.GetUserNameAsync(It.IsAny(), CancellationToken.None), Times.Never); } From ae740cf97d3a86ab0e423b882217de3ad9e64be0 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:51:30 +0100 Subject: [PATCH 06/16] Amends to candidate pages to use new candidates service --- .../Participants/Queries/ExistsById.cs | 34 +++++++ .../Components/AlreadyEnrolled.razor | 6 +- .../Components/CandidateFinderComponent.razor | 17 ++-- .../CandidateResultsComponent.razor | 85 +++++++++++++----- .../Candidates/Components/MatchFound.razor | 88 +++---------------- .../Candidates/Components/MatchFound.razor.cs | 81 +++++++++++++++++ ...ts.razor => MoreInformationRequired.razor} | 10 ++- src/Server.UI/Pages/Candidates/Search.razor | 23 ++--- 8 files changed, 220 insertions(+), 124 deletions(-) create mode 100644 src/Application/Features/Participants/Queries/ExistsById.cs create mode 100644 src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs rename src/Server.UI/Pages/Candidates/Components/{NoResults.razor => MoreInformationRequired.razor} (65%) diff --git a/src/Application/Features/Participants/Queries/ExistsById.cs b/src/Application/Features/Participants/Queries/ExistsById.cs new file mode 100644 index 00000000..d9dcbbc9 --- /dev/null +++ b/src/Application/Features/Participants/Queries/ExistsById.cs @@ -0,0 +1,34 @@ +using Cfo.Cats.Application.Common.Security; +using Cfo.Cats.Application.Features.Participants.Caching; +using Cfo.Cats.Application.SecurityConstants; + +namespace Cfo.Cats.Application.Features.Participants.Queries; + +public static class CheckParticipantExistsById +{ + [RequestAuthorize(Policy = PolicyNames.AllowEnrol)] + public class Query : ICacheableRequest + { + public required string Id { get; set; } + public string CacheKey => ParticipantCacheKey.GetCacheKey($"{Id}"); + public MemoryCacheEntryOptions? Options => ParticipantCacheKey.MemoryCacheEntryOptions; + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public Handler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + return await _context.Participants + .AnyAsync(p => p.Id == request.Id, cancellationToken); + } + } + +} + diff --git a/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor b/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor index 520f96b4..b1138ab9 100644 --- a/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor +++ b/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor @@ -1,10 +1,9 @@ -@using Cfo.Cats.Application.Features.Candidates.DTOs @using Humanizer - The candidate we have found is already enrolled. ( @Candidate.EnrolmentStatus!.Name.Humanize() ) + The candidate we have found is already enrolled. @@ -15,9 +14,6 @@ @code { - [Parameter] - public CandidateDto Candidate { get; set; } - [Parameter] public EventCallback OnCanceled { get; set; } diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor index 1b3e88e8..3d2ac559 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor @@ -2,7 +2,7 @@ @using Cfo.Cats.Application.Features.Candidates @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search -@using Cfo.Cats.Server.UI.Pages.Enrolments.Search +@using Cfo.Cats.Server.UI.Services.Candidate @using Faker @@ -33,18 +33,21 @@ @code { private MudForm? _form; - + + [Inject] + public CandidateService CandidateService { get; set; } + [EditorRequired] [Parameter] - public EventCallback OnCandidatesFound { get; set; } + public EventCallback OnSearch { get; set; } [EditorRequired] [Parameter] public CandidateSearchQuery? Query { get; set; } - + [CascadingParameter] private UserProfile? UserProfile { get; set; } - + CandidateFluentValidator candidateValidator = new(); public async Task Search() @@ -52,8 +55,8 @@ await _form!.Validate().ConfigureAwait(false); if (_form.IsValid) { - var result = await Mediator.Send(Query!); - await OnCandidatesFound.InvokeAsync(result.ToArray()); + var result = await CandidateService.SearchAsync(Query); + await OnSearch.InvokeAsync(result?.ToArray() ?? []); } } diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor index f898593b..e63082a1 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor @@ -1,5 +1,7 @@ @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search +@using Cfo.Cats.Application.Features.Participants.Queries +@using Cfo.Cats.Server.UI.Services.Candidate @if (_selectedType is not null) { @@ -9,21 +11,21 @@ @code { private Type? _selectedType = null; - private IDictionary? _selectedParameters; + private Dictionary _selectedParameters = []; private List? _comparisons; - + private bool Confirmation { get; set; } [Parameter, EditorRequired] public EventCallback OnCancelled { get; set; } [Parameter, EditorRequired] - public EventCallback OnCandidateEnrolled { get; set; } + public EventCallback OnCandidateEnrolled { get; set; } - [Parameter, EditorRequired] public CandidateDto[] SearchResults { get; set; } = default!; + [Parameter, EditorRequired] public SearchResult[] SearchResults { get; set; } = default!; [Parameter, EditorRequired] public CandidateSearchQuery Query { get; set; } = default!; - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { _selectedParameters = new Dictionary() { @@ -32,35 +34,76 @@ } }; - if (SearchResults is { Length: 0 }) + if (SearchReturnedNoCandidates()) + { + ShowMoreInformationRequired(); + } + else if (SearchReturnedMultipleAmbiguousCandidates()) { - _selectedType = typeof(NoResults); + ShowTooManyResults(); } - else if (SearchResults is { Length: > 1 }) + else if (SearchReturnedIdenticalCandidate()) { - _selectedType = typeof(TooManyResults); + await EnrolCandidateIfNotAlreadyEnrolled(); } - else if (SearchResults is - [ - { EnrolmentStatus: not null } - ]) + else if (SearchReturnedSingleCandidate()) { - _selectedParameters.Add("Candidate", SearchResults[0]); - _selectedType = typeof(AlreadyEnrolled); + if(CandidateMeetsMinimumPrecedence(10)) + { + await EnrolCandidateIfNotAlreadyEnrolled(); + } + else + { + ShowMoreInformationRequired(); + } + } + } + + bool SearchReturnedNoCandidates() => SearchResults is { Length: 0 }; + bool SearchReturnedSingleCandidate() => SearchResults is { Length: 1 }; + bool SearchReturnedIdenticalCandidate() => SearchResults.Any(result => result.Precedence is 1); + bool SearchReturnedMultipleAmbiguousCandidates() => SearchResults is { Length: > 1 } && SearchReturnedIdenticalCandidate() is false; + + void ShowCandidateFound() + { + _selectedParameters.Add("Id", Candidate.Upci); + _selectedParameters.Add("Query", Query); + _selectedParameters.Add("OnParticipantEnrolled", EventCallback.Factory.Create(this, OnParticipantEnrolledHandler)); + _selectedType = typeof(MatchFound); + } + + void ShowCandidateAlreadyEnrolled() => _selectedType = typeof(AlreadyEnrolled); + void ShowMoreInformationRequired() => _selectedType = typeof(MoreInformationRequired); + void ShowTooManyResults() => _selectedType = typeof(TooManyResults); + + async Task EnrolCandidateIfNotAlreadyEnrolled() + { + var result = SearchResults + .OrderBy(candidate => candidate.Precedence) + .First(); + + var enrolled = await Mediator.Send(new CheckParticipantExistsById.Query + { + Id = result.Upci + }); + + if (enrolled) + { + ShowCandidateAlreadyEnrolled(); } else { - _selectedParameters.Add("Candidate", SearchResults[0]); - _selectedParameters.Add("Query", Query); - _selectedParameters.Add("OnParticipantEnrolled", EventCallback.Factory.Create(this, OnParticipantEnrolledHandler)); - _selectedType = typeof(MatchFound); + ShowCandidateFound(); } } + bool CandidateMeetsMinimumPrecedence(int precedence) => Candidate.Precedence <= precedence; + + SearchResult Candidate => SearchResults.OrderBy(candidate => candidate.Precedence).First(); + private Task HandleOnCancelledClick() => OnCancelled.InvokeAsync(); - private Task OnParticipantEnrolledHandler() - => OnCandidateEnrolled.InvokeAsync(); + => OnCandidateEnrolled.InvokeAsync(Candidate.Upci); } \ No newline at end of file diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor index eb88618a..5c3d502c 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor @@ -1,11 +1,14 @@ @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search @using Cfo.Cats.Application.Features.Participants.Commands +@using Cfo.Cats.Server.UI.Services.Candidate - +@if(candidate is not null) +{ + - We have found a match (@Candidate.FirstName @Candidate.LastName (@Candidate.Identifier) - We have them located at @Candidate.CurrentLocation + We have found a match (@candidate.FirstName @candidate.LastName (@candidate.Identifier) + We have them located at @candidate.CurrentLocation @@ -15,8 +18,8 @@ @foreach (var item in PicklistService.DataSource - .Where(c => c.Name == Picklist.ReferralSource) - .OrderBy(c => c.Text)) + .Where(c => c.Name == Picklist.ReferralSource) + .OrderBy(c => c.Text)) { @item.Text } @@ -25,7 +28,7 @@ + RequiredError="Comments must be entered for this referral type"/> @@ -37,78 +40,11 @@ Back to Search + OnClick="EnrolCandidate" + Disabled="@(_confirmation == false)"> Enrol Candidate - - -@code -{ - - [Inject] - private IPicklistService PicklistService { get; set; } - - private MudForm? _form; - - private CreateParticipant.Command? Model; - - private bool _confirmation = false; - private List? _comparisons; - - [Parameter] - public CandidateDto Candidate { get; set; } = default!; - - [CascadingParameter] - public UserProfile? UserProfile { get; set; } - - [Parameter] - public CandidateSearchQuery Query { get; set; } = default!; - - [Parameter] - public EventCallback OnCanceled { get; set; } - - [Parameter] - public EventCallback OnParticipantEnrolled { get; set; } - - protected override void OnParametersSet() - { - _comparisons = new List - { - new ("First Name", Query.FirstName, Candidate.FirstName), - new ("Last Name", Query.LastName, Candidate.LastName), - new("Date Of Birth", Query.DateOfBirth.GetValueOrDefault().ToShortDateString(), - Candidate.DateOfBirth.GetValueOrDefault().ToShortDateString()) - }; - - foreach(var identifier in Candidate.ExternalIdentifiers) - { - _comparisons.Add(new( "Identifier", identifier, Query.ExternalIdentifier )); - } - Model = new CreateParticipant.Command - { - Identifier = Candidate.Identifier, - CurrentUser = UserProfile! - }; - } - - private Task BackToSearch() - { - return OnCanceled.InvokeAsync(); - } - - private async Task EnrolCandidate() - { - await _form!.Validate().ConfigureAwait(false); - if (_form!.IsValid) - { - await Mediator.Send(Model!); - await OnParticipantEnrolled.InvokeAsync(); - } - } - - + } - diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs new file mode 100644 index 00000000..110f96f6 --- /dev/null +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs @@ -0,0 +1,81 @@ + +using Cfo.Cats.Application.Common.Security; +using Cfo.Cats.Application.Features.Candidates.DTOs; +using Cfo.Cats.Application.Features.Candidates.Queries.Search; +using Cfo.Cats.Application.Features.Participants.Commands; +using Cfo.Cats.Server.UI.Services.Candidate; + +namespace Cfo.Cats.Server.UI.Pages.Candidates.Components; + +public partial class MatchFound : ComponentBase +{ + [Inject] + private IPicklistService PicklistService { get; set; } + + private MudForm? _form; + + private CreateParticipant.Command? Model; + + private CandidateDto? candidate; + + private bool _confirmation = false; + private List? _comparisons; + + [Parameter] + public string Id { get; set; } + + [CascadingParameter] + public UserProfile? UserProfile { get; set; } + + [Parameter] + public CandidateSearchQuery Query { get; set; } = default!; + + [Parameter] + public EventCallback OnCanceled { get; set; } + + [Parameter] + public EventCallback OnParticipantEnrolled { get; set; } + + [Inject] + public CandidateService CandidateService { get; set; } + + + protected override async Task OnParametersSetAsync() + { + candidate = await CandidateService.GetByUpciAsync(Id); + + _comparisons = new List + { + new ("First Name", Query.FirstName.ToUpper(), candidate.FirstName.ToUpper()), + new ("Last Name", Query.LastName.ToUpper(), candidate.LastName.ToUpper()), + new ("Date Of Birth", Query.DateOfBirth.GetValueOrDefault().ToShortDateString(), candidate.DateOfBirth.ToShortDateString()) + }; + + foreach (var identifier in candidate.ExternalIdentifiers) + { + _comparisons.Add(new("Identifier", identifier, Query.ExternalIdentifier)); + } + + Model = new CreateParticipant.Command + { + Candidate = candidate, + CurrentUser = UserProfile! + }; + } + + private Task BackToSearch() + { + return OnCanceled.InvokeAsync(); + } + + private async Task EnrolCandidate() + { + await _form!.Validate().ConfigureAwait(false); + if (_form!.IsValid) + { + await Mediator.Send(Model!); + await OnParticipantEnrolled.InvokeAsync(); + } + } + +} diff --git a/src/Server.UI/Pages/Candidates/Components/NoResults.razor b/src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor similarity index 65% rename from src/Server.UI/Pages/Candidates/Components/NoResults.razor rename to src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor index 3418e8ce..210bd68a 100644 --- a/src/Server.UI/Pages/Candidates/Components/NoResults.razor +++ b/src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor @@ -1,7 +1,7 @@ - + - No matching candidates found + You have not provided enough accurate information. Please double-check your criteria, and provide more information to narrow down your search. @@ -10,9 +10,11 @@ -@code + +@code { [Parameter] public EventCallback OnCanceled { get; set; } + private Task BackToSearch() => OnCanceled.InvokeAsync(); -} +} \ No newline at end of file diff --git a/src/Server.UI/Pages/Candidates/Search.razor b/src/Server.UI/Pages/Candidates/Search.razor index 5a963ce0..430e042b 100644 --- a/src/Server.UI/Pages/Candidates/Search.razor +++ b/src/Server.UI/Pages/Candidates/Search.razor @@ -2,6 +2,7 @@ @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search @using Cfo.Cats.Server.UI.Pages.Candidates.Components +@using Cfo.Cats.Server.UI.Services.Candidate @@ -21,10 +22,10 @@ @if (SearchResults is null) { - + } - @if (SearchResults is not null && SelectedCandidate is null) + @if (SearchResults is not null && SelectedCandidateId is null) { } - @if (SelectedCandidate is not null) + @if (SelectedCandidateId is not null) { @@ -41,7 +42,7 @@ Back to Search - Proceed with the enrolment + Proceed with the enrolment @@ -60,10 +61,10 @@ { Navigation.NavigateTo($"/pages/enrolments/{participantId}"); } - + private CandidateSearchQuery? Query { get; set; } - private CandidateDto[]? SearchResults { get; set; } - private CandidateDto? SelectedCandidate { get; set; } + private SearchResult[]? SearchResults { get; set; } + private string? SelectedCandidateId { get; set; } [CascadingParameter] public UserProfile UserProfile { get; set; } = default!; @@ -85,14 +86,14 @@ }; } - private void CandidatesFoundHandler(CandidateDto[] candidates) + private void SearchHandler(SearchResult[] candidates) { SearchResults = candidates; } - private void CandidateEnrolledHandler() + private void CandidateEnrolledHandler(string identifier) { - this.SelectedCandidate = SearchResults![0]; + this.SelectedCandidateId = identifier; } private string GetTitle() @@ -102,7 +103,7 @@ return "Find a candidate to enrol"; } - if (SelectedCandidate is not null) + if (SelectedCandidateId is not null) { return "Confirm selected candidate"; } From f0020abf99534f15e069dc05dc29211a7a909837 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:51:51 +0100 Subject: [PATCH 07/16] Show 'more information required' when no results are returned --- .../Components/CandidateResultsComponent.razor | 3 +-- .../Candidates/Components/TooManyResults.razor | 18 ------------------ 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 src/Server.UI/Pages/Candidates/Components/TooManyResults.razor diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor index e63082a1..0c16301e 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor @@ -40,7 +40,7 @@ } else if (SearchReturnedMultipleAmbiguousCandidates()) { - ShowTooManyResults(); + ShowMoreInformationRequired(); } else if (SearchReturnedIdenticalCandidate()) { @@ -74,7 +74,6 @@ void ShowCandidateAlreadyEnrolled() => _selectedType = typeof(AlreadyEnrolled); void ShowMoreInformationRequired() => _selectedType = typeof(MoreInformationRequired); - void ShowTooManyResults() => _selectedType = typeof(TooManyResults); async Task EnrolCandidateIfNotAlreadyEnrolled() { diff --git a/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor b/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor deleted file mode 100644 index ad06c94c..00000000 --- a/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor +++ /dev/null @@ -1,18 +0,0 @@ - - - - Too many results found. We require more information to find the participant - - - - Back to Search - - - - -@code -{ - [Parameter] - public EventCallback OnCanceled { get; set; } - private Task BackToSearch() => OnCanceled.InvokeAsync(); -} \ No newline at end of file From 3f3b5aab1b991f111083578981a24ca11d2913ab Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 20:52:39 +0100 Subject: [PATCH 08/16] Fix case-sensitivity issue with candidate comparison --- .../Components/CandidateComparisonComponent.razor | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateComparisonComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateComparisonComponent.razor index a603999f..a0538837 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateComparisonComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateComparisonComponent.razor @@ -33,7 +33,7 @@ @code { [Parameter] public List Comparisons { get; set; } = new(); - + private readonly List<(ComparisonRow row, string HighlightedDiff, bool HasDifferences)> diffResults = new(); @@ -42,8 +42,8 @@ diffResults.Clear(); foreach (var comparison in Comparisons) { - var diff = HighlightDifferences(comparison.OriginalText, comparison.NewText); bool hasDifferences = HasDifferences(comparison.OriginalText, comparison.NewText); + var diff = hasDifferences ? HighlightDifferences(comparison.OriginalText, comparison.NewText) : comparison.NewText; diffResults.Add((comparison, diff, hasDifferences)); } } @@ -81,6 +81,6 @@ return result.ToString(); } - public static bool HasDifferences(string str1, string str2) => str1.Equals(str2, StringComparison.CurrentCultureIgnoreCase) == false; + public static bool HasDifferences(string str1, string str2) => str1.Equals(str2, StringComparison.OrdinalIgnoreCase) == false; } \ No newline at end of file From b7a310752ee947c6e392439d1fcd15a0749ed72a Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 21:21:39 +0100 Subject: [PATCH 09/16] Revert "Show 'more information required' when no results are returned" This reverts commit f0020abf99534f15e069dc05dc29211a7a909837. --- .../Components/CandidateResultsComponent.razor | 3 ++- .../Candidates/Components/TooManyResults.razor | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/Server.UI/Pages/Candidates/Components/TooManyResults.razor diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor index 0c16301e..e63082a1 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor @@ -40,7 +40,7 @@ } else if (SearchReturnedMultipleAmbiguousCandidates()) { - ShowMoreInformationRequired(); + ShowTooManyResults(); } else if (SearchReturnedIdenticalCandidate()) { @@ -74,6 +74,7 @@ void ShowCandidateAlreadyEnrolled() => _selectedType = typeof(AlreadyEnrolled); void ShowMoreInformationRequired() => _selectedType = typeof(MoreInformationRequired); + void ShowTooManyResults() => _selectedType = typeof(TooManyResults); async Task EnrolCandidateIfNotAlreadyEnrolled() { diff --git a/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor b/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor new file mode 100644 index 00000000..ad06c94c --- /dev/null +++ b/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor @@ -0,0 +1,18 @@ + + + + Too many results found. We require more information to find the participant + + + + Back to Search + + + + +@code +{ + [Parameter] + public EventCallback OnCanceled { get; set; } + private Task BackToSearch() => OnCanceled.InvokeAsync(); +} \ No newline at end of file From 87c82ec60a48b994d17953e32333584c3d6713ea Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Wed, 26 Jun 2024 22:07:57 +0100 Subject: [PATCH 10/16] More descriptive messages for enrolments. --- .../Components/AlreadyEnrolled.razor | 9 ++++---- .../CandidateResultsComponent.razor | 22 ++++++++++++------- .../Candidates/Components/MatchFound.razor | 4 ++-- .../Candidates/Components/MatchFound.razor.cs | 4 ++-- .../Components/MoreInformationRequired.razor | 4 ++-- .../Candidates/Components/NoResults.razor | 18 +++++++++++++++ .../Components/TooManyResults.razor | 2 +- 7 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 src/Server.UI/Pages/Candidates/Components/NoResults.razor diff --git a/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor b/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor index b1138ab9..75a68032 100644 --- a/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor +++ b/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor @@ -1,9 +1,7 @@ -@using Humanizer - - The candidate we have found is already enrolled. + This candidate is already enrolled (@CandidateId); @@ -16,6 +14,9 @@ { [Parameter] public EventCallback OnCanceled { get; set; } - + + [Parameter] + public string CandidateId { get; set; } + private Task BackToSearch() => OnCanceled.InvokeAsync(); } \ No newline at end of file diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor index e63082a1..126e37f0 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor @@ -36,21 +36,21 @@ if (SearchReturnedNoCandidates()) { - ShowMoreInformationRequired(); + ShowNoResultsFound(); } else if (SearchReturnedMultipleAmbiguousCandidates()) { - ShowTooManyResults(); + ShowTooManyResultsFound(); } else if (SearchReturnedIdenticalCandidate()) { - await EnrolCandidateIfNotAlreadyEnrolled(); + await BeginEnrolmentIfNotAlreadyEnrolled(); } else if (SearchReturnedSingleCandidate()) { if(CandidateMeetsMinimumPrecedence(10)) { - await EnrolCandidateIfNotAlreadyEnrolled(); + await BeginEnrolmentIfNotAlreadyEnrolled(); } else { @@ -66,17 +66,23 @@ void ShowCandidateFound() { - _selectedParameters.Add("Id", Candidate.Upci); + _selectedParameters.Add("CandidateId", Candidate.Upci); _selectedParameters.Add("Query", Query); _selectedParameters.Add("OnParticipantEnrolled", EventCallback.Factory.Create(this, OnParticipantEnrolledHandler)); _selectedType = typeof(MatchFound); } - void ShowCandidateAlreadyEnrolled() => _selectedType = typeof(AlreadyEnrolled); + void ShowCandidateAlreadyEnrolled() + { + _selectedParameters.Add("CandidateId", Candidate.Upci); + _selectedType = typeof(AlreadyEnrolled); + } + + void ShowNoResultsFound() => _selectedType = typeof(NoResults); void ShowMoreInformationRequired() => _selectedType = typeof(MoreInformationRequired); - void ShowTooManyResults() => _selectedType = typeof(TooManyResults); + void ShowTooManyResultsFound() => _selectedType = typeof(TooManyResults); - async Task EnrolCandidateIfNotAlreadyEnrolled() + async Task BeginEnrolmentIfNotAlreadyEnrolled() { var result = SearchResults .OrderBy(candidate => candidate.Precedence) diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor index 5c3d502c..50fb9473 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor @@ -7,8 +7,8 @@ { - We have found a match (@candidate.FirstName @candidate.LastName (@candidate.Identifier) - We have them located at @candidate.CurrentLocation + We have found a match: @candidate.FirstName @candidate.LastName (@candidate.Identifier) + We have them located at @(candidate.CurrentLocation ?? "Unknown Location") diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs index 110f96f6..bdb95004 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs @@ -22,7 +22,7 @@ public partial class MatchFound : ComponentBase private List? _comparisons; [Parameter] - public string Id { get; set; } + public string CandidateId { get; set; } [CascadingParameter] public UserProfile? UserProfile { get; set; } @@ -42,7 +42,7 @@ public partial class MatchFound : ComponentBase protected override async Task OnParametersSetAsync() { - candidate = await CandidateService.GetByUpciAsync(Id); + candidate = await CandidateService.GetByUpciAsync(CandidateId); _comparisons = new List { diff --git a/src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor b/src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor index 210bd68a..7365d2c8 100644 --- a/src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor +++ b/src/Server.UI/Pages/Candidates/Components/MoreInformationRequired.razor @@ -1,7 +1,7 @@  - - You have not provided enough accurate information. Please double-check your criteria, and provide more information to narrow down your search. + + A candidate was found which loosely matches your search criteria, but you have not provided enough valid information. Please double-check your criteria, and provide more information to refine your search. diff --git a/src/Server.UI/Pages/Candidates/Components/NoResults.razor b/src/Server.UI/Pages/Candidates/Components/NoResults.razor new file mode 100644 index 00000000..6301826f --- /dev/null +++ b/src/Server.UI/Pages/Candidates/Components/NoResults.razor @@ -0,0 +1,18 @@ + + + + No candidates were found which match your search criteria. Please double-check your criteria, and provide more information to refine your search. + + + + Back to Search + + + + +@code +{ + [Parameter] + public EventCallback OnCanceled { get; set; } + private Task BackToSearch() => OnCanceled.InvokeAsync(); +} \ No newline at end of file diff --git a/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor b/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor index ad06c94c..89e69289 100644 --- a/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor +++ b/src/Server.UI/Pages/Candidates/Components/TooManyResults.razor @@ -1,7 +1,7 @@ - Too many results found. We require more information to find the participant + Too many results found. Please double-check your criteria, and provide more information to refine your search. From 98c9d8c5513b3a33385279b6ee72138b66fbf60a Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Thu, 27 Jun 2024 10:18:00 +0100 Subject: [PATCH 11/16] Add internal Nuget feed and required packages. --- NuGet.config | 7 +++++++ src/Server.UI/Server.UI.csproj | 1 + 2 files changed, 8 insertions(+) create mode 100644 NuGet.config diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 00000000..63cca013 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index 9968de61..b513a4b3 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -29,6 +29,7 @@ + From 243ec8d935c71d60638ba76905f093cff15d12a4 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Thu, 27 Jun 2024 10:18:51 +0100 Subject: [PATCH 12/16] Add NuGet.config as solution item --- cats.sln | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cats.sln b/cats.sln index c303fcb0..76e1f678 100644 --- a/cats.sln +++ b/cats.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -21,12 +21,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server.UI", "src\Server.UI\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_SolutionItems", "_SolutionItems", "{1B26F38D-22D1-4A00-B3D3-D77C5979281B}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore docker-compose.yml = docker-compose.yml .config\dotnet-tools.json = .config\dotnet-tools.json LICENSE = LICENSE + NuGet.config = NuGet.config README.md = README.md - .gitignore = .gitignore - .editorconfig = .editorconfig EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\Build.csproj", "{7CC5B8A2-2122-4907-B970-FD7A3EEA1C4C}" From decbc9957212d492c3cd09ada40484c9a2942826 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Thu, 27 Jun 2024 10:19:33 +0100 Subject: [PATCH 13/16] Fleshed out Candidates Dto --- .../Features/Candidates/DTOs/CandidateDto.cs | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Application/Features/Candidates/DTOs/CandidateDto.cs b/src/Application/Features/Candidates/DTOs/CandidateDto.cs index 9d20b5aa..022c5614 100644 --- a/src/Application/Features/Candidates/DTOs/CandidateDto.cs +++ b/src/Application/Features/Candidates/DTOs/CandidateDto.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Cfo.Cats.Application.Features.Candidates.DTOs; +namespace Cfo.Cats.Application.Features.Candidates.DTOs; public class CandidateDto { @@ -13,6 +11,11 @@ public class CandidateDto /// The first name of the candidate /// public string FirstName { get; set; } + + /// + /// The second (or middle) name of the candidate + /// + public string SecondName { get; set; } /// /// The candidates last name @@ -23,19 +26,41 @@ public class CandidateDto /// The candidates date of birth /// public DateTime DateOfBirth { get; set; } - + /// - /// A collection of identifiers from external systems. + /// The candidates NOMIS Number (if applicable). /// - public string[] ExternalIdentifiers { get; set; } = []; + public string? NomisNumber { get; set; } + /// + /// The candidates Crn (if applicable). + /// + public string? Crn { get; set; } /// - /// Indicates whether the candidate is marked as active in the data source(s). + /// Indicates whether the primary record is marked as active in the data source(s). /// public bool IsActive { get; set; } + /// + /// The candidates gender (Male/Female). + /// public string Gender { get; set; } + + /// + /// The candidates nationality + /// + public string Nationality { get; set; } + + /// + /// The candidates ethnicity + /// + public string Ethnicity { get; set; } + + /// + /// The primary source of information (NOMIS/DELIUS). + /// + public string Origin { get; set; } /// /// The location CATS thinks the user is registered at From 66c6001fd1007da854e69be94baa90626fbe78db Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Thu, 27 Jun 2024 10:20:36 +0100 Subject: [PATCH 14/16] Dummy Candidates service + DI --- src/Server.UI/DependencyInjection.cs | 24 +- .../Components/CandidateFinderComponent.razor | 2 +- .../Candidates/Components/MatchFound.razor.cs | 4 +- src/Server.UI/Program.cs | 9 +- .../Services/Candidate/CandidateService.cs | 6 +- .../Candidate/DummyCandidateService.cs | 219 ++++++++++++++++++ .../Services/Candidate/ICandidateService.cs | 12 + 7 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 src/Server.UI/Services/Candidate/DummyCandidateService.cs create mode 100644 src/Server.UI/Services/Candidate/ICandidateService.cs diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index 5f53e373..86272b96 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -16,7 +16,6 @@ using Microsoft.Extensions.FileProviders; using MudBlazor.Services; using MudExtensions.Services; -using Polly; using ActualLab.Fusion; using Toolbelt.Blazor.Extensions.DependencyInjection; using ActualLab.Fusion.Extensions; @@ -28,8 +27,12 @@ namespace Cfo.Cats.Server.UI; public static class DependencyInjection { - public static IServiceCollection AddServerUi(this IServiceCollection services, IConfiguration config) + public static WebApplicationBuilder AddServerUi(this WebApplicationBuilder builder) { + var services = builder.Services; + var config = builder.Configuration; + var environment = builder.Environment; + services.AddRazorComponents().AddInteractiveServerComponents(); services.AddCascadingAuthenticationState(); services.AddScoped(); @@ -106,11 +109,18 @@ public static IServiceCollection AddServerUi(this IServiceCollection services, I return service; }); - services.AddHttpClient((provider, client) => + if(environment.IsDevelopment()) { - client.DefaultRequestHeaders.Add("X-API-KEY", config.GetRequiredValue("DMS:ApiKey")); - client.BaseAddress = new Uri(config.GetRequiredValue("DMS:ApplicationUrl")); - }); + services.AddSingleton(); + } + else + { + services.AddHttpClient((provider, client) => + { + client.DefaultRequestHeaders.Add("X-API-KEY", config.GetRequiredValue("DMS:ApiKey")); + client.BaseAddress = new Uri(config.GetRequiredValue("DMS:ApplicationUrl")); + }); + } services.Configure(options => { @@ -118,7 +128,7 @@ public static IServiceCollection AddServerUi(this IServiceCollection services, I ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); - return services; + return builder; } public static WebApplication ConfigureServer(this WebApplication app, IConfiguration configuration) diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor index 3d2ac559..db7e6390 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor @@ -35,7 +35,7 @@ private MudForm? _form; [Inject] - public CandidateService CandidateService { get; set; } + public ICandidateService CandidateService { get; set; } [EditorRequired] [Parameter] diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs index bdb95004..ab2cd8f4 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs @@ -37,7 +37,7 @@ public partial class MatchFound : ComponentBase public EventCallback OnParticipantEnrolled { get; set; } [Inject] - public CandidateService CandidateService { get; set; } + public ICandidateService CandidateService { get; set; } protected override async Task OnParametersSetAsync() @@ -51,10 +51,12 @@ protected override async Task OnParametersSetAsync() new ("Date Of Birth", Query.DateOfBirth.GetValueOrDefault().ToShortDateString(), candidate.DateOfBirth.ToShortDateString()) }; + /* foreach (var identifier in candidate.ExternalIdentifiers) { _comparisons.Add(new("Identifier", identifier, Query.ExternalIdentifier)); } + */ Model = new CreateParticipant.Command { diff --git a/src/Server.UI/Program.cs b/src/Server.UI/Program.cs index bb0fedb4..800effd4 100644 --- a/src/Server.UI/Program.cs +++ b/src/Server.UI/Program.cs @@ -10,10 +10,11 @@ builder.RegisterSerilog(); builder.WebHost.UseStaticWebAssets(); -builder - .Services.AddApplication() - .AddInfrastructure(builder.Configuration) - .AddServerUi(builder.Configuration); +builder.AddServerUi(); + +builder.Services + .AddApplication() + .AddInfrastructure(builder.Configuration); var app = builder.Build(); diff --git a/src/Server.UI/Services/Candidate/CandidateService.cs b/src/Server.UI/Services/Candidate/CandidateService.cs index f9a11b0f..31fcb501 100644 --- a/src/Server.UI/Services/Candidate/CandidateService.cs +++ b/src/Server.UI/Services/Candidate/CandidateService.cs @@ -3,7 +3,7 @@ namespace Cfo.Cats.Server.UI.Services.Candidate; -public class CandidateService(HttpClient client) +public class CandidateService(HttpClient client) : ICandidateService { public async Task?> SearchAsync(CandidateSearchQuery searchQuery) { @@ -21,6 +21,4 @@ public class CandidateService(HttpClient client) { return await client.GetFromJsonAsync($"clustering/{upci}/Aggregate"); } -} - -public record SearchResult(string Upci, int Precedence); \ No newline at end of file +} \ No newline at end of file diff --git a/src/Server.UI/Services/Candidate/DummyCandidateService.cs b/src/Server.UI/Services/Candidate/DummyCandidateService.cs new file mode 100644 index 00000000..25bf5f88 --- /dev/null +++ b/src/Server.UI/Services/Candidate/DummyCandidateService.cs @@ -0,0 +1,219 @@ +using Cfo.Cats.Application.Features.Candidates.DTOs; +using Cfo.Cats.Application.Features.Candidates.Queries.Search; +using Matching.Core.Search; + +namespace Cfo.Cats.Server.UI.Services.Candidate; + +public class DummyCandidateService : ICandidateService +{ + IReadOnlyList Candidates => + [ + new CandidateDto + { + Identifier = "1CFG1789A", + FirstName = "Bruce", + LastName = "Wayne", + DateOfBirth = new DateTime(1970, 2, 19), + Gender = "Male", + Crn = "B001111", + NomisNumber = "A0001AA" + }, + new CandidateDto + { + Identifier = "1CFG2934F", + FirstName = "Peter", + LastName = "Parker", + DateOfBirth = new DateTime(2001, 8, 10), + Gender = "Male", + Crn = "B002222" + }, + new CandidateDto + { + Identifier = "1CFG3492J", + FirstName = "Tony", + LastName = "Stark", + DateOfBirth = new DateTime(1970, 5, 29), + Gender = "Male", + Crn = "B003333", + NomisNumber = "A0002BB" + }, + new CandidateDto + { + Identifier = "1CFG4567B", + FirstName = "Steve", + LastName = "Rogers", + DateOfBirth = new DateTime(1918, 7, 4), + Gender = "Male", + Crn = "B004444" + }, + new CandidateDto + { + Identifier = "1CFG6789L", + FirstName = "Clark", + LastName = "Kent", + DateOfBirth = new DateTime(1938, 6, 18), + Gender = "Male", + Crn = "B006666" + }, + new CandidateDto + { + Identifier = "1CFG7890M", + FirstName = "Barry", + LastName = "Allen", + DateOfBirth = new DateTime(1989, 3, 14), + Gender = "Male", + Crn = "B007777", + NomisNumber = "A0004DD" + }, + new CandidateDto + { + Identifier = "1CFG8901N", + FirstName = "Arthur", + LastName = "Curry", + DateOfBirth = new DateTime(1986, 1, 29), + Gender = "Male", + Crn = "B008888" + }, + new CandidateDto + { + Identifier = "1CFG9012O", + FirstName = "Natasha", + LastName = "Romanoff", + DateOfBirth = new DateTime(1984, 11, 22), + Gender = "Female", + Crn = "B009999", + NomisNumber = "A0005EE" + }, + new CandidateDto + { + Identifier = "1CFG0123P", + FirstName = "Wade", + LastName = "Wilson", + DateOfBirth = new DateTime(1974, 4, 10), + Gender = "Male", + Crn = "B001010" + }, + new CandidateDto + { + Identifier = "1CFG2345Q", + FirstName = "Scott", + LastName = "Lang", + DateOfBirth = new DateTime(1968, 8, 16), + Gender = "Male", + Crn = "B001111", + NomisNumber = "A0006FF" + }, + new CandidateDto + { + Identifier = "1CFG3456R", + FirstName = "Stephen", + LastName = "Strange", + DateOfBirth = new DateTime(1930, 11, 18), + Gender = "Male", + Crn = "B001212" + }, + new CandidateDto + { + Identifier = "1CFG5678T", + FirstName = "Bruce", + LastName = "Banner", + DateOfBirth = new DateTime(1969, 12, 18), + Gender = "Male", + Crn = "B001414" + }, + new CandidateDto + { + Identifier = "1CFG7890V", + FirstName = "Carol", + LastName = "Danvers", + DateOfBirth = new DateTime(1984, 5, 29), + Gender = "Female", + Crn = "B001616" + }, + new CandidateDto + { + Identifier = "1CFG8901W", + FirstName = "Matt", + LastName = "Murdock", + DateOfBirth = new DateTime(1979, 12, 15), + Gender = "Male", + Crn = "B001717", + NomisNumber = "A0009II" + }, + new CandidateDto + { + Identifier = "1CFG9012X", + FirstName = "Jessica", + LastName = "Jones", + DateOfBirth = new DateTime(1985, 7, 5), + Gender = "Female", + Crn = "B001818" + }, + new CandidateDto + { + Identifier = "1CFG0123Y", + FirstName = "Dick", + LastName = "Grayson", + DateOfBirth = new DateTime(1990, 3, 20), + Gender = "Male", + Crn = "B001919", + NomisNumber = "A0010JJ" + } + ]; + + public async Task GetByUpciAsync(string upci) + { + var candidate = Candidates.SingleOrDefault(c => c.Identifier == upci); + return await Task.FromResult(candidate); + } + + public async Task?> SearchAsync(CandidateSearchQuery searchQuery) + { + string lastName = searchQuery.LastName; + string identifier = searchQuery.ExternalIdentifier; + DateOnly dateOfBirth = DateOnly.FromDateTime((DateTime)searchQuery.DateOfBirth!); + + var blocks = Candidates + .Where(e => e.LastName == searchQuery.LastName && e.DateOfBirth == searchQuery.DateOfBirth) + .Union + ( + Candidates.Where(e => new[] { e.Crn, e.NomisNumber }.Contains(searchQuery.ExternalIdentifier)) + ); + + if(blocks is null || blocks.Count() is 0) + { + return []; + } + + var scores = blocks.Select(block => Score((identifier, lastName, dateOfBirth), block.Crn ?? string.Empty, block)) + .Union + ( + blocks.Select(block => Score((identifier, lastName, dateOfBirth), block.NomisNumber ?? string.Empty, block)) + ) + .GroupBy(result => result.Upci) + .Select(result => new SearchResult(result.Key, result.Min(r => r.Precedence))); + + return await Task.FromResult(scores); + } + + static SearchResult Score((string externalIdentifier, string lastName, DateOnly dateOfBirth) query, string identifier, CandidateDto block) => + new(block.Identifier, Precedence.GetPrecedence + ( + identifiers: + ( + query.externalIdentifier, + identifier + ), + lastNames: + ( + query.lastName, + block.LastName + ), + dateOfBirths: + ( + query.dateOfBirth, + DateOnly.FromDateTime(block.DateOfBirth) + ) + )); + +} diff --git a/src/Server.UI/Services/Candidate/ICandidateService.cs b/src/Server.UI/Services/Candidate/ICandidateService.cs new file mode 100644 index 00000000..1c914eb1 --- /dev/null +++ b/src/Server.UI/Services/Candidate/ICandidateService.cs @@ -0,0 +1,12 @@ +using Cfo.Cats.Application.Features.Candidates.DTOs; +using Cfo.Cats.Application.Features.Candidates.Queries.Search; + +namespace Cfo.Cats.Server.UI.Services.Candidate; + +public interface ICandidateService +{ + public Task?> SearchAsync(CandidateSearchQuery searchQuery); + public Task GetByUpciAsync(string upci); +} + +public record SearchResult(string Upci, int Precedence); \ No newline at end of file From 39e284eb149e07d4b3886c11519068952c3610ac Mon Sep 17 00:00:00 2001 From: Carl Sixsmith Date: Thu, 27 Jun 2024 11:26:35 +0100 Subject: [PATCH 15/16] Modifications from peer review. --- .../Common/Interfaces}/ICandidateService.cs | 2 +- src/Infrastructure/DependencyInjection.cs | 13 +++++++++++++ src/Infrastructure/Infrastructure.csproj | 2 +- .../Services/Candidates}/CandidateService.cs | 5 +++-- .../Candidates}/DummyCandidateService.cs | 2 +- src/Server.UI/DependencyInjection.cs | 16 ++-------------- .../Components/CandidateFinderComponent.razor | 1 - .../Components/CandidateResultsComponent.razor | 1 - .../Pages/Candidates/Components/MatchFound.razor | 1 - .../Candidates/Components/MatchFound.razor.cs | 1 - src/Server.UI/Pages/Candidates/Search.razor | 1 - src/Server.UI/Server.UI.csproj | 4 +++- src/Server.UI/appsettings.json | 1 + 13 files changed, 25 insertions(+), 25 deletions(-) rename src/{Server.UI/Services/Candidate => Application/Common/Interfaces}/ICandidateService.cs (88%) rename src/{Server.UI/Services/Candidate => Infrastructure/Services/Candidates}/CandidateService.cs (84%) rename src/{Server.UI/Services/Candidate => Infrastructure/Services/Candidates}/DummyCandidateService.cs (99%) diff --git a/src/Server.UI/Services/Candidate/ICandidateService.cs b/src/Application/Common/Interfaces/ICandidateService.cs similarity index 88% rename from src/Server.UI/Services/Candidate/ICandidateService.cs rename to src/Application/Common/Interfaces/ICandidateService.cs index 1c914eb1..79143998 100644 --- a/src/Server.UI/Services/Candidate/ICandidateService.cs +++ b/src/Application/Common/Interfaces/ICandidateService.cs @@ -1,7 +1,7 @@ using Cfo.Cats.Application.Features.Candidates.DTOs; using Cfo.Cats.Application.Features.Candidates.Queries.Search; -namespace Cfo.Cats.Server.UI.Services.Candidate; +namespace Cfo.Cats.Application.Common.Interfaces; public interface ICandidateService { diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 80c73f2b..abcf7041 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -7,6 +7,7 @@ using Cfo.Cats.Infrastructure.Configurations; using Cfo.Cats.Infrastructure.Constants.Database; using Cfo.Cats.Infrastructure.Persistence.Interceptors; +using Cfo.Cats.Infrastructure.Services.Candidates; using Cfo.Cats.Infrastructure.Services.MultiTenant; using Cfo.Cats.Infrastructure.Services.Serialization; using Microsoft.AspNetCore.DataProtection; @@ -146,6 +147,18 @@ private static IServiceCollection AddServices(this IServiceCollection services, } + if(configuration["UseDummyCandidateService"] == "True") + { + services.AddSingleton(); + } + else + { + services.AddHttpClient((provider, client) => + { + client.DefaultRequestHeaders.Add("X-API-KEY", configuration.GetRequiredValue("DMS:ApiKey")); + client.BaseAddress = new Uri(configuration.GetRequiredValue("DMS:ApplicationUrl")); + }); + } return services .AddSingleton() diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 1452d325..61515b0d 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -10,7 +10,6 @@ - @@ -36,6 +35,7 @@ + diff --git a/src/Server.UI/Services/Candidate/CandidateService.cs b/src/Infrastructure/Services/Candidates/CandidateService.cs similarity index 84% rename from src/Server.UI/Services/Candidate/CandidateService.cs rename to src/Infrastructure/Services/Candidates/CandidateService.cs index 31fcb501..7f2e0af1 100644 --- a/src/Server.UI/Services/Candidate/CandidateService.cs +++ b/src/Infrastructure/Services/Candidates/CandidateService.cs @@ -1,7 +1,8 @@ -using Cfo.Cats.Application.Features.Candidates.DTOs; +using System.Net.Http.Json; +using Cfo.Cats.Application.Features.Candidates.DTOs; using Cfo.Cats.Application.Features.Candidates.Queries.Search; -namespace Cfo.Cats.Server.UI.Services.Candidate; +namespace Cfo.Cats.Infrastructure.Services.Candidates; public class CandidateService(HttpClient client) : ICandidateService { diff --git a/src/Server.UI/Services/Candidate/DummyCandidateService.cs b/src/Infrastructure/Services/Candidates/DummyCandidateService.cs similarity index 99% rename from src/Server.UI/Services/Candidate/DummyCandidateService.cs rename to src/Infrastructure/Services/Candidates/DummyCandidateService.cs index 25bf5f88..fa0b4c6e 100644 --- a/src/Server.UI/Services/Candidate/DummyCandidateService.cs +++ b/src/Infrastructure/Services/Candidates/DummyCandidateService.cs @@ -2,7 +2,7 @@ using Cfo.Cats.Application.Features.Candidates.Queries.Search; using Matching.Core.Search; -namespace Cfo.Cats.Server.UI.Services.Candidate; +namespace Cfo.Cats.Infrastructure.Services.Candidates; public class DummyCandidateService : ICandidateService { diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index 86272b96..744d8260 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -20,8 +20,8 @@ using Toolbelt.Blazor.Extensions.DependencyInjection; using ActualLab.Fusion.Extensions; using Cfo.Cats.Server.UI.Middlewares; -using Cfo.Cats.Server.UI.Services.Candidate; using Cfo.Cats.Infrastructure; +using Cfo.Cats.Infrastructure.Services; namespace Cfo.Cats.Server.UI; @@ -109,19 +109,7 @@ public static WebApplicationBuilder AddServerUi(this WebApplicationBuilder build return service; }); - if(environment.IsDevelopment()) - { - services.AddSingleton(); - } - else - { - services.AddHttpClient((provider, client) => - { - client.DefaultRequestHeaders.Add("X-API-KEY", config.GetRequiredValue("DMS:ApiKey")); - client.BaseAddress = new Uri(config.GetRequiredValue("DMS:ApplicationUrl")); - }); - } - + services.Configure(options => { options.ForwardedHeaders = diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor index db7e6390..c528e82d 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateFinderComponent.razor @@ -2,7 +2,6 @@ @using Cfo.Cats.Application.Features.Candidates @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search -@using Cfo.Cats.Server.UI.Services.Candidate @using Faker diff --git a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor index 126e37f0..4987175c 100644 --- a/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor +++ b/src/Server.UI/Pages/Candidates/Components/CandidateResultsComponent.razor @@ -1,7 +1,6 @@ @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search @using Cfo.Cats.Application.Features.Participants.Queries -@using Cfo.Cats.Server.UI.Services.Candidate @if (_selectedType is not null) { diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor index 50fb9473..fef6a50c 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor @@ -1,7 +1,6 @@ @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search @using Cfo.Cats.Application.Features.Participants.Commands -@using Cfo.Cats.Server.UI.Services.Candidate @if(candidate is not null) { diff --git a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs index ab2cd8f4..2a96f789 100644 --- a/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs +++ b/src/Server.UI/Pages/Candidates/Components/MatchFound.razor.cs @@ -3,7 +3,6 @@ using Cfo.Cats.Application.Features.Candidates.DTOs; using Cfo.Cats.Application.Features.Candidates.Queries.Search; using Cfo.Cats.Application.Features.Participants.Commands; -using Cfo.Cats.Server.UI.Services.Candidate; namespace Cfo.Cats.Server.UI.Pages.Candidates.Components; diff --git a/src/Server.UI/Pages/Candidates/Search.razor b/src/Server.UI/Pages/Candidates/Search.razor index 430e042b..03aa1f3b 100644 --- a/src/Server.UI/Pages/Candidates/Search.razor +++ b/src/Server.UI/Pages/Candidates/Search.razor @@ -2,7 +2,6 @@ @using Cfo.Cats.Application.Features.Candidates.DTOs @using Cfo.Cats.Application.Features.Candidates.Queries.Search @using Cfo.Cats.Server.UI.Pages.Candidates.Components -@using Cfo.Cats.Server.UI.Services.Candidate diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index b513a4b3..8a86b1f1 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -29,7 +29,6 @@ - @@ -89,6 +88,9 @@ <_ContentIncludedByDefault Remove="Pages\Enrolments\Enrolment.razor" /> <_ContentIncludedByDefault Remove="Pages\Enrolments\_Imports.razor" /> + + + \ No newline at end of file diff --git a/src/Server.UI/appsettings.json b/src/Server.UI/appsettings.json index 90748869..8da67641 100644 --- a/src/Server.UI/appsettings.json +++ b/src/Server.UI/appsettings.json @@ -1,5 +1,6 @@ { "UseInMemoryDatabase": false, + "UseDummyCandidateService": "True", "DatabaseSettings": { "DbProvider": "mssql", "ConnectionString": "Server=localhost,1433;Database=CatsDb;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;" From 1135e3595ae2f985bdc9a36dc04bdf5941901877 Mon Sep 17 00:00:00 2001 From: samgibsonmoj Date: Thu, 27 Jun 2024 11:44:22 +0100 Subject: [PATCH 16/16] Fix floating semi-colon on page and remove unused project folder --- .../Pages/Candidates/Components/AlreadyEnrolled.razor | 2 +- src/Server.UI/Server.UI.csproj | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor b/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor index 75a68032..71fe62ce 100644 --- a/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor +++ b/src/Server.UI/Pages/Candidates/Components/AlreadyEnrolled.razor @@ -1,7 +1,7 @@ - This candidate is already enrolled (@CandidateId); + This candidate is already enrolled (@CandidateId) diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index 8a86b1f1..9968de61 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -88,9 +88,6 @@ <_ContentIncludedByDefault Remove="Pages\Enrolments\Enrolment.razor" /> <_ContentIncludedByDefault Remove="Pages\Enrolments\_Imports.razor" /> - - - \ No newline at end of file