diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EmailDelegatesControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EmailDelegatesControllerTests.cs new file mode 100644 index 0000000000..110cec9a56 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EmailDelegatesControllerTests.cs @@ -0,0 +1,127 @@ +namespace DigitalLearningSolutions.Web.Tests.Controllers.TrackingSystem.Delegates +{ + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Tests.ControllerHelpers; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates; + using FakeItEasy; + using FluentAssertions; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using NUnit.Framework; + + public class EmailDelegatesControllerTests + { + private CentreCustomPromptHelper centreCustomPromptsHelper = null!; + private EmailDelegatesController emailDelegatesController = null!; + + private HttpRequest httpRequest = null!; + private HttpResponse httpResponse = null!; + private IJobGroupsDataService jobGroupsDataService = null!; + private IPasswordResetService passwordResetService = null!; + private IUserService userService = null!; + + [SetUp] + public void Setup() + { + var centreCustomPromptsService = A.Fake(); + centreCustomPromptsHelper = new CentreCustomPromptHelper(centreCustomPromptsService); + userService = A.Fake(); + jobGroupsDataService = A.Fake(); + passwordResetService = A.Fake(); + + httpRequest = A.Fake(); + httpResponse = A.Fake(); + const string cookieName = "EmailDelegateFilter"; + const string cookieValue = "JobGroupId|JobGroupId|1"; + + emailDelegatesController = new EmailDelegatesController( + centreCustomPromptsHelper, + jobGroupsDataService, + passwordResetService, + userService + ) + .WithMockHttpContext(httpRequest, cookieName, cookieValue, httpResponse) + .WithMockUser(true) + .WithMockServices() + .WithMockTempData(); + } + + [Test] + public void Index_with_no_query_parameters_uses_cookie_value_for_filterBy() + { + // When + var result = emailDelegatesController.Index(); + + // Then + result.As().Model.As().FilterBy.Should() + .Be("JobGroupId|JobGroupId|1"); + } + + [Test] + public void Index_with_query_parameters_uses_query_parameter_value_for_filterBy() + { + // Given + const string filterBy = "JobGroupId|JobGroupId|2"; + A.CallTo(() => httpRequest.Query.ContainsKey("filterBy")).Returns(true); + + // When + var result = emailDelegatesController.Index(filterBy); + + // Then + result.As().Model.As().FilterBy.Should() + .Be(filterBy); + } + + [Test] + public void Index_with_CLEAR_filterBy_query_parameter_removes_cookie() + { + // Given + const string filterBy = "CLEAR"; + + // When + var result = emailDelegatesController.Index(filterBy); + + // Then + A.CallTo(() => httpResponse.Cookies.Delete("EmailDelegateFilter")).MustHaveHappened(); + result.As().Model.As().FilterBy.Should() + .BeNull(); + } + + [Test] + public void Index_with_null_filterBy_and_new_filter_query_parameter_adds_new_cookie_value() + { + // Given + const string? filterBy = null; + const string newFilterValue = "JobGroupId|JobGroupId|2"; + + // When + var result = emailDelegatesController.Index(filterBy, newFilterValue); + + // Then + A.CallTo(() => httpResponse.Cookies.Append("EmailDelegateFilter", newFilterValue, A._)) + .MustHaveHappened(); + result.As().Model.As().FilterBy.Should() + .Be(newFilterValue); + } + + [Test] + public void Index_with_CLEAR_filterBy_and_new_filter_query_parameter_sets_new_cookie_value() + { + // Given + const string filterBy = "CLEAR"; + const string newFilterValue = "JobGroupId|JobGroupId|2"; + + // When + var result = emailDelegatesController.Index(filterBy, newFilterValue); + + // Then + A.CallTo(() => httpResponse.Cookies.Append("EmailDelegateFilter", newFilterValue, A._)) + .MustHaveHappened(); + result.As().Model.As().FilterBy.Should() + .Be(newFilterValue); + } + } +} diff --git a/DigitalLearningSolutions.Web.Tests/Helpers/FilteringHelperTests.cs b/DigitalLearningSolutions.Web.Tests/Helpers/FilteringHelperTests.cs index cfbbc6b020..0786dce087 100644 --- a/DigitalLearningSolutions.Web.Tests/Helpers/FilteringHelperTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Helpers/FilteringHelperTests.cs @@ -3,16 +3,26 @@ using System.Linq; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Tests.TestHelpers; + using FakeItEasy; using FluentAssertions; + using Microsoft.AspNetCore.Http; using NUnit.Framework; public class FilteringHelperTests { private const string FilterByAlphaBravoCharlie = "Group|Name|Alpha╡Group|Name|Bravo╡Group|Name|Charlie"; + private const string CookieName = "TestFilterCookie"; private static readonly SortableItem ItemA1 = new SortableItem("a", 1); private static readonly SortableItem ItemA3 = new SortableItem("a", 3); private static readonly SortableItem ItemB2 = new SortableItem("b", 2); private static readonly IQueryable InputItems = new[] { ItemA1, ItemA3, ItemB2 }.AsQueryable(); + private HttpRequest httpRequest = null!; + + [SetUp] + public void Setup() + { + httpRequest = A.Fake(); + } [Test] public void FilterItems_returns_expected_items_with_single_filter() @@ -139,5 +149,60 @@ public void AddNewFilterToFilterBy_appends_new_filter_even_if_substring(string f // Then result.Should().Be($"{filterBy}╡{newFilterValue}"); } + + [Test] + public void GetFilterBy_with_no_parameters_returns_cookie_value() + { + // Given + const string CookieValue = "Cookie Value"; + A.CallTo(() => httpRequest.Cookies.ContainsKey(CookieName)).Returns(true); + A.CallTo(() => httpRequest.Cookies[CookieName]).Returns(CookieValue); + + // When + var result = FilteringHelper.GetFilterBy(null, null, httpRequest, CookieName); + + // Then + result.Should().Be(CookieValue); + } + + [Test] + public void GetFilterBy_with_no_parameters_and_no_cookies_returns_defaultFilterValue() + { + // When + var result = FilteringHelper.GetFilterBy(null, null, httpRequest, CookieName, "default-filter"); + + // Then + result.Should().Be("default-filter"); + } + + [Test] + public void GetFilterBy_with_CLEAR_filterBy_and_no_filterValue_returns_null() + { + // When + var result = FilteringHelper.GetFilterBy("CLEAR", null, httpRequest, CookieName); + + // Then + result.Should().BeNull(); + } + + [Test] + public void GetFilterBy_with_CLEAR_filterBy_and_set_filterValue_returns_filterValue() + { + // When + var result = FilteringHelper.GetFilterBy("CLEAR", "filter-value", httpRequest, CookieName); + + // Then + result.Should().Be("filter-value"); + } + + [Test] + public void GetFilterBy_with_filterBy_and_filterValue_returns_combined_filter_by() + { + // When + var result = FilteringHelper.GetFilterBy("filter-by", "filter-value", httpRequest, CookieName); + + // Then + result.Should().Be("filter-by╡filter-value"); + } } } diff --git a/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelFilterOptionsTests.cs b/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelFilterOptionsTests.cs new file mode 100644 index 0000000000..f67b397293 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelFilterOptionsTests.cs @@ -0,0 +1,137 @@ +namespace DigitalLearningSolutions.Web.Tests.ViewModels.TrackingSystem.Delegates.EmailDelegates +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.Models.CustomPrompts; + using DigitalLearningSolutions.Data.Tests.TestHelpers; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Models.Enums; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates; + using FluentAssertions; + using NUnit.Framework; + + public class EmailDelegatesViewModelFilterOptionsTests + { + [Test] + public void GetEmailDelegatesFilterViewModels_should_return_correct_job_group_filter() + { + // Given + var (jobGroups, expectedFilter) = GetSampleJobGroupsAndFilter(); + + // When + var result = + EmailDelegatesViewModelFilterOptions.GetEmailDelegatesFilterViewModels(jobGroups, new List()); + + // Then + result.Should().ContainEquivalentOf(expectedFilter); + } + + [Test] + public void GetEmailDelegatesFilterViewModels_should_return_correct_custom_prompt_filters() + { + // Given + var (customPrompts, expectedFilters) = GetSampleCustomPromptsAndFilters(); + + // When + var result = EmailDelegatesViewModelFilterOptions.GetEmailDelegatesFilterViewModels( + new List<(int, string)>(), + customPrompts + ); + + // Then + expectedFilters.ForEach(expectedFilter => result.Should().ContainEquivalentOf(expectedFilter)); + } + + private (IEnumerable<(int id, string name)> jobGroups, FilterViewModel filter) GetSampleJobGroupsAndFilter() + { + var jobGroups = new List<(int id, string name)> { (1, "J 1"), (2, "J 2") }; + + var jobGroupOptions = new[] + { + new FilterOptionViewModel( + "J 1", + "JobGroupId" + FilteringHelper.Separator + + "JobGroupId" + FilteringHelper.Separator + 1, + FilterStatus.Default + ), + new FilterOptionViewModel( + "J 2", + "JobGroupId" + FilteringHelper.Separator + + "JobGroupId" + FilteringHelper.Separator + 2, + FilterStatus.Default + ) + }; + var jobGroupFilter = new FilterViewModel("JobGroupId", "Job Group", jobGroupOptions); + + return (jobGroups, jobGroupFilter); + } + + private (List customPrompts, List filters) GetSampleCustomPromptsAndFilters() + { + var customPrompt1 = CustomPromptsTestHelper.GetDefaultCustomPrompt( + 1, + "First prompt", + "Clinical\r\nNon-Clinical" + ); + var customPrompt3 = CustomPromptsTestHelper.GetDefaultCustomPrompt(3); + var customPrompt4 = CustomPromptsTestHelper.GetDefaultCustomPrompt(4, "Fourth prompt", "C 1\r\nC 2\r\nC 3"); + var customPrompts = new List { customPrompt1, customPrompt3, customPrompt4 }; + + var customPrompt1Options = new[] + { + new FilterOptionViewModel( + "Clinical", + "Answer1" + FilteringHelper.Separator + + "Answer1" + FilteringHelper.Separator + "Clinical", + FilterStatus.Default + ), + new FilterOptionViewModel( + "Non-Clinical", + "Answer1" + FilteringHelper.Separator + + "Answer1" + FilteringHelper.Separator + "Non-Clinical", + FilterStatus.Default + ), + new FilterOptionViewModel( + "No option selected", + "Answer1" + FilteringHelper.Separator + + "Answer1" + FilteringHelper.Separator + FilteringHelper.EmptyValue, + FilterStatus.Default + ) + }; + var customPrompt4Options = new[] + { + new FilterOptionViewModel( + "C 1", + "Answer4" + FilteringHelper.Separator + + "Answer4" + FilteringHelper.Separator + "C 1", + FilterStatus.Default + ), + new FilterOptionViewModel( + "C 2", + "Answer4" + FilteringHelper.Separator + + "Answer4" + FilteringHelper.Separator + "C 2", + FilterStatus.Default + ), + new FilterOptionViewModel( + "C 3", + "Answer4" + FilteringHelper.Separator + + "Answer4" + FilteringHelper.Separator + "C 3", + FilterStatus.Default + ), + new FilterOptionViewModel( + "No option selected", + "Answer4" + FilteringHelper.Separator + + "Answer4" + FilteringHelper.Separator + FilteringHelper.EmptyValue, + FilterStatus.Default + ) + }; + var customPromptFilters = new List + { + new FilterViewModel("CustomPrompt1", "First prompt", customPrompt1Options), + new FilterViewModel("CustomPrompt4", "Fourth prompt", customPrompt4Options) + }; + + return (customPrompts, customPromptFilters); + } + } +} diff --git a/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelTests.cs b/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelTests.cs new file mode 100644 index 0000000000..ec66787261 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelTests.cs @@ -0,0 +1,192 @@ +namespace DigitalLearningSolutions.Web.Tests.ViewModels.TrackingSystem.Delegates.EmailDelegates +{ + using System; + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.CustomPrompts; + using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates; + using FluentAssertions; + using NUnit.Framework; + + public class EmailDelegatesViewModelTests + { + private readonly DelegateUserCard[] delegateUsers = + { + new DelegateUserCard { Id = 1, FirstName = "a", LastName = "Surname", Answer4 = string.Empty }, + new DelegateUserCard { Id = 2, FirstName = "b purple", LastName = "Surname", Answer4 = string.Empty }, + new DelegateUserCard { Id = 3, FirstName = "c", LastName = "Surname" }, + new DelegateUserCard { Id = 4, FirstName = "d purple", LastName = "Surname" }, + new DelegateUserCard { Id = 5, FirstName = "e", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 6, FirstName = "f", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 7, FirstName = "g", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 8, FirstName = "h", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 9, FirstName = "i", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 10, FirstName = "j", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 11, FirstName = "k", LastName = "Surname", DateRegistered = DateTime.Today }, + new DelegateUserCard { Id = 12, FirstName = "l", LastName = "Surname", Answer4 = "C 1" }, + new DelegateUserCard { Id = 13, FirstName = "m", LastName = "Surname", Answer4 = "C 2" }, + new DelegateUserCard { Id = 14, FirstName = "n", LastName = "Surname", Answer4 = "C 2" }, + new DelegateUserCard + { Id = 15, FirstName = "o", LastName = "Surname", DateRegistered = DateTime.Today.AddDays(1) } + }; + + [Test] + public void EmailDelegatesViewModel_should_return_all_delegates_on_one_page() + { + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int, string)>(), + new List(), + null + ); + + // Then + model.Delegates!.Should().HaveCount(delegateUsers.Length); + model.Delegates!.First().Name.Should().Be("a Surname"); + model.Delegates!.Last().Name.Should().Be("o Surname"); + } + + [Test] + public void EmailDelegatesViewModel_sets_delivery_date_today_by_default() + { + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int, string)>(), + new List(), + null + ); + + // Then + model.Day.Should().Be(DateTime.Today.Day); + model.Month.Should().Be(DateTime.Today.Month); + model.Year.Should().Be(DateTime.Today.Year); + } + + [Test] + public void EmailDelegatesViewModel_should_only_include_custom_prompts_with_options() + { + // Given + var customPrompts = new List + { + new CustomPrompt(1, "free text", null, true), + new CustomPrompt(2, "with options", "A\r\nB", true) + }; + + // When + var model = new EmailDelegatesViewModel(delegateUsers, new List<(int, string)>(), customPrompts, null); + + // Then + model.Filters.Should().NotContain(filter => filter.FilterProperty == "CustomPrompt1"); + model.Filters.Should().Contain(filter => filter.FilterProperty == "CustomPrompt2"); + } + + [Test] + public void EmailDelegatesViewModel_should_filter_delegates_correctly() + { + // Given + var filterBy = "Answer4" + FilteringHelper.Separator + "Answer4" + FilteringHelper.Separator + "C 2"; + + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int id, string name)>(), + new List(), + filterBy + ); + + // Then + model.Delegates!.Should().HaveCount(2); + model.Delegates!.ToList()[0].Name.Should().Be("m Surname"); + model.Delegates!.ToList()[1].Name.Should().Be("n Surname"); + model.MatchingSearchResults.Should().Be(2); + } + + [Test] + public void EmailDelegatesViewModel_should_filter_delegates_correctly_by_empty_values() + { + // Given + var filterBy = "Answer4" + FilteringHelper.Separator + "Answer4" + FilteringHelper.Separator + + FilteringHelper.EmptyValue; + + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int id, string name)>(), + new List(), + filterBy + ); + + // Then + model.Delegates!.Should().HaveCount(6); + model.Delegates!.ToList()[0].Name.Should().Be("a Surname"); + model.Delegates!.ToList()[1].Name.Should().Be("b purple Surname"); + model.Delegates!.ToList()[2].Name.Should().Be("c Surname"); + model.Delegates!.ToList()[3].Name.Should().Be("d purple Surname"); + model.Delegates!.ToList()[4].Name.Should().Be("k Surname"); + model.Delegates!.ToList()[5].Name.Should().Be("o Surname"); + model.MatchingSearchResults.Should().Be(6); + } + + [Test] + public void EmailDelegatesViewModel_should_set_all_delegates_if_filterBy_is_null() + { + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int id, string name)>(), + new List(), + null + ); + + // Then + model.Delegates!.Count().Should().Be(delegateUsers.Length); + model.MatchingSearchResults.Should().Be(delegateUsers.Length); + } + + [Test] + public void EmailDelegatesViewModel_should_set_IsDelegateSelected_values_based_on_selectedDelegateIds() + { + // Given + var selectedDelegateIds = new List { 1, 4, 7 }; + + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int id, string name)>(), + new List(), + null + ) { SelectedDelegateIds = selectedDelegateIds }; + + // Then + model.Delegates!.Count().Should().Be(delegateUsers.Length); + model.MatchingSearchResults.Should().Be(delegateUsers.Length); + model.Delegates!.Where(x => x.IsDelegateSelected).Select(x => x.Id).Should() + .BeEquivalentTo(selectedDelegateIds); + } + + [Test] + public void EmailDelegatesViewModel_should_set_all_items_IsDelegateSelected_true_if_selectAll_true() + { + // When + var model = new EmailDelegatesViewModel( + delegateUsers, + new List<(int id, string name)>(), + new List(), + null, + true + ); + + // Then + model.Delegates!.Count().Should().Be(delegateUsers.Length); + model.MatchingSearchResults.Should().Be(delegateUsers.Length); + foreach (var emailDelegatesItemViewModel in model.Delegates!) + { + emailDelegatesItemViewModel.IsDelegateSelected.Should().BeTrue(); + } + } + } +} diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/AllDelegatesController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/AllDelegatesController.cs index 7f79992f71..351f36e800 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/AllDelegatesController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/AllDelegatesController.cs @@ -41,19 +41,14 @@ public IActionResult Index( string? filterValue = null ) { - if (filterBy == null && filterValue == null) - { - filterBy = Request.Cookies.ContainsKey(DelegateFilterCookieName) - ? Request.Cookies[DelegateFilterCookieName] - : DelegateActiveStatusFilterOptions.IsActive.FilterValue; - } - else if (filterBy?.ToUpper() == FilteringHelper.ClearString) - { - filterBy = null; - } - sortBy ??= DefaultSortByOptions.Name.PropertyName; - filterBy = FilteringHelper.AddNewFilterToFilterBy(filterBy, filterValue); + filterBy = FilteringHelper.GetFilterBy( + filterBy, + filterValue, + Request, + DelegateFilterCookieName, + DelegateActiveStatusFilterOptions.IsActive.FilterValue + ); var centreId = User.GetCentreId(); var jobGroups = jobGroupsDataService.GetJobGroupsAlphabetical(); diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/EmailDelegatesController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/EmailDelegatesController.cs index c5d751d8d7..0125b243b2 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/EmailDelegatesController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/EmailDelegatesController.cs @@ -1,8 +1,9 @@ -namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates +namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates { using System; using System.Collections.Generic; using System.Linq; + using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Data.Services; using DigitalLearningSolutions.Web.Helpers; @@ -16,33 +17,68 @@ [Route("TrackingSystem/Delegates/Email")] public class EmailDelegatesController : Controller { + private const string EmailDelegateFilterCookieName = "EmailDelegateFilter"; + private readonly CentreCustomPromptHelper centreCustomPromptHelper; + private readonly IJobGroupsDataService jobGroupsDataService; private readonly IPasswordResetService passwordResetService; private readonly IUserService userService; - public EmailDelegatesController(IUserService userService, IPasswordResetService passwordResetService) + public EmailDelegatesController( + CentreCustomPromptHelper centreCustomPromptHelper, + IJobGroupsDataService jobGroupsDataService, + IPasswordResetService passwordResetService, + IUserService userService + ) { - this.userService = userService; + this.centreCustomPromptHelper = centreCustomPromptHelper; + this.jobGroupsDataService = jobGroupsDataService; this.passwordResetService = passwordResetService; + this.userService = userService; } [HttpGet] - public IActionResult Index() + public IActionResult Index( + string? filterBy = null, + string? filterValue = null, + bool selectAll = false + ) { + var newFilterBy = FilteringHelper.GetFilterBy(filterBy, filterValue, Request, EmailDelegateFilterCookieName); + var jobGroups = jobGroupsDataService.GetJobGroupsAlphabetical(); + var customPrompts = centreCustomPromptHelper.GetCustomPromptsForCentre(User.GetCentreId()); var delegateUsers = GetDelegateUserCards(); - var model = new EmailDelegatesViewModel(delegateUsers); + + var model = new EmailDelegatesViewModel( + delegateUsers, + jobGroups, + customPrompts, + newFilterBy, + selectAll + ); + + Response.UpdateOrDeleteFilterCookie(EmailDelegateFilterCookieName, newFilterBy); return View(model); } [HttpPost] - public IActionResult Index(EmailDelegatesViewModel model) + public IActionResult Index(EmailDelegatesViewModel model, string? filterBy = null, string? filterValue = null) { var delegateUsers = GetDelegateUserCards(); if (!ModelState.IsValid) { - model.SetDelegates(delegateUsers); - return View(model); + var newFilterBy = FilteringHelper.GetFilterBy(filterBy, filterValue, Request, EmailDelegateFilterCookieName); + var jobGroups = jobGroupsDataService.GetJobGroupsAlphabetical(); + var customPrompts = centreCustomPromptHelper.GetCustomPromptsForCentre(User.GetCentreId()); + var viewModel = new EmailDelegatesViewModel(delegateUsers, jobGroups, customPrompts, newFilterBy) + { + SelectedDelegateIds = model.SelectedDelegateIds, + Day = model.Day, + Month = model.Month, + Year = model.Year + }; + return View(viewModel); } var selectedUsers = delegateUsers.Where(user => model.SelectedDelegateIds!.Contains(user.Id)).ToList(); @@ -54,6 +90,18 @@ public IActionResult Index(EmailDelegatesViewModel model) return View("Confirmation", selectedUsers.Count); } + [Route("AllEmailDelegateItems")] + public IActionResult AllEmailDelegateItems(IEnumerable selectedIds) + { + var jobGroups = jobGroupsDataService.GetJobGroupsAlphabetical(); + var customPrompts = centreCustomPromptHelper.GetCustomPromptsForCentre(User.GetCentreId()); + var delegateUsers = GetDelegateUserCards(); + + var model = new AllEmailDelegateItemsViewModel(delegateUsers, jobGroups, customPrompts, selectedIds); + + return View(model); + } + private IEnumerable GetDelegateUserCards() { var centreId = User.GetCentreId(); diff --git a/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj b/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj index 4c3d986605..f5361fa7a2 100644 --- a/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj +++ b/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj @@ -40,6 +40,7 @@ + @@ -98,6 +99,7 @@ + diff --git a/DigitalLearningSolutions.Web/Helpers/FilteringHelper.cs b/DigitalLearningSolutions.Web/Helpers/FilteringHelper.cs index b0e5e69707..f29ee301d5 100644 --- a/DigitalLearningSolutions.Web/Helpers/FilteringHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/FilteringHelper.cs @@ -5,6 +5,7 @@ using System.Linq; using DigitalLearningSolutions.Data.Models; using DigitalLearningSolutions.Web.Extensions; + using Microsoft.AspNetCore.Http; public static class FilteringHelper { @@ -33,6 +34,29 @@ public static string BuildFilterValueString(string group, string propertyName, s return filterBy + FilterSeparator + newFilterValue; } + public static string? GetFilterBy( + string? filterBy, + string? filterValue, + HttpRequest request, + string cookieName, + string? defaultFilterValue = null + ) + { + if (filterBy == null && filterValue == null) + { + return request.Cookies.ContainsKey(cookieName) + ? request.Cookies[cookieName] + : defaultFilterValue; + } + + if (filterBy?.ToUpper() == ClearString) + { + filterBy = null; + } + + return AddNewFilterToFilterBy(filterBy, filterValue); + } + public static IEnumerable FilterItems( IQueryable items, string? filterBy diff --git a/DigitalLearningSolutions.Web/Scripts/learningPortal/available.ts b/DigitalLearningSolutions.Web/Scripts/learningPortal/available.ts index 30fcdbfd33..c874e35ee5 100644 --- a/DigitalLearningSolutions.Web/Scripts/learningPortal/available.ts +++ b/DigitalLearningSolutions.Web/Scripts/learningPortal/available.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('LearningPortal/AllAvailableItems'); +new SearchSortFilterAndPaginate('LearningPortal/AllAvailableItems', true, true, false); diff --git a/DigitalLearningSolutions.Web/Scripts/learningPortal/completed.ts b/DigitalLearningSolutions.Web/Scripts/learningPortal/completed.ts index 53bf52a7bd..88dc35289f 100644 --- a/DigitalLearningSolutions.Web/Scripts/learningPortal/completed.ts +++ b/DigitalLearningSolutions.Web/Scripts/learningPortal/completed.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('LearningPortal/AllCompletedItems'); +new SearchSortFilterAndPaginate('LearningPortal/AllCompletedItems', true, true, false); diff --git a/DigitalLearningSolutions.Web/Scripts/learningPortal/current.ts b/DigitalLearningSolutions.Web/Scripts/learningPortal/current.ts index 10aefed5c2..c753a38e47 100644 --- a/DigitalLearningSolutions.Web/Scripts/learningPortal/current.ts +++ b/DigitalLearningSolutions.Web/Scripts/learningPortal/current.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('LearningPortal/AllCurrentItems'); +new SearchSortFilterAndPaginate('LearningPortal/AllCurrentItems', true, true, false); diff --git a/DigitalLearningSolutions.Web/Scripts/searchSortFilterAndPaginate/searchSortFilterAndPaginate.ts b/DigitalLearningSolutions.Web/Scripts/searchSortFilterAndPaginate/searchSortFilterAndPaginate.ts index 2640d9dda9..6644949459 100644 --- a/DigitalLearningSolutions.Web/Scripts/searchSortFilterAndPaginate/searchSortFilterAndPaginate.ts +++ b/DigitalLearningSolutions.Web/Scripts/searchSortFilterAndPaginate/searchSortFilterAndPaginate.ts @@ -20,11 +20,17 @@ export interface ISearchableData { export class SearchSortFilterAndPaginate { private page: number; + private readonly searchEnabled: boolean; + + private readonly paginationEnabled: boolean; + private readonly filterEnabled: boolean; // Route proved should be a relative path with no leading / - constructor(route: string, filterEnabled = false, filterCookieName = '') { + constructor(route: string, searchEnabled: boolean, paginationEnabled: boolean, filterEnabled: boolean, filterCookieName = '') { this.page = 1; + this.searchEnabled = searchEnabled; + this.paginationEnabled = paginationEnabled; this.filterEnabled = filterEnabled; SearchSortFilterAndPaginate.getSearchableElements(route).then((searchableData) => { @@ -35,13 +41,18 @@ export class SearchSortFilterAndPaginate { if (filterEnabled) { setUpFilter(() => this.onFilterUpdated(searchableData), filterCookieName); } + if (searchEnabled) { + setUpSearch(() => this.onSearchUpdated(searchableData)); + } - setUpSearch(() => this.onSearchUpdated(searchableData)); setUpSort(() => this.searchSortAndPaginate(searchableData)); - setUpPagination( - () => this.onNextPagePressed(searchableData), - () => this.onPreviousPagePressed(searchableData), - ); + + if (paginationEnabled) { + setUpPagination( + () => this.onNextPagePressed(searchableData), + () => this.onPreviousPagePressed(searchableData), + ); + } this.searchSortAndPaginate(searchableData); }); } @@ -70,7 +81,9 @@ export class SearchSortFilterAndPaginate { } private searchSortAndPaginate(searchableData: ISearchableData): void { - const searchedElements = search(searchableData.searchableElements); + const searchedElements = this.searchEnabled + ? search(searchableData.searchableElements) + : searchableData.searchableElements; const filteredElements = this.filterEnabled ? filterSearchableElements(searchedElements, searchableData.possibleFilters) : searchedElements; @@ -79,7 +92,9 @@ export class SearchSortFilterAndPaginate { SearchSortFilterAndPaginate.updateResultCount(sortedElements.length); const totalPages = Math.ceil(sortedElements.length / ITEMS_PER_PAGE); - const paginatedElements = paginateResults(sortedElements, this.page, totalPages); + const paginatedElements = this.paginationEnabled + ? paginateResults(sortedElements, this.page, totalPages) + : sortedElements; SearchSortFilterAndPaginate.displaySearchableElements(paginatedElements); } diff --git a/DigitalLearningSolutions.Web/Scripts/supervisor/staffList.ts b/DigitalLearningSolutions.Web/Scripts/supervisor/staffList.ts index 18a506e8ea..86212df57d 100644 --- a/DigitalLearningSolutions.Web/Scripts/supervisor/staffList.ts +++ b/DigitalLearningSolutions.Web/Scripts/supervisor/staffList.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('Supervisor/AllStaffList'); +new SearchSortFilterAndPaginate('Supervisor/AllStaffList', true, true, false); diff --git a/DigitalLearningSolutions.Web/Scripts/trackingSystem/allDelegates.ts b/DigitalLearningSolutions.Web/Scripts/trackingSystem/allDelegates.ts index 3cc6d0a7a6..5c852f257e 100644 --- a/DigitalLearningSolutions.Web/Scripts/trackingSystem/allDelegates.ts +++ b/DigitalLearningSolutions.Web/Scripts/trackingSystem/allDelegates.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('TrackingSystem/Delegates/All/AllDelegateItems', true, 'DelegateFilter'); +new SearchSortFilterAndPaginate('TrackingSystem/Delegates/All/AllDelegateItems', true, true, true, 'DelegateFilter'); diff --git a/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreAdministrators.ts b/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreAdministrators.ts index 8efd2e00d1..14bb679db7 100644 --- a/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreAdministrators.ts +++ b/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreAdministrators.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('TrackingSystem/Centre/Administrators/AllAdmins', true, 'AdminFilter'); +new SearchSortFilterAndPaginate('TrackingSystem/Centre/Administrators/AllAdmins', true, true, true, 'AdminFilter'); diff --git a/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreCourseSetup.ts b/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreCourseSetup.ts index 7cd0f853dc..1dcdf7fddc 100644 --- a/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreCourseSetup.ts +++ b/DigitalLearningSolutions.Web/Scripts/trackingSystem/centreCourseSetup.ts @@ -1,7 +1,7 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('TrackingSystem/CourseSetup/AllCourseStatistics', true, 'CourseFilter'); +new SearchSortFilterAndPaginate('TrackingSystem/CourseSetup/AllCourseStatistics', true, true, true, 'CourseFilter'); const copyCourseLinkClass = 'copy-course-button'; const copyLinkIdPrefix = 'copy-course-'; diff --git a/DigitalLearningSolutions.Web/Scripts/trackingSystem/delegateGroups.ts b/DigitalLearningSolutions.Web/Scripts/trackingSystem/delegateGroups.ts index 4b0e6d84cd..1be0cee83a 100644 --- a/DigitalLearningSolutions.Web/Scripts/trackingSystem/delegateGroups.ts +++ b/DigitalLearningSolutions.Web/Scripts/trackingSystem/delegateGroups.ts @@ -1,4 +1,4 @@ import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; // eslint-disable-next-line no-new -new SearchSortFilterAndPaginate('TrackingSystem/Delegates/Groups/AllDelegateGroups', true, 'DelegateGroupsFilter'); +new SearchSortFilterAndPaginate('TrackingSystem/Delegates/Groups/AllDelegateGroups', true, true, true, 'DelegateGroupsFilter'); diff --git a/DigitalLearningSolutions.Web/Scripts/trackingSystem/emailDelegates.ts b/DigitalLearningSolutions.Web/Scripts/trackingSystem/emailDelegates.ts new file mode 100644 index 0000000000..d688903034 --- /dev/null +++ b/DigitalLearningSolutions.Web/Scripts/trackingSystem/emailDelegates.ts @@ -0,0 +1,31 @@ +import { SearchSortFilterAndPaginate } from '../searchSortFilterAndPaginate/searchSortFilterAndPaginate'; + +const selectedElements = document.querySelectorAll('.delegate-checkbox:checked') as NodeListOf; +const selectedIds = Array.from(selectedElements).map((el) => el.value); +const queryParams = selectedIds.map((id, index) => `selectedIds[${index}]=${id}`); +const route = `TrackingSystem/Delegates/Email/AllEmailDelegateItems?${queryParams.join('&')}`; + +// eslint-disable-next-line no-new +new SearchSortFilterAndPaginate(route, false, false, true, 'EmailDelegateFilter'); + +function selectAll(): void { + const allCheckboxes = document.querySelectorAll('.delegate-checkbox') as NodeListOf; + allCheckboxes.forEach((checkbox) => { + if (!checkbox.checked) checkbox.click(); + }); +} + +function deselectAll(): void { + const allCheckboxes = document.querySelectorAll('.delegate-checkbox') as NodeListOf; + allCheckboxes.forEach((checkbox) => { + if (checkbox.checked) checkbox.click(); + }); +} + +const selectAllForm = document.getElementById('select-all-form') as HTMLFormElement; +const selectAllButton = document.getElementById('select-all-button') as HTMLButtonElement; +const deselectAllButton = document.getElementById('deselect-all-button') as HTMLButtonElement; + +selectAllForm.addEventListener('submit', (e) => e.preventDefault()); +selectAllButton.addEventListener('click', selectAll); +deselectAllButton.addEventListener('click', deselectAll); diff --git a/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/emailDelegates.scss b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/emailDelegates.scss new file mode 100644 index 0000000000..d5f5e2788d --- /dev/null +++ b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/emailDelegates.scss @@ -0,0 +1,7 @@ +@import "../../shared/searchableElements/searchableElements"; + +.select-all-button { + @extend .right-submit-button; + border-radius: 4px; + margin-right: nhsuk-spacing(2); +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModelFilterOptions.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModelFilterOptions.cs index b93f133758..75b7c3c929 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModelFilterOptions.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/AllDelegatesViewModelFilterOptions.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Linq; using DigitalLearningSolutions.Data.Models.CustomPrompts; - using DigitalLearningSolutions.Data.Models.User; - using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Helpers.FilterOptions; - using DigitalLearningSolutions.Web.Models.Enums; using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.Shared; public static class AllDelegatesViewModelFilterOptions { @@ -36,51 +34,6 @@ public static class AllDelegatesViewModelFilterOptions DelegateRegistrationTypeFilterOptions.RegisteredByCentre }; - public static IEnumerable GetJobGroupOptions( - IEnumerable<(int id, string name)> jobGroups - ) - { - return jobGroups.Select( - jobGroup => new FilterOptionViewModel( - jobGroup.name, - FilteringHelper.BuildFilterValueString( - nameof(DelegateUserCard.JobGroupId), - nameof(DelegateUserCard.JobGroupId), - jobGroup.id.ToString() - ), - FilterStatus.Default - ) - ); - } - - public static IEnumerable GetCustomPromptOptions( - CustomPrompt customPrompt - ) - { - string filterValueName = - CentreCustomPromptHelper.GetDelegateCustomPromptAnswerName(customPrompt.CustomPromptNumber); - - var options = customPrompt.Options.Select( - option => new FilterOptionViewModel( - option, - FilteringHelper.BuildFilterValueString(filterValueName, filterValueName, option), - FilterStatus.Default - ) - ).ToList(); - options.Add( - new FilterOptionViewModel( - "No option selected", - FilteringHelper.BuildFilterValueString( - filterValueName, - filterValueName, - FilteringHelper.EmptyValue.ToString() - ), - FilterStatus.Default - ) - ); - return options; - } - public static List GetAllDelegatesFilterViewModels( IEnumerable<(int id, string name)> jobGroups, IEnumerable promptsWithOptions @@ -88,38 +41,22 @@ IEnumerable promptsWithOptions { var filters = new List { - new FilterViewModel( - "PasswordStatus", - "Password Status", - PasswordStatusOptions - ), - new FilterViewModel( - "AdminStatus", - "Admin Status", - AdminStatusOptions - ), - new FilterViewModel( - "ActiveStatus", - "Active Status", - ActiveStatusOptions - ), + new FilterViewModel("PasswordStatus", "Password Status", PasswordStatusOptions), + new FilterViewModel("AdminStatus", "Admin Status", AdminStatusOptions), + new FilterViewModel("ActiveStatus", "Active Status", ActiveStatusOptions), new FilterViewModel( "JobGroupId", "Job Group", - GetJobGroupOptions(jobGroups) + DelegatesViewModelFilters.GetJobGroupOptions(jobGroups) ), - new FilterViewModel( - "RegistrationType", - "Registration Type", - RegistrationTypeOptions - ) + new FilterViewModel("RegistrationType", "Registration Type", RegistrationTypeOptions) }; filters.AddRange( promptsWithOptions.Select( customPrompt => new FilterViewModel( $"CustomPrompt{customPrompt.CustomPromptNumber}", customPrompt.CustomPromptText, - GetCustomPromptOptions(customPrompt) + DelegatesViewModelFilters.GetCustomPromptOptions(customPrompt) ) ) ); diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/SearchableDelegateViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/SearchableDelegateViewModel.cs index 8ada9531a4..6a516706ef 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/SearchableDelegateViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/AllDelegates/SearchableDelegateViewModel.cs @@ -1,7 +1,6 @@ namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.AllDelegates { using System.Collections.Generic; - using System.Linq; using DigitalLearningSolutions.Data.Models.CustomPrompts; using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Web.Helpers; @@ -19,7 +18,10 @@ IEnumerable promptsWithOptions { DelegateInfo = new DelegateInfoViewModel(delegateUser, customFields); Tags = FilterableTagHelper.GetCurrentTagsForDelegateUser(delegateUser); - CustomPromptFilters = GetCustomPromptFilters(promptsWithOptions); + CustomPromptFilters = DelegatesViewModelFilters.GetCustomPromptFilters( + DelegateInfo.CustomFields, + promptsWithOptions + ); } public DelegateInfoViewModel DelegateInfo { get; set; } @@ -31,33 +33,5 @@ IEnumerable promptsWithOptions ); public Dictionary CustomPromptFilters { get; set; } - - private Dictionary GetCustomPromptFilters(IEnumerable promptsWithOptions) - { - var closedCustomPromptIds = promptsWithOptions.Select(c => c.CustomPromptNumber); - var closedCustomFields = DelegateInfo.CustomFields - .Where(customField => closedCustomPromptIds.Contains(customField.CustomFieldId)); - return closedCustomFields - .Select( - customField => new KeyValuePair( - customField.CustomFieldId, - GetFilterValueForCustomField(customField) - ) - ).ToDictionary(x => x.Key, x => x.Value); - } - - private string GetFilterValueForCustomField(CustomFieldViewModel customField) - { - string filterValueName = - CentreCustomPromptHelper.GetDelegateCustomPromptAnswerName(customField.CustomFieldId); - string propertyValue = string.IsNullOrEmpty(customField.Answer) - ? FilteringHelper.EmptyValue.ToString() - : customField.Answer; - return FilteringHelper.BuildFilterValueString( - filterValueName, - filterValueName, - propertyValue - ); - } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/AllEmailDelegateItemsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/AllEmailDelegateItemsViewModel.cs new file mode 100644 index 0000000000..e192897a20 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/AllEmailDelegateItemsViewModel.cs @@ -0,0 +1,37 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.CustomPrompts; + using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + + public class AllEmailDelegateItemsViewModel : BaseJavaScriptFilterableViewModel + { + public readonly IEnumerable Delegates; + + public AllEmailDelegateItemsViewModel( + IEnumerable delegateUserCards, + IEnumerable<(int id, string name)> jobGroups, + IEnumerable customPrompts, + IEnumerable selectedDelegateIds + ) + { + var promptsWithOptions = customPrompts.Where(customPrompt => customPrompt.Options.Count > 0); + Delegates = delegateUserCards.Select( + delegateUser => + { + var isDelegateSelected = selectedDelegateIds.Contains(delegateUser.Id); + var customFields = CentreCustomPromptHelper.GetCustomFieldViewModels(delegateUser, customPrompts); + return new EmailDelegatesItemViewModel(delegateUser, isDelegateSelected, customFields, promptsWithOptions); + } + ); + + Filters = EmailDelegatesViewModelFilterOptions + .GetEmailDelegatesFilterViewModels(jobGroups, promptsWithOptions) + .SelectAppliedFilterViewModels(); + } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs index fad9b31785..47c6bdd375 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs @@ -1,11 +1,38 @@ namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates { - using DigitalLearningSolutions.Web.Helpers; + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.CustomPrompts; using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.Common; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.Shared; public class EmailDelegatesItemViewModel { - public EmailDelegatesItemViewModel(DelegateUserCard delegateUser) + public EmailDelegatesItemViewModel( + DelegateUser delegateUser, + bool isDelegateSelected + ) + { + Id = delegateUser.Id; + Name = delegateUser.SearchableName; + Email = delegateUser.EmailAddress; + if (delegateUser.DateRegistered.HasValue) + { + RegistrationDate = delegateUser.DateRegistered.Value.ToString(DateHelper.StandardDateFormat); + } + + IsDelegateSelected = isDelegateSelected; + CustomPromptFilters = new Dictionary(); + } + + public EmailDelegatesItemViewModel( + DelegateUser delegateUser, + bool isDelegateSelected, + IEnumerable customFields, + IEnumerable promptsWithOptions + ) { Id = delegateUser.Id; Name = delegateUser.SearchableName; @@ -14,11 +41,25 @@ public EmailDelegatesItemViewModel(DelegateUserCard delegateUser) { RegistrationDate = delegateUser.DateRegistered.Value.ToString(DateHelper.StandardDateFormat); } + + IsDelegateSelected = isDelegateSelected; + JobGroupId = delegateUser.JobGroupId; + CustomPromptFilters = DelegatesViewModelFilters.GetCustomPromptFilters(customFields, promptsWithOptions); } public int Id { get; set; } public string Name { get; set; } public string? Email { get; set; } public string? RegistrationDate { get; set; } + public bool IsDelegateSelected { get; set; } + private int JobGroupId { get; } + + public string JobGroupFilter => FilteringHelper.BuildFilterValueString( + nameof(DelegateUserCard.JobGroupId), + nameof(DelegateUserCard.JobGroupId), + JobGroupId.ToString() + ); + + public Dictionary CustomPromptFilters { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModel.cs index 163dc52a8a..669f7efa2f 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModel.cs @@ -4,24 +4,38 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; + using DigitalLearningSolutions.Data.Models.CustomPrompts; using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; - public class EmailDelegatesViewModel : IValidatableObject + public class EmailDelegatesViewModel : BaseSearchablePageViewModel, IValidatableObject { - public EmailDelegatesViewModel() { } + public EmailDelegatesViewModel(string? filterBy) : base( + null, + 1, + true, + DelegateSortByOptions.RegistrationDate.PropertyName, + Ascending, + filterBy, + int.MaxValue + ) { } - public EmailDelegatesViewModel(IEnumerable delegateUsers) + public EmailDelegatesViewModel() : this(null) { } + + public EmailDelegatesViewModel( + IEnumerable delegateUsers, + IEnumerable<(int id, string name)> jobGroups, + IEnumerable customPrompts, + string? filterBy, + bool selectAll = false + ) : this(filterBy) { - SetDelegates(delegateUsers); Day = DateTime.Today.Day; Month = DateTime.Today.Month; Year = DateTime.Today.Year; - } - - public void SetDelegates(IEnumerable delegateUsers) - { - Delegates = delegateUsers.Select(delegateUser => new EmailDelegatesItemViewModel(delegateUser)); + SetDelegates(delegateUsers, filterBy, selectAll); + SetFilters(jobGroups, customPrompts); } public IEnumerable? Delegates { get; set; } @@ -32,11 +46,38 @@ public void SetDelegates(IEnumerable delegateUsers) public int? Day { get; set; } public int? Month { get; set; } public int? Year { get; set; } - + + public override IEnumerable<(string, string)> SortOptions { get; } = new List<(string, string)>(); + + public override bool NoDataFound => Delegates != null && !Delegates.Any() && NoSearchOrFilter; + public IEnumerable Validate(ValidationContext validationContext) { return DateValidator.ValidateDate(Day, Month, Year, "Email delivery date", true) .ToValidationResultList(nameof(Day), nameof(Month), nameof(Year)); } + + private void SetFilters(IEnumerable<(int id, string name)> jobGroups, IEnumerable customPrompts) + { + var promptsWithOptions = customPrompts.Where(customPrompt => customPrompt.Options.Count > 0); + Filters = EmailDelegatesViewModelFilterOptions.GetEmailDelegatesFilterViewModels( + jobGroups, + promptsWithOptions + ); + } + + private void SetDelegates(IEnumerable delegateUsers, string? filterBy, bool selectAll = false) + { + var filteredItems = FilteringHelper.FilterItems(delegateUsers.AsQueryable(), filterBy).ToList(); + Delegates = filteredItems.Select( + delegateUser => + { + var delegateSelected = selectAll || + SelectedDelegateIds != null && SelectedDelegateIds.Contains(delegateUser.Id); + return new EmailDelegatesItemViewModel(delegateUser, delegateSelected); + } + ); + MatchingSearchResults = Delegates.Count(); + } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelFilterOptions.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelFilterOptions.cs new file mode 100644 index 0000000000..758be5cbb0 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesViewModelFilterOptions.cs @@ -0,0 +1,36 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.CustomPrompts; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.Shared; + + public static class EmailDelegatesViewModelFilterOptions + { + public static List GetEmailDelegatesFilterViewModels( + IEnumerable<(int id, string name)> jobGroups, + IEnumerable promptsWithOptions + ) + { + var filters = new List + { + new FilterViewModel( + "JobGroupId", + "Job Group", + DelegatesViewModelFilters.GetJobGroupOptions(jobGroups) + ) + }; + filters.AddRange( + promptsWithOptions.Select( + customPrompt => new FilterViewModel( + $"CustomPrompt{customPrompt.CustomPromptNumber}", + customPrompt.CustomPromptText, + DelegatesViewModelFilters.GetCustomPromptOptions(customPrompt) + ) + ) + ); + return filters; + } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegatesViewModelFilters.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegatesViewModelFilters.cs new file mode 100644 index 0000000000..11a66a2b73 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegatesViewModelFilters.cs @@ -0,0 +1,84 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.Shared +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.CustomPrompts; + using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Models.Enums; + using DigitalLearningSolutions.Web.ViewModels.Common; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + + public static class DelegatesViewModelFilters + { + public static IEnumerable GetJobGroupOptions( + IEnumerable<(int id, string name)> jobGroups + ) + { + return jobGroups.Select( + jobGroup => new FilterOptionViewModel( + jobGroup.name, + FilteringHelper.BuildFilterValueString( + nameof(DelegateUserCard.JobGroupId), + nameof(DelegateUserCard.JobGroupId), + jobGroup.id.ToString() + ), + FilterStatus.Default + ) + ); + } + + public static IEnumerable GetCustomPromptOptions(CustomPrompt customPrompt) + { + string filterValueName = + CentreCustomPromptHelper.GetDelegateCustomPromptAnswerName(customPrompt.CustomPromptNumber); + + var options = customPrompt.Options.Select( + option => new FilterOptionViewModel( + option, + FilteringHelper.BuildFilterValueString(filterValueName, filterValueName, option), + FilterStatus.Default + ) + ).ToList(); + options.Add( + new FilterOptionViewModel( + "No option selected", + FilteringHelper.BuildFilterValueString( + filterValueName, + filterValueName, + FilteringHelper.EmptyValue.ToString() + ), + FilterStatus.Default + ) + ); + return options; + } + + public static Dictionary GetCustomPromptFilters( + IEnumerable customFields, + IEnumerable promptsWithOptions + ) + { + var promptsWithOptionsIds = promptsWithOptions.Select(c => c.CustomPromptNumber); + var customFieldsWithOptions = + customFields.Where(customField => promptsWithOptionsIds.Contains(customField.CustomFieldId)); + return customFieldsWithOptions + .Select( + customField => new KeyValuePair( + customField.CustomFieldId, + GetFilterValueForCustomField(customField) + ) + ).ToDictionary(x => x.Key, x => x.Value); + } + + private static string GetFilterValueForCustomField(CustomFieldViewModel customField) + { + string filterValueName = + CentreCustomPromptHelper.GetDelegateCustomPromptAnswerName(customField.CustomFieldId); + string propertyValue = string.IsNullOrEmpty(customField.Answer) + ? FilteringHelper.EmptyValue.ToString() + : customField.Answer; + return FilteringHelper.BuildFilterValueString(filterValueName, filterValueName, propertyValue); + } + } +} diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/AllEmailDelegateItems.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/AllEmailDelegateItems.cshtml new file mode 100644 index 0000000000..f7eb22f7f1 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/AllEmailDelegateItems.cshtml @@ -0,0 +1,22 @@ +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates +@model AllEmailDelegateItemsViewModel + +@{ + Layout = null; +} + + + + + + + + +@foreach (var delegateUser in Model.Delegates) { + +} +@foreach (var filter in Model.Filters) { + +} + + diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/Index.cshtml index a754278664..779603e642 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/Index.cshtml @@ -1,9 +1,10 @@ -@inject IConfiguration Configuration -@using Dapper +@inject IConfiguration Configuration @using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates @using Microsoft.Extensions.Configuration @model EmailDelegatesViewModel + + @{ ViewData["Title"] = "Send welcome messages"; ViewData["Application"] = "Tracking System"; @@ -11,7 +12,6 @@ ViewData["HeaderPathName"] = "Tracking System"; var errorHasOccurred = !ViewData.ModelState.IsValid; var exampleDate = DateTime.Today; - var selectedDelegateIds = Model.SelectedDelegateIds.AsList(); } @section NavMenuItems { @@ -25,32 +25,32 @@ } - Select delegates to send email to. + Filter and select delegates to send emails to. + + - @if (Model.Delegates == null || !Model.Delegates.Any()) { + @if (Model.Delegates == null || Model.NoDataFound) { } else { +
+
+
+ + + +
+
+
+ +
-
+
-
+
@foreach (var delegateModel in Model.Delegates) { -
- - -
- Registered on @delegateModel.RegistrationDate -
-
+ }
@@ -69,3 +69,7 @@
+ +@section scripts { + +} diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/_SearchableDelegateCheckbox.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/_SearchableDelegateCheckbox.cshtml new file mode 100644 index 0000000000..b37e0ca054 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/EmailDelegates/_SearchableDelegateCheckbox.cshtml @@ -0,0 +1,21 @@ +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates +@model EmailDelegatesItemViewModel + +
+ + +
+ Registered on @Model.RegistrationDate +
+ + @foreach ((_, string filterValue) in Model.CustomPromptFilters) { + + } +