diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Person.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Person.cs index 5104b8141..2faf9183c 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Person.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Person.cs @@ -280,6 +280,7 @@ public static bool ValidateInductionData( { var requiresStartDate = status.RequiresStartDate(); var requiresCompletedDate = status.RequiresCompletedDate(); + var requiresExemptionReason = status.RequiresExemptionReasons(); if (requiresStartDate && startDate is null) { @@ -305,13 +306,13 @@ public static bool ValidateInductionData( return false; } - if (status is InductionStatus.Exempt && exemptionReasons == InductionExemptionReasons.None) + if (requiresExemptionReason && exemptionReasons == InductionExemptionReasons.None) { error = $"Exemption reasons cannot be {nameof(InductionExemptionReasons.None)} when the status is: '{status}'."; return false; } - if (status is not InductionStatus.Exempt && exemptionReasons != InductionExemptionReasons.None) + if (!requiresExemptionReason && exemptionReasons != InductionExemptionReasons.None) { error = $"Exemption reasons must be {nameof(InductionExemptionReasons.None)} when the status is: '{status}'."; return false; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/InductionStatus.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/InductionStatus.cs index 12aaf4f84..40d3eb519 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/InductionStatus.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Models/InductionStatus.cs @@ -8,7 +8,7 @@ public enum InductionStatus None = 0, [InductionStatusInfo("required to complete", requiresStartDate: false, requiresCompletedDate: false)] RequiredToComplete = 1, - [InductionStatusInfo("exempt", requiresStartDate: false, requiresCompletedDate: false)] + [InductionStatusInfo("exempt", requiresStartDate: false, requiresCompletedDate: false, requiresExemptionReasons: true)] Exempt = 2, [InductionStatusInfo("in progress", requiresStartDate: true, requiresCompletedDate: false)] InProgress = 3, @@ -35,6 +35,8 @@ public static class InductionStatusRegistry public static bool RequiresCompletedDate(this InductionStatus status) => _info[status].RequiresCompletedDate; + public static bool RequiresExemptionReasons(this InductionStatus status) => _info[status].RequiresExemptionReasons; + public static InductionStatus ToInductionStatus(this dfeta_InductionStatus status) => ToInductionStatus((dfeta_InductionStatus?)status); @@ -61,19 +63,20 @@ private static InductionStatusInfo GetInfo(InductionStatus status) .GetCustomAttribute() ?? throw new Exception($"{nameof(InductionStatus)}.{status} is missing the {nameof(InductionStatusInfoAttribute)} attribute."); - return new InductionStatusInfo(status, attr.Name, attr.RequiresStartDate, attr.RequiresCompletedDate); + return new InductionStatusInfo(status, attr.Name, attr.RequiresStartDate, attr.RequiresCompletedDate, attr.RequiresExemptionReason); } } -public sealed record InductionStatusInfo(InductionStatus Value, string Name, bool RequiresStartDate, bool RequiresCompletedDate) +public sealed record InductionStatusInfo(InductionStatus Value, string Name, bool RequiresStartDate, bool RequiresCompletedDate, bool RequiresExemptionReasons = false) { public string Title => Name[0..1].ToUpper() + Name[1..]; } [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] -file sealed class InductionStatusInfoAttribute(string name, bool requiresStartDate, bool requiresCompletedDate) : Attribute +file sealed class InductionStatusInfoAttribute(string name, bool requiresStartDate, bool requiresCompletedDate, bool requiresExemptionReasons = false) : Attribute { public string Name => name; public bool RequiresStartDate => requiresStartDate; public bool RequiresCompletedDate => requiresCompletedDate; + public bool RequiresExemptionReason => requiresExemptionReasons; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs index 77108efd8..34f697f1b 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs @@ -18,4 +18,5 @@ public static class JourneyNames public const string CloseAlert = nameof(CloseAlert); public const string ReopenAlert = nameof(ReopenAlert); public const string DeleteAlert = nameof(DeleteAlert); + public const string EditInduction = nameof(EditInduction); } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CheckYourAnswers.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CheckYourAnswers.cshtml new file mode 100644 index 000000000..5824efdd8 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CheckYourAnswers.cshtml @@ -0,0 +1,22 @@ +@page "/persons/{PersonId}/edit-induction/check-answers" +@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.CheckYourAnswersModel +@{ + ViewBag.Title = "Check your answers"; +} + +@section BeforeContent { + Back +} + +

@ViewBag.Title

+ +
+
+
+
+ Confirm induction details + Cancel and return to record +
+
+
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CheckYourAnswers.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CheckYourAnswers.cshtml.cs new file mode 100644 index 000000000..0cb48a0dc --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CheckYourAnswers.cshtml.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +[Journey(JourneyNames.EditInduction), RequireJourneyInstance] +public class CheckYourAnswersModel : CommonJourneyPage +{ + public CheckYourAnswersModel(TrsLinkGenerator linkGenerator) : base(linkGenerator) + { + } + + public string BackLink => PageLink(InductionJourneyPage.ChangeReasons); + + public void OnGet() + { + } + + public IActionResult OnPost() + { + // TODO - end of journey logic + + return Redirect(NextPage()(PersonId, JourneyInstance!.InstanceId)); + } + + public Func NextPage() + { + return (Id, journeyInstanceId) => LinkGenerator.PersonInduction(Id); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CommonJourneyPage.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CommonJourneyPage.cs new file mode 100644 index 000000000..43e154e93 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CommonJourneyPage.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using TeachingRecordSystem.SupportUi; +using TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +public abstract class CommonJourneyPage : PageModel +{ + protected TrsLinkGenerator LinkGenerator { get; set; } + public JourneyInstance? JourneyInstance { get; set; } + + [FromRoute] + public Guid PersonId { get; set; } + + protected CommonJourneyPage(TrsLinkGenerator linkGenerator) + { + LinkGenerator = linkGenerator; + } + + public async Task OnPostCancelAsync() + { + await JourneyInstance!.DeleteAsync(); + return Redirect(LinkGenerator.PersonInduction(PersonId)); + } + + protected string PageLink(InductionJourneyPage? pageName) + { + return pageName switch + { + InductionJourneyPage.Status => LinkGenerator.InductionEditStatus(PersonId, JourneyInstance!.InstanceId), + InductionJourneyPage.CompletedDate => LinkGenerator.InductionEditCompletedDate(PersonId, JourneyInstance!.InstanceId), + InductionJourneyPage.ExemptionReason => LinkGenerator.InductionEditExemptionReason(PersonId, JourneyInstance!.InstanceId), + InductionJourneyPage.StartDate => LinkGenerator.InductionEditStartDate(PersonId, JourneyInstance!.InstanceId), + InductionJourneyPage.ChangeReasons => LinkGenerator.InductionChangeReason(PersonId, JourneyInstance!.InstanceId), + InductionJourneyPage.CheckAnswers => LinkGenerator.InductionCheckYourAnswers(PersonId, JourneyInstance!.InstanceId), + _ => LinkGenerator.PersonInduction(PersonId) + }; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CompletedDate.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CompletedDate.cshtml new file mode 100644 index 000000000..9b9bb4eaf --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CompletedDate.cshtml @@ -0,0 +1,22 @@ +@page "/persons/{PersonId}/edit-induction/date-completed" +@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.CompletedDateModel +@{ + ViewBag.Title = "Date completed"; +} + +@section BeforeContent { + Back +} + +

@ViewBag.Title

+ +
+
+
+
+ Continue + Cancel and return to record +
+
+ +
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CompletedDate.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CompletedDate.cshtml.cs new file mode 100644 index 000000000..8037d2f82 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/CompletedDate.cshtml.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +[Journey(JourneyNames.EditInduction), ActivatesJourney, RequireJourneyInstance] +public class CompletedDateModel : CommonJourneyPage +{ + public InductionJourneyPage NextPage => InductionJourneyPage.ChangeReasons; + public string BackLink + { + // TODO - more logic needed when other routes to completed-date are added + get => PageLink(InductionJourneyPage.StartDate); + } + + public CompletedDateModel(TrsLinkGenerator linkGenerator) : base(linkGenerator) + { + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + await JourneyInstance!.UpdateStateAsync(state => + { + // TODO - store the completed date + if (state.JourneyStartPage == null) + { + state.JourneyStartPage = InductionJourneyPage.CompletedDate; + } + }); + + return Redirect(PageLink(NextPage)); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/EditInductionState.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/EditInductionState.cs new file mode 100644 index 000000000..a170d5e5e --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/EditInductionState.cs @@ -0,0 +1,38 @@ +using TeachingRecordSystem.Core.DataStore.Postgres; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +public class EditInductionState : IRegisterJourney +{ + public static JourneyDescriptor Journey => new( + JourneyNames.EditInduction, + typeof(EditInductionState), + requestDataKeys: ["personId"], + appendUniqueKey: true); + + public InductionStatus InductionStatus { get; set; } + public DateOnly? StartDate { get; set; } + public DateOnly? CompletedDate { get; set; } + public InductionExemptionReasons? ExemptionReasons { get; set; } + public string? ChangeReason { get; set; } + public InductionJourneyPage? JourneyStartPage { get; set; } + + public bool Initialized { get; set; } + + public async Task EnsureInitializedAsync(TrsDbContext dbContext, Guid personId, InductionJourneyPage startPage) + { + if (Initialized) + { + return; + } + var person = await dbContext.Persons + .SingleAsync(q => q.PersonId == personId); + InductionStatus = person!.InductionStatus; + if (JourneyStartPage == null) + { + JourneyStartPage = startPage; + } + + Initialized = true; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/ExemptionReason.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/ExemptionReason.cshtml new file mode 100644 index 000000000..9b80c7b4d --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/ExemptionReason.cshtml @@ -0,0 +1,22 @@ +@page "/persons/{PersonId}/edit-induction/exemption-reasons" +@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.ExemptionReasonModel +@{ + ViewBag.Title = "Exemption reason"; +} + +@section BeforeContent { + Back +} + +

@ViewBag.Title

+ +
+
+
+
+ Continue + Cancel and return to record +
+
+
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/ExemptionReason.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/ExemptionReason.cshtml.cs new file mode 100644 index 000000000..d24de4eee --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/ExemptionReason.cshtml.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +[Journey(JourneyNames.EditInduction), RequireJourneyInstance] +public class ExemptionReasonModel : CommonJourneyPage +{ + public InductionJourneyPage NextPage => InductionJourneyPage.ChangeReasons; + public string BackLink + { + // TODO - more logic needed when other routes to exemption reason are added + get => PageLink(InductionJourneyPage.Status); + } + + public ExemptionReasonModel(TrsLinkGenerator linkGenerator) : base(linkGenerator) + { + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + await JourneyInstance!.UpdateStateAsync(state => + { + // TODO - store the exemption reason + if (state.JourneyStartPage == null) + { + state.JourneyStartPage = InductionJourneyPage.ExemptionReason; + } + }); + + return Redirect(PageLink(NextPage)); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionChangeReason.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionChangeReason.cshtml new file mode 100644 index 000000000..3b79e20d1 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionChangeReason.cshtml @@ -0,0 +1,22 @@ +@page "/persons/{PersonId}/edit-induction/change-reason" +@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.InductionChangeReasonModel +@{ + ViewBag.Title = "Change reason"; +} + +@section BeforeContent { + Back +} + +

@ViewBag.Title

+ +
+
+
+
+ Continue + Cancel and return to record +
+
+
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionChangeReason.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionChangeReason.cshtml.cs new file mode 100644 index 000000000..fbfbdbe2d --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionChangeReason.cshtml.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +[Journey(JourneyNames.EditInduction), RequireJourneyInstance] +public class InductionChangeReasonModel : CommonJourneyPage +{ + public InductionStatus InductionStatus => JourneyInstance!.State.InductionStatus; + public InductionJourneyPage NextPage => InductionJourneyPage.CheckAnswers; + public string BackLink + { + get + { + return InductionStatus switch + { + _ when InductionStatus.RequiresCompletedDate() => PageLink(InductionJourneyPage.CompletedDate), + _ when InductionStatus.RequiresStartDate() => PageLink(InductionJourneyPage.StartDate), + _ when InductionStatus.RequiresExemptionReasons() => PageLink(InductionJourneyPage.ExemptionReason), + _ => PageLink(InductionJourneyPage.Status), + }; + } + } + + public InductionChangeReasonModel(TrsLinkGenerator linkGenerator) : base(linkGenerator) + { + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + await JourneyInstance!.UpdateStateAsync(state => + { + // TODO - store the change reason + }); + + return Redirect(PageLink(NextPage)); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionJourneyPage.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionJourneyPage.cs new file mode 100644 index 000000000..0d7684c05 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/InductionJourneyPage.cs @@ -0,0 +1,11 @@ +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +public enum InductionJourneyPage +{ + Status, + ExemptionReason, + StartDate, + CompletedDate, + ChangeReasons, + CheckAnswers +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/StartDate.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/StartDate.cshtml new file mode 100644 index 000000000..8ea588a48 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/StartDate.cshtml @@ -0,0 +1,22 @@ +@page "/persons/{PersonId}/edit-induction/start-date" +@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.StartDateModel +@{ + ViewBag.Title = "Start date"; +} + +@section BeforeContent { + Back +} + +

@ViewBag.Title

+ +
+
+
+
+ Continue + Cancel and return to record +
+
+ +
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/StartDate.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/StartDate.cshtml.cs new file mode 100644 index 000000000..f3f1f6eba --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/StartDate.cshtml.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +[Journey(JourneyNames.EditInduction), ActivatesJourney, RequireJourneyInstance] +public class StartDateModel : CommonJourneyPage +{ + public InductionStatus InductionStatus => JourneyInstance!.State.InductionStatus; + + public InductionJourneyPage NextPage + { + get + { + return InductionStatus.RequiresCompletedDate() + ? InductionJourneyPage.CompletedDate + : InductionJourneyPage.ChangeReasons; + } + } + + public string BackLink + { + // TODO - more logic needed when other routes to start-date are added + get => PageLink(InductionJourneyPage.Status); + } + + public StartDateModel(TrsLinkGenerator linkGenerator) : base(linkGenerator) + { + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + await JourneyInstance!.UpdateStateAsync(state => + { + // TODO - store the start date + if (state.JourneyStartPage == null) + { + state.JourneyStartPage = InductionJourneyPage.StartDate; + } + }); + + return Redirect(PageLink(NextPage)); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/Status.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/Status.cshtml new file mode 100644 index 000000000..ecd6a2881 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/Status.cshtml @@ -0,0 +1,23 @@ +@page "/persons/{PersonId}/edit-induction/status" +@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.StatusModel +@{ + ViewBag.Title = "Edit status: " + Model.InductionStatus.GetName(); +} + +@section BeforeContent { + Back +} + +

@ViewBag.Title

+ +
+
+
+ +
+ Continue + Cancel and return to record +
+
+ +
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/Status.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/Status.cshtml.cs new file mode 100644 index 000000000..9901837d0 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/EditInduction/Status.cshtml.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using TeachingRecordSystem.Core.DataStore.Postgres; + +namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +[Journey(JourneyNames.EditInduction), ActivatesJourney, RequireJourneyInstance] +public class StatusModel : CommonJourneyPage +{ + protected TrsDbContext _dbContext; + + [BindProperty] + public InductionStatus InductionStatus { get; set; } + + public InductionJourneyPage NextPage + { + get + { + return InductionStatus switch + { + _ when InductionStatus.RequiresExemptionReasons() => InductionJourneyPage.ExemptionReason, + _ when InductionStatus.RequiresStartDate() => InductionJourneyPage.StartDate, + _ => InductionJourneyPage.ChangeReasons + }; + } + } + public string BackLink => LinkGenerator.PersonInduction(PersonId); + + public StatusModel(TrsLinkGenerator linkGenerator, TrsDbContext dbContext) : base(linkGenerator) + { + _dbContext = dbContext; + } + + public void OnGet() + { + InductionStatus = JourneyInstance!.State.InductionStatus; + } + + public async Task OnPostAsync() + { + await JourneyInstance!.UpdateStateAsync(state => + { + state.InductionStatus = InductionStatus; + if (state.JourneyStartPage == null) + { + state.JourneyStartPage = InductionJourneyPage.Status; + } + }); + + return Redirect(PageLink(NextPage)); + } + + public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + await JourneyInstance!.State.EnsureInitializedAsync(_dbContext, PersonId, InductionJourneyPage.Status); + + await next(); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml index f0db4cf99..6c60b0601 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml @@ -21,50 +21,36 @@ Induction details - - Induction status - - - @Model.Status.GetTitle() - + Induction status + @Model.Status.GetTitle() + + Change + @if (Model.ExemptionReasons != InductionExemptionReasons.None) // how to enforce that InductionExemptionReasons.None means no other flags are set? { - - Exemption reason - - - @Model.ExemptionReasonsText - + Exemption reason + @Model.ExemptionReasonsText } @if (Model.ShowStartDate) { - - Induction start date - - - @Model.StartDate?.ToString(UiDefaults.DateOnlyDisplayFormat) - + Induction start date + @Model.StartDate?.ToString(UiDefaults.DateOnlyDisplayFormat) } @if (Model.ShowCompletionDate) { - - Induction completion date - - - @Model.CompletionDate?.ToString(UiDefaults.DateOnlyDisplayFormat) - + Induction completion date + @Model.CompletionDate?.ToString(UiDefaults.DateOnlyDisplayFormat) } - } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml.cs index 541fcec4f..62335778a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/Induction.cshtml.cs @@ -16,6 +16,7 @@ public class InductionModel(TrsDbContext dbContext, ICrmQueryDispatcher crmQuery [FromRoute] public Guid PersonId { get; set; } + [BindProperty] public InductionStatus Status { get; set; } public DateOnly? StartDate { get; set; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs index 450fdecf2..9e57f1c46 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs @@ -169,6 +169,42 @@ public string AlertDeleteCheckAnswers(Guid alertId, JourneyInstanceId journeyIns public string AlertDeleteCheckAnswersCancel(Guid alertId, JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/Alerts/DeleteAlert/CheckAnswers", "cancel", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); + public string InductionEditStatus(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/Status", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditStatusCancel(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/Status", "cancel", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditExemptionReason(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/ExemptionReason", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditExemptionReasonCancel(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/ExemptionReason", "cancel", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditStartDate(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/StartDate", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditStartDateCancel(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/StartDate", "cancel", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditCompletedDate(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/CompletedDate", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionEditCompletedDateCancel(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/CompletedDate", "cancel", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionChangeReason(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/InductionChangeReason", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionChangeReasonCancel(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/InductionChangeReason", "cancel", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionCheckYourAnswers(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/CheckYourAnswers", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string InductionCheckYourAnswersCancel(Guid personId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Persons/PersonDetail/EditInduction/CheckYourAnswers", routeValues: new { personId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + public string EditChangeRequest(string ticketNumber) => GetRequiredPathByPage("/ChangeRequests/EditChangeRequest/Index", routeValues: new { ticketNumber }); public string ChangeRequestDocument(string ticketNumber, Guid documentId) => GetRequiredPathByPage("/ChangeRequests/EditChangeRequest/Index", "documents", routeValues: new { ticketNumber, id = documentId }); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/EditInduction/CommonPageTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/EditInduction/CommonPageTests.cs new file mode 100644 index 000000000..785d05eb9 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/EditInduction/CommonPageTests.cs @@ -0,0 +1,185 @@ +using AngleSharp.Html.Dom; +using TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Persons.PersonDetail.EditInduction; + +public class CommonPageTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Theory] + [InlineData("edit-induction/status", InductionStatus.Exempt, "edit-induction/exemption-reasons")] + [InlineData("edit-induction/status", InductionStatus.InProgress, "edit-induction/start-date")] + [InlineData("edit-induction/status", InductionStatus.Failed, "edit-induction/start-date")] + [InlineData("edit-induction/status", InductionStatus.FailedInWales, "edit-induction/start-date")] + [InlineData("edit-induction/status", InductionStatus.Passed, "edit-induction/start-date")] + [InlineData("edit-induction/status", InductionStatus.RequiredToComplete, "edit-induction/change-reason")] + [InlineData("edit-induction/exemption-reasons", InductionStatus.Exempt, "edit-induction/change-reason")] + [InlineData("edit-induction/start-date", InductionStatus.InProgress, "edit-induction/change-reason")] + [InlineData("edit-induction/start-date", InductionStatus.Failed, "edit-induction/date-completed")] + [InlineData("edit-induction/start-date", InductionStatus.FailedInWales, "edit-induction/date-completed")] + [InlineData("edit-induction/start-date", InductionStatus.Passed, "edit-induction/date-completed")] + [InlineData("edit-induction/date-completed", InductionStatus.Failed, "edit-induction/change-reason")] + [InlineData("edit-induction/date-completed", InductionStatus.FailedInWales, "edit-induction/change-reason")] + [InlineData("edit-induction/date-completed", InductionStatus.Passed, "edit-induction/change-reason")] + [InlineData("edit-induction/change-reason", InductionStatus.Exempt, "edit-induction/check-answers")] + [InlineData("edit-induction/change-reason", InductionStatus.InProgress, "edit-induction/check-answers")] + [InlineData("edit-induction/change-reason", InductionStatus.Failed, "edit-induction/check-answers")] + [InlineData("edit-induction/change-reason", InductionStatus.FailedInWales, "edit-induction/check-answers")] + [InlineData("edit-induction/change-reason", InductionStatus.Passed, "edit-induction/check-answers")] + [InlineData("edit-induction/change-reason", InductionStatus.RequiredToComplete, "edit-induction/check-answers")] + [InlineData("edit-induction/check-answers", InductionStatus.RequiredToComplete, "induction")] + public async Task Post_RedirectsToExpectedPage(string fromPage, InductionStatus inductionStatus, string expectedNextPageUrl) + { + // Arrange + var person = await TestData.CreatePersonAsync( + p => p + .WithQts() + .WithInductionStatus(i => i + .WithStatus(inductionStatus))); + + var journeyInstance = await CreateJourneyInstanceAsync( + person.PersonId, + new EditInductionState() + { + Initialized = true, + InductionStatus = inductionStatus + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/persons/{person.PersonId}/{fromPage}?{journeyInstance.GetUniqueIdQueryParameter()}"); + if (fromPage == "edit-induction/status") + { + request.Content = new FormUrlEncodedContent(new Dictionary + { + { "InductionStatus", inductionStatus.ToString() } + }); + } + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + var location = response.Headers.Location?.OriginalString; + var expectedUrl = expectedNextPageUrl == "induction" + ? $"/persons/{person.PersonId}/{expectedNextPageUrl}" + : $"/persons/{person.PersonId}/{expectedNextPageUrl}?{journeyInstance.GetUniqueIdQueryParameter()}"; + Assert.Equal(expectedUrl, location); + } + + [Theory] + [InlineData("edit-induction/status")] + [InlineData("edit-induction/exemption-reasons")] + [InlineData("edit-induction/start-date")] + [InlineData("edit-induction/date-completed")] + [InlineData("edit-induction/change-reason")] + [InlineData("edit-induction/check-answers")] + public async Task Cancel_RedirectsToExpectedPage(string fromPage) + { + // Arrange + var person = await TestData.CreatePersonAsync(p => p.WithQts()); + + var journeyInstance = await CreateJourneyInstanceAsync( + person.PersonId, + new EditInductionState() + { + Initialized = true, + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/{fromPage}?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + var doc = await AssertEx.HtmlResponseAsync(response); + var cancelButton = doc.GetElementByTestId("cancel-button") as IHtmlButtonElement; + + // Act + var redirectRequest = new HttpRequestMessage(HttpMethod.Post, cancelButton!.FormAction); + var redirectResponse = await HttpClient.SendAsync(redirectRequest); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)redirectResponse.StatusCode); + var location = redirectResponse.Headers.Location?.OriginalString; + Assert.Equal($"/persons/{person.PersonId}/induction", location); + } + + [Theory] + [InlineData("edit-induction/status", InductionJourneyPage.Status)] + [InlineData("edit-induction/exemption-reasons", InductionJourneyPage.ExemptionReason)] + [InlineData("edit-induction/start-date", InductionJourneyPage.StartDate)] + [InlineData("edit-induction/date-completed", InductionJourneyPage.CompletedDate)] + public async Task Post_PersistsStartPage(string page, InductionJourneyPage pageName) + { + // Arrange + InductionStatus inductionStatus = InductionStatus.Passed; + var person = await TestData.CreatePersonAsync(p => p.WithQts()); + + var journeyInstance = await CreateJourneyInstanceAsync( + person.PersonId, + new EditInductionState() + { + Initialized = true, + InductionStatus = inductionStatus + }); + var request = new HttpRequestMessage(HttpMethod.Post, $"/persons/{person.PersonId}/{page}?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + journeyInstance = await ReloadJourneyInstance(journeyInstance); + Assert.Equal(pageName, journeyInstance.State.JourneyStartPage); + } + + [Theory] + [InlineData("edit-induction/status", InductionStatus.Exempt)] + [InlineData("edit-induction/status", InductionStatus.InProgress)] + [InlineData("edit-induction/status", InductionStatus.Failed)] + [InlineData("edit-induction/status", InductionStatus.FailedInWales)] + [InlineData("edit-induction/status", InductionStatus.Passed)] + [InlineData("edit-induction/status", InductionStatus.RequiredToComplete)] + [InlineData("edit-induction/exemption-reasons", InductionStatus.Exempt)] + [InlineData("edit-induction/start-date", InductionStatus.InProgress)] + [InlineData("edit-induction/start-date", InductionStatus.Failed)] + [InlineData("edit-induction/start-date", InductionStatus.FailedInWales)] + [InlineData("edit-induction/start-date", InductionStatus.Passed)] + [InlineData("edit-induction/date-completed", InductionStatus.Failed)] + [InlineData("edit-induction/date-completed", InductionStatus.FailedInWales)] + [InlineData("edit-induction/date-completed", InductionStatus.Passed)] + [InlineData("edit-induction/change-reason", InductionStatus.Exempt)] + [InlineData("edit-induction/change-reason", InductionStatus.InProgress)] + [InlineData("edit-induction/change-reason", InductionStatus.Failed)] + [InlineData("edit-induction/change-reason", InductionStatus.FailedInWales)] + [InlineData("edit-induction/change-reason", InductionStatus.Passed)] + [InlineData("edit-induction/change-reason", InductionStatus.RequiredToComplete)] + public async Task FollowingRedirect_BacklinkContainsExpected(string fromPage, InductionStatus inductionStatus) + { + // Arrange + var person = await TestData.CreatePersonAsync(p => p.WithQts()); + + var journeyInstance = await CreateJourneyInstanceAsync( + person.PersonId, + new EditInductionState() + { + Initialized = true, + InductionStatus = inductionStatus + }); + var request = new HttpRequestMessage(HttpMethod.Post, $"/persons/{person.PersonId}/{fromPage}?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + var redirectResponse = await response.FollowRedirectAsync(HttpClient); + var redirectDoc = await redirectResponse.GetDocumentAsync(); + var backlink = redirectDoc.GetElementByTestId("back-link") as IHtmlAnchorElement; + Assert.Contains(fromPage, backlink!.Href); + } + + private Task> CreateJourneyInstanceAsync(Guid personId, EditInductionState? state = null) => + CreateJourneyInstance( + JourneyNames.EditInduction, + state ?? new EditInductionState(), + new KeyValuePair("personId", personId)); +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/EditInduction/EditInductionStatusTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/EditInduction/EditInductionStatusTests.cs new file mode 100644 index 000000000..2ee303e71 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/EditInduction/EditInductionStatusTests.cs @@ -0,0 +1,43 @@ +using AngleSharp.Html.Dom; +using TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction; + +namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Persons.PersonDetail.EditInduction; + +public class EditInductionStatusTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task ContinueAndCancelButtons_ExistOnPage() + { + // Arrange + InductionStatus inductionStatus = InductionStatus.Passed; + var person = await TestData.CreatePersonAsync(); + + var journeyInstance = await CreateJourneyInstanceAsync( + person.PersonId, + new EditInductionState() + { + Initialized = true, + InductionStatus = inductionStatus + }); + var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/edit-induction/status?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + var doc = await AssertEx.HtmlResponseAsync(response); + var form = doc.GetElementByTestId("submit-form") as IHtmlFormElement; + Assert.NotNull(form); + Assert.Contains($"/persons/{person.PersonId}/edit-induction/status", form.Action); + var buttons = form.GetElementsByTagName("button").Select(button => button as IHtmlButtonElement); + Assert.Equal(2, buttons.Count()); + Assert.Equal("Continue", buttons.ElementAt(0)!.TextContent); + Assert.Equal("Cancel and return to record", buttons.ElementAt(1)!.TextContent); + } + + private Task> CreateJourneyInstanceAsync(Guid personId, EditInductionState? state = null) => + CreateJourneyInstance( + JourneyNames.EditInduction, + state ?? new EditInductionState(), + new KeyValuePair("personId", personId)); +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/InductionTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/InductionTests.cs index cca14c0bc..171050df4 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/InductionTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Persons/PersonDetail/InductionTests.cs @@ -286,4 +286,28 @@ await WithDbContext(async dbContext => var doc = await AssertEx.HtmlResponseAsync(response); Assert.Null(doc.GetElementByTestId("induction-status-warning")); } + + [Fact] + public async Task Get_WithPersonId_ShowsLinkToChangeStatus() + { + // Arrange + var setInductionStatus = InductionStatus.RequiredToComplete; + var person = await TestData.CreatePersonAsync( + builder => builder + .WithQts() + .WithInductionStatus(builder => builder + .WithStatus(setInductionStatus) + ) + ); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.ContactId}/induction"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + var doc = await AssertEx.HtmlResponseAsync(response); + var inductionStatus = doc.GetElementByTestId("induction-status"); + Assert.Contains("Change", inductionStatus!.GetElementsByTagName("a")[0].TextContent); + } }