Skip to content

Commit dca9c81

Browse files
Introduce labels and improve participant search functionality. (#963)
* Adds comprehensive participant labeling system Introduces a robust participant labeling system to enhance participant management and categorization. This includes: - Implements `ParticipantLabel` domain entity with its own lifecycle, business rules, and events. - Provides dedicated application services (commands, queries, and handlers) for adding, closing, and retrieving participant labels. - Integrates `IParticipantLabelsCounter` for efficient tracking of active labels. - Introduces an automated process to assign/remove "Veteran" labels based on participant bio submissions using integration events. - Refactors existing `Label` management commands into separate command, handler, and validator files for improved modularity and maintainability. - Updates participant list view to display assigned labels and allows filtering participants by specific labels. - Adds a new database migration to create the `Participant.Label` table. - Enhances unit test coverage for the new labeling features and refactored label commands. - Configures RabbitMQ in Aspire for persistent data volumes. This feature enables richer participant profiling and more precise filtering capabilities within the application. * Extend search functionality. - The search box is now clearable. - If you enter two words, the search assumes you are looking for someone and only applies that search (specifically splitting the search term in two) - Will now also filter for external identifiers so you can search for NOMIS and CRNs * Fix naming conventions. Allow system to specify the creating user and remove unused ISoftDelete code. * Adjust exports for better formatting. - Add a column per label with a Y/N indicator - Globally apply the autofilter on all exports that use the basic table dump * Add ignore flag to smart enum property that is not serializable. * Participants V2 optional from menu
1 parent d23aa4a commit dca9c81

106 files changed

Lines changed: 9423 additions & 726 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="10.0.0" />
5959
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
6060
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
61+
<PackageVersion Include="Moq" Version="4.20.72" />
62+
<PackageVersion Include="Moq.Dapper" Version="1.0.7" />
6163
<PackageVersion Include="MudBlazor" Version="8.15.0" />
6264
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2" />
6365
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />

src/Application/Common/Extensions/QueryableExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ public static async Task<PaginatedData<TResult>> ProjectToPaginatedDataAsync<T,
3737
.ProjectTo<TResult>(configuration)
3838
.ToListAsync(cancellationToken);
3939
return new PaginatedData<TResult>(data, count, pageNumber, pageSize);
40-
}
40+
}
41+
4142
}

src/Application/Common/Interfaces/IApplicationDbContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Cfo.Cats.Domain.Entities.ManagementInformation;
1515
using Cfo.Cats.Domain.Entities.PRIs;
1616
using Cfo.Cats.Domain.Labels;
17+
using Cfo.Cats.Domain.ParticipantLabels;
1718

1819
namespace Cfo.Cats.Application.Common.Interfaces;
1920

@@ -123,4 +124,6 @@ public interface IApplicationDbContext
123124
DbSet<ArchivedCase> ArchivedCases { get; }
124125
DbSet<ProviderFeedbackEnrolment> ProviderFeedbackEnrolments { get; }
125126
DbSet<ProviderFeedbackActivity> ProviderFeedbackActivities { get; }
127+
128+
DbSet<ParticipantLabel> ParticipantLabels { get; }
126129
}

src/Application/DependencyInjection.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
using Cfo.Cats.Application.Features.Labels;
44
using Cfo.Cats.Application.Features.ManagementInformation;
55
using Cfo.Cats.Application.Features.ManagementInformation.Providers;
6+
using Cfo.Cats.Application.Features.ParticipantLabels;
67
using Cfo.Cats.Application.Features.PerformanceManagement.Providers;
78
using Cfo.Cats.Application.Pipeline;
89
using Cfo.Cats.Domain.Labels;
10+
using Cfo.Cats.Domain.ParticipantLabels;
911
using Microsoft.Extensions.Configuration;
1012
using Microsoft.Extensions.DependencyInjection;
1113

@@ -70,6 +72,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
7072
.WithScopedLifetime());
7173

7274
services.AddScoped<ILabelCounter, LabelCounter>();
75+
services.AddScoped<IParticipantLabelsCounter, ParticipantLabelsCounter>();
7376

