diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index ec884b479..838aba189 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs b/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs index 921d1e16f..46e449dba 100644 --- a/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs @@ -736,6 +736,77 @@ public async Task ViewProgress(int resourceId, int resourceRefere return this.View(vm); } + /// + /// Get user certificates. + /// + /// The certificateRequest. + /// A representing the result of the asynchronous operation. + [Route("mylearning/certificates")] + [HttpGet] + [HttpPost] + public async Task Certificates(MyLearningUserCertificatesViewModel certificateRequest = null) + { + var myLearningRequestModel = new MyLearningRequestModel + { + SearchText = certificateRequest.SearchText?.Trim(), + Skip = certificateRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + File = certificateRequest.File, + Video = certificateRequest.Video, + Article = certificateRequest.Article, + Case = certificateRequest.Case, + Image = certificateRequest.Image, + Audio = certificateRequest.Audio, + Elearning = certificateRequest.Elearning, + Html = certificateRequest.Html, + Assessment = certificateRequest.Assessment, + Courses = certificateRequest.Courses, + }; + + switch (certificateRequest.MyLearningFormActionType) + { + case MyLearningFormActionTypeEnum.NextPageChange: + certificateRequest.CurrentPageIndex += 1; + myLearningRequestModel.Skip = certificateRequest.CurrentPageIndex * MyLearningPageSize; + break; + + case MyLearningFormActionTypeEnum.PreviousPageChange: + certificateRequest.CurrentPageIndex -= 1; + myLearningRequestModel.Skip = certificateRequest.CurrentPageIndex * MyLearningPageSize; + break; + case MyLearningFormActionTypeEnum.BasicSearch: + + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = certificateRequest.SearchText?.Trim(), + Skip = certificateRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + case MyLearningFormActionTypeEnum.ClearAllFilters: + + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = certificateRequest.SearchText?.Trim(), + Skip = certificateRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + } + + var result = await this.myLearningService.GetUserCertificateDetails(myLearningRequestModel); + var response = new MyLearningUserCertificatesViewModel(myLearningRequestModel); + + if (result != null) + { + response.TotalCount = result.TotalCount; + response.UserCertificates = result.Certificates; + } + + response.MyLearningPaging = new MyLearningPagingModel() { CurrentPage = certificateRequest.CurrentPageIndex, PageSize = MyLearningPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; + return this.View(response); + } + /// /// Gets the certificate details of an activity. /// @@ -784,6 +855,7 @@ public async Task GetCertificateDetails(int resourceReferenceId, /// The minorVersion. /// The userId. /// The . + [HttpGet] [HttpPost] [Route("mylearning/downloadcertificate")] public async Task DownloadCertificate(int resourceReferenceId, int? majorVersion = 0, int? minorVersion = 0, int? userId = 0) diff --git a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs index 6a83b80f2..44af540da 100644 --- a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs @@ -437,6 +437,7 @@ public static Dictionary GetActivityParameters(object model) var routeData = model.GetType().GetProperties().ToDictionary(p => p.Name, p => p.GetValue(model)?.ToString()); routeData.Remove("MostRecentResources"); routeData.Remove("Activities"); + routeData.Remove("UserCertificates"); routeData.Remove("TotalCount"); routeData.Remove("MyLearningPaging"); routeData.Remove("Skip"); diff --git a/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs b/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs index ad61e4efe..e9b584cc4 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs @@ -49,6 +49,13 @@ public interface IMyLearningService /// The . Task> GetResourceCertificateDetails(int resourceReferenceId, int? majorVersion = 0, int? minorVersion = 0, int? userId = 0); + /// + /// Gets the user certificates. + /// + /// The request model. + /// The . + Task GetUserCertificateDetails(MyLearningRequestModel requestModel); + /// /// Gets the resource URL for a given resource reference ID. /// diff --git a/LearningHub.Nhs.WebUI/Models/MyLearningUserCertificatesViewModel.cs b/LearningHub.Nhs.WebUI/Models/MyLearningUserCertificatesViewModel.cs new file mode 100644 index 000000000..b1abc7c52 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/MyLearningUserCertificatesViewModel.cs @@ -0,0 +1,84 @@ +namespace LearningHub.Nhs.WebUI.Models +{ + using System.Collections.Generic; + using System.Reflection; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Models.Learning; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// Defines the . + /// + public class MyLearningUserCertificatesViewModel : MyLearningRequestModel + { + /// + /// Initializes a new instance of the class. + /// + public MyLearningUserCertificatesViewModel() + { + this.UserCertificates = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// MyLearningRequestModel. + public MyLearningUserCertificatesViewModel(MyLearningRequestModel requestModel) + { + this.UserCertificates = new List(); + foreach (PropertyInfo prop in requestModel.GetType().GetProperties()) + { + this.GetType().GetProperty(prop.Name).SetValue(this, prop.GetValue(requestModel, null), null); + } + } + + /// + /// Gets or sets the learning form event. + /// + public MyLearningFormActionTypeEnum MyLearningFormActionType { get; set; } + + /// + /// Gets or sets the page item index. + /// + public int CurrentPageIndex { get; set; } = 0; + + /// + /// Gets or sets the TotalCount. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the Activities. + /// + public List UserCertificates { get; set; } + + /// + /// Gets or sets the learning result paging. + /// + public PagingViewModel MyLearningPaging { get; set; } + + /// + /// sets the list of type checkboxes. + /// + /// The . + public List TypeFilterCheckbox() + { + var checkboxes = new List() + { + new CheckboxListItemViewModel("Article", "Article", null), + new CheckboxListItemViewModel("Assessment", "Assessment", null), + new CheckboxListItemViewModel("Audio", "Audio", null), + new CheckboxListItemViewModel("Case", "Case", null), + new CheckboxListItemViewModel("Elearning", "elearning", null), + new CheckboxListItemViewModel("File", "File", null), + new CheckboxListItemViewModel("Html", "HTML", null), + new CheckboxListItemViewModel("Image", "Image", null), + new CheckboxListItemViewModel("Video", "Video", null), + new CheckboxListItemViewModel("Weblink", "Weblink", null), + new CheckboxListItemViewModel("Courses", "Courses", null), + }; + return checkboxes; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs b/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs index e39f9463e..108c594cb 100644 --- a/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs +++ b/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs @@ -77,7 +77,7 @@ public static IEnumerable GetGroupedMenus() Text = "Certificates", Controller = "MyLearning", Action = "Certificates", - IsActive = route => MatchRoute(route, "Activity", "Certificates"), + IsActive = route => MatchRoute(route, "MyLearning", "Certificates"), }, new SideNavigationItem { diff --git a/LearningHub.Nhs.WebUI/Services/MyLearningService.cs b/LearningHub.Nhs.WebUI/Services/MyLearningService.cs index 0dfa182eb..cb75f1530 100644 --- a/LearningHub.Nhs.WebUI/Services/MyLearningService.cs +++ b/LearningHub.Nhs.WebUI/Services/MyLearningService.cs @@ -5,7 +5,6 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; - using elfhHub.Nhs.Models.Common; using LearningHub.Nhs.Models.MyLearning; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; @@ -189,6 +188,38 @@ public async Task> GetResourceCertif return viewModel; } + /// + /// Gets the user certificates. + /// + /// The request model. + /// The . + public async Task GetUserCertificateDetails(MyLearningRequestModel requestModel) + { + MyLearningCertificatesDetailedViewModel viewModel = null; + + var json = JsonConvert.SerializeObject(requestModel); + var stringContent = new StringContent(json, Encoding.UTF8, "application/json"); + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"MyLearning/GetUserCertificateDetails"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + viewModel = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized + || + response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return viewModel; + } + /// /// GetCourseUrl. /// diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss index 46bf7eb76..efa5de26a 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss @@ -196,6 +196,15 @@ } } + .nhsuk-card--radius-6 { + border-radius: 6px; + overflow: hidden; + } + + .nhsuk-card__heading.nhsuk-heading-m.nhsuk-font-weight-regular { + font-weight: 400 !important; + } + label { font-family: $font-stack; } diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/Certificates.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/Certificates.cshtml new file mode 100644 index 000000000..1a79e6919 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/Certificates.cshtml @@ -0,0 +1,262 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@model MyLearningUserCertificatesViewModel; +@{ + ViewData["Title"] = "Certificates"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var errorHasOccurred = !ViewData.ModelState.IsValid; + var distintResourceType = new List(); + var routeData = ViewActivityHelper.GetActivityParameters(Model); + if (Model.UserCertificates.Any()) + { + var distinctIds = Model.UserCertificates.Select(c => c.ResourceTypeId).Distinct(); + distintResourceType = distinctIds.Where(id => Enum.IsDefined(typeof(ResourceTypeEnum), id)) + .Select(id => Enum.GetName(typeof(ResourceTypeEnum), id)).OrderBy(name => name).ToList(); + } + var searchMessage = string.Empty; + if (Model.UserCertificates.Any()) + { + int currentPage = Model.MyLearningPaging.CurrentPage; + int pageSize = Model.MyLearningPaging.PageSize; + int totalCount = Model.TotalCount; + + int startIndex = (currentPage * pageSize) + 1; + int endIndex = Math.Min(startIndex + Model.UserCertificates.Count() - 1, totalCount); + + if (startIndex == endIndex) + { + searchMessage = $"Showing {endIndex} of {totalCount} certificate{(totalCount == 1 ? "" : "s")}"; + } + else + { + searchMessage = $"Showing {startIndex}–{endIndex} of {totalCount} certificates"; + } + } + else + { + searchMessage = "No certificates found"; + } + +} + +@functions { + bool IsEntryActive(string entry) + { + var prop = Model.GetType().GetProperty(entry); + return prop != null && prop.PropertyType == typeof(bool) && (bool)prop.GetValue(Model); + } +} +@functions { + public string CertificatePreviewUrl(int resourceReferenceId) => + Url.Action("Certificate", "MyLearning", new { resourceReferenceId }); + + public string DownloadCertificateUrl( + int resourceReferenceId, + int? majorVersion = 1, + int? minorVersion = 0 + ) => + Url.Action( + "DownloadCertificate", + "MyLearning", + new + { + resourceReferenceId, + majorVersion, + minorVersion, + } + ); +} + + +@section styles { + + + +} +@section NavBreadcrumbs { + +
+
+
+
+ +
+ Home + +
+
+

