Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pathway Plan - Objectives and Tasks #145

Closed
wants to merge 45 commits into from
Closed
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
db4447c
WIP: Pathway Plan / Objectives screen
samgibsonmoj Aug 7, 2024
a06991d
Objective task completion dates
samgibsonmoj Aug 7, 2024
5730d19
Objective/Task data modelling and migrations
samgibsonmoj Aug 9, 2024
45ea502
WIP: Add tasks on a month basis, instead of specific dates
samgibsonmoj Aug 9, 2024
a2854f8
Objective/Task dto's and command handlers
samgibsonmoj Aug 12, 2024
d93bf2e
Merge remote-tracking branch 'origin/main' into feature/objectives
samgibsonmoj Aug 12, 2024
0660e2b
Merge remote-tracking branch 'origin/main' into feature/objectives
samgibsonmoj Aug 13, 2024
d67a0ae
Merge remote-tracking branch 'origin/main' into feature/objectives
samgibsonmoj Aug 13, 2024
215fbaf
Objective/task create and update commands
samgibsonmoj Aug 13, 2024
ce4d2d7
Merge remote-tracking branch 'origin/main' into feature/objectives
samgibsonmoj Aug 13, 2024
28759bb
Fix styling and add close button for dialogs
samgibsonmoj Aug 14, 2024
2b4d461
Remove 'close' option for task and allow selection of completion reason
samgibsonmoj Aug 14, 2024
7e9ee47
Objective sorting
samgibsonmoj Aug 14, 2024
db17891
Add ability to rename objectives
samgibsonmoj Aug 15, 2024
1af4f65
Objective status & review
samgibsonmoj Aug 15, 2024
3dad9d2
Cleanup pathway plan + separate objective/task components
samgibsonmoj Aug 15, 2024
17a5639
Re-add real-time status to objective
samgibsonmoj Aug 15, 2024
926d93a
WIP - Pathway Plan reviews
samgibsonmoj Aug 15, 2024
551fdfe
CatsComponentBase - OnUpdate propagation
samgibsonmoj Aug 15, 2024
2ff9b02
Add pathway plan
samgibsonmoj Aug 15, 2024
418e15a
Pathway plan review command
samgibsonmoj Aug 15, 2024
79b3c2e
Pathway plan review functionality + case summary information
samgibsonmoj Aug 16, 2024
5086eae
Remove review action from case summary
samgibsonmoj Aug 16, 2024
b12a338
Fix typos and ensure immutability in objectives
samgibsonmoj Aug 16, 2024
818436e
Identifiers for objectives/tasks
samgibsonmoj Aug 16, 2024
ce4bd8f
Fix typo for regex validator name
samgibsonmoj Aug 16, 2024
7ff4db6
Restrict allowed characters for objective/task titles
samgibsonmoj Aug 16, 2024
6cb45d8
Add expansion dialogs for displaying completion reason
samgibsonmoj Aug 16, 2024
7de98d3
Merge remote-tracking branch 'origin/main' into feature/objectives
samgibsonmoj Aug 16, 2024
79a6e15
Resolve merge conflict
samgibsonmoj Aug 16, 2024
6693f70
Squash development migrations => PathwayPlan
samgibsonmoj Aug 16, 2024
90a9c84
Allow a less restrictive character set for objective/task titles
samgibsonmoj Aug 16, 2024
6221975
Merge branch 'main' into feature/objectives
samgibsonmoj Aug 19, 2024
44b99c0
Hide action buttons when not available
samgibsonmoj Aug 19, 2024
7a4c834
'Completed by' for objectives and tasks
samgibsonmoj Aug 19, 2024
dce2761
Persist completed by to task review
samgibsonmoj Aug 19, 2024
fd79dbc
WIP: Timeline events - pathway plan
samgibsonmoj Aug 19, 2024
d9ece49
Remove reoccurrence frequency
samgibsonmoj Aug 19, 2024
5e5d97e
Objective completed domain event
samgibsonmoj Aug 20, 2024
ae7822a
Merge remote-tracking branch 'origin/main' into feature/objectives
samgibsonmoj Aug 20, 2024
93cb96c
Merge branch 'main' into feature/objectives
samgibsonmoj Aug 20, 2024
0876133
Feedback re wording
samgibsonmoj Aug 20, 2024
513bb81
Merge branch 'main' into feature/objectives
carlsixsmith-moj Aug 20, 2024
7213510
Merge branch 'main' into feature/objectives
carlsixsmith-moj Aug 20, 2024
5e05a17
Rename 'Extend date' to 'Adjust date'
samgibsonmoj Aug 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Application/Common/Interfaces/IApplicationDbContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Cfo.Cats.Domain.Entities.Administration;
using Cfo.Cats.Domain.Entities.Administration;
using Cfo.Cats.Domain.Entities.Assessments;
using Cfo.Cats.Domain.Entities.Documents;
using Cfo.Cats.Domain.Entities.Participants;
Expand All @@ -25,6 +25,8 @@ public interface IApplicationDbContext
DbSet<Document> Documents { get; }

