diff --git a/DigitalLearningSolutions.Data.Tests/DataServices/CourseDataServiceTests.cs b/DigitalLearningSolutions.Data.Tests/DataServices/CourseDataServiceTests.cs index c6234d0fa6..072ece846e 100644 --- a/DigitalLearningSolutions.Data.Tests/DataServices/CourseDataServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/DataServices/CourseDataServiceTests.cs @@ -9,6 +9,7 @@ namespace DigitalLearningSolutions.Data.Tests.DataServices using DigitalLearningSolutions.Data.Tests.TestHelpers; using FakeItEasy; using FluentAssertions; + using FluentAssertions.Execution; using Microsoft.Extensions.Logging; using NUnit.Framework; @@ -218,14 +219,14 @@ public void GetNumberOfActiveCoursesAtCentre_with_filtered_category_returns_expe } [Test] - public void GetCourseStatisticsAtCentreForCategoryID_should_return_course_statistics_correctly() + public void GetCourseStatisticsAtCentreForAdminCategoryId_should_return_course_statistics_correctly() { // Given const int centreId = 101; const int categoryId = 0; // When - var result = courseDataService.GetCourseStatisticsAtCentreForCategoryId(centreId, categoryId).ToList(); + var result = courseDataService.GetCourseStatisticsAtCentreForAdminCategoryId(centreId, categoryId).ToList(); // Then var expectedFirstCourse = new CourseStatistics @@ -234,6 +235,7 @@ public void GetCourseStatisticsAtCentreForCategoryID_should_return_course_statis CentreId = 101, Active = false, AllCentres = false, + ApplicationId = 1, ApplicationName = "Entry Level - Win XP, Office 2003/07 OLD", CustomisationName = "Standard", DelegateCount = 25, @@ -251,7 +253,7 @@ public void GetCourseStatisticsAtCentreForCategoryID_should_return_course_statis } [Test] - public void GetCourseDetailsByIdAtCentreForCategoryId_should_return_course_details_correctly() + public void GetCourseDetailsForAdminCategoryId_should_return_course_details_correctly() { // Given const int customisationId = 100; @@ -266,7 +268,7 @@ public void GetCourseDetailsByIdAtCentreForCategoryId_should_return_course_detai // When var result = - courseDataService.GetCourseDetails(customisationId, centreId, categoryId)!; + courseDataService.GetCourseDetailsForAdminCategoryId(customisationId, centreId, categoryId)!; // Overwrite the created time as it is populated by a default constraint and not consistent over different databases result.CreatedDate = fixedCreationDateTime; @@ -316,5 +318,30 @@ public void GetDelegateCoursesAttemptStats_should_return_delegate_course_info_co totalAttempts.Should().Be(23); attemptsPassed.Should().Be(11); } + + [Test] + public void GetCoursesAtCentreForAdminCategoryId_returns_expected_values() + { + // Given + var expectedFirstCourse = new Course + { + CustomisationId = 1, + CentreId = 2, + ApplicationId = 1, + ApplicationName = "Entry Level - Win XP, Office 2003/07 OLD", + CustomisationName = "Standard", + Active = false + }; + + // When + var result = courseDataService.GetCoursesAtCentreForAdminCategoryId(2, 0).ToList(); + + // Then + using (new AssertionScope()) + { + result.Should().HaveCount(69); + result.First(c => c.CustomisationId == 1).Should().BeEquivalentTo(expectedFirstCourse); + } + } } } diff --git a/DigitalLearningSolutions.Data.Tests/DataServices/CourseDelegatesDataServiceTests.cs b/DigitalLearningSolutions.Data.Tests/DataServices/CourseDelegatesDataServiceTests.cs new file mode 100644 index 0000000000..b1794a7978 --- /dev/null +++ b/DigitalLearningSolutions.Data.Tests/DataServices/CourseDelegatesDataServiceTests.cs @@ -0,0 +1,57 @@ +namespace DigitalLearningSolutions.Data.Tests.DataServices +{ + using System; + using System.Linq; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Models.CourseDelegates; + using DigitalLearningSolutions.Data.Tests.TestHelpers; + using FluentAssertions; + using FluentAssertions.Execution; + using NUnit.Framework; + + public class CourseDelegatesDataServiceTests + { + private ICourseDelegatesDataService courseDelegatesDataService = null!; + + [SetUp] + public void Setup() + { + var connection = ServiceTestHelper.GetDatabaseConnection(); + courseDelegatesDataService = new CourseDelegatesDataService(connection); + } + + [Test] + public void GetDelegatesOnCourse_returns_expected_values() + { + // Given + var expectedFirstRecord = new CourseDelegate + { + Active = true, + CandidateNumber = "PC97", + CompleteBy = null, + DelegateId = 32926, + EmailAddress = "erpock.hs@5bntu", + Enrolled = new DateTime(2012, 07, 02, 13, 30, 37, 807), + FirstName = "xxxxx", + LastName = "xxxx", + LastUpdated = new DateTime(2012, 07, 31, 10, 18, 39, 993), + Locked = false, + ProgressId = 18395, + RemovedDate = null, + Completed = null, + AllAttempts = 0, + AttemptsPassed = 0 + }; + + // When + var result = courseDelegatesDataService.GetDelegatesOnCourse(1, 2).ToList(); + + // Then + using (new AssertionScope()) + { + result.Should().HaveCount(3); + result.First().Should().BeEquivalentTo(expectedFirstRecord); + } + } + } +} diff --git a/DigitalLearningSolutions.Data.Tests/Models/CourseStatisticTests.cs b/DigitalLearningSolutions.Data.Tests/Models/CourseStatisticTests.cs index f659460cb7..66a98005f3 100644 --- a/DigitalLearningSolutions.Data.Tests/Models/CourseStatisticTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Models/CourseStatisticTests.cs @@ -33,34 +33,5 @@ public void InProgressCount_should_be_calculated_from_total_delegates_and_total_ // Then courseStatistics.InProgressCount.Should().Be(42); } - - [Test] - public void Course_name_should_be_application_name_if_customisation_name_is_null() - { - // When - var courseStatistics = new CourseStatistics - { - ApplicationName = "Test application", - CustomisationName = null, - }; - - // Then - courseStatistics.CourseName.Should().BeEquivalentTo("Test application"); - } - - [Test] - public void Course_name_should_include_customisation_name_if_it_is_not_null() - { - // When - var courseStatistics = new CourseStatistics - { - ApplicationName = "Test application", - CustomisationName = "customisation", - }; - - // Then - courseStatistics.CourseName.Should().BeEquivalentTo("Test application - customisation"); - } - } } diff --git a/DigitalLearningSolutions.Data.Tests/Models/User/CourseTests.cs b/DigitalLearningSolutions.Data.Tests/Models/User/CourseTests.cs new file mode 100644 index 0000000000..e7b3842357 --- /dev/null +++ b/DigitalLearningSolutions.Data.Tests/Models/User/CourseTests.cs @@ -0,0 +1,37 @@ +namespace DigitalLearningSolutions.Data.Tests.Models.User +{ + using DigitalLearningSolutions.Data.Models.Courses; + using FluentAssertions; + using NUnit.Framework; + + public class CourseTests + { + [Test] + public void Course_name_should_be_application_name_if_customisation_name_is_null() + { + // When + var courseStatistics = new Course + { + ApplicationName = "Test application", + CustomisationName = string.Empty + }; + + // Then + courseStatistics.CourseName.Should().BeEquivalentTo("Test application"); + } + + [Test] + public void Course_name_should_include_customisation_name_if_it_is_not_null() + { + // When + var courseStatistics = new Course + { + ApplicationName = "Test application", + CustomisationName = "customisation" + }; + + // Then + courseStatistics.CourseName.Should().BeEquivalentTo("Test application - customisation"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Tests/Services/CourseDelegatesServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/CourseDelegatesServiceTests.cs new file mode 100644 index 0000000000..8cfb9e0bec --- /dev/null +++ b/DigitalLearningSolutions.Data.Tests/Services/CourseDelegatesServiceTests.cs @@ -0,0 +1,103 @@ +namespace DigitalLearningSolutions.Data.Tests.Services +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Models.CourseDelegates; + using DigitalLearningSolutions.Data.Models.Courses; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Data.Tests.TestHelpers; + using FakeItEasy; + using FluentAssertions; + using FluentAssertions.Execution; + using NUnit.Framework; + + public class CourseDelegatesServiceTests + { + private ICourseDataService courseDataService = null!; + private ICourseDelegatesDataService courseDelegatesDataService = null!; + private ICourseDelegatesService courseDelegatesService = null!; + + [SetUp] + public void Setup() + { + courseDataService = A.Fake(); + courseDelegatesDataService = A.Fake(); + + courseDelegatesService = new CourseDelegatesService( + courseDataService, + courseDelegatesDataService + ); + } + + [Test] + public void GetCoursesAndCourseDelegatesForCentre_populates_course_delegates_data() + { + // Given + const int centreId = 2; + const int categoryId = 1; + A.CallTo(() => courseDataService.GetCoursesAtCentreForAdminCategoryId(centreId, categoryId)) + .Returns(new List { new Course { CustomisationId = 1 } }); + A.CallTo(() => courseDelegatesDataService.GetDelegatesOnCourse(A._, A._)) + .Returns(new List { new CourseDelegate() }); + + // When + var result = courseDelegatesService.GetCoursesAndCourseDelegatesForCentre( + centreId, + categoryId, + null + ); + + // Then + using (new AssertionScope()) + { + result.Courses.Should().HaveCount(1); + result.Delegates.Should().HaveCount(1); + result.CustomisationId.Should().Be(1); + } + } + + [Test] + public void GetCoursesAndCourseDelegatesForCentre_contains_empty_lists_with_no_courses_in_category() + { + // Given + A.CallTo(() => courseDataService.GetCoursesAtCentreForAdminCategoryId(2, 7)).Returns(new List()); + + // When + var result = courseDelegatesService.GetCoursesAndCourseDelegatesForCentre(2, 7, null); + + // Then + using (new AssertionScope()) + { + A.CallTo(() => courseDelegatesDataService.GetDelegatesOnCourse(A._, A._)) + .MustNotHaveHappened(); + result.Courses.Should().BeEmpty(); + result.Delegates.Should().BeEmpty(); + result.CustomisationId.Should().BeNull(); + } + } + + [Test] + public void GetCoursesAndCourseDelegatesForCentre_uses_passed_in_customisation_id() + { + // Given + const int customisationId = 2; + const int centreId = 2; + const int categoryId = 1; + A.CallTo(() => courseDataService.GetCoursesAtCentreForAdminCategoryId(centreId, categoryId)) + .Returns(new List { new Course { CustomisationId = 1 } }); + A.CallTo(() => courseDelegatesDataService.GetDelegatesOnCourse(A._, A._)) + .Returns(new List { new CourseDelegate() }); + + // When + var result = courseDelegatesService.GetCoursesAndCourseDelegatesForCentre( + centreId, + categoryId, + customisationId + ); + + // Then + A.CallTo(() => courseDelegatesDataService.GetDelegatesOnCourse(customisationId, centreId)) + .MustHaveHappened(); + } + } +} diff --git a/DigitalLearningSolutions.Data.Tests/Services/CourseServiceTests.cs b/DigitalLearningSolutions.Data.Tests/Services/CourseServiceTests.cs index d3f37a0385..a2f8a38d5e 100644 --- a/DigitalLearningSolutions.Data.Tests/Services/CourseServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/Services/CourseServiceTests.cs @@ -21,7 +21,7 @@ public class CourseServiceTests public void Setup() { courseDataService = A.Fake(); - A.CallTo(() => courseDataService.GetCourseStatisticsAtCentreForCategoryId(CentreId, AdminCategoryId)) + A.CallTo(() => courseDataService.GetCourseStatisticsAtCentreForAdminCategoryId(CentreId, AdminCategoryId)) .Returns(GetSampleCourses()); courseAdminFieldsService = A.Fake(); courseService = new CourseService(courseDataService, courseAdminFieldsService); diff --git a/DigitalLearningSolutions.Data/DataServices/CourseDataService.cs b/DigitalLearningSolutions.Data/DataServices/CourseDataService.cs index 371ff1c94c..8c5f6619a3 100644 --- a/DigitalLearningSolutions.Data/DataServices/CourseDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CourseDataService.cs @@ -17,10 +17,11 @@ public interface ICourseDataService void RemoveCurrentCourse(int progressId, int candidateId); void EnrolOnSelfAssessment(int selfAssessmentId, int candidateId); int GetNumberOfActiveCoursesAtCentreForCategory(int centreId, int categoryId); - IEnumerable GetCourseStatisticsAtCentreForCategoryId(int centreId, int categoryId); + IEnumerable GetCourseStatisticsAtCentreForAdminCategoryId(int centreId, int categoryId); IEnumerable GetDelegateCoursesInfo(int delegateId); (int totalAttempts, int attemptsPassed) GetDelegateCourseAttemptStats(int delegateId, int customisationId); - CourseDetails? GetCourseDetails(int customisationId, int centreId, int categoryId); + CourseDetails? GetCourseDetailsForAdminCategoryId(int customisationId, int centreId, int categoryId); + IEnumerable GetCoursesAtCentreForAdminCategoryId(int centreId, int categoryId); } public class CourseDataService : ICourseDataService @@ -171,7 +172,9 @@ FROM Customisations AS c ); } - public IEnumerable GetCourseStatisticsAtCentreForCategoryId(int centreId, int categoryId) + // Admins have a non-nullable category ID where 0 = all. This is why we have the + // @categoryId = 0 in the WHERE clause, to prevent filtering on category ID when it is 0 + public IEnumerable GetCourseStatisticsAtCentreForAdminCategoryId(int centreId, int categoryId) { return connection.Query( @$"SELECT @@ -179,6 +182,7 @@ public IEnumerable GetCourseStatisticsAtCentreForCategoryId(in cu.CentreID, cu.Active, cu.AllCentres, + ap.ApplicationId, ap.ApplicationName, cu.CustomisationName, {DelegateCountQuery}, @@ -252,7 +256,9 @@ FROM AssessAttempts aa ); } - public CourseDetails? GetCourseDetails(int customisationId, int centreId, int categoryId) + // Admins have a non-nullable category ID where 0 = all. This is why we have the + // @categoryId = 0 in the WHERE clause, to prevent filtering on category ID when it is 0 + public CourseDetails? GetCourseDetailsForAdminCategoryId(int customisationId, int centreId, int categoryId) { return connection.Query( @$"SELECT @@ -298,5 +304,25 @@ AND ap.ArchivedDate IS NULL new { customisationId, centreId, categoryId } ).FirstOrDefault(); } + + // Admins have a non-nullable category ID where 0 = all. This is why we have the + // @categoryId = 0 in the WHERE clause, to prevent filtering on category ID when it is 0 + public IEnumerable GetCoursesAtCentreForAdminCategoryId(int centreId, int categoryId) + { + return connection.Query( + @"SELECT + c.CustomisationID, + c.CentreID, + c.ApplicationID, + a.ApplicationName, + c.CustomisationName, + c.Active + FROM Customisations AS c + JOIN Applications AS a on a.ApplicationID = c.ApplicationID + WHERE (CentreID = @centreId OR CentreID = 0) + AND (a.CourseCategoryID = @categoryId OR @categoryId = 0)", + new { centreId, categoryId } + ); + } } } diff --git a/DigitalLearningSolutions.Data/DataServices/CourseDelegatesDataService.cs b/DigitalLearningSolutions.Data/DataServices/CourseDelegatesDataService.cs new file mode 100644 index 0000000000..7f9127c9c0 --- /dev/null +++ b/DigitalLearningSolutions.Data/DataServices/CourseDelegatesDataService.cs @@ -0,0 +1,64 @@ +namespace DigitalLearningSolutions.Data.DataServices +{ + using System.Collections.Generic; + using System.Data; + using Dapper; + using DigitalLearningSolutions.Data.Models.CourseDelegates; + + public interface ICourseDelegatesDataService + { + IEnumerable GetDelegatesOnCourse(int customisationId, int centreId); + } + + public class CourseDelegatesDataService : ICourseDelegatesDataService + { + private readonly IDbConnection connection; + + private const string AllAttemptsQuery = + @"(SELECT COUNT(aa.AssessAttemptID) + FROM dbo.AssessAttempts AS aa + INNER JOIN dbo.Candidates AS can ON can.CandidateID = aa.CandidateID + WHERE aa.CustomisationID = cu.CustomisationID AND aa.[Status] IS NOT NULL + AND can.CentreID = @centreId) AS AllAttempts"; + + private const string AttemptsPassedQuery = + @"(SELECT COUNT(aa.AssessAttemptID) + FROM dbo.AssessAttempts AS aa + INNER JOIN dbo.Candidates AS can ON can.CandidateID = aa.CandidateID + WHERE aa.CustomisationID = cu.CustomisationID AND aa.[Status] = 1 + AND can.CentreID = @centreId) AS AttemptsPassed"; + + public CourseDelegatesDataService(IDbConnection connection) + { + this.connection = connection; + } + + public IEnumerable GetDelegatesOnCourse(int customisationId, int centreId) + { + return connection.Query( + $@"SELECT + c.CandidateID AS DelegateId, + c.CandidateNumber, + c.FirstName, + c.LastName, + c.EmailAddress, + c.Active, + p.ProgressID, + p.PLLocked AS Locked, + p.SubmittedTime AS LastUpdated, + c.DateRegistered AS Enrolled, + p.CompleteByDate AS CompleteBy, + p.RemovedDate, + p.Completed, + {AllAttemptsQuery}, + {AttemptsPassedQuery} + FROM Candidates AS c + INNER JOIN Progress AS p ON p.CandidateID = c.CandidateID + INNER JOIN Customisations cu ON cu.CustomisationID = p.CustomisationID + WHERE c.CentreID = @centreId + AND p.CustomisationID = @customisationId", + new { customisationId, centreId } + ); + } + } +} diff --git a/DigitalLearningSolutions.Data/Helpers/DateHelper.cs b/DigitalLearningSolutions.Data/Helpers/DateHelper.cs index 835b779a61..ff30284847 100644 --- a/DigitalLearningSolutions.Data/Helpers/DateHelper.cs +++ b/DigitalLearningSolutions.Data/Helpers/DateHelper.cs @@ -7,7 +7,6 @@ public static class DateHelper { - public static string StandardDateFormat = "dd/MM/yyyy"; public static DateTime ReferenceDate => new DateTime(1905, 1, 1); public static IEnumerable GetPeriodsBetweenDates( @@ -111,30 +110,6 @@ DateTime endDate ); } - public static string GetFormatStringForGraphLabel(ReportInterval interval) - { - return interval switch - { - ReportInterval.Days => "d/M/y", - ReportInterval.Weeks => "wc d/M/y", - ReportInterval.Months => "MMM yyyy", - ReportInterval.Quarters => "yyyy q", - _ => "yyyy" - }; - } - - public static string GetFormatStringForDateInTable(ReportInterval interval) - { - return interval switch - { - ReportInterval.Days => "d/MM/yyyy", - ReportInterval.Weeks => "Week commencing d/MM/yyyy", - ReportInterval.Months => "MMMM, yyyy", - ReportInterval.Quarters => "Q, yyyy", - _ => "yyyy" - }; - } - private static int ConvertMonthToQuarter(int month) { return (month - 1) / 3 + 1; diff --git a/DigitalLearningSolutions.Data/Models/CourseDelegates/CourseDelegate.cs b/DigitalLearningSolutions.Data/Models/CourseDelegates/CourseDelegate.cs new file mode 100644 index 0000000000..d83a54a865 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/CourseDelegates/CourseDelegate.cs @@ -0,0 +1,31 @@ +namespace DigitalLearningSolutions.Data.Models.CourseDelegates +{ + using System; + + public class CourseDelegate + { + public int DelegateId { get; set; } + public string CandidateNumber { get; set; } + public string? FirstName { get; set; } + public string LastName { get; set; } + public string? EmailAddress { get; set; } + public bool Active { get; set; } + public int ProgressId { get; set; } + public bool Locked { get; set; } + public DateTime LastUpdated { get; set; } + public DateTime Enrolled { get; set; } + public DateTime? CompleteBy { get; set; } + public DateTime? RemovedDate { get; set; } + public DateTime? Completed { get; set; } + public int AllAttempts { get; set; } + public int AttemptsPassed { get; set; } + + public string FullName => (string.IsNullOrEmpty(FirstName) ? "" : $"{FirstName} ") + LastName; + + public string TitleName => FullName + (string.IsNullOrEmpty(EmailAddress) ? "" : $" ({EmailAddress})"); + + public bool Removed => RemovedDate.HasValue; + + public double PassRate => AllAttempts == 0 ? 0 : Math.Round(100 * AttemptsPassed / (double)AllAttempts); + } +} diff --git a/DigitalLearningSolutions.Data/Models/CourseDelegates/CourseDelegatesData.cs b/DigitalLearningSolutions.Data/Models/CourseDelegates/CourseDelegatesData.cs new file mode 100644 index 0000000000..d10d489cb5 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/CourseDelegates/CourseDelegatesData.cs @@ -0,0 +1,25 @@ +namespace DigitalLearningSolutions.Data.Models.CourseDelegates +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.Models.Courses; + + public class CourseDelegatesData + { + public CourseDelegatesData( + int? customisationId, + IEnumerable courses, + IEnumerable delegates + ) + { + CustomisationId = customisationId; + Courses = courses; + Delegates = delegates; + } + + public int? CustomisationId { get; set; } + + public IEnumerable Courses { get; set; } + + public IEnumerable Delegates { get; set; } + } +} diff --git a/DigitalLearningSolutions.Data/Models/Courses/Course.cs b/DigitalLearningSolutions.Data/Models/Courses/Course.cs new file mode 100644 index 0000000000..88a5467543 --- /dev/null +++ b/DigitalLearningSolutions.Data/Models/Courses/Course.cs @@ -0,0 +1,24 @@ +namespace DigitalLearningSolutions.Data.Models.Courses +{ + public class Course : BaseSearchableItem + { + public int CustomisationId { get; set; } + public int CentreId { get; set; } + public int ApplicationId { get; set; } + public string ApplicationName { get; set; } + public string CustomisationName { get; set; } + public bool Active { get; set; } + + public string CourseName => string.IsNullOrWhiteSpace(CustomisationName) + ? ApplicationName + : ApplicationName + " - " + CustomisationName; + + public string CourseNameWithInactiveFlag => !Active ? "Inactive - " + CourseName : CourseName; + + public override string SearchableName + { + get => SearchableNameOverrideForFuzzySharp ?? CourseName; + set => SearchableNameOverrideForFuzzySharp = value; + } + } +} diff --git a/DigitalLearningSolutions.Data/Models/Courses/CourseDetails.cs b/DigitalLearningSolutions.Data/Models/Courses/CourseDetails.cs index b84b30d20f..8b6e6b9ed2 100644 --- a/DigitalLearningSolutions.Data/Models/Courses/CourseDetails.cs +++ b/DigitalLearningSolutions.Data/Models/Courses/CourseDetails.cs @@ -2,13 +2,8 @@ { using System; - public class CourseDetails + public class CourseDetails : Course { - public int CustomisationId { get; set; } - public int CentreId { get; set; } - public int ApplicationId { get; set; } - public string ApplicationName { get; set; } - public string CustomisationName { get; set; } public int CurrentVersion { get; set; } public DateTime CreatedDate { get; set; } public DateTime? LastAccessed { get; set; } @@ -22,7 +17,6 @@ public class CourseDetails public bool SelfRegister { get; set; } public bool DiagObjSelect { get; set; } public bool HideInLearnerPortal { get; set; } - public bool Active { get; set; } public int DelegateCount { get; set; } public int CompletedCount { get; set; } public int CompleteWithinMonths { get; set; } @@ -36,11 +30,7 @@ public class CourseDetails public bool ApplyLpDefaultsToSelfEnrol { get; set; } public int InProgressCount => DelegateCount - CompletedCount; - - public string CourseName => string.IsNullOrWhiteSpace(CustomisationName) - ? ApplicationName - : ApplicationName + " - " + CustomisationName; - + public string? RefreshToCourseName { get diff --git a/DigitalLearningSolutions.Data/Models/Courses/CourseStatistics.cs b/DigitalLearningSolutions.Data/Models/Courses/CourseStatistics.cs index 12f2aabdf8..95283f9fe6 100644 --- a/DigitalLearningSolutions.Data/Models/Courses/CourseStatistics.cs +++ b/DigitalLearningSolutions.Data/Models/Courses/CourseStatistics.cs @@ -2,14 +2,9 @@ { using System; - public class CourseStatistics : BaseSearchableItem + public class CourseStatistics : Course { - public int CustomisationId { get; set; } - public int CentreId { get; set; } - public bool Active { get; set; } public bool AllCentres { get; set; } - public string ApplicationName { get; set; } - public string? CustomisationName { get; set; } public int DelegateCount { get; set; } public int CompletedCount { get; set; } public int InProgressCount => DelegateCount - CompletedCount; @@ -20,16 +15,6 @@ public class CourseStatistics : BaseSearchableItem public string CourseTopic { get; set; } public string LearningMinutes { get; set; } - public string CourseName => string.IsNullOrWhiteSpace(CustomisationName) - ? ApplicationName - : ApplicationName + " - " + CustomisationName; - public double PassRate => AllAttempts == 0 ? 0 : Math.Round(100 * AttemptsPassed / (double)AllAttempts); - - public override string SearchableName - { - get => SearchableNameOverrideForFuzzySharp ?? CourseName; - set => SearchableNameOverrideForFuzzySharp = value; - } } } diff --git a/DigitalLearningSolutions.Data/Services/CourseDelegatesService.cs b/DigitalLearningSolutions.Data/Services/CourseDelegatesService.cs new file mode 100644 index 0000000000..175dbfe492 --- /dev/null +++ b/DigitalLearningSolutions.Data/Services/CourseDelegatesService.cs @@ -0,0 +1,54 @@ +namespace DigitalLearningSolutions.Data.Services +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Models.CourseDelegates; + + public interface ICourseDelegatesService + { + CourseDelegatesData GetCoursesAndCourseDelegatesForCentre( + int centreId, + int categoryId, + int? customisationId + ); + } + + public class CourseDelegatesService : ICourseDelegatesService + { + private readonly ICourseDataService courseDataService; + private readonly ICourseDelegatesDataService courseDelegatesDataService; + + public CourseDelegatesService( + ICourseDataService courseDataService, + ICourseDelegatesDataService courseDelegatesDataService + ) + { + this.courseDataService = courseDataService; + this.courseDelegatesDataService = courseDelegatesDataService; + } + + public CourseDelegatesData GetCoursesAndCourseDelegatesForCentre( + int centreId, + int categoryId, + int? customisationId + ) + { + var courses = courseDataService.GetCoursesAtCentreForAdminCategoryId(centreId, categoryId).ToList(); + var activeCoursesAlphabetical = courses.Where(c => c.Active).OrderBy(c => c.CourseName); + var inactiveCoursesAlphabetical = + courses.Where(c => !c.Active).OrderBy(c => c.CourseName); + + var orderedCourses = activeCoursesAlphabetical.Concat(inactiveCoursesAlphabetical).ToList(); + + var currentCustomisationId = customisationId ?? orderedCourses.FirstOrDefault()?.CustomisationId; + + var courseDelegates = currentCustomisationId.HasValue + ? courseDelegatesDataService.GetDelegatesOnCourse(currentCustomisationId.Value, centreId) + .ToList() + : new List(); + + return new CourseDelegatesData(currentCustomisationId, orderedCourses, courseDelegates); + } + } +} diff --git a/DigitalLearningSolutions.Data/Services/CourseService.cs b/DigitalLearningSolutions.Data/Services/CourseService.cs index 14e052f53c..4a37781e7b 100644 --- a/DigitalLearningSolutions.Data/Services/CourseService.cs +++ b/DigitalLearningSolutions.Data/Services/CourseService.cs @@ -25,13 +25,13 @@ public CourseService(ICourseDataService courseDataService, ICourseAdminFieldsSer public IEnumerable GetTopCourseStatistics(int centreId, int categoryId) { - var allCourses = courseDataService.GetCourseStatisticsAtCentreForCategoryId(centreId, categoryId); + var allCourses = courseDataService.GetCourseStatisticsAtCentreForAdminCategoryId(centreId, categoryId); return allCourses.Where(c => c.Active).OrderByDescending(c => c.InProgressCount); } public IEnumerable GetCentreSpecificCourseStatistics(int centreId, int categoryId) { - var allCourses = courseDataService.GetCourseStatisticsAtCentreForCategoryId(centreId, categoryId); + var allCourses = courseDataService.GetCourseStatisticsAtCentreForAdminCategoryId(centreId, categoryId); return allCourses.Where(c => c.CentreId == centreId); } diff --git a/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAuthenticatedAccessibilityTests.cs b/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAuthenticatedAccessibilityTests.cs index ff478a1843..1074d9dafb 100644 --- a/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAuthenticatedAccessibilityTests.cs +++ b/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAuthenticatedAccessibilityTests.cs @@ -44,12 +44,16 @@ public BasicAuthenticatedAccessibilityTests(AuthenticatedAccessibilityTestsFixtu [InlineData("/TrackingSystem/Delegates/All", "Delegates")] [InlineData("/TrackingSystem/Delegates/Groups", "Groups")] [InlineData("/TrackingSystem/Delegates/Groups/5/Delegates", "Group delegates")] - [InlineData("/TrackingSystem/Delegates/Groups/5/Delegates/Remove/245969", "Are you sure you would like to remove xxxxx xxxx from this group?")] + [InlineData( + "/TrackingSystem/Delegates/Groups/5/Delegates/Remove/245969", + "Are you sure you would like to remove xxxxx xxxx from this group?" + )] [InlineData("/TrackingSystem/Delegates/Groups/5/Courses", "Group courses")] [InlineData("/TrackingSystem/Delegates/View/3", "xxxx xxxxxx")] [InlineData("/TrackingSystem/Delegates/Approve", "Approve delegate registrations")] [InlineData("/TrackingSystem/Delegates/BulkUpload", "Bulk upload/update delegates")] [InlineData("/TrackingSystem/Delegates/Email", "Send welcome messages")] + [InlineData("/TrackingSystem/Delegates/CourseDelegates", "Course delegates")] [InlineData("/NotificationPreferences", "Notification preferences")] [InlineData("/NotificationPreferences/Edit/AdminUser", "Update notification preferences")] [InlineData("/NotificationPreferences/Edit/DelegateUser", "Update notification preferences")] diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/CourseContentControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/CourseContentControllerTests.cs index 66337e2240..44e7a64beb 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/CourseContentControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/CourseContentControllerTests.cs @@ -30,7 +30,7 @@ public void Setup() public void Index_returns_NotFound_when_no_appropriate_course_found() { // Given - A.CallTo(() => courseDataService.GetCourseDetails(A._, A._, A._)).Returns(null); + A.CallTo(() => courseDataService.GetCourseDetailsForAdminCategoryId(A._, A._, A._)).Returns(null); // When var result = controller.Index(1); @@ -43,7 +43,7 @@ public void Index_returns_NotFound_when_no_appropriate_course_found() public void Index_returns_Index_page_when_appropriate_course_found() { // Given - A.CallTo(() => courseDataService.GetCourseDetails(A._, A._, A._)) + A.CallTo(() => courseDataService.GetCourseDetailsForAdminCategoryId(A._, A._, A._)) .Returns(CourseDetailsTestHelper.GetDefaultCourseDetails()); A.CallTo(() => sectionService.GetSectionsAndTutorialsForCustomisation(A._, A._)) .Returns(new List
()); diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/ManageCourseControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/ManageCourseControllerTests.cs index 162757bdd0..28e6382838 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/ManageCourseControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CourseSetup/ManageCourseControllerTests.cs @@ -26,7 +26,7 @@ public void Setup() public void Index_returns_NotFound_when_no_appropriate_course_found() { // Given - A.CallTo(() => courseDataService.GetCourseDetails(A._, A._, A._)) + A.CallTo(() => courseDataService.GetCourseDetailsForAdminCategoryId(A._, A._, A._)) .Returns(null); // When @@ -40,7 +40,7 @@ public void Index_returns_NotFound_when_no_appropriate_course_found() public void Index_returns_ManageCourse_page_when_appropriate_course_found() { // Given - A.CallTo(() => courseDataService.GetCourseDetails(A._, A._, A._)) + A.CallTo(() => courseDataService.GetCourseDetailsForAdminCategoryId(A._, A._, A._)) .Returns(new CourseDetails()); // When diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Reports/ReportsController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Reports/ReportsController.cs index 799d5a3663..6a76810082 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Reports/ReportsController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Reports/ReportsController.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using DigitalLearningSolutions.Data.Enums; - using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Models.TrackingSystem; using DigitalLearningSolutions.Data.Services; using DigitalLearningSolutions.Web.Helpers; diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/CourseContentController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/CourseContentController.cs index 1c86722af2..5d27718091 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/CourseContentController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/CourseContentController.cs @@ -28,7 +28,7 @@ public IActionResult Index(int customisationId) { var centreId = User.GetCentreId(); var categoryId = User.GetAdminCategoryId()!; - var courseDetails = courseDataService.GetCourseDetails(customisationId, centreId, categoryId.Value); + var courseDetails = courseDataService.GetCourseDetailsForAdminCategoryId(customisationId, centreId, categoryId.Value); if (courseDetails == null) { diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/ManageCourseController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/ManageCourseController.cs index 8dea8b9842..d7dcf5139f 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/ManageCourseController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CourseSetup/ManageCourseController.cs @@ -26,7 +26,7 @@ public IActionResult Index(int customisationId) var centreId = User.GetCentreId(); var categoryId = User.GetAdminCategoryId()!; - var courseDetails = courseDataService.GetCourseDetails( + var courseDetails = courseDataService.GetCourseDetailsForAdminCategoryId( customisationId, centreId, categoryId.Value diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/CourseDelegatesController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/CourseDelegatesController.cs new file mode 100644 index 0000000000..c8a1e37019 --- /dev/null +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/CourseDelegatesController.cs @@ -0,0 +1,36 @@ +namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Delegates +{ + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.CourseDelegates; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.FeatureManagement.Mvc; + + [FeatureGate(FeatureFlags.RefactoredTrackingSystem)] + [Authorize(Policy = CustomPolicies.UserCentreAdmin)] + [Route("TrackingSystem/Delegates/CourseDelegates")] + public class CourseDelegatesController : Controller + { + private readonly ICourseDelegatesService courseDelegatesService; + + public CourseDelegatesController( + ICourseDelegatesService courseDelegatesService + ) + { + this.courseDelegatesService = courseDelegatesService; + } + + public IActionResult Index(int? customisationId = null) + { + var centreId = User.GetCentreId(); + var categoryId = User.GetAdminCategoryId()!.Value; + var courseDelegatesData = + courseDelegatesService.GetCoursesAndCourseDelegatesForCentre(centreId, categoryId, customisationId); + + var model = new CourseDelegatesViewModel(courseDelegatesData); + + return View(model); + } + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/DateHelper.cs b/DigitalLearningSolutions.Web/Helpers/DateHelper.cs new file mode 100644 index 0000000000..6e391eb873 --- /dev/null +++ b/DigitalLearningSolutions.Web/Helpers/DateHelper.cs @@ -0,0 +1,35 @@ +namespace DigitalLearningSolutions.Web.Helpers +{ + using DigitalLearningSolutions.Data.Enums; + + public static class DateHelper + { + public static string StandardDateFormat = "dd/MM/yyyy"; + + public static string StandardDateAndTimeFormat = "dd/MM/yyyy hh:mm"; + + public static string GetFormatStringForGraphLabel(ReportInterval interval) + { + return interval switch + { + ReportInterval.Days => "d/M/y", + ReportInterval.Weeks => "wc d/M/y", + ReportInterval.Months => "MMM yyyy", + ReportInterval.Quarters => "yyyy q", + _ => "yyyy" + }; + } + + public static string GetFormatStringForDateInTable(ReportInterval interval) + { + return interval switch + { + ReportInterval.Days => "d/MM/yyyy", + ReportInterval.Weeks => "Week commencing d/MM/yyyy", + ReportInterval.Months => "MMMM, yyyy", + ReportInterval.Quarters => "Q, yyyy", + _ => "yyyy" + }; + } + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/FilterOptions/CourseDelegateFilterOptions.cs b/DigitalLearningSolutions.Web/Helpers/FilterOptions/CourseDelegateFilterOptions.cs new file mode 100644 index 0000000000..1045c0c5b0 --- /dev/null +++ b/DigitalLearningSolutions.Web/Helpers/FilterOptions/CourseDelegateFilterOptions.cs @@ -0,0 +1,57 @@ +namespace DigitalLearningSolutions.Web.Helpers.FilterOptions +{ + using DigitalLearningSolutions.Data.Models.CourseDelegates; + using DigitalLearningSolutions.Web.Models.Enums; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + + public static class CourseDelegateAccountStatusFilterOptions + { + private const string Group = "AccountStatus"; + + public static readonly FilterOptionViewModel Active = new FilterOptionViewModel( + "Active", + Group + FilteringHelper.Separator + nameof(CourseDelegate.Active) + FilteringHelper.Separator + "true", + FilterStatus.Success + ); + + public static readonly FilterOptionViewModel Inactive = new FilterOptionViewModel( + "Inactive", + Group + FilteringHelper.Separator + nameof(CourseDelegate.Active) + FilteringHelper.Separator + "false", + FilterStatus.Warning + ); + } + + public static class CourseDelegateProgressLockedFilterOptions + { + private const string Group = "ProgressLocked"; + + public static readonly FilterOptionViewModel Locked = new FilterOptionViewModel( + "Locked", + Group + FilteringHelper.Separator + nameof(CourseDelegate.Locked) + FilteringHelper.Separator + "true", + FilterStatus.Warning + ); + + public static readonly FilterOptionViewModel NotLocked = new FilterOptionViewModel( + "Not locked", + Group + FilteringHelper.Separator + nameof(CourseDelegate.Locked) + FilteringHelper.Separator + "false", + FilterStatus.Default + ); + } + + public static class CourseDelegateProgressRemovedFilterOptions + { + private const string Group = "ProgressRemoved"; + + public static readonly FilterOptionViewModel Removed = new FilterOptionViewModel( + "Removed", + Group + FilteringHelper.Separator + nameof(CourseDelegate.Removed) + FilteringHelper.Separator + "true", + FilterStatus.Warning + ); + + public static readonly FilterOptionViewModel NotRemoved = new FilterOptionViewModel( + "Not removed", + Group + FilteringHelper.Separator + nameof(CourseDelegate.Removed) + FilteringHelper.Separator + "false", + FilterStatus.Default + ); + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/FilterableTagHelper.cs b/DigitalLearningSolutions.Web/Helpers/FilterableTagHelper.cs index ba403634ac..3dc28dfdbc 100644 --- a/DigitalLearningSolutions.Web/Helpers/FilterableTagHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/FilterableTagHelper.cs @@ -1,6 +1,7 @@ namespace DigitalLearningSolutions.Web.Helpers { using System.Collections.Generic; + using DigitalLearningSolutions.Data.Models.CourseDelegates; using DigitalLearningSolutions.Data.Models.Courses; using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Web.Helpers.FilterOptions; @@ -81,6 +82,40 @@ CourseStatistics courseStatistics return tags; } + public static IEnumerable GetCurrentTagsForCourseDelegate(CourseDelegate courseDelegate) + { + var tags = new List(); + + if (courseDelegate.Active) + { + tags.Add(new SearchableTagViewModel(CourseDelegateAccountStatusFilterOptions.Active)); + } + else + { + tags.Add(new SearchableTagViewModel(CourseDelegateAccountStatusFilterOptions.Inactive)); + } + + if (courseDelegate.Locked) + { + tags.Add(new SearchableTagViewModel(CourseDelegateProgressLockedFilterOptions.Locked)); + } + else + { + tags.Add(new SearchableTagViewModel(CourseDelegateProgressLockedFilterOptions.NotLocked, true)); + } + + if (courseDelegate.RemovedDate.HasValue) + { + tags.Add(new SearchableTagViewModel(CourseDelegateProgressRemovedFilterOptions.Removed)); + } + else + { + tags.Add(new SearchableTagViewModel(CourseDelegateProgressRemovedFilterOptions.NotRemoved, true)); + } + + return tags; + } + public static IEnumerable GetCurrentTagsForDelegateUser( DelegateUserCard delegateUser ) diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index f8349d9e6f..8ed674ed37 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -141,67 +141,84 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(_ => new SqlConnection(defaultConnectionString)); // Register services. + RegisterServices(services); + RegisterDataServices(services); + RegisterHelpers(services); + RegisterWebServiceFilters(services); + } + + private static void RegisterServices(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddHttpClient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); + } + + private static void RegisterDataServices(IServiceCollection services) + { services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - RegisterWebServiceFilters(services); + services.AddScoped(); + services.AddScoped(); + } + + private static void RegisterHelpers(IServiceCollection services) + { + services.AddHttpClient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } private static void RegisterWebServiceFilters(IServiceCollection services) diff --git a/DigitalLearningSolutions.Web/Styles/shared/cardWithThreeButtons.scss b/DigitalLearningSolutions.Web/Styles/shared/cardWithThreeButtons.scss new file mode 100644 index 0000000000..a0df785b43 --- /dev/null +++ b/DigitalLearningSolutions.Web/Styles/shared/cardWithThreeButtons.scss @@ -0,0 +1,12 @@ +@import "~nhsuk-frontend/packages/core/all"; + +.nhsuk-button.expander-card__button { + margin-right: nhsuk-spacing(2); + margin-bottom: nhsuk-spacing(3); + margin-top: nhsuk-spacing(0); + + @include govuk-media-query($until: tablet) { + margin-top: nhsuk-spacing(2); + margin-bottom: nhsuk-spacing(2); + } +} diff --git a/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss new file mode 100644 index 0000000000..e4c20f6deb --- /dev/null +++ b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/courseDelegates.scss @@ -0,0 +1,13 @@ +@import '../../shared/searchableElements/searchableElements'; +@import '../../shared/headingButtons'; +@import '../../shared/cardWithThreeButtons'; + +// TODO Fix this to match the new styles for the filter components from 580 +.course-dropdown { + @extend .input-with-submit-button; + width: 63%; + + @include mq($from: $xl-desktop) { + width: 80%; + } +} diff --git a/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/delegateGroups.scss b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/delegateGroups.scss index edf09307d1..568c5198c2 100644 --- a/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/delegateGroups.scss +++ b/DigitalLearningSolutions.Web/Styles/trackingSystem/delegates/delegateGroups.scss @@ -1,13 +1,3 @@ @import '../../shared/searchableElements/searchableElements'; @import '../../shared/headingButtons'; - -.nhsuk-button.expander-card__button { - margin-right: nhsuk-spacing(2); - margin-bottom: nhsuk-spacing(3); - margin-top: nhsuk-spacing(0); - - @include govuk-media-query($until: tablet) { - margin-top: nhsuk-spacing(2); - margin-bottom: nhsuk-spacing(2); - } -} +@import '../../shared/cardWithThreeButtons'; diff --git a/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/SummaryViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/SummaryViewModel.cs index 09df0abb63..1145759d8b 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/SummaryViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/SummaryViewModel.cs @@ -1,6 +1,7 @@ namespace DigitalLearningSolutions.Web.ViewModels.Register.RegisterDelegateByCentre { using System.Collections.Generic; + using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Models; using DigitalLearningSolutions.Web.ViewModels.Common; @@ -17,7 +18,7 @@ public SummaryViewModel(DelegateRegistrationByCentreData data) IsPasswordSet = data.IsPasswordSet; if (data.ShouldSendEmail) { - WelcomeEmailDate = data.WelcomeEmailDate!.Value.ToString("dd/MM/yyyy"); + WelcomeEmailDate = data.WelcomeEmailDate!.Value.ToString(DateHelper.StandardDateFormat); } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/ReportsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/ReportsViewModel.cs index 5a8aaa3dac..c6930b3128 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/ReportsViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/Reports/ReportsViewModel.cs @@ -1,9 +1,9 @@ namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.Reports { + using DigitalLearningSolutions.Data.Models.TrackingSystem; + using DigitalLearningSolutions.Web.Helpers; using System.Collections.Generic; using System.Linq; - using DigitalLearningSolutions.Data.Helpers; - using DigitalLearningSolutions.Data.Models.TrackingSystem; public class ReportsViewModel { diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/SystemNotifications/UnacknowledgedNotificationViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/SystemNotifications/UnacknowledgedNotificationViewModel.cs index c7c1f7bceb..d7680add74 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/SystemNotifications/UnacknowledgedNotificationViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/SystemNotifications/UnacknowledgedNotificationViewModel.cs @@ -1,7 +1,7 @@ namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.SystemNotifications { - using DigitalLearningSolutions.Data.Helpers; using DigitalLearningSolutions.Data.Models; + using DigitalLearningSolutions.Web.Helpers; public class UnacknowledgedNotificationViewModel { diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CourseSetup/CourseDetails/CourseSummaryViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CourseSetup/CourseDetails/CourseSummaryViewModel.cs index 4d8a6beda1..2de055f352 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CourseSetup/CourseDetails/CourseSummaryViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CourseSetup/CourseDetails/CourseSummaryViewModel.cs @@ -1,14 +1,15 @@ namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.CourseSetup.CourseDetails { using DigitalLearningSolutions.Data.Models.Courses; + using DigitalLearningSolutions.Web.Helpers; public class CourseSummaryViewModel { public CourseSummaryViewModel(CourseDetails courseDetails) { CurrentVersion = courseDetails.CurrentVersion; - CreatedDate = courseDetails.CreatedDate.ToString("dd/MM/yyyy"); - LastAccessed = courseDetails.LastAccessed?.ToString("dd/MM/yyyy"); + CreatedDate = courseDetails.CreatedDate.ToString(DateHelper.StandardDateFormat); + LastAccessed = courseDetails.LastAccessed?.ToString(DateHelper.StandardDateFormat); Completions = courseDetails.CompletedCount; ActiveLearners = courseDetails.InProgressCount; } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/CourseDelegatesViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/CourseDelegatesViewModel.cs new file mode 100644 index 0000000000..a360ab162d --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/CourseDelegatesViewModel.cs @@ -0,0 +1,37 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.CourseDelegates +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.CourseDelegates; + using DigitalLearningSolutions.Web.Helpers; + using Microsoft.AspNetCore.Mvc.Rendering; + + public class CourseDelegatesViewModel + { + public CourseDelegatesViewModel(CourseDelegatesData courseDelegatesData) + { + CustomisationId = courseDelegatesData.CustomisationId; + + var courseOptions = courseDelegatesData.Courses + .Select(c => (c.CustomisationId, c.CourseNameWithInactiveFlag)); + Courses = SelectListHelper.MapOptionsToSelectListItems(courseOptions, courseDelegatesData.CustomisationId); + + // TODO: HEEDLS-564 - paginate properly instead of taking 10. + var delegates = courseDelegatesData.Delegates.Take(10) + .Select(cd => new SearchableCourseDelegateViewModel(cd)); + CourseDetails = courseDelegatesData.CustomisationId.HasValue + ? new SelectedCourseDetails( + courseDelegatesData.CustomisationId.Value, + courseDelegatesData.Courses, + delegates + ) + : null; + } + + public int? CustomisationId { get; set; } + + public IEnumerable Courses { get; set; } + + public SelectedCourseDetails? CourseDetails { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/SearchableCourseDelegateViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/SearchableCourseDelegateViewModel.cs new file mode 100644 index 0000000000..a6de5e19c6 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/SearchableCourseDelegateViewModel.cs @@ -0,0 +1,39 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.CourseDelegates +{ + using DigitalLearningSolutions.Data.Models.CourseDelegates; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; + + public class SearchableCourseDelegateViewModel : BaseFilterableViewModel + { + public SearchableCourseDelegateViewModel(CourseDelegate courseDelegate) + { + DelegateId = courseDelegate.DelegateId; + CandidateNumber = courseDelegate.CandidateNumber; + TitleName = courseDelegate.TitleName; + Active = courseDelegate.Active; + ProgressId = courseDelegate.ProgressId; + Locked = courseDelegate.Locked; + LastUpdated = courseDelegate.LastUpdated.ToString(DateHelper.StandardDateAndTimeFormat); + Enrolled = courseDelegate.Enrolled.ToString(DateHelper.StandardDateAndTimeFormat); + CompleteBy = courseDelegate.CompleteBy?.ToString(DateHelper.StandardDateAndTimeFormat); + Completed = courseDelegate.Completed?.ToString(DateHelper.StandardDateAndTimeFormat); + RemovedDate = courseDelegate.RemovedDate?.ToString(DateHelper.StandardDateAndTimeFormat); + PassRate = courseDelegate.PassRate; + Tags = FilterableTagHelper.GetCurrentTagsForCourseDelegate(courseDelegate); + } + + public int DelegateId { get; set; } + public string CandidateNumber { get; set; } + public string TitleName { get; set; } + public bool Active { get; set; } + public int ProgressId { get; set; } + public bool Locked { get; set; } + public string LastUpdated { get; set; } + public string Enrolled { get; set; } + public string? CompleteBy { get; set; } + public string? Completed { get; set; } + public string? RemovedDate { get; set; } + public double PassRate { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/SelectedCourseDetails.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/SelectedCourseDetails.cs new file mode 100644 index 0000000000..58deb91406 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/CourseDelegates/SelectedCourseDetails.cs @@ -0,0 +1,19 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.CourseDelegates +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models.Courses; + + public class SelectedCourseDetails + { + public SelectedCourseDetails(int customisationId, IEnumerable courses, IEnumerable delegates) + { + Active = courses.Single(c => c.CustomisationId == customisationId).Active; + Delegates = delegates; + } + + public bool Active { get; set; } + + public IEnumerable Delegates { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/DelegateGroups/GroupCourseViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/DelegateGroups/GroupCourseViewModel.cs index 93160909ff..907c0dc21d 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/DelegateGroups/GroupCourseViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/DelegateGroups/GroupCourseViewModel.cs @@ -14,7 +14,7 @@ public GroupCourseViewModel(GroupCourse groupCourse) : null; IsMandatory = groupCourse.IsMandatory ? "Mandatory" : "Not mandatory"; IsAssessed = groupCourse.IsAssessed ? "Assessed" : "Not assessed"; - AddedToGroup = groupCourse.AddedToGroup.ToString("dd/MM/yyyy"); + AddedToGroup = groupCourse.AddedToGroup.ToString(DateHelper.StandardDateFormat); CompleteWithin = DisplayStringHelper.ConvertNumberToMonthsString(groupCourse.CompleteWithinMonths); ValidFor = DisplayStringHelper.ConvertNumberToMonthsString(groupCourse.ValidityMonths); } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs index d82763cf69..fad9b31785 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/EmailDelegates/EmailDelegatesItemViewModel.cs @@ -1,6 +1,6 @@ namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.EmailDelegates { - using DigitalLearningSolutions.Data.Helpers; + using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Data.Models.User; public class EmailDelegatesItemViewModel diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs index 4c1fc103bb..3a78c2cb43 100644 --- a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Delegates/Shared/DelegateInfoViewModel.cs @@ -4,6 +4,7 @@ using System.Linq; using DigitalLearningSolutions.Data.Enums; using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.Common; public class DelegateInfoViewModel @@ -24,7 +25,7 @@ public DelegateInfoViewModel(DelegateUserCard delegateUser, IEnumerable "Self enrolled", diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/TopCourses/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/TopCourses/Index.cshtml index e256f31f9d..9763a08c0a 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/TopCourses/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/TopCourses/Index.cshtml @@ -72,7 +72,7 @@ - View more + View more diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/CourseDelegates/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/CourseDelegates/Index.cshtml new file mode 100644 index 0000000000..0802a1b20a --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/CourseDelegates/Index.cshtml @@ -0,0 +1,106 @@ +@inject IConfiguration Configuration +@using DigitalLearningSolutions.Web.Models.Enums +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.CourseDelegates +@using Microsoft.Extensions.Configuration +@model CourseDelegatesViewModel + + + +@{ + ViewData["Title"] = "Course delegates"; + ViewData["Application"] = "Tracking System"; + ViewData["HeaderPath"] = $"{Configuration["AppRootPath"]}/TrackingSystem/Centre/Dashboard"; + ViewData["HeaderPathName"] = "Tracking System"; +} + +@section NavMenuItems { + +} + +
+
+ +
+ +
+
+
+

Course delegates

+
+ +
+ +
+
+

+ Choose a course to see delegates enrolled on it. If you'd like to export delegates on all courses, click "Export all" above. +

+
+
+ + @if (Model.Courses.Any() && Model.CourseDetails != null) { +
+
+
+ + + +
+
+
+ + @if (!Model.CourseDetails.Active) { +
+
+
+ Information: +

The currently selected course is inactive

+
+
+
+ } + +
+
+ @if (!Model.CourseDetails.Delegates.Any()) { + + } else { +
+ @foreach (var groupModel in Model.CourseDetails.Delegates) { + + } +
+ } +
+
+ } else { +
+
+

There are no courses set up in your centre for the category you manage.

+ +
+
+ } + + +
+
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/CourseDelegates/_SearchableCourseDelegateCard.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/CourseDelegates/_SearchableCourseDelegateCard.cshtml new file mode 100644 index 0000000000..e86c5eb04e --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/CourseDelegates/_SearchableCourseDelegateCard.cshtml @@ -0,0 +1,81 @@ +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.CourseDelegates +@model SearchableCourseDelegateViewModel + +
+
+ + + @Model.TitleName + + +
+ + + +
+
+
+ Delegate ID +
+
+ @Model.CandidateNumber +
+
+
+
+ Last updated +
+
+ @Model.LastUpdated +
+
+
+
+ Enrolled +
+
+ @Model.Enrolled +
+
+
+
+ Complete by +
+ +
+
+
+ Completed date +
+ +
+
+
+ Removed date +
+ +
+
+
+ Pass rate +
+
+ @Model.PassRate% +
+
+
+ + + View progress + + @if (Model.Locked) { + + Unlock progress + + } + + Remove from course + +
+
+
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/Shared/_DelegatesSideNavMenu.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/Shared/_DelegatesSideNavMenu.cshtml index 8bb0913e0f..c49c724ec9 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/Shared/_DelegatesSideNavMenu.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/Shared/_DelegatesSideNavMenu.cshtml @@ -19,8 +19,9 @@ link-text="Delegate groups" is-current-page="Model == DelegatePage.DelegateGroups" /> -