My learning activity

+
+
+
+
+
+} + + + +
+
+
+ +
+ @await Component.InvokeAsync("SideNav", new { groupTitle = "Activity" }) +
+ + +
+
+ + + + + + + + + + + + + + + + + + + Certificates +
+ +
+ @if (errorHasOccurred) + { + + } + +
+
+
+
+ + + + +
+
+
+

@searchMessage

+ +
+
+
+
+
+ +
+
+ Sorted by Date order +
+ + Clear all filters + +
+
+ + + + Filter results + + + +
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+
    + @foreach (var certificate in Model.UserCertificates) + { + + var activityDate = certificate.AwardedDate.Date; + var today = DateTime.Today; + var dateTimeText = activityDate == today ? "Today" + : activityDate == today.AddDays(-1) ? "Yesterday" + : activityDate.ToString("dd MMM yyyy"); + + + +
  • +
    + +
    +

    + @if ((ResourceTypeEnum)certificate.ResourceTypeId == ResourceTypeEnum.Moodle) + { + @certificate.Title + } + else + { + @certificate.Title + } + +

    +
    +
    + Type: @UtilityHelper.GetPrettifiedResourceTypeName((ResourceTypeEnum)certificate.ResourceTypeId) +
    +
    + @if ((ResourceTypeEnum)certificate.ResourceTypeId == ResourceTypeEnum.Moodle) + { + Download + } + else + { + @certificate.Title + + } +
    +
    + Awarded: @(dateTimeText) +
    +
    +
    +
    +
  • + } +