DbSet<Participant> Participants { get; }

DbSet<PathwayPlan> PathwayPlans { get; }

DbSet<Risk> Risks { get; }

Expand Down
1 change: 1 addition & 0 deletions src/Application/Common/Interfaces/Identity/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public interface IUserService
event Action? OnChange;
void Initialize();
void Refresh();
string? GetDisplayName(string userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public static class ValidationConstants
public const string LettersSpacesUnderscores = @"^[A-Za-z_ ]+$";
public const string LettersSpacesUnderscoresMessage = "{0} must contain only letters, spaces, and underscores.";

public const string LettersSpacesCommaApostorphe = @"^[A-Za-z ',’]+$";
public const string LettersSpacesCommaApostrophe = @"^[A-Za-z ',’]+$";
public const string LettersSpacesCommaApostropheMessage = "{0} must contain only letters, spaces, comma and an apostrophe.";

public const string AlphabetsDigitsSpaceSlashHyphenDot= @"^[a-zA-Z0-9\s\\\/\-.]+$";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public ApplicationUserDtoValidator(

RuleFor(x => x.MemorablePlace)
.MaximumLength(50).WithMessage(_localizer["Memorable place must be less than or equal to 50 characters"])
.Matches(ValidationConstants.LettersSpacesCommaApostorphe).WithMessage(_localizer[string.Format(ValidationConstants.LettersSpacesCommaApostropheMessage, "Memorable place")]);
.Matches(ValidationConstants.LettersSpacesCommaApostrophe).WithMessage(_localizer[string.Format(ValidationConstants.LettersSpacesCommaApostropheMessage, "Memorable place")]);

RuleFor(x => x.MemorableDate)
.NotEmpty().WithMessage(_localizer["Memorable date is required"])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.SecurityConstants;
using Cfo.Cats.Domain.Entities.Documents;
using Cfo.Cats.Domain.Entities.Participants;

namespace Cfo.Cats.Application.Features.Participants.Commands;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Cfo.Cats.Application.Features.PathwayPlans.DTOs;
using Cfo.Cats.Application.Features.Bios.DTOs;
using Cfo.Cats.Domain.Entities.Assessments;
using Cfo.Cats.Domain.Entities.Participants;
Expand Down Expand Up @@ -44,6 +45,8 @@ public class ParticipantSummaryDto
public RiskSummaryDto? LatestRisk { get; set; }

public BioSummaryDto? BioSummary { get; set; }

public PathwayPlanSummaryDto? PathwayPlan { get; set; }

private class Mapping : Profile
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.Features.Participants.Caching;
using Cfo.Cats.Application.Features.Participants.DTOs;
using Cfo.Cats.Application.Features.PathwayPlans.DTOs;
using Cfo.Cats.Application.SecurityConstants;

namespace Cfo.Cats.Application.Features.Participants.Queries;
Expand Down Expand Up @@ -41,20 +42,25 @@ public async Task<Result<ParticipantSummaryDto>> Handle(Query request, Cancellat
.ProjectTo<AssessmentSummaryDto>(mapper.ConfigurationProvider)
.ToArrayAsync(cancellationToken);

var risk = await unitOfWork.DbContext.Risks
summary.LatestRisk = await unitOfWork.DbContext.Risks
.OrderByDescending(x => x.Created)
.ProjectTo<RiskSummaryDto>(mapper.ConfigurationProvider)
.FirstOrDefaultAsync(x => x.ParticipantId == request.ParticipantId, cancellationToken);

summary.LatestRisk = mapper.Map<RiskSummaryDto>(risk);

summary.PathwayPlan = await unitOfWork.DbContext.PathwayPlans
.IgnoreAutoIncludes()
.Include(x => x.ReviewHistories)
.Where(x => x.ParticipantId == request.ParticipantId)
.ProjectTo<PathwayPlanSummaryDto>(mapper.ConfigurationProvider)
.FirstOrDefaultAsync(cancellationToken);

var bio = await unitOfWork.DbContext.ParticipantBios
.OrderByDescending(x => x.Created)
.FirstOrDefaultAsync(x => x.ParticipantId == request.ParticipantId, cancellationToken);

summary.BioSummary = mapper.Map<BioSummaryDto>(bio);

return Result<ParticipantSummaryDto>.Success(summary);

}
}

Expand Down
61 changes: 61 additions & 0 deletions src/Application/Features/PathwayPlans/Commands/AddObjective.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.SecurityConstants;
using Cfo.Cats.Domain.Entities.Participants;

namespace Cfo.Cats.Application.Features.PathwayPlans.Commands;

public static class AddObjective
{
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
public class Command : IRequest<Result>
{
[Description("Pathway Plan Id")]
public required Guid PathwayPlanId { get; set; }

public string? Title { get; set; }

public class Mapping : Profile
{
public Mapping()
{
CreateMap<Command, Objective>(MemberList.None)
.ConstructUsing(dto => Objective.Create(dto.Title!, dto.PathwayPlanId));
}
}
}

public class Handler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler<Command, Result>
{
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
{
var objective = mapper.Map<Objective>(request);
var pathwayPlan = await unitOfWork.DbContext.PathwayPlans.FindAsync(request.PathwayPlanId);

if (pathwayPlan is null)
{
throw new NotFoundException("Cannot find pathway plan", request.PathwayPlanId);
}

pathwayPlan.AddObjective(objective);

return Result.Success();
}
}

public class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.PathwayPlanId)
.NotNull();

RuleFor(x => x.Title)
.NotEmpty()
.WithMessage("You must provide a title")
.Matches(ValidationConstants.AlphabetsDigitsSpaceSlashHyphenDot)
.WithMessage(string.Format(ValidationConstants.AlphabetsDigitsSpaceSlashHyphenDotMessage, "Title"));
}

}
}
54 changes: 54 additions & 0 deletions src/Application/Features/PathwayPlans/Commands/AddPathwayPlan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.SecurityConstants;
using Cfo.Cats.Domain.Entities.Participants;