7477
return services;
7578
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Cfo.Cats.Application.Features.Bios.IntegrationEvents;
2+
using Cfo.Cats.Application.Outbox;
3+
using Cfo.Cats.Domain.Events;
4+
5+
namespace Cfo.Cats.Application.Features.Bios.EventHandlers;
6+
7+
public class PublishBioSubmittedEventHandler(IUnitOfWork unitOfWork) : INotificationHandler<BioSubmittedDomainEvent>
8+
{
9+
public async Task Handle(BioSubmittedDomainEvent notification, CancellationToken cancellationToken)
10+
=> await unitOfWork.DbContext.InsertOutboxMessage(new BioSubmittedIntegrationEvent(notification.Item.Id));
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Cfo.Cats.Application.Features.Bios.IntegrationEvents;
2+
3+
public record BioSubmittedIntegrationEvent(Guid BioId)
4+
{
5+
}

src/Application/Features/Documents/IntegrationEventHandlers/DocumentExportParticipantsIntegrationEventConsumer.cs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ public class DocumentExportParticipantsIntegrationEventConsumer(
1111
IUnitOfWork unitOfWork,
1212
IExcelService excelService,
1313
IUploadService uploadService,
14-
IMapper mapper,
1514
IDomainEventDispatcher domainEventDispatcher,
1615
ILogger<DocumentExportParticipantsIntegrationEventConsumer> logger) : IHandleMessages<ExportDocumentIntegrationEvent>
1716
{
@@ -39,21 +38,38 @@ public async Task Handle(ExportDocumentIntegrationEvent context)
3938
request.PageSize = int.MaxValue;
4039

4140
// Hack: call handler directly (skips Authorization pipeline, as we're outside of the HttpContext).
42-
var data = await new ParticipantsWithPagination.Handler(unitOfWork, mapper).Handle(request!, CancellationToken.None);
41+
var data = await new ParticipantsWithPagination.Handler(unitOfWork).Handle(request!, CancellationToken.None);
4342

43+
var dataToColumnMapper = new Dictionary<string, Func<ParticipantPaginationDto, object?>>
44+
{
45+
{ "Id", item => item.Id },
46+
{ "Participant", item => item.ParticipantName },
47+
{ "Status", item => item.EnrolmentStatus },
48+
{ "Consent", item => item.ConsentStatus },
49+
{ "Location", item => item.CurrentLocation.Name },
50+
{ "Enrolled At", item => item.EnrolmentLocation?.Name },
51+
{ "Assignee", item => item.Owner },
52+
{ "Risk Due", item => item.RiskDue },
53+
{ "Risk Due Reason", item => item.RiskDueReason.Name },
54+
};
55+
56+
if(!data.Succeeded || data?.Data is null)
57+
{
58+
throw new ApplicationException("Failed to fetch participant data");
59+
}
60+
61+
// get a list of labels from all the participants
62+
var allLabels = data.Data.Items.SelectMany(p => p.Labels)
63+
.GroupBy(label => label.Name)
64+
.Select(labelGroup => labelGroup.Key);
65+
66+
foreach (var label in allLabels)
67+
{
68+
dataToColumnMapper.Add(label, dto => dto.Labels.Any(l => l.Name == label) ? "Y" : "N");
69+
}
70+
4471
var results = await excelService.ExportAsync(data?.Data?.Items ?? [],
45-
new Dictionary<string, Func<ParticipantPaginationDto, object?>>
46-
{
47-
{ "Id", item => item.Id },
48-
{ "Participant", item => item.ParticipantName },
49-
{ "Status", item => item.EnrolmentStatus },
50-
{ "Consent", item => item.ConsentStatus },
51-
{ "Location", item => item.CurrentLocation.Name },
52-
{ "Enrolled At", item => item.EnrolmentLocation?.Name },
53-
{ "Assignee", item => item.Owner },
54-
{ "Risk Due", item => item.RiskDue },
55-
{ "Risk Due Reason", item => item.RiskDueReason.Name }
56-
}
72+
dataToColumnMapper
5773
);
5874

5975
var uploadRequest = new UploadRequest(document.Title!, UploadType.Document, results);

src/Application/Features/Labels/Commands/AddLabel.cs

Lines changed: 0 additions & 82 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Cfo.Cats.Application.Common.Security;
3+
using Cfo.Cats.Application.SecurityConstants;
4+
using Cfo.Cats.Domain.Labels;
5+
6+
namespace Cfo.Cats.Application.Features.Labels.Commands;
7+
8+
[RequestAuthorize(Policy = SecurityPolicies.ManageLabels)]
9+
public class AddLabelCommand : IRequest<Result>
10+
{
11+
[Display(Name = "Name", Description = "The display name of the label. Must be unique within a contract")]
12+
public required string Name { get; set; }
13+
14+
[Display(Name="Description", Description = "A textual description of the label. Will appear as a tool tip")]
15+
public required string Description { get; set; }
16+
17+
[Display(Name = "Scope", Description = "The scope for adding a label (system labels are not available for user selection)")]
18+
public required LabelScope Scope { get; set; }
19+
20+
[Display(Name="Colour", Description = "The colour for the label. More pronounced on Filled labels.")]
21+
public required AppColour Colour { get; set; }
22+
23+
[Display(Name = "Variant", Description = "Label display mode (Text/Outlined/Filled)")]
24+
public required AppVariant Variant { get; set; }
25+
26+
[Display(Name ="Icon", Description = "The icon to display (or None)")]
27+
public AppIcon AppIcon { get; set; }
28+
29+
[Display(Name="Contract", Description = "Option contract to limit user applicability and visibility")]
30+
public string? ContractId { get; set; }
31+
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Cfo.Cats.Domain.Labels;
2+
3+
namespace Cfo.Cats.Application.Features.Labels.Commands;
4+
5+
public class AddLabelCommandHandler(
6+
ILabelRepository repository,
7+
ILabelCounter labelCounter) : IRequestHandler<AddLabelCommand, Result>
8+
{
9+
public async Task<Result> Handle(
10+
AddLabelCommand request,
11+
CancellationToken cancellationToken)
12+
{
13+
var l = Label.Create(
14+
request.Name,
15+
request.Description,
16+
request.Scope,
17+
request.Colour,
18+
request.Variant,
19+
request.AppIcon,
20+
request.ContractId,
21+
labelCounter);
22+
await repository.AddAsync(l);
23+
return Result.Success();
24+
}
25+
}

0 commit comments

Comments
 (0)