+ +
+ +
+ @await Html.PartialAsync("_CertificatePaging", Model) +
+
+
+
+
\ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/_CertificatePaging.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/_CertificatePaging.cshtml new file mode 100644 index 000000000..9c44c4a3d --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/_CertificatePaging.cshtml @@ -0,0 +1,76 @@ +@using System.Web; +@using LearningHub.Nhs.WebUI.Models.Learning +@using LearningHub.Nhs.WebUI.Models.Search; +@model MyLearningUserCertificatesViewModel; + + +@{ + var pagingModel = Model.MyLearningPaging; + var showPaging = pagingModel.CurrentPage >= 0 && pagingModel.CurrentPage <= pagingModel.TotalPages - 1; + var previousMessage = $"{pagingModel.CurrentPage} of {pagingModel.TotalPages}"; + int CurrentPageNumber = pagingModel.CurrentPage + 1; + var nextMessage = string.Empty; + if (CurrentPageNumber <= pagingModel.TotalPages) + { + nextMessage = $"{CurrentPageNumber + 1} of {pagingModel.TotalPages}"; + } + else + { + previousMessage = $"{CurrentPageNumber - 1} of {pagingModel.TotalPages}"; + nextMessage = $"{CurrentPageNumber} of {pagingModel.TotalPages}"; + } + + var routeData = ViewActivityHelper.GetActivityParameters(Model); + routeData["CurrentPageIndex"] = pagingModel.CurrentPage.ToString(); + var nextRouteData = new Dictionary(routeData); + var previousRouteData = new Dictionary(routeData); + nextRouteData["MyLearningFormActionType"] = MyLearningFormActionTypeEnum.NextPageChange.ToString(); + previousRouteData["MyLearningFormActionType"] = MyLearningFormActionTypeEnum.PreviousPageChange.ToString(); +} + +@if (pagingModel.TotalPages > 1) +{ + +} \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index 059d93749..e95f53986 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs index 171adfe9e..18177fb9d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs @@ -2,9 +2,11 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories { using System.Collections.Generic; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.MyLearning; /// /// Resource repository interface. @@ -40,6 +42,14 @@ Task> GetResourceReferencesByOriginalResourceRefe /// . Task> GetAchievedCertificatedResourceIds(int currentUserId); + /// + /// GetUserCertificateDetails + /// + /// The current user Id. + /// The filter text + /// A representing the result of the asynchronous operation. + Task> GetUserCertificateDetails(int userId, string filterText = ""); + /// /// The get by id async. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs index 44e678633..613012ce8 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs @@ -351,6 +351,13 @@ public LearningHubDbContextOptions Options /// public virtual DbSet DashboardResourceDto { get; set; } + + /// + /// Gets or sets the UserCertificateViewModel + /// Gets or sets DashboardResourceDto. These are not entities. They are returned from the [resources].[GetUserCertificateDetails] stored proc.. + /// + public virtual DbSet UserCertificateViewModel { get; set; } + /// /// Gets or sets the ExternalContentDetailsViewModel. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index 476ecb503..4e1da0017 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs index ee2cc2714..e07c580b7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs @@ -5,9 +5,11 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Repositories using System.Data; using System.Linq; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.MyLearning; using LearningHub.Nhs.OpenApi.Repositories.EntityFramework; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; using Microsoft.Data.SqlClient; @@ -87,6 +89,25 @@ public async Task> GetAchievedCertificatedResourceIds(int currentUserI return achievedCertificatedResourceIds; } + + /// + /// GetUserCertificateDetails + /// + /// The current user Id. + /// The filter text + /// A representing the result of the asynchronous operation. + public async Task> GetUserCertificateDetails(int userId, string filterText = "") + { + var result = new List(); + var param0 = new SqlParameter("@UserId", SqlDbType.Int) { Value = userId }; + var param1 = new SqlParameter("@FilterText", SqlDbType.NVarChar, 200) { Value = filterText.Trim() ?? string.Empty }; + + result = await this.DbContext.UserCertificateViewModel + .FromSqlRaw("resources.GetUserCertificateDetails @UserId = @UserId, @FilterText = @FilterText", param0, param1).AsNoTracking().ToListAsync(); + return result; + } + + // // // diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj index 45e615ea1..e4c4b05df 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs index a697a48f5..31278d2f5 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs @@ -50,5 +50,13 @@ public interface IMoodleApiService /// pageNumber. /// List of MoodleCourseResponseModel. Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber); + + /// + /// GetUserLearningHistory. + /// + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + Task> GetUserCertificateAsync(int userId, string filterText = ""); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs index 3b53d59cc..04a68d86f 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.MyLearning; @@ -61,5 +62,13 @@ public interface IMyLearningService /// The user id. /// The . Task> PopulateMyLearningDetailedItemViewModels(List resourceActivities, int userId); + + /// + /// Gets the resource certificate details. + /// + /// The user id. + /// The request model + /// The . + Task GetUserCertificateDetails(int userId, MyLearningRequestModel requestModel); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj index 3ed37bafb..b8fd1e54d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs index 09b783fab..57c2ce89d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs @@ -207,6 +207,40 @@ public async Task> GetEnrolledCoursesHis } } + /// + /// GetUserLearningHistory. + /// + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + public async Task> GetUserCertificateAsync(int userId, string filterText="") + { + try + { + userId = 3; + var parameters = new Dictionary + { + { "userid", userId.ToString() }, + { "searchterm", filterText } + }; + + // Fetch enrolled courses + var userCertificates = await GetCallMoodleApiAsync>( + "mylearningservice_get_user_certificates", + parameters + ); + + if (userCertificates == null || userCertificates.Count == 0) + return new List(); + + return userCertificates.ToList(); + } + catch (Exception ex) + { + return null; + } + } + /// /// GetEnrolledCoursesAsync. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs index e4dbb7dbc..1e30482dc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs @@ -7,19 +7,23 @@ using System.Linq; using System.Threading.Tasks; using AutoMapper; + using Azure.Core; + using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; - using LearningHub.Nhs.Models.Enums.Report; using LearningHub.Nhs.Models.Moodle.API; using LearningHub.Nhs.Models.MyLearning; using LearningHub.Nhs.OpenApi.Models.Configuration; using LearningHub.Nhs.OpenApi.Repositories.Helpers; + using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories.Activity; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories.Hierarchy; using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; + using Newtonsoft.Json; /// /// The rating service. @@ -61,6 +65,11 @@ public class MyLearningService : IMyLearningService /// private readonly IMediaResourceActivityRepository mediaResourceActivity; + /// + /// The resource repository. + /// + private readonly IResourceRepository resourceRepository; + /// /// The moodleApiService. /// @@ -88,6 +97,7 @@ public class MyLearningService : IMyLearningService /// The settings. /// The scormActivityRepository. /// The mediaResourceActivity. + /// The resourceActivity /// The moodleApiService. public MyLearningService( IResourceActivityRepository resourceActivityRepository, @@ -99,6 +109,7 @@ public MyLearningService( IOptions settings, IScormActivityRepository scormActivityRepository, IMediaResourceActivityRepository mediaResourceActivity, + IResourceRepository resourceRepository, IMoodleApiService moodleApiService) { this.resourceActivityRepository = resourceActivityRepository; @@ -110,6 +121,7 @@ public MyLearningService( this.settings = settings.Value; this.scormActivityRepository = scormActivityRepository; this.mediaResourceActivity = mediaResourceActivity; + this.resourceRepository = resourceRepository; this.moodleApiService = moodleApiService; } @@ -597,6 +609,86 @@ public async Task> PopulateMyLearningDetai return viewModels; } + /// + /// Gets the resource certificate details. + /// + /// The user id. + /// The request model + /// The . + public async Task GetUserCertificateDetails(int userId, MyLearningRequestModel requestModel) + { + Task>? courseCertificatesTask = null; + var filteredResource = GetFilteredResourceType(requestModel); + + if (filteredResource.Count() == 0 || (filteredResource.Any() && requestModel.Courses)) + { + courseCertificatesTask = !string.IsNullOrWhiteSpace(requestModel.SearchText) ? + moodleApiService.GetUserCertificateAsync(userId, requestModel.SearchText) : moodleApiService.GetUserCertificateAsync(userId); + + } + + var resourceCertificatesTask = !string.IsNullOrWhiteSpace(requestModel.SearchText) ? + resourceRepository.GetUserCertificateDetails(userId, requestModel.SearchText) : resourceRepository.GetUserCertificateDetails(userId); + + + // Await all active tasks in parallel + if (courseCertificatesTask != null) + await Task.WhenAll(courseCertificatesTask, resourceCertificatesTask); + else + await resourceCertificatesTask; + + var resourceCertificates = resourceCertificatesTask.Result ?? Enumerable.Empty(); + + IEnumerable mappedCourseCertificates = Enumerable.Empty(); + + if (courseCertificatesTask != null) + { + var courseCertificates = courseCertificatesTask.Result ?? Enumerable.Empty(); + + mappedCourseCertificates = courseCertificates.Select(c => new UserCertificateViewModel + { + Title = string.IsNullOrWhiteSpace(c.ResourceTitle) ? c.ResourceName : c.ResourceTitle, + ResourceTypeId = (int)ResourceTypeEnum.Moodle, + ResourceReferenceId = 0, + MajorVersion = 0, + MinorVersion = 0, + AwardedDate = c.AwardedDate.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(c.AwardedDate.Value) + : DateTimeOffset.MinValue, + CertificatePreviewUrl = c.PreviewLink, + CertificateDownloadUrl = c.DownloadLink + }); + } + + var allCertificates = resourceCertificates.Concat(mappedCourseCertificates); + + if (filteredResource != null && filteredResource.Any()) + { + var allowedTypeIds = filteredResource + .Select(entry => Enum.TryParse(entry, true, out var parsed) ? (int?)parsed : null) + .Where(id => id.HasValue).Select(id => id.Value).ToHashSet(); + + allCertificates = allCertificates.Where(c => allowedTypeIds.Contains(c.ResourceTypeId)); + } + + var orderedCertificates = allCertificates.OrderByDescending(c => c.AwardedDate); + + var totalCount = orderedCertificates.Count(); + var pagedResults = orderedCertificates + .Skip(requestModel.Skip) + .Take(requestModel.Take) + .ToList(); + + return new MyLearningCertificatesDetailedViewModel + { + Certificates = pagedResults, + TotalCount = totalCount + }; + } + + + + private IQueryable ApplyFilters(IQueryable query, MyLearningRequestModel requestModel) { // Text filter - Title, Keywords or Description. @@ -704,5 +796,33 @@ private IQueryable ApplyFilters(IQueryable q return query; } + + private static List GetFilteredResourceType(MyLearningRequestModel model) + { + var selectors = new Dictionary> + { + { nameof(model.Weblink), m => m.Weblink }, + { nameof(model.File), m => m.File }, + { nameof(model.Video), m => m.Video }, + { nameof(model.Article), m => m.Article }, + { nameof(model.Case), m => m.Case }, + { nameof(model.Image), m => m.Image }, + { nameof(model.Audio), m => m.Audio }, + { nameof(model.Elearning), m => m.Elearning }, + { nameof(model.Html), m => m.Html }, + { nameof(model.Assessment), m => m.Assessment }, + { nameof(model.Courses), m => m.Courses } + }; + + var normalisationMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { nameof(model.Courses), "Moodle" } + }; + + return selectors + .Where(kvp => kvp.Value(model)) + .Select(kvp => normalisationMap.TryGetValue(kvp.Key, out var mapped) ? mapped : kvp.Key).ToList(); + } + } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index b1cddf18b..40287528c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs index e810e95d2..4763f21cc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs @@ -108,5 +108,20 @@ public async Task GetResourceCertificateDetails(int resourceRefer var certificateDetails = await this.myLearningService.GetResourceCertificateDetails((userId == 0) ? this.CurrentUserId.GetValueOrDefault() : (int)userId, resourceReferenceId, majorVersion, minorVersion); return this.Ok(certificateDetails); } + + /// + /// Gets the user certificate details. + /// + /// + /// The requestModel. + /// + /// A representing the result of the asynchronous operation. + [HttpPost] + [Route("GetUserCertificateDetails")] + public async Task GetUserCertificateDetails([FromBody] MyLearningRequestModel requestModel) + { + var certificateDetails = await this.myLearningService.GetUserCertificateDetails(this.CurrentUserId.GetValueOrDefault(), requestModel); + return this.Ok(certificateDetails); + } } } diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql new file mode 100644 index 000000000..efbacc9d9 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql @@ -0,0 +1,129 @@ +------------------------------------------------------------------------------- +-- Author Tobi +-- Created 22-08-2025 +-- Purpose Gets the certificate details for all completed resources. +-- +-- Modification History +-- +-- 22-08-2025 Tobi Initial Revision +------------------------------------------------------------------------------- +CREATE PROCEDURE [resources].[GetUsercertificateDetails] + @UserId INT, + @FilterText NVARCHAR(200) = N'' +AS +BEGIN + SET NOCOUNT ON; + + -- Temp table for better stats and indexing + IF OBJECT_ID('tempdb..#MyActivity') IS NOT NULL + DROP TABLE #MyActivity; + + CREATE TABLE #MyActivity ( + ResourceId INT NOT NULL, + ResourceActivityId INT NOT NULL, + PRIMARY KEY CLUSTERED (ResourceActivityId) + ); + + INSERT INTO #MyActivity (ResourceId, ResourceActivityId) + SELECT + ra.ResourceId, + MAX(ra.Id) AS ResourceActivityId + FROM activity.ResourceActivity ra + JOIN resources.Resource r + ON ra.ResourceId = r.Id + JOIN resources.ResourceVersion rv + ON rv.Id = ra.ResourceVersionId + WHERE ra.UserId = @UserId + AND rv.CertificateEnabled = 1 + AND ( + (r.ResourceTypeId IN (2, 7) AND ra.ActivityStatusId = 3) + OR (ra.ActivityStart < '2020-09-07T00:00:00+00:00') + OR EXISTS ( + SELECT 1 + FROM activity.MediaResourceActivity mar + WHERE mar.ResourceActivityId = ra.Id + AND mar.PercentComplete = 100 + ) + OR (r.ResourceTypeId = 6 AND ( + EXISTS ( + SELECT 1 + FROM activity.ScormActivity sa + WHERE sa.ResourceActivityId = ra.Id + AND sa.CmiCoreLesson_status IN (3,5) + ) + OR ra.ActivityStatusId IN (3,5) + )) + OR (r.ResourceTypeId = 11 AND ( + EXISTS ( + SELECT 1 + FROM activity.AssessmentResourceActivity ara + JOIN resources.AssessmentResourceVersion arv + ON arv.ResourceVersionId = ra.ResourceVersionId + WHERE ara.ResourceActivityId = ra.Id + AND ara.Score >= arv.PassMark + ) + OR ra.ActivityStatusId IN (3,5) + )) + OR (r.ResourceTypeId IN (1, 5, 8, 9, 10, 12) + AND ra.ActivityStatusId = 3) + ) + GROUP BY ra.ResourceId; + + -- Full result set without paging + SELECT + rv.Id, + rv.Title, + r.ResourceTypeId, + ( + SELECT TOP (1) rr.OriginalResourceReferenceId + FROM resources.ResourceReference rr + JOIN hierarchy.NodePath np + ON np.Id = rr.NodePathId + AND np.NodeId = n.Id + AND np.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId + AND rr.Deleted = 0 + ) AS ResourceReferenceID, + rv.MajorVersion, + rv.MinorVersion, + ra.ActivityStart AS AwardedDate, + NULL AS CertificateDownloadUrl, + NULL AS CertificatePreviewUrl + FROM #MyActivity ma + JOIN activity.ResourceActivity ra + ON ra.Id = ma.ResourceActivityId + JOIN resources.ResourceVersion rv + ON rv.Id = ra.ResourceVersionId + AND rv.Deleted = 0 + JOIN resources.Resource r + ON r.Id = rv.ResourceId + JOIN hierarchy.Publication p + ON rv.PublicationId = p.Id + AND p.Deleted = 0 + JOIN hierarchy.NodeResource nr + ON r.Id = nr.ResourceId + AND nr.Deleted = 0 + JOIN hierarchy.Node n + ON n.Id = nr.NodeId + AND n.Hidden = 0 + AND n.Deleted = 0 + JOIN hierarchy.NodePath np + ON np.NodeId = n.Id + AND np.Deleted = 0 + AND np.IsActive = 1 + JOIN hierarchy.NodeVersion nv + ON nv.NodeId = np.CatalogueNodeId + AND nv.VersionStatusId = 2 + AND nv.Deleted = 0 + JOIN hierarchy.CatalogueNodeVersion cnv + ON cnv.NodeVersionId = nv.Id + AND cnv.Deleted = 0 + WHERE ( + @FilterText = N'' + OR rv.Title LIKE @FilterText + N'%' + ) + ORDER BY ma.ResourceActivityId DESC, rv.Title + OPTION (RECOMPILE); +END; +GO +