namespace Cfo.Cats.Application.Features.PathwayPlans.Commands;

public static class AddPathwayPlan
{
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
public class Command : IRequest<Result>
{
public required string ParticipantId { get; set; }

public class Mapping : Profile
{
public Mapping()
{
CreateMap<Command, PathwayPlan>(MemberList.None)
.ConstructUsing(dto => PathwayPlan.Create(dto.ParticipantId));
}
}
}

public class Handler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler<Command, Result>
{
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
{
if (await unitOfWork.DbContext.PathwayPlans.AnyAsync(p => p.ParticipantId == request.ParticipantId))
{
return Result.Failure();
}

var pathwayPlan = mapper.Map<PathwayPlan>(request);

await unitOfWork.DbContext.PathwayPlans.AddAsync(pathwayPlan);

return Result.Success();
}
}

public class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.ParticipantId)
.MinimumLength(9)
.MaximumLength(9)
.Matches(ValidationConstants.AlphaNumeric)
.WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "Participant Id"));
}

}
}
85 changes: 85 additions & 0 deletions src/Application/Features/PathwayPlans/Commands/AddTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.SecurityConstants;
using Cfo.Cats.Domain.Entities.Participants;

namespace Cfo.Cats.Application.Features.PathwayPlans.Commands;

public static class AddTask
{
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
public class Command : IRequest<Result>
{
[Description("Pathway Plan Id")]
public required Guid PathwayPlanId { get; set; }

[Description("Objective Id")]
public required Guid ObjectiveId { get; set; }

public string? Title { get; set; }

public DateTime? Due { get; set; }

public class Mapping : Profile
{
public Mapping()
{
CreateMap<Command, ObjectiveTask>(MemberList.None)
.ConstructUsing(dto => ObjectiveTask.Create(dto.Title!, dto.Due!.Value));
}
}
}

public class Handler(IUnitOfWork unitOfWork, IMapper mapper) : IRequestHandler<Command, Result>
{
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
{
var pathwayPlan = await unitOfWork.DbContext.PathwayPlans.FindAsync(request.PathwayPlanId)
?? throw new NotFoundException("Cannot find pathway plan", request.PathwayPlanId);

var objective = pathwayPlan.Objectives.FirstOrDefault(o => o.Id == request.ObjectiveId)
?? throw new NotFoundException("Cannot find objective", request.ObjectiveId);

var task = mapper.Map<ObjectiveTask>(request);

objective.AddTask(task);

return Result.Success();
}
}

public class Validator : AbstractValidator<Command>
{
public Validator()
{
var today = DateTime.UtcNow;

RuleFor(x => x.ObjectiveId)
.NotNull();

RuleFor(x => x.PathwayPlanId)
.NotNull();

RuleFor(x => x.Title)
.NotEmpty()
.WithMessage("You must provide a title")
.Matches(ValidationConstants.AlphabetsDigitsSpaceSlashHyphenDot)
.WithMessage(string.Format(ValidationConstants.AlphabetsDigitsSpaceSlashHyphenDotMessage, "Title"));

RuleFor(x => x.Due)
.Must(x => x.HasValue)
.WithMessage("You must provide a Due date");

When(x => x.Due.HasValue, () =>
{
RuleFor(x => x.Due)
.GreaterThanOrEqualTo(new DateTime(today.Year, today.Month, 1))
.WithMessage(ValidationConstants.DateMustBeInFuture)
.Must(x => x!.Value.Day.Equals(1))
.WithMessage("Due date must fall on the first day of the month");
});
}

}

}
58 changes: 58 additions & 0 deletions src/Application/Features/PathwayPlans/Commands/EditObjective.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Cfo.Cats.Application.Common.Security;
using Cfo.Cats.Application.Common.Validators;
using Cfo.Cats.Application.SecurityConstants;

