Skip to content

Commit af80451

Browse files
CFODEV-1578 Allow user to edit pathway plan reviews (#965)
* CFODEV-1578: Allow user to edit pathway plan review * CFODEV-1578: Make the panel fully readonly so no confusion by user. Also moved to code behind.
1 parent aec8005 commit af80451

13 files changed

Lines changed: 457 additions & 128 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using Cfo.Cats.Application.Common.Security;
2+
using Cfo.Cats.Application.Common.Validators;
3+
using Cfo.Cats.Application.SecurityConstants;
4+
5+
namespace Cfo.Cats.Application.Features.PathwayPlans.Commands;
6+
7+
public static class EditPathwayPlanReview
8+
{
9+
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
10+
public class Command : IRequest<Result>
11+
{
12+
public Guid ReviewId { get; init; }
13+
public int LocationId { get; set; }
14+
public DateTime? ReviewDate { get; set; }
15+
public string? Review { get; set; }
16+
public PathwayPlanReviewReason? ReviewReason { get; set; }
17+
}
18+
19+
public class Handler(IUnitOfWork unitOfWork)
20+
: IRequestHandler<Command, Result>
21+
{
22+
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
23+
{
24+
var pathwayPlanReview = await unitOfWork.DbContext.PathwayPlanReviews
25+
.Include(r => r.PathwayPlan)
26+
.FirstOrDefaultAsync(r => r.Id == request.ReviewId, cancellationToken);
27+
28+
if (pathwayPlanReview is null)
29+
{
30+
throw new NotFoundException("Pathway Plan Review not found", request.ReviewId);
31+
}
32+
33+
pathwayPlanReview.Update(
34+
request.LocationId,
35+
request.ReviewDate!.Value,
36+
request.Review,
37+
request.ReviewReason!);
38+
39+
await unitOfWork.SaveChangesAsync(cancellationToken);
40+
41+
return Result.Success();
42+
}
43+
}
44+
45+
public class Validator : AbstractValidator<Command>
46+
{
47+
private readonly IUnitOfWork _unitOfWork;
48+
49+
public Validator(IUnitOfWork unitOfWork)
50+
{
51+
_unitOfWork = unitOfWork;
52+
53+
RuleFor(x => x.ReviewId)
54+
.NotNull()
55+
.WithMessage("You must provide a Pathway Plan Review");
56+
57+
RuleFor(x => x.Review)
58+
.NotNull()
59+
.WithMessage("You must provide a review comment")
60+
.MaximumLength(ValidationConstants.NotesLength)
61+
.WithMessage($"Maximum length of a review comment is {ValidationConstants.NotesLength}")
62+
.Matches(ValidationConstants.Notes)
63+
.WithMessage(string.Format(ValidationConstants.NotesMessage, "Review"));
64+
65+
RuleSet(ValidationConstants.RuleSet.MediatR, () =>
66+
{
67+
RuleFor(x => x.ReviewId)
68+
.MustAsync(ParticipantMustNotBeArchived)
69+
.WithMessage("Participant is archived");
70+
71+
RuleFor(c => c.ReviewDate)
72+
.NotNull().WithMessage("Review date is required");
73+
});
74+
75+
RuleFor(x => x.ReviewReason)
76+
.NotNull()
77+
.Must(r => r!.IsValidSelection())
78+
.WithMessage("A review reason must be selected.");
79+
80+
RuleFor(x => x.LocationId)
81+
.NotNull()
82+
.WithMessage("You must provide a location");
83+
}
84+
85+
private async Task<bool> ParticipantMustNotBeArchived(Guid pathwayPlanReviewId, CancellationToken cancellationToken)
86+
{
87+
var participantId = await _unitOfWork.DbContext.PathwayPlanReviews
88+
.Where(r => r.Id == pathwayPlanReviewId)
89+
.Select(r => r.PathwayPlan.ParticipantId)
90+
.AsNoTracking()
91+
.FirstOrDefaultAsync(cancellationToken);
92+
93+
if (participantId == null)
94+
{return false;}
95+
96+
return await _unitOfWork.DbContext.Participants
97+
.AsNoTracking()
98+
.AnyAsync(p => p.Id == participantId &&
99+
p.EnrolmentStatus != EnrolmentStatus.ArchivedStatus.Value, cancellationToken);
100+
}
101+
}
102+
}

src/Application/Features/PathwayPlans/Commands/ReviewPathwayPlan.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ public static class ReviewPathwayPlan
1010
[RequestAuthorize(Policy = SecurityPolicies.Enrol)]
1111
public class Command : IRequest<Result>
1212
{
13-
public required Guid PathwayPlanId { get; set; }
14-
public required string ParticipantId { get; set; }
13+
public required Guid PathwayPlanId { get; init; }
14+
public required string ParticipantId { get; init; }
1515
public LocationDto? Location { get; set; }
1616

1717
[Description(description: "Review Date")]
@@ -28,17 +28,17 @@ public async Task<Result> Handle(Command request, CancellationToken cancellation
2828
{
2929
var pathwayPlan = await unitOfWork.DbContext.PathwayPlans.FirstOrDefaultAsync(x => x.Id == request.PathwayPlanId, cancellationToken);
3030

31-
if (pathwayPlan is not null)
31+
if (pathwayPlan is null)
3232
{
33-
pathwayPlan.Review(request.PathwayPlanId, request.ParticipantId, request.Location!.Id,
34-
request.ReviewDate!.Value, request.Review, request.ReviewReason);
33+
throw new NotFoundException("Cannot find pathway plan", request.PathwayPlanId);
34+
}
3535

36-
await unitOfWork.SaveChangesAsync(cancellationToken);
36+
pathwayPlan.Review(request.PathwayPlanId, request.ParticipantId, request.Location!.Id,
37+
request.ReviewDate!.Value, request.Review, request.ReviewReason);
3738

38-
return Result.Success();
39-
}
39+
await unitOfWork.SaveChangesAsync(cancellationToken);
4040

41-
throw new NotFoundException("Cannot find pathway plan", request.PathwayPlanId);
41+
return Result.Success();
4242
}
4343
}
4444

@@ -106,10 +106,9 @@ private async Task<bool> HaveOccurredOnOrAfterLastReview(string participantId, D
106106
return false;
107107
}
108108

109-
var latestReviewDate = await _unitOfWork.DbContext.PathwayPlans
110-
.Where(pp => pp.ParticipantId == participantId)
111-
.SelectMany(pp => pp.PathwayPlanReviews)
112-
.MaxAsync(ppr => (DateTime?)ppr.ReviewDate, cancellationToken);
109+
var latestReviewDate = await _unitOfWork.DbContext.PathwayPlanReviews
110+
.Where(r => r.PathwayPlan.ParticipantId == participantId)
111+
.MaxAsync(r => (DateTime?)r.ReviewDate, cancellationToken);
113112

114113
if (latestReviewDate is null)
115114
{

src/Application/Features/PathwayPlans/DTOs/PathwayPlanReviewHistoryDto.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
public class PathwayPlanReviewHistoryDto
44
{
5-
public required Guid Id { get; set; }
5+
public required Guid Id { get; init; }
66
public required string ParticipantId { get; set; }
7-
public string? Review { get; set; }
8-
public PathwayPlanReviewReason? ReviewReason { get; set; }
9-
public required DateTime ReviewDate { get; set; }
10-
public required string ReviewedBy { get; set; }
11-
public int LocationId { get; set; }
12-
public string LocationName { get; set; } = string.Empty;
13-
public required DateTime Created { get; set; }
14-
public int? DaysSinceLastReview { get; set; }
7+
public string? Review { get; init; }
8+
public PathwayPlanReviewReason? ReviewReason { get; init; }
9+
public required DateTime ReviewDate { get; init; }
10+
public required string ReviewedBy { get; init; }
11+
public int LocationId { get; init; }
12+
public string LocationName { get; init; } = string.Empty;
13+
public required DateTime Created { get; init; }
14+
public int? DaysSinceLastReview { get; init; }
15+
public bool IsEditable { get; init; }
1516
}

src/Application/Features/PathwayPlans/Queries/GetPathwayPlanReviewHistory.cs

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Cfo.Cats.Application.Features.PathwayPlans.Queries;
88

9-
public static class GetPathwayPlanReviewHistoryHistory
9+
public static class GetPathwayPlanReviewHistory
1010
{
1111
[RequestAuthorize(Policy = SecurityPolicies.AuthorizedUser)]
1212
public class Query : ParticipantDetailsQuery<PathwayPlanReviewHistoryDto[]>;
@@ -16,59 +16,79 @@ public class Handler(IUnitOfWork unitOfWork) : IRequestHandler<Query, Result<Pat
1616
public async Task<Result<PathwayPlanReviewHistoryDto[]>> Handle(Query request,
1717
CancellationToken cancellationToken)
1818
{
19-
var baseQuery = unitOfWork.DbContext.PathwayPlans
20-
.Where(pp => pp.ParticipantId == request.ParticipantId)
21-
.SelectMany(pp => pp.PathwayPlanReviews, (pp, review) => new { pp, review })
22-
.Join(unitOfWork.DbContext.Locations, x => x.review.LocationId, l => l.Id,
23-
(x, l) => new { x.review, x.pp, Location = l })
24-
.Join(unitOfWork.DbContext.Users, x => x.review.CreatedBy, u => u.Id, (x, u) =>
25-
new PathwayPlanReviewHistoryDto
26-
{
27-
Id = x.review.Id,
28-
ParticipantId = x.review.ParticipantId,
29-
ReviewDate = x.review.ReviewDate,
30-
ReviewedBy = u.DisplayName!,
31-
LocationId = x.Location.Id,
32-
LocationName = x.Location.Name,
33-
ReviewReason = x.review.ReviewReason,
34-
Created = x.review.Created!.Value,
35-
Review = x.review.Review
36-
})
37-
.AsNoTracking()
38-
.OrderByDescending(r => r.ReviewDate);
39-
40-
var queryResultList = await baseQuery.ToListAsync(cancellationToken);
19+
var baseQuery =
20+
from review in unitOfWork.DbContext.PathwayPlanReviews.AsNoTracking()
21+
22+
join participant in unitOfWork.DbContext.Participants
23+
on review.PathwayPlan.ParticipantId equals participant.Id
24+
join location in unitOfWork.DbContext.Locations
25+
on review.LocationId equals location.Id
26+
join user in unitOfWork.DbContext.Users
27+
on review.CreatedBy equals user.Id
28+
29+
where review.PathwayPlan.ParticipantId == request.ParticipantId
30+
orderby review.ReviewDate descending
31+
select new
32+
{
33+
review.Id,
34+
review.ParticipantId,
35+
review.ReviewDate,
36+
review.ReviewReason,
37+
review.Review,
38+
review.Created,
39+
review.CreatedBy,
40+
41+
UserDisplayName = user.DisplayName,
4142

43+
LocationId = location.Id,
44+
LocationName = location.Name,
45+
46+
EnrolmentStatus = participant.EnrolmentStatus
47+
};
48+
49+
var queryResultList = await baseQuery
50+
.ToListAsync(cancellationToken);
51+
52+
var aWeekAgo = DateTime.UtcNow.Date.AddDays(-7);
53+
var count = queryResultList.Count;
54+
4255
var result = queryResultList
43-
.Select((item, index) =>
56+
.Select((x, index) =>
4457
{
4558
int? daysSinceLastReview = null;
4659

47-
if (index < queryResultList.Count - 1)
60+
if (index < count - 1)
4861
{
4962
var previousReviewDate = queryResultList[index + 1].ReviewDate.Date;
50-
var currentReviewDate = item.ReviewDate.Date;
63+
var currentReviewDate = x.ReviewDate.Date;
5164

5265
daysSinceLastReview =
5366
(currentReviewDate - previousReviewDate).Days;
5467
}
5568

69+
var isActive = x.EnrolmentStatus?.ParticipantIsActive() == true;
70+
71+
var canEdit = x.Created >= aWeekAgo;
72+
73+
var correctUser = x.CreatedBy == request.CurrentUser.UserId;
74+
5675
return new PathwayPlanReviewHistoryDto
5776
{
58-
Id = item.Id,
59-
ParticipantId = item.ParticipantId,
60-
ReviewDate = item.ReviewDate,
61-
ReviewedBy = item.ReviewedBy,
62-
LocationId = item.LocationId,
63-
LocationName = item.LocationName,
64-
ReviewReason = item.ReviewReason,
65-
Review = item.Review,
66-
Created = item.Created,
67-
DaysSinceLastReview = daysSinceLastReview
77+
Id = x.Id,
78+
ParticipantId = x.ParticipantId,
79+
ReviewDate = x.ReviewDate,
80+
ReviewedBy = x.UserDisplayName,
81+
LocationId = x.LocationId,
82+
LocationName = x.LocationName,
83+
ReviewReason = x.ReviewReason,
84+
Review = x.Review,
85+
Created = x.Created!.Value,
86+
DaysSinceLastReview = daysSinceLastReview,
87+
IsEditable = isActive && canEdit && correctUser
6888
};
6989
})
7090
.ToArray();
71-
91+
7292
return Result<PathwayPlanReviewHistoryDto[]>.Success(result);
7393
}
7494
}
@@ -82,22 +102,19 @@ public Validator(IUnitOfWork unitOfWork)
82102
_unitOfWork = unitOfWork;
83103

84104
RuleFor(x => x.ParticipantId)
85-
.NotNull();
86-
87-
RuleFor(x => x.ParticipantId)
88-
.MinimumLength(9)
89-
.MaximumLength(9)
105+
.NotEmpty()
106+
.Length(9)
90107
.Matches(ValidationConstants.AlphaNumeric)
91108
.WithMessage(string.Format(ValidationConstants.AlphaNumericMessage, "Participant Id"));
92-
109+
93110
RuleSet(ValidationConstants.RuleSet.MediatR, () =>
94111
{
95112
RuleFor(c => c.ParticipantId)
96113
.MustAsync(Exist)
97114
.WithMessage("Participant does not exist");
98115
});
99116
}
100-
117+
101118
private async Task<bool> Exist(string identifier, CancellationToken cancellationToken)
102119
=> await _unitOfWork.DbContext.Participants.AnyAsync(e => e.Id == identifier, cancellationToken);
103120
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Cfo.Cats.Domain.Events;
2+
3+
namespace Cfo.Cats.Application.Features.Timelines.EventHandlers;
4+
5+
public class PathwayPlanReviewUpdatedDomainEventHandler(ICurrentUserService currentUserService, IUnitOfWork unitOfWork) : TimelineNotificationHandler<PathwayPlanReviewUpdatedDomainEvent>(currentUserService, unitOfWork)
6+
{
7+
protected override TimelineEventType GetEventType() => TimelineEventType.PathwayPlan;
8+
9+
protected override string GetLine1(PathwayPlanReviewUpdatedDomainEvent notification) => "Pathway Plan Review updated.";
10+
11+
protected override string GetParticipantId(PathwayPlanReviewUpdatedDomainEvent notification) => notification.Item.ParticipantId;
12+
}

src/Domain/Entities/Participants/PathwayPlanReview.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ private PathwayPlanReview()
1313

1414
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
1515

16+
public PathwayPlan PathwayPlan { get; private set; } = null!;
17+
1618
public static PathwayPlanReview Create(Guid pathwayPlanId, string participantId, int locationId,
1719
DateTime reviewDate, string? review, PathwayPlanReviewReason reviewReason)
1820
{
@@ -30,7 +32,21 @@ public static PathwayPlanReview Create(Guid pathwayPlanId, string participantId,
3032

3133
return pathwayPlanReview;
3234
}
35+
36+
public void Update(
37+
int locationId,
38+
DateTime reviewDate,
39+
string? review,
40+
PathwayPlanReviewReason reviewReason)
41+
{
42+
LocationId = locationId;
43+
ReviewDate = reviewDate;
44+
Review = review;
45+
ReviewReason = reviewReason;
3346

47+
AddDomainEvent(new PathwayPlanReviewUpdatedDomainEvent(this));
48+
}
49+
3450
public Guid PathwayPlanId { get; private set; }
3551
public int LocationId { get; private set; }
3652
public DateTime ReviewDate { get; private set; }

src/Domain/Events/PathwayPlanEvents.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ public sealed class PathwayPlanReviewAddedDomainEvent(PathwayPlanReview pathwayP
3737
{
3838
public PathwayPlanReview Item { get; set; } = pathwayPlanReview;
3939
}
40+
41+
public sealed class PathwayPlanReviewUpdatedDomainEvent(PathwayPlanReview pathwayPlanReview) : DomainEvent
42+
{
43+
public PathwayPlanReview Item { get; set; } = pathwayPlanReview;
44+
}

0 commit comments

Comments
 (0)