namespace Cfo.Cats.Application.Features.PathwayPlans.Commands;

public static class EditObjective
{
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
public class Command : IRequest<Result>
{
[Description("Objective Id")]
public required Guid ObjectiveId { get; set; }

[Description("Pathway Plan Id")]
public required Guid PathwayPlanId { get; set; }

[Description("Title")]
public required string Title { get; set; }
}

public class Handler(IUnitOfWork unitOfWork) : IRequestHandler<Command, Result>
{
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
{
var pathwayPlan = await unitOfWork.DbContext.PathwayPlans.FindAsync(request.PathwayPlanId)
?? throw new NotFoundException("Cannot find pathway plan", request.PathwayPlanId);

var objective = pathwayPlan.Objectives.FirstOrDefault(o => o.Id == request.ObjectiveId)
?? throw new NotFoundException("Cannot find objective", request.ObjectiveId);

objective.Rename(request.Title);

return Result.Success();
}
}

public class Validator : AbstractValidator<Command>
{
public Validator()
{
var today = DateTime.UtcNow;

RuleFor(x => x.ObjectiveId)
.NotNull();

RuleFor(x => x.PathwayPlanId)
.NotNull();

RuleFor(x => x.Title)
.NotEmpty()
.WithMessage("You must provide a title")
.Matches(ValidationConstants.AlphabetsDigitsSpaceSlashHyphenDot)
.WithMessage(string.Format(ValidationConstants.AlphabetsDigitsSpaceSlashHyphenDotMessage, "Title"));
}

}
}
Loading
Loading