diff --git a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index a0de54f8f..e5fff6967 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj +++ b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj @@ -89,7 +89,7 @@ - + diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index 1569f9734..536fb9b3a 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -12,6 +12,7 @@ + diff --git a/LearningHub.Nhs.WebUI/Configuration/MoodleApiConfig.cs b/LearningHub.Nhs.WebUI/Configuration/MoodleApiConfig.cs new file mode 100644 index 000000000..ec874477e --- /dev/null +++ b/LearningHub.Nhs.WebUI/Configuration/MoodleApiConfig.cs @@ -0,0 +1,33 @@ +namespace LearningHub.Nhs.WebUI.Configuration +{ + /// + /// The Moodle Settings. + /// + public class MoodleApiConfig + { + /// + /// Gets or sets the base url for the Moodle service. + /// + public string BaseUrl { get; set; } = null!; + + /// + /// Gets or sets the Web service Rest Format. + /// + public string MoodleWSRestFormat { get; set; } = null!; + + /// + /// Gets or sets the token. + /// + public string WSToken { get; set; } = null!; + + /// + /// Gets or sets the token. + /// + public string ApiPath { get; set; } = "webservice/rest/server.php"; + + /// + /// Gets or sets the token. + /// + public string CoursePath { get; set; } = "course/view.php"; + } +} diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/MyLearningController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/MyLearningController.cs index bcd13c08a..64b6de28d 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/MyLearningController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/MyLearningController.cs @@ -44,6 +44,34 @@ public async Task GetActivityDetailed([FromBody] MyLearningRequest return this.Ok(activity); } + /// + /// Gets the detailed activity data. + /// + /// The request model - filter settings. + /// The . + [HttpPost] + [Route("GetUserRecentMyLearningActivities")] + public async Task GetUserRecentMyLearningActivities([FromBody] MyLearningRequestModel requestModel) + { + var activity = await this.myLearningService.GetUserRecentMyLearningActivities(requestModel); + + return this.Ok(activity); + } + + /// + /// Gets the detailed activity data. + /// + /// The request model - filter settings. + /// The . + [HttpPost] + [Route("GetUserLearningHistory")] + public async Task GetUserLearningHistory([FromBody] MyLearningRequestModel requestModel) + { + var activity = await this.myLearningService.GetUserLearningHistory(requestModel); + + return this.Ok(activity); + } + /// /// Gets the played segment data for the progress modal in My Learning screen. /// diff --git a/LearningHub.Nhs.WebUI/Controllers/ContributeController.cs b/LearningHub.Nhs.WebUI/Controllers/ContributeController.cs index fc3b7f76a..27ae615ce 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ContributeController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ContributeController.cs @@ -28,6 +28,7 @@ public class ContributeController : BaseController private readonly IFileService fileService; private readonly IResourceService resourceService; private readonly IUserService userService; + private readonly IUserGroupService userGroupService; /// /// Initializes a new instance of the class. @@ -37,6 +38,7 @@ public class ContributeController : BaseController /// Logger. /// Settings. /// User service. + /// userGroupService. /// File service. /// Resource service. /// Azure media service. @@ -48,6 +50,7 @@ public ContributeController( ILogger logger, IOptions settings, IUserService userService, + IUserGroupService userGroupService, IFileService fileService, IResourceService resourceService, IAzureMediaService azureMediaService, @@ -58,6 +61,7 @@ public ContributeController( this.authConfig = authConfig; this.userService = userService; + this.userGroupService = userGroupService; this.fileService = fileService; this.resourceService = resourceService; this.azureMediaService = azureMediaService; @@ -167,7 +171,8 @@ public async Task CreateVersion(int resourceId) [Route("my-contributions/{selectedTab}/{catalogueId}/{nodeId}")] public async Task MyContributions() { - if ((this.User.IsInRole("ReadOnly") || this.User.IsInRole("BasicUser")) && !await this.resourceService.UserHasPublishedResourcesAsync()) + bool catalogueContributionPermission = await this.userGroupService.UserHasCatalogueContributionPermission(); + if ((this.User.IsInRole("ReadOnly") || this.User.IsInRole("BasicUser")) || (!catalogueContributionPermission && (!await this.resourceService.UserHasPublishedResourcesAsync()))) { return this.RedirectToAction("AccessDenied", "Home"); } diff --git a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs index d5f53c00d..57c170ae6 100644 --- a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs @@ -11,6 +11,7 @@ namespace LearningHub.Nhs.WebUI.Controllers using LearningHub.Nhs.Models.Content; using LearningHub.Nhs.Models.Enums.Content; using LearningHub.Nhs.Models.Extensions; + using LearningHub.Nhs.Models.Moodle.API; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Filters; using LearningHub.Nhs.WebUI.Helpers; @@ -218,13 +219,12 @@ public async Task Index(string myLearningDashboard = "my-in-progr var cataloguesTask = this.dashboardService.GetCataloguesAsync(catalogueDashboard, 1); var userGroupsTask = this.userGroupService.UserHasCatalogueContributionPermission(); - var enrolledCoursesTask = Task.FromResult(new List()); - var enableMoodle = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.EnableMoodle)).Result; - this.ViewBag.EnableMoodle = enableMoodle; - this.ViewBag.ValidMoodleUser = this.CurrentMoodleUserId > 0; + var enrolledCoursesTask = Task.FromResult(new List()); + (bool enableMoodle, int currentMoodleUserId) = await this.GetMoodleFeatureStateAsync(); + if (enableMoodle && myLearningDashboard == "my-enrolled-courses") { - enrolledCoursesTask = this.dashboardService.GetEnrolledCoursesFromMoodleAsync(this.CurrentMoodleUserId, 1); + enrolledCoursesTask = this.dashboardService.GetEnrolledCoursesFromMoodleAsync(currentMoodleUserId, 1); } await Task.WhenAll(learningTask, resourcesTask, cataloguesTask, userGroupsTask); @@ -280,9 +280,7 @@ public async Task LoadPage(string dashBoardTray = "my-learning", Catalogues = new Nhs.Models.Dashboard.DashboardCatalogueResponseViewModel { Type = catalogueDashBoard }, }; - var enableMoodle = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.EnableMoodle)).Result; - this.ViewBag.EnableMoodle = enableMoodle; - this.ViewBag.ValidMoodleUser = this.CurrentMoodleUserId > 0; + (bool enableMoodle, int currentMoodleUserId) = await this.GetMoodleFeatureStateAsync(); bool isAjax = this.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest"; @@ -449,5 +447,28 @@ private async Task GetLandingPageContent(bool preview = fa return new LandingPageViewModel { PageSectionDetailViewModels = new List(), PageViewModel = new PageViewModel { PageSections = new List { } } }; } } + + /// + /// Asynchronously retrieves the state of the Moodle feature and the current Moodle user ID. + /// + /// The method checks if the Moodle feature is enabled and retrieves the current Moodle + /// user ID. If the user ID is not already set, it attempts to obtain it asynchronously from the dashboard + /// service. + /// A tuple containing a boolean indicating whether the Moodle feature is enabled and an integer representing + /// the current Moodle user ID. + private async Task<(bool enableMoodle, int currentMoodleUserId)> GetMoodleFeatureStateAsync() + { + var enableMoodle = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.EnableMoodle)).Result; + this.ViewBag.EnableMoodle = enableMoodle; + int currentMoodleUserId = this.CurrentMoodleUserId; + + if (currentMoodleUserId == 0) + { + currentMoodleUserId = await this.dashboardService.GetMoodleUserIdAsync(this.CurrentUserId); + } + + this.ViewBag.ValidMoodleUser = currentMoodleUserId > 0; + return (enableMoodle, currentMoodleUserId); + } } } diff --git a/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs b/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs index a791bd5d1..a1762ec71 100644 --- a/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs @@ -31,7 +31,7 @@ [ServiceFilter(typeof(LoginWizardFilter))] public class MyLearningController : BaseController { - private const int MyLearningPageSize = 10; + private const int MyLearningPageSize = 5; private readonly IMyLearningService myLearningService; private readonly IResourceService resourceService; private readonly IHierarchyService hierarchyService; @@ -94,6 +94,181 @@ public static string RenderRazorViewToString(Controller controller, string viewN } } + /////// + /////// Index. + /////// + /////// learningRequest. + /////// The my learning dashboard type. + /////// IActionResult. + ////[Route("MyLearning")] + ////[Route("MyLearning/activity")] + ////[HttpGet] + ////[HttpPost] + ////public async Task Index(MyLearningViewModel learningRequest = null, string myLearningDashboard = null) + ////{ + //// var myLearningRequestModel = new MyLearningRequestModel + //// { + //// SearchText = learningRequest.SearchText?.Trim(), + //// Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + //// Take = MyLearningPageSize, + //// TimePeriod = !string.IsNullOrWhiteSpace(learningRequest.TimePeriod) ? learningRequest.TimePeriod : "allDates", + //// StartDate = learningRequest.StartDate, + //// EndDate = learningRequest.EndDate, + //// Weblink = learningRequest.Weblink, + //// File = learningRequest.File, + //// Video = learningRequest.Video, + //// Article = learningRequest.Article, + //// Case = learningRequest.Case, + //// Image = learningRequest.Image, + //// Audio = learningRequest.Audio, + //// Elearning = learningRequest.Elearning, + //// Html = learningRequest.Html, + //// Assessment = learningRequest.Assessment, + //// Complete = learningRequest.Complete, + //// Incomplete = learningRequest.Incomplete, + //// Passed = learningRequest.Passed, + //// Failed = learningRequest.Failed, + //// Downloaded = learningRequest.Downloaded, + //// Viewed = learningRequest.Viewed, + //// Launched = learningRequest.Launched, + //// CertificateEnabled = learningRequest.CertificateEnabled, + //// }; + + //// if (myLearningDashboard != null) + //// { + //// if (myLearningDashboard == "my-in-progress") + //// { + //// myLearningRequestModel.Incomplete = true; + //// myLearningRequestModel.Failed = true; + //// } + //// else if (myLearningDashboard == "my-recent-completed") + //// { + //// myLearningRequestModel.Complete = true; + //// myLearningRequestModel.Passed = true; + //// myLearningRequestModel.Downloaded = true; + //// } + //// else if (myLearningDashboard == "my-certificates") + //// { + //// myLearningRequestModel.CertificateEnabled = true; + //// myLearningRequestModel.Complete = true; + //// myLearningRequestModel.Passed = true; + //// myLearningRequestModel.Downloaded = true; + //// } + //// } + + //// switch (learningRequest.MyLearningFormActionType) + //// { + //// case MyLearningFormActionTypeEnum.NextPageChange: + //// learningRequest.CurrentPageIndex += 1; + //// myLearningRequestModel.Skip = learningRequest.CurrentPageIndex * MyLearningPageSize; + //// break; + + //// case MyLearningFormActionTypeEnum.PreviousPageChange: + //// learningRequest.CurrentPageIndex -= 1; + //// myLearningRequestModel.Skip = learningRequest.CurrentPageIndex * MyLearningPageSize; + //// break; + //// case MyLearningFormActionTypeEnum.BasicSearch: + + //// myLearningRequestModel = new MyLearningRequestModel + //// { + //// SearchText = learningRequest.SearchText?.Trim(), + //// TimePeriod = !string.IsNullOrWhiteSpace(learningRequest.TimePeriod) ? learningRequest.TimePeriod : "allDates", + //// Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + //// Take = MyLearningPageSize, + //// }; + //// break; + + //// case MyLearningFormActionTypeEnum.ApplyWeekFilter: + //// myLearningRequestModel = new MyLearningRequestModel + //// { + //// SearchText = learningRequest.SearchText?.Trim(), + //// TimePeriod = "thisWeek", + //// Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + //// Take = MyLearningPageSize, + //// }; + //// break; + //// case MyLearningFormActionTypeEnum.ApplyMonthFilter: + //// myLearningRequestModel = new MyLearningRequestModel + //// { + //// SearchText = learningRequest.SearchText?.Trim(), + //// TimePeriod = "thisMonth", + //// Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + //// Take = MyLearningPageSize, + //// }; + //// break; + + //// case MyLearningFormActionTypeEnum.ApplyTwelveMonthFilter: + //// myLearningRequestModel = new MyLearningRequestModel + //// { + //// SearchText = learningRequest.SearchText?.Trim(), + //// TimePeriod = "last12Months", + //// Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + //// Take = MyLearningPageSize, + //// }; + //// break; + + //// case MyLearningFormActionTypeEnum.ApplyMajorFilters: + //// if (learningRequest.TimePeriod == "dateRange") + //// { + //// if (!this.ModelState.IsValid) + //// { + //// break; + //// } + + //// myLearningRequestModel.TimePeriod = learningRequest.TimePeriod; + //// myLearningRequestModel.StartDate = learningRequest.GetStartDate().HasValue ? learningRequest.GetStartDate().Value : null; + //// myLearningRequestModel.EndDate = learningRequest.GetEndDate().HasValue ? learningRequest.GetEndDate().Value : null; + //// } + + //// break; + + //// case MyLearningFormActionTypeEnum.ClearAllFilters: + + //// myLearningRequestModel = new MyLearningRequestModel + //// { + //// SearchText = learningRequest.SearchText?.Trim(), + //// Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + //// TimePeriod = "allDates", + //// Take = MyLearningPageSize, + //// }; + //// break; + //// } + + //// ////var result = await this.myLearningService.GetUserRecentMyLearningActivities(myLearningRequestModel); + //// var result = await this.myLearningService.GetActivityDetailed(myLearningRequestModel); + //// var response = new MyLearningViewModel(myLearningRequestModel); + //// if (learningRequest.TimePeriod == "dateRange") + //// { + //// response.StartDay = learningRequest.StartDay; + //// response.StartMonth = learningRequest.StartMonth; + //// response.StartYear = learningRequest.StartYear; + //// response.EndDay = learningRequest.EndDay; + //// response.EndMonth = learningRequest.EndMonth; + //// response.EndYear = learningRequest.EndYear; + //// } + + //// if (result != null) + //// { + //// response.TotalCount = result.TotalCount; + //// response.Activities = result.Activities.Select(entry => new ActivityDetailedItemViewModel(entry)).ToList(); + //// if (response.Activities.Any()) + //// { + //// foreach (var activity in response.Activities) + //// { + //// if (!response.MostRecentResources.Contains(activity.ResourceId)) + //// { + //// activity.IsMostRecent = true; + //// response.MostRecentResources.Add(activity.ResourceId); + //// } + //// } + //// } + //// } + + //// response.MyLearningPaging = new MyLearningPagingModel() { CurrentPage = learningRequest.CurrentPageIndex, PageSize = MyLearningPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; + //// this.ViewBag.MyLearningHelpUrl = this.Settings.SupportUrls.MyLearningHelpUrl; + //// return this.View(response); + ////} + /// /// Index. /// @@ -104,7 +279,7 @@ public static string RenderRazorViewToString(Controller controller, string viewN [Route("MyLearning/activity")] [HttpGet] [HttpPost] - public async Task Index(MyLearningViewModel learningRequest = null, string myLearningDashboard = null) + public async Task Index(MyLearningUserActivitiesViewModel learningRequest = null, string myLearningDashboard = null) { var myLearningRequestModel = new MyLearningRequestModel { @@ -216,8 +391,6 @@ public async Task Index(MyLearningViewModel learningRequest = nul } myLearningRequestModel.TimePeriod = learningRequest.TimePeriod; - myLearningRequestModel.StartDate = learningRequest.GetStartDate().HasValue ? learningRequest.GetStartDate().Value : null; - myLearningRequestModel.EndDate = learningRequest.GetEndDate().HasValue ? learningRequest.GetEndDate().Value : null; } break; @@ -234,33 +407,164 @@ public async Task Index(MyLearningViewModel learningRequest = nul break; } - var result = await this.myLearningService.GetActivityDetailed(myLearningRequestModel); - var response = new MyLearningViewModel(myLearningRequestModel); - if (learningRequest.TimePeriod == "dateRange") - { - response.StartDay = learningRequest.StartDay; - response.StartMonth = learningRequest.StartMonth; - response.StartYear = learningRequest.StartYear; - response.EndDay = learningRequest.EndDay; - response.EndMonth = learningRequest.EndMonth; - response.EndYear = learningRequest.EndYear; - } + var result = await this.myLearningService.GetUserRecentMyLearningActivities(myLearningRequestModel); + var response = new MyLearningUserActivitiesViewModel(myLearningRequestModel); if (result != null) { response.TotalCount = result.TotalCount; - response.Activities = result.Activities.Select(entry => new ActivityDetailedItemViewModel(entry)).ToList(); - if (response.Activities.Any()) + response.Activities = result.Activities; + } + + response.MyLearningPaging = new MyLearningPagingModel() { CurrentPage = learningRequest.CurrentPageIndex, PageSize = MyLearningPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; + this.ViewBag.MyLearningHelpUrl = this.Settings.SupportUrls.MyLearningHelpUrl; + return this.View(response); + } + + /// + /// Index. + /// + /// learningRequest. + /// The my learning dashboard type. + /// IActionResult. + [Route("MyLearning/LearningHistory")] + [HttpGet] + [HttpPost] + public async Task LearningHistory(MyLearningUserActivitiesViewModel learningRequest = null, string myLearningDashboard = null) + { + var myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + TimePeriod = !string.IsNullOrWhiteSpace(learningRequest.TimePeriod) ? learningRequest.TimePeriod : "allDates", + StartDate = learningRequest.StartDate, + EndDate = learningRequest.EndDate, + Weblink = learningRequest.Weblink, + File = learningRequest.File, + Video = learningRequest.Video, + Article = learningRequest.Article, + Case = learningRequest.Case, + Image = learningRequest.Image, + Audio = learningRequest.Audio, + Elearning = learningRequest.Elearning, + Html = learningRequest.Html, + Assessment = learningRequest.Assessment, + Complete = learningRequest.Complete, + Incomplete = learningRequest.Incomplete, + Passed = learningRequest.Passed, + Failed = learningRequest.Failed, + Downloaded = learningRequest.Downloaded, + Viewed = learningRequest.Viewed, + Launched = learningRequest.Launched, + CertificateEnabled = learningRequest.CertificateEnabled, + }; + + if (myLearningDashboard != null) + { + if (myLearningDashboard == "my-in-progress") { - foreach (var activity in response.Activities) + myLearningRequestModel.Incomplete = true; + myLearningRequestModel.Failed = true; + } + else if (myLearningDashboard == "my-recent-completed") + { + myLearningRequestModel.Complete = true; + myLearningRequestModel.Passed = true; + myLearningRequestModel.Downloaded = true; + } + else if (myLearningDashboard == "my-certificates") + { + myLearningRequestModel.CertificateEnabled = true; + myLearningRequestModel.Complete = true; + myLearningRequestModel.Passed = true; + myLearningRequestModel.Downloaded = true; + } + } + + switch (learningRequest.MyLearningFormActionType) + { + case MyLearningFormActionTypeEnum.NextPageChange: + learningRequest.CurrentPageIndex += 1; + myLearningRequestModel.Skip = learningRequest.CurrentPageIndex * MyLearningPageSize; + break; + + case MyLearningFormActionTypeEnum.PreviousPageChange: + learningRequest.CurrentPageIndex -= 1; + myLearningRequestModel.Skip = learningRequest.CurrentPageIndex * MyLearningPageSize; + break; + case MyLearningFormActionTypeEnum.BasicSearch: + + myLearningRequestModel = new MyLearningRequestModel { - if (!response.MostRecentResources.Contains(activity.ResourceId)) + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = !string.IsNullOrWhiteSpace(learningRequest.TimePeriod) ? learningRequest.TimePeriod : "allDates", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + + case MyLearningFormActionTypeEnum.ApplyWeekFilter: + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = "thisWeek", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + case MyLearningFormActionTypeEnum.ApplyMonthFilter: + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = "thisMonth", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + + case MyLearningFormActionTypeEnum.ApplyTwelveMonthFilter: + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = "last12Months", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + + case MyLearningFormActionTypeEnum.ApplyMajorFilters: + if (learningRequest.TimePeriod == "dateRange") + { + if (!this.ModelState.IsValid) { - activity.IsMostRecent = true; - response.MostRecentResources.Add(activity.ResourceId); + break; } + + myLearningRequestModel.TimePeriod = learningRequest.TimePeriod; } - } + + break; + + case MyLearningFormActionTypeEnum.ClearAllFilters: + + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + TimePeriod = "allDates", + Take = MyLearningPageSize, + }; + break; + } + + var result = await this.myLearningService.GetUserLearningHistory(myLearningRequestModel); + var response = new MyLearningUserActivitiesViewModel(myLearningRequestModel); + + if (result != null) + { + response.TotalCount = result.TotalCount; + response.Activities = result.Activities; } response.MyLearningPaging = new MyLearningPagingModel() { CurrentPage = learningRequest.CurrentPageIndex, PageSize = MyLearningPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; diff --git a/LearningHub.Nhs.WebUI/Controllers/MyRecentLearningController.cs b/LearningHub.Nhs.WebUI/Controllers/MyRecentLearningController.cs new file mode 100644 index 000000000..2d909f839 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Controllers/MyRecentLearningController.cs @@ -0,0 +1,495 @@ +namespace LearningHub.Nhs.WebUI.Controllers +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Report; + using LearningHub.Nhs.WebUI.Configuration; + using LearningHub.Nhs.WebUI.Extensions; + using LearningHub.Nhs.WebUI.Filters; + using LearningHub.Nhs.WebUI.Helpers; + using LearningHub.Nhs.WebUI.Interfaces; + using LearningHub.Nhs.WebUI.Models; + using LearningHub.Nhs.WebUI.Models.Learning; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Rendering; + using Microsoft.AspNetCore.Mvc.ViewEngines; + using Microsoft.AspNetCore.Mvc.ViewFeatures; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + /// + /// The MyLearningController. + /// + [Authorize] + [ServiceFilter(typeof(LoginWizardFilter))] + public class MyRecentLearningController : BaseController + { + private const int MyLearningPageSize = 10; + private readonly IMyLearningService myLearningService; + private readonly IResourceService resourceService; + private readonly IHierarchyService hierarchyService; + private readonly IUserService userService; + private readonly IPDFReportService pdfReportService; + private readonly IFileService fileService; + private readonly string filePath; + + /// + /// Initializes a new instance of the class. + /// + /// The hostingEnvironment. + /// The logger. + /// The settings. + /// The httpClientFactory. + /// myLearning service. + /// resource Service. + /// hierarchy Service. + /// user Service. + /// PDF Report Service. + /// fileService. + public MyRecentLearningController(IWebHostEnvironment hostingEnvironment, ILogger logger, IOptions settings, IHttpClientFactory httpClientFactory, IMyLearningService myLearningService, IResourceService resourceService, IHierarchyService hierarchyService, IUserService userService, IPDFReportService pdfReportService, IFileService fileService) + : base(hostingEnvironment, httpClientFactory, logger, settings.Value) + { + this.myLearningService = myLearningService; + this.resourceService = resourceService; + this.userService = userService; + this.pdfReportService = pdfReportService; + this.hierarchyService = hierarchyService; + this.fileService = fileService; + this.filePath = "CatalogueImageDirectory"; + } + + /// + /// RenderRazorViewToString. + /// + /// controller. + /// viewName. + /// model. + /// Html as string. + public static string RenderRazorViewToString(Controller controller, string viewName, object model = null) + { + controller.ViewData.Model = model; + using (var sw = new StringWriter()) + { + IViewEngine viewEngine = + controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as + ICompositeViewEngine; + ViewEngineResult viewResult = viewEngine.FindView(controller.ControllerContext, viewName, false); + + ViewContext viewContext = new ViewContext( + controller.ControllerContext, + viewResult.View, + controller.ViewData, + controller.TempData, + sw, + new HtmlHelperOptions()); + viewResult.View.RenderAsync(viewContext); + return sw.GetStringBuilder().ToString(); + } + } + + /// + /// Index. + /// + /// learningRequest. + /// The my learning dashboard type. + /// IActionResult. + [Route("MyRecentLearning")] + [Route("MyRecentLearning/activity")] + [HttpGet] + [HttpPost] + public async Task Index(MyLearningViewModel learningRequest = null, string myLearningDashboard = null) + { + var myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + TimePeriod = !string.IsNullOrWhiteSpace(learningRequest.TimePeriod) ? learningRequest.TimePeriod : "allDates", + StartDate = learningRequest.StartDate, + EndDate = learningRequest.EndDate, + Weblink = learningRequest.Weblink, + File = learningRequest.File, + Video = learningRequest.Video, + Article = learningRequest.Article, + Case = learningRequest.Case, + Image = learningRequest.Image, + Audio = learningRequest.Audio, + Elearning = learningRequest.Elearning, + Html = learningRequest.Html, + Assessment = learningRequest.Assessment, + Complete = learningRequest.Complete, + Incomplete = learningRequest.Incomplete, + Passed = learningRequest.Passed, + Failed = learningRequest.Failed, + Downloaded = learningRequest.Downloaded, + Viewed = learningRequest.Viewed, + Launched = learningRequest.Launched, + CertificateEnabled = learningRequest.CertificateEnabled, + }; + + if (myLearningDashboard != null) + { + if (myLearningDashboard == "my-in-progress") + { + myLearningRequestModel.Incomplete = true; + myLearningRequestModel.Failed = true; + } + else if (myLearningDashboard == "my-recent-completed") + { + myLearningRequestModel.Complete = true; + myLearningRequestModel.Passed = true; + myLearningRequestModel.Downloaded = true; + } + else if (myLearningDashboard == "my-certificates") + { + myLearningRequestModel.CertificateEnabled = true; + myLearningRequestModel.Complete = true; + myLearningRequestModel.Passed = true; + myLearningRequestModel.Downloaded = true; + } + } + + switch (learningRequest.MyLearningFormActionType) + { + case MyLearningFormActionTypeEnum.NextPageChange: + learningRequest.CurrentPageIndex += 1; + myLearningRequestModel.Skip = learningRequest.CurrentPageIndex * MyLearningPageSize; + break; + + case MyLearningFormActionTypeEnum.PreviousPageChange: + learningRequest.CurrentPageIndex -= 1; + myLearningRequestModel.Skip = learningRequest.CurrentPageIndex * MyLearningPageSize; + break; + case MyLearningFormActionTypeEnum.BasicSearch: + + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = !string.IsNullOrWhiteSpace(learningRequest.TimePeriod) ? learningRequest.TimePeriod : "allDates", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + + case MyLearningFormActionTypeEnum.ApplyWeekFilter: + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = "thisWeek", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + case MyLearningFormActionTypeEnum.ApplyMonthFilter: + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = "thisMonth", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + + case MyLearningFormActionTypeEnum.ApplyTwelveMonthFilter: + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + TimePeriod = "last12Months", + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + Take = MyLearningPageSize, + }; + break; + + case MyLearningFormActionTypeEnum.ApplyMajorFilters: + if (learningRequest.TimePeriod == "dateRange") + { + if (!this.ModelState.IsValid) + { + break; + } + + myLearningRequestModel.TimePeriod = learningRequest.TimePeriod; + myLearningRequestModel.StartDate = learningRequest.GetStartDate().HasValue ? learningRequest.GetStartDate().Value : null; + myLearningRequestModel.EndDate = learningRequest.GetEndDate().HasValue ? learningRequest.GetEndDate().Value : null; + } + + break; + + case MyLearningFormActionTypeEnum.ClearAllFilters: + + myLearningRequestModel = new MyLearningRequestModel + { + SearchText = learningRequest.SearchText?.Trim(), + Skip = learningRequest.CurrentPageIndex * MyLearningPageSize, + TimePeriod = "allDates", + Take = MyLearningPageSize, + }; + break; + } + + var result = await this.myLearningService.GetUserRecentMyLearningActivities(myLearningRequestModel); + var response = new MyLearningUserActivitiesViewModel(myLearningRequestModel); + + if (result != null) + { + response.TotalCount = result.TotalCount; + response.Activities = result.Activities; + } + + response.MyLearningPaging = new MyLearningPagingModel() { CurrentPage = learningRequest.CurrentPageIndex, PageSize = MyLearningPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; + this.ViewBag.MyLearningHelpUrl = this.Settings.SupportUrls.MyLearningHelpUrl; + return this.View(response); + } + + /// + /// Function to export activity report to pdf. + /// + /// myLearningRequestModel. + /// A representing the result of the asynchronous operation. + [Route("/MyLearning/ExportToPDF")] + [HttpPost] + public async Task ExportToPDF(MyLearningRequestModel myLearningRequestModel) + { + var filter = myLearningRequestModel; + filter.Skip = 0; + filter.Take = 999; + var userDetails = await this.userService.GetCurrentUserBasicDetailsAsync(); + var response = new MyLearningViewModel(); + var result = await this.myLearningService.GetActivityDetailed(filter); + if (result != null) + { + response.TotalCount = result.TotalCount; + response.Activities = result.Activities.Select(entry => new ActivityDetailedItemViewModel(entry)).ToList(); + if (response.Activities.Any()) + { + foreach (var activity in response.Activities) + { + if (!response.MostRecentResources.Contains(activity.ResourceId)) + { + activity.IsMostRecent = true; + response.MostRecentResources.Add(activity.ResourceId); + } + } + } + } + + Tuple modelData = Tuple.Create(userDetails, response); + var renderedViewHTML = RenderRazorViewToString(this, "ExportToPDF", modelData); + ReportStatusModel reportStatusModel = new ReportStatusModel(); + var pdfReportResponse = await this.pdfReportService.PdfReport(renderedViewHTML, userDetails.Id); + if (pdfReportResponse != null) + { + do + { + reportStatusModel = await this.pdfReportService.PdfReportStatus(pdfReportResponse); + } + while (reportStatusModel.Id == 1); + + var pdfReportFile = await this.pdfReportService.GetPdfReportFile(pdfReportResponse); + if (pdfReportFile != null) + { + var fileName = "ActivityReport.pdf"; + return this.File(pdfReportFile, FileHelper.GetContentTypeFromFileName(fileName), fileName); + } + } + + return this.View(new Tuple(userDetails, response)); + } + + /// + /// Gets the played segment data for the progress modal in My Learning screen. + /// + /// The resourceId. + /// The resourceVersionId. + /// The majorVersion. + /// The max time. + /// Return url. + /// The . + [HttpGet] + [Route("my-learning/activity/{resourceId}/view-progress")] + public async Task ViewProgress(int resourceId, int resourceReferenceId, int version, long maxTime, string returnUrl = "/") + { + this.ViewBag.ReturnUrl = returnUrl; + var playedSegments = await this.myLearningService.GetPlayedSegments(resourceId, version); + + var allSegments = new List(); + var currentTime = 0; + var mediaLengthInSeconds = maxTime / 1000M; + + foreach (var segment in playedSegments.OrderBy(p => p.SegmentStartTime)) + { + if (segment.SegmentStartTime > currentTime) + { + allSegments.Add(new ResourcePlayedSegment + { + SegmentStartTime = currentTime, + SegmentEndTime = segment.SegmentStartTime, + Played = false, + }); + } + + allSegments.Add(new ResourcePlayedSegment + { + SegmentStartTime = segment.SegmentStartTime, + SegmentEndTime = segment.SegmentEndTime, + Played = true, + }); + + currentTime = segment.SegmentEndTime; + } + + if (currentTime < mediaLengthInSeconds) + { + allSegments.Add(new ResourcePlayedSegment + { + SegmentStartTime = currentTime, + SegmentEndTime = (int)mediaLengthInSeconds, + Played = false, + }); + } + + allSegments.ForEach(s => s.Percentage = Math.Round((s.SegmentEndTime - s.SegmentStartTime) / mediaLengthInSeconds * 100, 2)); + + var vm = new ActivityViewProgress + { + ResourceReferenceId = resourceReferenceId, + Segments = allSegments, + MediaLength = ResourcePlayedSegment.GetDurationHhmmss((int)mediaLengthInSeconds), + }; + + return this.View(vm); + } + + /// + /// Gets the certificate details of an activity. + /// + /// The resourceReferenceId. + /// The majorVersion. + /// The minorVersion. + /// The user Id. + /// The downloadCert flag. + /// The . + [HttpGet] + [Route("mylearning/certificate/{resourceReferenceId}")] + [Route("mylearning/certificate/{resourceReferenceId}/{userId}")] + [Route("mylearning/certificate/{*path}")] + public async Task GetCertificateDetails(int resourceReferenceId, int? majorVersion = 0, int? minorVersion = 0, int? userId = 0, bool downloadCert = false) + { + CertificateDetails certificateDetails = null; + string base64Image = string.Empty; + var activity = await this.myLearningService.GetResourceCertificateDetails(resourceReferenceId, majorVersion, minorVersion, userId); + if (activity.Item1 > 0 && activity.Item2 != null && ViewActivityHelper.CanDownloadCertificate(new ActivityDetailedItemViewModel(activity.Item2))) + { + var resource = await this.resourceService.GetItemByIdAsync(resourceReferenceId); + var nodePathNodes = await this.hierarchyService.GetNodePathNodes(resource.NodePathId); + var currentUser = await this.userService.GetUserByUserIdAsync((userId == 0) ? this.CurrentUserId : (int)userId); + var userEmployment = await this.userService.GetUserEmploymentByIdAsync(currentUser.PrimaryUserEmploymentId ?? 0); + if (activity.Item2.CertificateUrl != null && downloadCert) + { + var file = await this.fileService.DownloadFileAsync(this.filePath, activity.Item2.CertificateUrl); + if (file != null) + { + byte[] imageArray = new BinaryReader(file.Content).ReadBytes((int)file.ContentLength); + base64Image = Convert.ToBase64String(imageArray); + } + } + + certificateDetails = new CertificateDetails { AccessCount = activity.Item1, ProfessionalRegistrationNumber = userEmployment?.MedicalCouncilNo, NodeViewModels = nodePathNodes, UserViewModel = currentUser, ResourceItemViewModel = resource, ActivityDetailedItemViewModel = new ActivityDetailedItemViewModel(activity.Item2), DownloadCertificate = downloadCert, CertificateBase64Image = base64Image }; + } + + return this.View("LearningCertificate", certificateDetails); + } + + /// + /// Gets the certificate details of an activity. + /// + /// The resourceReferenceId. + /// The majorVersion. + /// The minorVersion. + /// The userId. + /// The . + [HttpPost] + [Route("mylearning/downloadcertificate")] + public async Task DownloadCertificate(int resourceReferenceId, int? majorVersion = 0, int? minorVersion = 0, int? userId = 0) + { + CertificateDetails certificateDetails = null; + string base64Image = string.Empty; + var activity = await this.myLearningService.GetResourceCertificateDetails(resourceReferenceId, majorVersion, minorVersion, userId); + if (activity.Item1 > 0 && activity.Item2 != null && ViewActivityHelper.CanDownloadCertificate(new ActivityDetailedItemViewModel(activity.Item2))) + { + var resource = await this.resourceService.GetItemByIdAsync(resourceReferenceId); + var nodePathNodes = await this.hierarchyService.GetNodePathNodes(resource.NodePathId); + var currentUser = await this.userService.GetUserByUserIdAsync((userId == 0) ? this.CurrentUserId : (int)userId); + var userEmployment = await this.userService.GetUserEmploymentByIdAsync(currentUser.PrimaryUserEmploymentId ?? 0); + var resourceItemUrl = this.Settings.LearningHubWebUiUrl.Trim() + "Resource/" + resourceReferenceId + "/Item"; + if (activity.Item2.CertificateUrl != null) + { + var file = await this.fileService.DownloadFileAsync(this.filePath, activity.Item2.CertificateUrl); + if (file != null) + { + byte[] imageArray = new BinaryReader(file.Content).ReadBytes((int)file.ContentLength); + base64Image = Convert.ToBase64String(imageArray); + } + } + + certificateDetails = new CertificateDetails { AccessCount = activity.Item1, ProfessionalRegistrationNumber = userEmployment?.MedicalCouncilNo, NodeViewModels = nodePathNodes, UserViewModel = currentUser, ResourceItemViewModel = resource, ActivityDetailedItemViewModel = new ActivityDetailedItemViewModel(activity.Item2), DownloadCertificate = true, CertificateBase64Image = base64Image, PdfResoureItemUrl = resourceItemUrl }; + var renderedViewHTML = new List(); + certificateDetails.PageNo++; + renderedViewHTML.Add(RenderRazorViewToString(this, "LearningCertificate", certificateDetails)); + certificateDetails.PageNo++; + renderedViewHTML.Add(RenderRazorViewToString(this, "LearningCertificate", certificateDetails)); + + ReportStatusModel reportStatusModel = new ReportStatusModel(); + var pdfReportResponse = await this.pdfReportService.PdfReport(renderedViewHTML, currentUser.Id); + if (pdfReportResponse != null) + { + do + { + reportStatusModel = await this.pdfReportService.PdfReportStatus(pdfReportResponse); + } + while (reportStatusModel.Id == 1); + + var pdfReportFile = await this.pdfReportService.GetPdfReportFile(pdfReportResponse); + if (pdfReportFile != null) + { + string fileName = this.GenerateCertificateName(certificateDetails.ActivityDetailedItemViewModel.Title); + return this.File(pdfReportFile, FileHelper.GetContentTypeFromFileName(fileName), fileName); + } + } + } + + return this.View("LearningCertificate", certificateDetails); + } + + /// + /// Gets the Certificate name. + /// + /// The resourceTitile. + /// The . + private string GenerateCertificateName(string resourceTitile) + { + if (!string.IsNullOrEmpty(resourceTitile)) + { + if (resourceTitile.Length <= 71) + { + string filename = "LH_Certificate_" + resourceTitile + ".pdf"; + return filename; + } + else if (resourceTitile.Length > 71) + { + string filename = "LH_Certificate_" + resourceTitile.Truncate(67, true) + "_ " + ".pdf"; + return filename; + } + } + + return "LearningCertificate.pdf"; + } + } +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Helpers/ResourceTypeEnumMoodle.cs b/LearningHub.Nhs.WebUI/Helpers/ResourceTypeEnumMoodle.cs deleted file mode 100644 index e0601d156..000000000 --- a/LearningHub.Nhs.WebUI/Helpers/ResourceTypeEnumMoodle.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Helpers; - -/// -/// Represents the types of resources available in Moodle. -/// -public enum ResourceTypeEnumMoodle -{ - /// - /// The undefined resource type. - /// - Undefined, - - /// - /// The article resource type. - /// - Article, - - /// - /// The audio resource type. - /// - Audio, - - /// - /// The embedded resource type. - /// - Embedded, - - /// - /// The equipment resource type. - /// - Equipment, - - /// - /// The image resource type. - /// - Image, - - /// - /// The SCORM resource type. - /// - Scorm, - - /// - /// The video resource type. - /// - Video, - - /// - /// The web link resource type. - /// - WebLink, - - /// - /// The generic file resource type. - /// - GenericFile, - - /// - /// The clinical case resource type. - /// - Case, - - /// - /// The assessment resource type. - /// - Assessment, - - /// - /// The HTML resource type. - /// - Html, - - /// - /// The Moodle resource type. - /// - Moodle, - - /// - /// The Moodle course resource type. - /// - Course, -} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs index ef71191f9..e1732d5a3 100644 --- a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs @@ -30,25 +30,7 @@ public static class UtilityHelper { "genericfile", ResourceTypeEnum.GenericFile }, { "image", ResourceTypeEnum.Image }, { "html", ResourceTypeEnum.Html }, - }; - - /// TODO: Remove this method after adding to Moodle resource types to models project. - /// - /// Findwise Moodle resource type dictionary. - /// - public static readonly Dictionary FindwiseResourceMoodleTypeDict = new Dictionary() - { - { "video", ResourceTypeEnumMoodle.Video }, - { "article", ResourceTypeEnumMoodle.Article }, - { "case", ResourceTypeEnumMoodle.Case }, - { "weblink", ResourceTypeEnumMoodle.WebLink }, - { "audio", ResourceTypeEnumMoodle.Audio }, - { "scorm", ResourceTypeEnumMoodle.Scorm }, - { "assessment", ResourceTypeEnumMoodle.Assessment }, - { "genericfile", ResourceTypeEnumMoodle.GenericFile }, - { "image", ResourceTypeEnumMoodle.Image }, - { "html", ResourceTypeEnumMoodle.Html }, - { "moodle", ResourceTypeEnumMoodle.Course }, + { "moodle", ResourceTypeEnum.Moodle }, }; /// @@ -166,54 +148,6 @@ public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType } } - /// TODO: Remove this method after adding to Moodle resource types to models project. - /// - /// Returns a prettified resource type name, suitable for display in the UI. Includes video/audio duration string. - /// - /// The resource type. - /// The media duration in milliseconds. - /// The resource type name, and duration if applicable. - public static string GetPrettifiedResourceTypeNameMoodle(ResourceTypeEnumMoodle resourceType, int? durationInMilliseconds = 0) - { - switch (resourceType) - { - case ResourceTypeEnumMoodle.Assessment: - return "Assessment"; - case ResourceTypeEnumMoodle.Article: - return "Article"; - case ResourceTypeEnumMoodle.Audio: - string durationText = GetDurationText(durationInMilliseconds ?? 0); - durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; - return "Audio" + durationText; - case ResourceTypeEnumMoodle.Equipment: - return "Equipment"; - case ResourceTypeEnumMoodle.Image: - return "Image"; - case ResourceTypeEnumMoodle.Scorm: - return "elearning"; - case ResourceTypeEnumMoodle.Video: - durationText = GetDurationText(durationInMilliseconds ?? 0); - durationText = string.IsNullOrEmpty(durationText) ? string.Empty : " - " + durationText; - return "Video" + durationText; - case ResourceTypeEnumMoodle.WebLink: - return "Web link"; - case ResourceTypeEnumMoodle.GenericFile: - return "File"; - case ResourceTypeEnumMoodle.Embedded: - return "Embedded"; - case ResourceTypeEnumMoodle.Case: - return "Case"; - case ResourceTypeEnumMoodle.Html: - return "HTML"; - case ResourceTypeEnumMoodle.Moodle: - return "Course"; - case ResourceTypeEnumMoodle.Course: - return "Course"; - default: - return "File"; - } - } - /// /// Returns a prettified resource type name, suitable for display in the UI. Excludes video/audio duration string. /// @@ -247,6 +181,8 @@ public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType return "Case"; case ResourceTypeEnum.Html: return "HTML"; + case ResourceTypeEnum.Moodle: + return "Course"; default: return "File"; } diff --git a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs index f3bd9259c..6a83b80f2 100644 --- a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.MyLearning; using LearningHub.Nhs.WebUI.Models; /// @@ -71,6 +72,102 @@ public static string GetResourceTypeText(this ActivityDetailedItemViewModel acti return typeText; } + /// + /// Get resource type details. + /// + /// resourceType. + /// The string. + public static string GetResourceTypeDesc(ResourceTypeEnum resourceType) + { + string typeText = string.Empty; + switch (resourceType) + { + case ResourceTypeEnum.Assessment: + typeText = "Assessment"; + break; + case ResourceTypeEnum.Article: + typeText = "Article"; + break; + case ResourceTypeEnum.Audio: + typeText = "Audio"; + break; + case ResourceTypeEnum.GenericFile: + typeText = "File"; + break; + case ResourceTypeEnum.Image: + typeText = "Image"; + break; + case ResourceTypeEnum.Scorm: + typeText = "elearning"; + break; + case ResourceTypeEnum.Video: + typeText = "Video"; + break; + case ResourceTypeEnum.WebLink: + typeText = "Web link"; + break; + case ResourceTypeEnum.Case: + typeText = "Case"; + break; + case ResourceTypeEnum.Html: + typeText = "HTML"; + break; + case ResourceTypeEnum.Moodle: + typeText = "Course"; + break; + default: + typeText = string.Empty; + break; + } + + return typeText; + } + + /// + /// GetActivityStatusDisplayText. + /// + /// The activity. + /// The string. + public static string GetActivityStatusDisplayText(MyLearningCombinedActivitiesViewModel activity) + { + if (activity.ActivityStatus == ActivityStatusEnum.Completed || activity.ActivityStatus == ActivityStatusEnum.Passed || activity.ActivityStatus == ActivityStatusEnum.Passed || activity.ActivityStatus == ActivityStatusEnum.Viewed || activity.ActivityStatus == ActivityStatusEnum.Downloaded) + { + return "Completed"; + } + else + { + return "InProgress"; + } + } + + /// + /// CanDownloadCertificate. + /// + /// The activityDetailedItemViewModel. + /// The bool. + public static bool CanCertificateawarded(this MyLearningCombinedActivitiesViewModel activitiesViewModel) + { + if (activitiesViewModel.CertificateEnabled == true) + { + if (activitiesViewModel.ResourceType == ResourceTypeEnum.Scorm) + { + if (GetActivityStatusDisplayText(activitiesViewModel) == "Completed" || GetActivityStatusDisplayText(activitiesViewModel) == "Passed") + { + return true; + } + } + else + { + if (GetActivityStatusDisplayText(activitiesViewModel) == "Completed" || GetActivityStatusDisplayText(activitiesViewModel) == "Passed" || GetActivityStatusDisplayText(activitiesViewModel) == "Downloaded") + { + return true; + } + } + } + + return false; + } + /// /// GetResourceTypeVerb. /// diff --git a/LearningHub.Nhs.WebUI/Interfaces/IDashboardService.cs b/LearningHub.Nhs.WebUI/Interfaces/IDashboardService.cs index 9eb8c3266..70a39d2fb 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IDashboardService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IDashboardService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LearningHub.Nhs.Models.Dashboard; + using LearningHub.Nhs.Models.Moodle.API; using LearningHub.Nhs.WebUI.Models; /// @@ -47,6 +48,13 @@ public interface IDashboardService /// The current User Id type. /// The page Number. /// A representing the result of the asynchronous operation. - Task> GetEnrolledCoursesFromMoodleAsync(int currentUserId, int pageNumber); + Task> GetEnrolledCoursesFromMoodleAsync(int currentUserId, int pageNumber); + + /// + /// GetEnrolledCoursesFromMoodleAsync. + /// + /// The current User Id type. + /// A representing the result of the asynchronous operation. + Task GetMoodleUserIdAsync(int currentUserId); } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IMoodleApiService.cs b/LearningHub.Nhs.WebUI/Interfaces/IMoodleApiService.cs index d92c01fad..5796c3482 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IMoodleApiService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IMoodleApiService.cs @@ -2,29 +2,34 @@ { using System.Collections.Generic; using System.Threading.Tasks; - using LearningHub.Nhs.Models.Dashboard; - using LearningHub.Nhs.WebUI.Models; + using LearningHub.Nhs.Models.Moodle.API; + using MoodleCourseCompletionModel = LearningHub.Nhs.Models.Moodle.API.MoodleCourseCompletionModel; /// /// IMoodleApiService. /// public interface IMoodleApiService { + /// + /// GetMoodleUserIdByUsernameAsync. + /// + /// The current LH User Id. + /// A representing the result of the asynchronous operation. + Task GetMoodleUserIdByUsernameAsync(int currentUserId); + /// /// GetEnrolledCoursesAsync. /// /// Moodle user id. /// pageNumber. - /// List of MoodleCourseResponseViewModel. - Task> GetEnrolledCoursesAsync(int currentUserId, int pageNumber); + /// List of MoodleCourseResponseModel. + Task> GetEnrolledCoursesAsync(int currentUserId, int pageNumber); /// - /// GetEnrolledCoursesAsync. + /// GetCourseUrl. /// - /// Moodle user id. - /// Moodle course id. - /// pageNumber. - /// List of MoodleCourseResponseViewModel. - Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber); + /// course Id. + /// return course URL. + string GetCourseUrl(int courseId); } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs b/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs index 726f78477..773b4791f 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IMyLearningService.cs @@ -17,6 +17,20 @@ public interface IMyLearningService /// The . Task GetActivityDetailed(MyLearningRequestModel requestModel); + /// + /// Gets the user recent my leraning activities.. + /// + /// The request model. + /// The . + Task GetUserRecentMyLearningActivities(MyLearningRequestModel requestModel); + + /// + /// Gets the user leraning history. + /// + /// The request model. + /// The . + Task GetUserLearningHistory(MyLearningRequestModel requestModel); + /// /// Gets the played segment data for the progress modal in My Learning screen. /// diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index dc2cf661d..a65aa93dd 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Models/DashboardViewModel.cs b/LearningHub.Nhs.WebUI/Models/DashboardViewModel.cs index 9ac98c7e0..d7ddeda0c 100644 --- a/LearningHub.Nhs.WebUI/Models/DashboardViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/DashboardViewModel.cs @@ -2,6 +2,7 @@ { using System.Collections.Generic; using LearningHub.Nhs.Models.Dashboard; + using LearningHub.Nhs.Models.Moodle.API; /// /// Defines the . @@ -33,6 +34,6 @@ public DashboardViewModel() /// /// Gets or sets a list of enrolled courses to be displayed in the dashboard. /// - public List EnrolledCourses { get; set; } + public List EnrolledCourses { get; set; } } } diff --git a/LearningHub.Nhs.WebUI/Models/MoodleCompletionResponseViewModel.cs b/LearningHub.Nhs.WebUI/Models/MoodleCompletionResponseViewModel.cs deleted file mode 100644 index 1692a32e6..000000000 --- a/LearningHub.Nhs.WebUI/Models/MoodleCompletionResponseViewModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Models -{ - /// - /// MoodleCompletionResponseViewModel. - /// - public class MoodleCompletionResponseViewModel - { - /// - /// Gets or sets the completion status. - /// - public string Exception { get; set; } - - /// - /// Gets or sets error code. - /// - public string Errorcode { get; set; } - - /// - /// Gets or sets Error message. - /// - public string Message { get; set; } - - /// - /// Gets or sets Debug info. - /// - public string Debuginfo { get; set; } - } -} diff --git a/LearningHub.Nhs.WebUI/Models/MoodleCourseCompletionViewModel.cs b/LearningHub.Nhs.WebUI/Models/MoodleCourseCompletionViewModel.cs deleted file mode 100644 index 06eb5cb8f..000000000 --- a/LearningHub.Nhs.WebUI/Models/MoodleCourseCompletionViewModel.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Models -{ - using System.Collections.Generic; - - /// - /// MoodleCourseCompletionViewModel. - /// - public class MoodleCourseCompletionViewModel - { - /// - /// Gets or sets the completion status. - /// - public CompletStatus CompletionStatus { get; set; } - - /// - /// Gets or sets the list of warnings. - /// - public List Warnings { get; set; } - - /// - /// CompletionStatus. - /// - public class CompletStatus - { - /// - /// Gets or sets a value indicating whether the course is completed. - /// - public bool Completed { get; set; } - - /// - /// Gets or sets the aggregation method. - /// - public int Aggregation { get; set; } - - /// - /// Gets or sets the list of completions. - /// - public List Completions { get; set; } - - /// - /// Completion. - /// - public class Completion - { - /// - /// Gets or sets the type of completion. - /// - public int Type { get; set; } - - /// - /// Gets or sets the title of the completion requirement. - /// - public string Title { get; set; } - - /// - /// Gets or sets the status of the completion. - /// - public string Status { get; set; } - - /// - /// Gets or sets a value indicating whether the requirement is complete. - /// - public bool Complete { get; set; } - - /// - /// Gets or sets the timestamp when completion was achieved. - /// - public long? TimeCompleted { get; set; } - - /// - /// Gets or sets the completion details. - /// - public CompletionDetails Details { get; set; } - - /// - /// CompletionDetails. - /// - public class CompletionDetails - { - /// - /// Gets or sets the type of completion requirement. - /// - public string Type { get; set; } - - /// - /// Gets or sets the criteria for completion. - /// - public string Criteria { get; set; } - - /// - /// Gets or sets the requirement for completion. - /// - public string Requirement { get; set; } - - /// - /// Gets or sets the status of the requirement. - /// - public string Status { get; set; } - } - } - } - } -} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Models/MoodleCourseResponseViewModel.cs b/LearningHub.Nhs.WebUI/Models/MoodleCourseResponseViewModel.cs deleted file mode 100644 index b214cf072..000000000 --- a/LearningHub.Nhs.WebUI/Models/MoodleCourseResponseViewModel.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Models -{ - using System.Collections.Generic; - - /// - /// MoodleCourseResponseViewModel. - /// - public class MoodleCourseResponseViewModel - { - /// - /// Gets or sets the ID. - /// - public int? Id { get; set; } - - /// - /// Gets or sets the short name. - /// - public string ShortName { get; set; } - - /// - /// Gets or sets the full name. - /// - public string FullName { get; set; } - - /// - /// Gets or sets the display name. - /// - public string DisplayName { get; set; } - - /// - /// Gets or sets the enrolled user count. - /// - public int? EnrolledUserCount { get; set; } - - /// - /// Gets or sets the ID number. - /// - public string IdNumber { get; set; } - - /// - /// Gets or sets the visibility status. - /// - public int? Visible { get; set; } - - /// - /// Gets or sets the summary. - /// - public string Summary { get; set; } - - /// - /// Gets or sets the summary format. - /// - public int? SummaryFormat { get; set; } - - /// - /// Gets or sets the format. - /// - public string Format { get; set; } - - /// - /// Gets or sets the course image URL. - /// - public string CourseImage { get; set; } - - /// - /// Gets or sets a value indicating whether grades are shown. - /// - public bool? ShowGrades { get; set; } - - /// - /// Gets or sets the language. - /// - public string Lang { get; set; } - - /// - /// Gets or sets a value indicating whether completion is enabled. - /// - public bool? EnableCompletion { get; set; } - - /// - /// Gets or sets a value indicating whether completion has criteria. - /// - public bool? CompletionHasCriteria { get; set; } - - /// - /// Gets or sets a value indicating whether completion is user-tracked. - /// - public bool? CompletionUserTracked { get; set; } - - /// - /// Gets or sets the category ID. - /// - public int? Category { get; set; } - - /// - /// Gets the progress percentage formatted as a string. - /// - public string ProgressPercentage => $"{System.Math.Round(this.Progress ?? 0)}%"; - - /// - /// Gets or sets the progress. - /// - public double? Progress { get; set; } - - /// - /// Gets or sets the completion status. - /// - public bool? Completed { get; set; } - - /// - /// Gets or sets the start date (Unix timestamp). - /// - public long? StartDate { get; set; } - - /// - /// Gets or sets the end date. - /// - public int? EndDate { get; set; } - - /// - /// Gets or sets the marker. - /// - public int? Marker { get; set; } - - /// - /// Gets or sets the last access timestamp. - /// - public int? LastAccess { get; set; } - - /// - /// Gets or sets a value indicating whether the course is a favorite. - /// - public bool? IsFavourite { get; set; } - - /// - /// Gets or sets a value indicating whether the course is hidden. - /// - public bool? Hidden { get; set; } - - /// - /// Gets or sets the list of overview files. - /// - public List OverviewFiles { get; set; } - - /// - /// Gets or sets a value indicating whether activity dates are shown. - /// - public bool? ShowActivityDates { get; set; } - - /// - /// Gets or sets a value indicating whether completion conditions are shown. - /// - public bool? ShowCompletionConditions { get; set; } - - /// - /// Gets or sets the last modified timestamp (Unix timestamp). - /// - public long? TimeModified { get; set; } - - /// - /// Gets or sets the moodle course completion view model. - /// - public MoodleCourseCompletionViewModel CourseCompletionViewModel { get; set; } - } -} diff --git a/LearningHub.Nhs.WebUI/Models/MoodleOverviewFileViewModel.cs b/LearningHub.Nhs.WebUI/Models/MoodleOverviewFileViewModel.cs deleted file mode 100644 index 3dd335c41..000000000 --- a/LearningHub.Nhs.WebUI/Models/MoodleOverviewFileViewModel.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Models -{ - /// - /// MoodleOverviewFileViewModel. - /// - public class MoodleOverviewFileViewModel - { - /// - /// Gets or sets the file name. - /// - public string? FileName { get; set; } - - /// - /// Gets or sets the file path. - /// - public string? FilePath { get; set; } - - /// - /// Gets or sets the file size in bytes. - /// - public int FileSize { get; set; } - - /// - /// Gets or sets the file URL. - /// - public string? FileUrl { get; set; } - - /// - /// Gets or sets the time the file was modified (Unix timestamp). - /// - public long TimeModified { get; set; } - - /// - /// Gets or sets the MIME type of the file. - /// - public string? MimeType { get; set; } - } -} diff --git a/LearningHub.Nhs.WebUI/Models/MoodleUserResponseViewModel.cs b/LearningHub.Nhs.WebUI/Models/MoodleUserResponseViewModel.cs deleted file mode 100644 index 7940dc725..000000000 --- a/LearningHub.Nhs.WebUI/Models/MoodleUserResponseViewModel.cs +++ /dev/null @@ -1,121 +0,0 @@ -namespace LearningHub.Nhs.WebUI.Models -{ - using System.Collections.Generic; - - /// - /// MoodleUserResponseViewModel. - /// - public class MoodleUserResponseViewModel - { - /// - /// Gets or sets the list of users. - /// - public List Users { get; set; } - - /// - /// Gets or sets the warnings. - /// - public List Warnings { get; set; } - - /// - /// MoodleUser. - /// - public class MoodleUser - { - /// - /// Gets or sets the user ID. - /// - public int Id { get; set; } - - /// - /// Gets or sets the username. - /// - public string Username { get; set; } - - /// - /// Gets or sets the first name. - /// - public string FirstName { get; set; } - - /// - /// Gets or sets the last name. - /// - public string LastName { get; set; } - - /// - /// Gets or sets the full name. - /// - public string FullName { get; set; } - - /// - /// Gets or sets the email. - /// - public string Email { get; set; } - - /// - /// Gets or sets the department. - /// - public string Department { get; set; } - - /// - /// Gets or sets the first access timestamp. - /// - public long FirstAccess { get; set; } - - /// - /// Gets or sets the last access timestamp. - /// - public long LastAccess { get; set; } - - /// - /// Gets or sets the authentication method. - /// - public string Auth { get; set; } - - /// - /// Gets or sets a value indicating whether the user is suspended. - /// - public bool Suspended { get; set; } - - /// - /// Gets or sets a value indicating whether the user is confirmed. - /// - public bool Confirmed { get; set; } - - /// - /// Gets or sets the language. - /// - public string Lang { get; set; } - - /// - /// Gets or sets the theme. - /// - public string Theme { get; set; } - - /// - /// Gets or sets the timezone. - /// - public string Timezone { get; set; } - - /// - /// Gets or sets the mail format. - /// - public int MailFormat { get; set; } - - /// - /// Gets or sets the forum tracking preference. - /// - public int TrackForums { get; set; } - - /// - /// Gets or sets the small profile image URL. - /// - public string ProfileImageUrlSmall { get; set; } - - /// - /// Gets or sets the profile image URL. - /// - public string ProfileImageUrl { get; set; } - } - } -} diff --git a/LearningHub.Nhs.WebUI/Models/MyLearningUserActivitiesViewModel.cs b/LearningHub.Nhs.WebUI/Models/MyLearningUserActivitiesViewModel.cs new file mode 100644 index 000000000..8aa35c88b --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/MyLearningUserActivitiesViewModel.cs @@ -0,0 +1,72 @@ +namespace LearningHub.Nhs.WebUI.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Reflection; + using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.Models.Search; + using LearningHub.Nhs.WebUI.Helpers; + using LearningHub.Nhs.WebUI.Models.Learning; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// Defines the . + /// + public class MyLearningUserActivitiesViewModel : MyLearningRequestModel + { + /// + /// Initializes a new instance of the class. + /// + public MyLearningUserActivitiesViewModel() + { + this.Activities = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// MyLearningRequestModel. + public MyLearningUserActivitiesViewModel(MyLearningRequestModel requestModel) + { + this.Activities = 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 MostRecentResources. + /// + public List MostRecentResources { get; set; } + + /// + /// Gets or sets the Activities. + /// + public List Activities { get; set; } + + /// + /// Gets or sets the learning result paging. + /// + public PagingViewModel MyLearningPaging { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs b/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs index 9f95fc173..6b7801793 100644 --- a/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs +++ b/LearningHub.Nhs.WebUI/Models/SideMenu/SideNavigationConfiguration.cs @@ -60,31 +60,31 @@ public static IEnumerable GetGroupedMenus() { new SideNavigationItem { - Text = "Recent", - Controller = "Activity", - Action = "Recent", + Text = "Recent learning", + Controller = "MyLearning", + Action = "Index", IsActive = route => MatchRoute(route, "Activity", "Recent"), }, new SideNavigationItem { - Text = "Bookmark", - Controller = "Activity", + Text = "Bookmarks", + Controller = "MyLearning", Action = "Bookmark", IsActive = route => MatchRoute(route, "Activity", "Bookmark"), }, new SideNavigationItem { Text = "Certificates", - Controller = "Activity", + Controller = "MyLearning", Action = "Certificates", IsActive = route => MatchRoute(route, "Activity", "Certificates"), }, new SideNavigationItem { Text = "Learning history", - Controller = "Activity", - Action = "Learninghistory", - IsActive = route => MatchRoute(route, "Activity", "Learninghistory"), + Controller = "MyLearning", + Action = "LearningHistory", + IsActive = route => MatchRoute(route, "Activity", "LearningHistory"), }, }, }, diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeProvideByTab.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeProvideByTab.vue index a177d7002..608af2201 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeProvideByTab.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeProvideByTab.vue @@ -3,7 +3,7 @@

Content developed with

-

When applicable pease select the provider of this content. This will enable users to search for content produced by specific organisations.

+

When applicable please select the provider of this content. This will enable users to search for content produced by specific organisations.

Not applicable diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue index 338817b8b..11b2c7d8f 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue @@ -83,7 +83,7 @@
-

+

diff --git a/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs b/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs index 5ed5cc926..eb2c2fa2c 100644 --- a/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs +++ b/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs @@ -78,6 +78,9 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + var moodleApiSettings = configuration.GetSection("MoodleAPIConfig"); + services.Configure(moodleApiSettings); + var settingsSection = configuration.GetSection("Settings"); services.Configure(settingsSection); diff --git a/LearningHub.Nhs.WebUI/Services/DashboardService.cs b/LearningHub.Nhs.WebUI/Services/DashboardService.cs index 607d4f2be..bde127b81 100644 --- a/LearningHub.Nhs.WebUI/Services/DashboardService.cs +++ b/LearningHub.Nhs.WebUI/Services/DashboardService.cs @@ -2,14 +2,11 @@ { using System; using System.Collections.Generic; - using System.Net.Http; - using System.Runtime.InteropServices.WindowsRuntime; using System.Text; using System.Threading.Tasks; using LearningHub.Nhs.Models.Dashboard; using LearningHub.Nhs.Models.Entities.Analytics; - using LearningHub.Nhs.Models.Entities.Reporting; - using LearningHub.Nhs.Services.Interface; + using LearningHub.Nhs.Models.Moodle.API; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; using Microsoft.Extensions.Logging; @@ -20,7 +17,7 @@ /// public class DashboardService : BaseService, IDashboardService { - private readonly IMoodleHttpClient moodleHttpClient; + private readonly IMoodleApiService moodleApiService; /// /// Initializes a new instance of the class. @@ -28,11 +25,11 @@ public class DashboardService : BaseService, IDashboardService /// learningHubHttpClient. /// The Open Api Http Client. /// logger. - /// MoodleHttpClient. - public DashboardService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger, IMoodleHttpClient moodleHttpClient) + /// MoodleApiService. + public DashboardService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger, IMoodleApiService moodleApiService) : base(learningHubHttpClient, openApiHttpClient, logger) { - this.moodleHttpClient = moodleHttpClient; + this.moodleApiService = moodleApiService; } /// @@ -128,14 +125,24 @@ public async Task GetResourcesAsync(string d /// The dashboard type. /// The page Number. /// A representing the result of the asynchronous operation. - public async Task> GetEnrolledCoursesFromMoodleAsync(int currentUserId, int pageNumber) + public async Task> GetEnrolledCoursesFromMoodleAsync(int currentUserId, int pageNumber) { - List viewmodel = new List { }; - MoodleApiService moodleApiService = new MoodleApiService(this.moodleHttpClient); - viewmodel = await moodleApiService.GetEnrolledCoursesAsync(currentUserId, pageNumber); + List viewmodel = new List { }; + viewmodel = await this.moodleApiService.GetEnrolledCoursesAsync(currentUserId, pageNumber); return viewmodel; } + /// + /// GetEnrolledCoursesFromMoodleAsync. + /// + /// The current User Id type. + /// A representing the result of the asynchronous operation. + public async Task GetMoodleUserIdAsync(int currentUserId) + { + var moodleUserId = await this.moodleApiService.GetMoodleUserIdByUsernameAsync(currentUserId); + return moodleUserId; + } + /// /// Logs Dashboared viewed event. /// diff --git a/LearningHub.Nhs.WebUI/Services/MoodleApiService.cs b/LearningHub.Nhs.WebUI/Services/MoodleApiService.cs index 068219b42..c80a764f0 100644 --- a/LearningHub.Nhs.WebUI/Services/MoodleApiService.cs +++ b/LearningHub.Nhs.WebUI/Services/MoodleApiService.cs @@ -2,15 +2,11 @@ { using System; using System.Collections.Generic; - using System.Net.Http; - using System.Text.Json; using System.Threading.Tasks; - using LearningHub.Nhs.Models.Entities.Reporting; - using LearningHub.Nhs.Services.Interface; + using LearningHub.Nhs.Models.Moodle.API; + using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; - using LearningHub.Nhs.WebUI.Models; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; using Newtonsoft.Json; /// @@ -18,103 +14,101 @@ /// public class MoodleApiService : IMoodleApiService { - private readonly IMoodleHttpClient moodleHttpClient; + private readonly IOpenApiHttpClient openApiHttpClient; + private readonly MoodleApiConfig configuration; /// /// Initializes a new instance of the class. /// - /// moodleHttpClient. - public MoodleApiService(IMoodleHttpClient moodleHttpClient) + /// The Open Api Http Client. + /// configuration. + public MoodleApiService(IOpenApiHttpClient openApiHttpClient, IOptions configuration) { - this.moodleHttpClient = moodleHttpClient; + this.openApiHttpClient = openApiHttpClient; + this.configuration = configuration.Value; } /// - /// GetEnrolledCoursesAsync. + /// GetMoodleUserIdByUsernameAsync. /// - /// Moodle user id. - /// The page Number. - /// A representing the result of the asynchronous operation. - public async Task> GetEnrolledCoursesAsync(int userId, int pageNumber) + /// current User Id. + /// UserId from Moodle. + public async Task GetMoodleUserIdByUsernameAsync(int currentUserId) { - List viewmodel = new List { }; - MoodleApiService moodleApiService = new MoodleApiService(this.moodleHttpClient); - - var client = await this.moodleHttpClient.GetClient(); - string additionalParameters = $"userid={userId}"; - string defaultParameters = this.moodleHttpClient.GetDefaultParameters(); - string url = $"&wsfunction=core_enrol_get_users_courses&{additionalParameters}"; - - HttpResponseMessage response = await client.GetAsync("?" + defaultParameters + url); + int moodleUserId = 0; - if (response.IsSuccessStatusCode) + try { - var result = response.Content.ReadAsStringAsync().Result; + var client = await this.openApiHttpClient.GetClientAsync(); - using var document = JsonDocument.Parse(result); - var root = document.RootElement; + var request = $"Moodle/GetMoodleUserId/{currentUserId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); - // Check if it's a JSON object and contains "exception" - if (!(root.ValueKind == JsonValueKind.Object && root.TryGetProperty("exception", out _))) + if (response.IsSuccessStatusCode) { - viewmodel = JsonConvert.DeserializeObject>(result); - - foreach (var course in viewmodel) - { - course.CourseCompletionViewModel = await moodleApiService.GetCourseCompletionAsync(userId, course.Id.Value, pageNumber); - } + var result = response.Content.ReadAsStringAsync().Result; + moodleUserId = JsonConvert.DeserializeObject(result); } - else + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) { - // Contains error, handle it as needed. + throw new Exception("AccessDenied"); } + + return moodleUserId; } - else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || - response.StatusCode == System.Net.HttpStatusCode.Forbidden) + catch (Exception ex) { - throw new Exception("AccessDenied"); + // this.Logger.LogError(string.Format("Error occurred in GetSearchResultAsync: {0}", ex.Message)); + return moodleUserId; } - - return viewmodel; } /// /// GetEnrolledCoursesAsync. /// - /// Moodle user id. - /// Moodle course id. - /// pageNumber. - /// List of MoodleCourseResponseViewModel. - public async Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber) + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + public async Task> GetEnrolledCoursesAsync(int currentUserId, int pageNumber) { - MoodleCourseCompletionViewModel viewmodel = new MoodleCourseCompletionViewModel { }; - MoodleApiService moodleApiService = new MoodleApiService(this.moodleHttpClient); + List viewmodel = new List(); - var client = await this.moodleHttpClient.GetClient(); - string additionalParameters = $"userid={userId}&courseid={courseId}"; - string defaultParameters = this.moodleHttpClient.GetDefaultParameters(); - string url = $"&wsfunction=core_completion_get_course_completion_status&{additionalParameters}"; - - HttpResponseMessage response = await client.GetAsync("?" + defaultParameters + url); - - if (response.IsSuccessStatusCode) + try { - var result = response.Content.ReadAsStringAsync().Result; + var client = await this.openApiHttpClient.GetClientAsync(); - var canViewReport = JsonConvert.DeserializeObject(result); + var request = $"Moodle/GetEnrolledCourses/{currentUserId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); - if (string.IsNullOrEmpty(canViewReport.Exception)) + if (response.IsSuccessStatusCode) { - viewmodel = JsonConvert.DeserializeObject(result); + 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; } - else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || - response.StatusCode == System.Net.HttpStatusCode.Forbidden) + catch (Exception ex) { - throw new Exception("AccessDenied"); + // this.Logger.LogError(string.Format("Error occurred in GetSearchResultAsync: {0}", ex.Message)); + return viewmodel; } + } - return viewmodel; + /// + /// GetCourseUrl. + /// + /// course Id. + /// return course URL. + public string GetCourseUrl(int courseId) + { + var apiBaseUrl = this.configuration.BaseUrl; + string path = this.configuration.CoursePath; + return $"{apiBaseUrl}{path}?id={courseId}"; } } } diff --git a/LearningHub.Nhs.WebUI/Services/MyLearningService.cs b/LearningHub.Nhs.WebUI/Services/MyLearningService.cs index 1e1662201..df38b15ef 100644 --- a/LearningHub.Nhs.WebUI/Services/MyLearningService.cs +++ b/LearningHub.Nhs.WebUI/Services/MyLearningService.cs @@ -59,6 +59,70 @@ public async Task GetActivityDetailed(MyLearningReq return viewModel; } + /// + /// Gets the user recent my leraning activities. + /// + /// The request model. + /// The . + public async Task GetUserRecentMyLearningActivities(MyLearningRequestModel requestModel) + { + MyLearningActivitiesDetailedViewModel 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/GetUserRecentMyLearningActivities"; + 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; + } + + /// + /// Gets the user recent my leraning activities. + /// + /// The request model. + /// The . + public async Task GetUserLearningHistory(MyLearningRequestModel requestModel) + { + MyLearningActivitiesDetailedViewModel 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/GetUserLearningHistory"; + 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; + } + /// /// Gets the played segment data for the progress modal in My Learning screen. /// diff --git a/LearningHub.Nhs.WebUI/Services/SearchService.cs b/LearningHub.Nhs.WebUI/Services/SearchService.cs index 578a5861f..333848a30 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchService.cs @@ -164,10 +164,10 @@ public async Task PerformSearch(IPrincipal user, SearchRe { var filter = filters.Where(x => x.DisplayName == filteritem).FirstOrDefault(); - if (filter != null && UtilityHelper.FindwiseResourceMoodleTypeDict.ContainsKey(filter.DisplayName)) + if (filter != null && UtilityHelper.FindwiseResourceTypeDict.ContainsKey(filter.DisplayName)) { - var resourceTypeEnum = UtilityHelper.FindwiseResourceMoodleTypeDict[filter.DisplayName]; - var searchfilter = new SearchFilterModel() { DisplayName = UtilityHelper.GetPrettifiedResourceTypeNameMoodle(resourceTypeEnum), Count = filter.Count, Value = filteritem, Selected = searchRequest.Filters?.Contains(filter.DisplayName) ?? false }; + var resourceTypeEnum = UtilityHelper.FindwiseResourceTypeDict[filter.DisplayName]; + var searchfilter = new SearchFilterModel() { DisplayName = UtilityHelper.GetPrettifiedResourceTypeName(resourceTypeEnum), Count = filter.Count, Value = filteritem, Selected = searchRequest.Filters?.Contains(filter.DisplayName) ?? false }; searchfilters.Add(searchfilter); } } diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index 0a0c04c49..9fd65bd60 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -3,8 +3,6 @@ using System.Net.Http; using GDS.MultiPageFormData; using LearningHub.Nhs.Models.OpenAthens; - using LearningHub.Nhs.Services; - using LearningHub.Nhs.Services.Interface; using LearningHub.Nhs.WebUI.Filters; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; @@ -62,13 +60,6 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, }); - services.AddHttpClient() - .ConfigurePrimaryHttpMessageHandler( - () => new HttpClientHandler - { - ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, - }); } else { @@ -76,7 +67,6 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddHttpClient(); services.AddHttpClient(); services.AddHttpClient(); - services.AddHttpClient(); } // Config @@ -108,6 +98,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/common.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/common.scss index 1c18cc3cc..e7e49e651 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/common.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/common.scss @@ -1,80 +1,6 @@ @use "../abstracts/all" as *; @use "nhsuk" as *; -.nhsuk-u-font-style-italic { - font-style: italic; -} - -.nhsuk-button--red { - background-color: $nhsuk-red !important; - - &:hover { - background-color: $nhsuk-red-hover !important; - border-color: $nhsuk-red-hover !important; - } -} - -.nhsuk-button--beta-login { - background-color: $nhsuk-blue; - box-shadow: 0 4px 0 #002f5c; -} - -.nhsuk-button--beta-login:hover { - background-color: $nhsuk-btn-blue-hover; -} - -// By default, the text on reverse (white) buttons turns white once clicked, rendering them invisible. -.nhsuk-button--reverse:visited { - color: $nhsuk-black -} - -.nhsuk-back-link { - padding: px2rem(20) 0; - margin-bottom: 0; -} - -.nhsuk-radios__divider { - text-align: left; - width: unset; -} - -.nhsuk-radios__item label { - font-family: $font-stack; -} - -/* Conditional radio buttons - Note: The nhsuk-radios__conditional element needs to be a SIBLING of the radio button input element - otherwise the CSS selector won't work. See Views/Bookmark/Move.cshtml for a usage example. - The NHSUK component (nhsuk-radios__conditional) requires JavaScript to work. These tweaks allow it to work without. -*/ -.nhsuk-radios__conditional { - display: none; - margin-left: -22px; - margin-top: 8px; -} - -.nhsuk-radios__input:checked ~ .nhsuk-radios__conditional { - display: block !important; -} - -/* jquery unbobtrusive validation style over */ -.nhsuk-error-summary__list li { - color: $nhsuk-red; -} - -.nhsuk-form-group.input-validation-error { - @extend .nhsuk-form-group--error; -} - -.nhsuk-input.input-validation-error { - @extend .nhsuk-input--error -} - -.nhsuk-input:focus { - border: 2px solid #212b32; - box-shadow: inset 0 0 0 2px; - outline: 4px solid #ffeb3b; /* 1 */ - outline-offset: 0; -} .display--hide { display: none !important; @@ -120,92 +46,6 @@ } -.nhsuk-bg-light-blue { - background-color: $nhsuk-light-blue-color; -} - -.nhsuk-bg-pale-blue { - background-color: $nhsuk-pale-blue-color; -} - -.nhsuk-bg-white { - background-color: $color_nhsuk-white; -} - -.nhsuk-width-container.search-width-container { - max-width: px2rem(752); - margin: 0 auto; - padding-left: px2rem(68); - padding-right: px2rem(68); -} - -#maincontent { - - button[class^='nhsuk-search__submit'] span.nhsuk-u-visually-hidden { - color: $color_nhsuk-grey-1; - background-color: $color_nhsuk-white; - } -} - -form label.nhsuk-u-visually-hidden { - color: $color_nhsuk-grey-1; - background-color: $color_nhsuk-white; -} - -/* One third column layout that switches to full width at the small desktop breakpoint (990px) instead of mobile. */ -.nhsuk-grid-column-one-third-small-desktop { - @extend .nhsuk-grid-column-one-third; - width: 33.3333333333% !important; - - @media (max-width: px2rem(990)) { - width: 100% !important; - } -} - - -/* Tweaks to styling for single card view. */ -@media(min-width: 768px) and (max-width: 990px) { - .nhsuk-card-group .nhsuk-grid-column-one-third-small-desktop { - max-width: 600px !important; - } - - .nhsuk-card-group--centred { - justify-content: center !important; - } -} - -.nhsuk-card-banner-container { - padding-bottom: 42.86%; - position: relative; -} - -.nhsuk-card-banner { - height: 100%; - object-fit: cover; - object-position: left; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -.nhsuk-card-banner-empty { - background-color: $nhsuk-pale-blue-color; - border-bottom: 1px solid #d8dde0; -} - -.nhsuk-error-message.error-message--margin-bottom-1 { - margin-bottom: nhsuk-spacing(1); -} - -.nhsuk-u-margin-bottom-2point5 { - margin-bottom: 12px; - - @media(max-width: 641px) { - margin-bottom: 10px; - } -} .word-break__break-word { word-break: break-word; @@ -216,9 +56,7 @@ form label.nhsuk-u-visually-hidden { justify-content: space-between; } -.nhsuk-button--no-shrink { - white-space: nowrap; -} + .modal-footer--buttons { justify-content: space-between !important; @@ -233,14 +71,6 @@ form label.nhsuk-u-visually-hidden { box-shadow: 0 0 0 0.2rem $nhsuk-yellow !important; background-color: $govuk-focus-highlight-yellow; } -/*Add a background color to the radio button when focused */ -.nhsuk-radios__input:focus + .radioButton { - box-shadow: 0 0 0 3px $nhsuk-yellow; -} -/*Add a background color to the radio button when focused */ -.nhsuk-checkboxes__input:focus + .checkmark { - box-shadow: 0 0 0 4px $nhsuk-yellow; -} .accessible-link:focus { outline: none; diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss index 0594ad9db..915f4721f 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss @@ -1,145 +1,15 @@ -@use "../abstracts/all" as *; +@use "../abstracts/all" as *; @use "nhsuk" as *; body { overflow-wrap: break-word; } -.nhsuk-header { - padding: 0 px2rem(32); -} - -.nhsuk-hero { - background-color: $color_nhsuk-blue; - color: #fff; - position: relative; -} - -.nhsuk-hero a, -.nhsuk-hero a:visited, -.nhsuk-hero a:hover, -.nhsuk-hero a:active, -.nhsuk-hero i { - color: $color_nhsuk-white; -} - - -.nhsuk-width-container.app-width-container { - max-width: px2rem(1208); - margin: 0 auto; - padding-left: px2rem(32); - padding-right: px2rem(32); -} - -.nhsuk-header .nhsuk-width-container.app-width-container { - max-width: px2rem(1144); - margin: 0 auto; -} - -.nhsuk-width-container.app-width-container.beta-banner { - padding: px2rem(8) px2rem(32); - max-width: px2rem(1208); - margin: 0 auto; -} - -.nhsuk-header .nhsuk-header__container::after { - content: none; -} - -.nhsuk-header__navigation.app-width-container { - max-width: px2rem(1144); -} - -.app-width-container--full { - margin: 0; - max-width: none -} .app-main-wrapper--no-padding { padding: 0 } -.nhsuk-header__container.app-width-container { - display: flex; - justify-content: space-between; - gap: 0 px2rem(24); - padding: px2rem(16) 0; -} - -.nhsuk-header__content { - display: flex; - align-items: center; - min-height: px2rem(40); - margin-left: auto; -} - -.nhsuk-header__logo { - flex: 1 0 0; -} - -.nhsuk-header__logo .nhsuk-header__link--service { - display: inline-flex; -} - -.nhsuk-header__service-name { - font-size: px2rem(19); -} - -.nhsuk-account__login { - font-size: px2rem(14); - float: right; - position: relative; - z-index: 2; - display: flex; - justify-content: space-between; - align-items: center; - gap: px2rem(24); -} - -.nhsuk-header__notification-dot { - position: absolute; - top: px2rem(8); - right: px2rem(-10); - font-size: px2rem(11); - line-height: px2rem(18); - font-weight: 900; - background: $nhsuk-error-color; - color: white; - min-width: px2rem(18); - height: px2rem(18); - text-align: center; - border-radius: px2rem(9); - padding: px2rem(1) px2rem(3) 0; -} - -.nhsuk-header__menu { - display: none; -} - -.nhsuk-header__search .nhsuk-search__input { - width: px2rem(260); -} - -.nhsuk-header__search { - .nhsuk-search__input { - width: px2rem(260); - - &::-moz-placeholder { - opacity: 1; - } - } - - #search > label.nhsuk-u-visually-hidden { - background-color: $nhsuk-white; - } -} - -.nhsuk-account__login--link, -.nhsuk-account__login--link:visited, -.nhsuk-account__login--link:hover { - color: #fff; -} - .beta-banner { background-color: $color_nhsuk-grey-5; color: $nhsuk-text-color; @@ -170,42 +40,6 @@ body { font-size: px2rem(16); } -.nhsuk-footer { - padding: px2rem(48) 0; -} - - -#header-dropdown-menu-control { - opacity: 0; - position: absolute; -} - -#header-dropdown-menu-control:checked ~ .nhsuk-header__navigation:not(.js-show) { - display: block -} - -#header-mobile-search-control { - display: none; -} - -.nhsuk-header__break { - display: none; -} - -.nhsuk-header__mobile-only-nav { - display: none; -} - -.nhsuk-header__mobile-break { - display: none; -} - -.nhsuk-header__navigation-item--current { - a { - font-weight: bold; - } -} - button[data-toggle="modal"] { color: #005eb8; padding: 0; @@ -270,490 +104,22 @@ li.autosuggestion-option:last-of-type { border-bottom: none !important; } - -/* side navigation styles */ -.side-nav__list { - list-style: none; - margin: 0; - padding: 0; - border-right: 1px solid #d8dde0; -} - -.side-nav__item { - border-bottom: 1px solid #d8dde0; - margin-bottom: 0 !important; - padding:0; -} - - - - -.side-nav__item:last-child { - border-bottom: none; -} - -.side-nav__item a { - font-weight: 700; - display: block; - padding: 22px 12px; -} - - - -.side-nav__item--active a { - text-decoration: none; - color: $nhsuk-black; -} - -.side-nav__item--active { - background-color: #e8f0f7; -} - - - -.side-nav__toggle, -.side-nav__close { - display: none; - background: #e8f0f7; - color: #005eb8; - border: none; - font-size: 16px; - padding: 10px 16px; - border-radius: 25px; - margin: 10px; - cursor: pointer; -} - -.menu-icon { - display: inline-flex; - justify-content: center; - align-items: center; - width: 20px; - height: 20px; - box-sizing: border-box; - padding: 0; - border: 1px solid #d8dde0; - border-radius: 50%; - background-color: white; - transition: background-color 0.3s ease; -} - - -.side-nav__container { - position: fixed; - top: 0; - left: 0; - width: 80%; - max-width: 320px; - height: 100%; - background-color: #e8f0f7; - border-right: 40px solid #005eb8; - box-sizing: border-box; - transform: translateX(-100%); - transition: transform 0.3s ease-in-out; - z-index: 1000; - box-shadow: 4px 0 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - overflow: visible; -} - - -.side-nav__close { - position: relative; - right: -35px; - z-index: 2; - align-self: flex-end; - text-align: right; -} - - -.nav-toggle:checked + label.side-nav__toggle + .side-nav__container { - transform: translateX(0); -} - - -.nav-toggle:checked + label .menu-icon { - transform: rotate(180deg); -} - - - -/* large desktop */ -@media (min-width: px2rem(990)) { - - .nhsuk-header__navigation-item--current a { - border-bottom: 4px solid $nhsuk-grey-lighter; - font-weight: normal; - } - - .nhsuk-header__navigation-link { - position: relative; - } - - .nhsuk-header__navigation-item:last-child { - margin-right: 16px; - } -} - -@media (min-width: px2rem(768)) { - .nav-toggle, - .side-nav__toggle, - .side-nav__close { - display: none !important; - } - - .side-nav__container { - position: static; - transform: none !important; - height: auto; - box-shadow: none; - display: block; - width: auto; - background-color: #ffffff; - border-right: none; - } -} - - /* small desktop */ @media (max-width: px2rem(989)) { - - .nhsuk-header__container { - flex-wrap: wrap; - } - - .nhsuk-header { - padding: 0; - } - - .nhsuk-header__container.app-width-container { - flex-wrap: wrap; - gap: 0 0; - padding: px2rem(16) px2rem(32); - } - - .nhsuk-header__link--service { - align-items: center; - -ms-flex-align: center; - margin-bottom: 0; - width: auto; - } - - .nhsuk-header__service-name { - padding-left: px2rem(16); - } - - .nhsuk-header__logo { - order: 0; - } - - .nhsuk-account__login { - order: 1; - margin-left: auto; - margin-right: 0px; - } - - .nhsuk-header__break { - display: block; - width: 100%; - height: px2rem(24); - order: 2 - } - - .nhsuk-header__search { - order: 3; - flex-grow: 1; - margin-left: 0; - margin-right: px2rem(24); - } - - .nhsuk-header__menu { - display: block; - position: relative; - order: 4; - flex: 0 0 px2rem(74); - } - - .nhsuk-header__navigation-list .nhsuk-header__navigation-item, - .nhsuk-header__navigation .nhsuk-header__navigation-title { - border-top: 1px solid $color_nhsuk-grey-4; - } - - .nhsuk-header__menu-toggle { - text-align: center; - margin: 0; - right: 0; - font-weight: 600; - } - - .nhsuk-header__search-form { - display: flex; - } - - .nhsuk-header__search .nhsuk-search__input { - flex: 1 0 0; - } - - #header-dropdown-menu-control:checked ~ .nhsuk-header__navigation:not(.js-show) { - display: block; - } - - .nhsuk-header__notification-dot { - position: absolute; - top: px2rem(15); - left: px2rem(115); - font-size: px2rem(11); - line-height: px2rem(18); - font-weight: 900; - background: $nhsuk-error-color; - color: $nhsuk-white; - min-width: px2rem(18); - width: fit-content; - height: px2rem(18); - text-align: center; - border-radius: px2rem(9); - } - - .nhsuk-header__navigation-item--current .nhsuk-header__notification-dot { - left: px2rem(125); - } - - .nhsuk-header__menu-notification-dot { - position: absolute; - top: px2rem(-5); - right: px2rem(-6); - background: $nhsuk-error-color; - width: px2rem(12); - height: px2rem(12); - border-radius: px2rem(6); - box-shadow: 0 0 0 2px white; - z-index: 10; - } - .autosuggestion-menu { top: 100%; } } - /* tablet */ @media (max-width: px2rem(768)) { - - .nhsuk-width-container.app-width-container, - .nhsuk-width-container.app-width-container.beta-banner { - padding-left: px2rem(16); - padding-right: px2rem(16); - } - - .nhsuk-back-link { - padding: 0.5rem 0; - } - - .nhsuk-header__menu .nhsuk-header__not-mobile { - display: none; - } - .autosuggestion-menu { top: 100%; } - - .nhsuk-header__not-mobile { - display: none; - } - - .nhsuk-header__mobile-only-nav { - display: flex; - order: 1; - justify-content: space-around; - gap: 0 px2rem(16); - align-items: flex-start; - flex-wrap: wrap; - width: px2rem(166); - } - - .nhsuk-header__mobile-only-nav .nhsuk-header__menu { - margin-right: px2rem(12); - } - - .nhsuk-header__mobile-only-nav .nhsuk-header__search-toggle { - margin-left: px2rem(12); - } - - .nhsuk-header__break { - display: none; - } - - .nhsuk-header__mobile-break { - display: block; - width: 100%; - height: 0; - } - - .nhsuk-header__link--service { - flex-direction: column; - align-items: flex-start; - } - - .nhsuk-header__notification-dot { - top: px2rem(13); - left: px2rem(100); - } - - .nhsuk-header__service-name { - padding: px2rem(12) 0 0; - } - - .nhsuk-header__search-toggle { - position: relative; - height: px2rem(40); - order: 2; - padding: px2rem(7) px2rem(10) 0; - margin: 0 - } - - .nhsuk-header__search .nhsuk-search__submit { - padding-top: nhsuk-spacing(1); - } - - .nhsuk-header__menu { - order: 3; - } - - .nhsuk-header__search { - order: 4; - width: 100%; - flex-grow: 1; - margin: px2rem(16) px2rem(-16) 0; - border-bottom: 1px solid $color_nhsuk-grey-4; - } - - #header-mobile-search-control { - display: block; - opacity: 0; - position: absolute; - } - - #header-mobile-search-control:checked ~ .nhsuk-header__search .nhsuk-header__search-wrap { - display: block; - } - - .nhsuk-width-container.nhsuk-header__container.app-width-container { - padding-bottom: 0; - } - - - .side-nav__toggle, .side-nav__close { - display: inline-block; - } - - .side-nav__close { - background-color: transparent; - } - - .nav-toggle:checked + label.side-nav__toggle .menu-icon, - .nav-toggle:checked + label.side-nav__toggle + .side-nav__container .side-nav__close .menu-icon { - background-color: #e8f0f7; - } } /* mobile */ @media (max-width: px2rem(640)) { - - .nhsuk-header__not-mobile { - display: none; - } - - .nhsuk-header__logo { - max-width: none; - } - - .nhsuk-header__mobile-only-nav { - display: flex; - order: 1; - justify-content: space-around; - gap: 0 px2rem(16); - align-items: flex-start; - flex-wrap: wrap; - width: px2rem(166); - } - - .nhsuk-header__pre-login .nhsuk-header__mobile-only-nav { - align-items: center; - justify-content: flex-end; - padding-bottom: px2rem(16); - } - - .nhsuk-header__mobile-only-nav .nhsuk-header__menu { - margin-right: px2rem(12); - } - - .nhsuk-header__mobile-only-nav .nhsuk-header__search-toggle { - margin-left: px2rem(12); - } - - .nhsuk-header__break { - display: none; - } - - .nhsuk-header__mobile-break { - display: block; - width: 100%; - height: 0; - } - - .nhsuk-header__link--service { - flex-direction: column; - align-items: flex-start; - } - - .nhsuk-header__notification-dot { - top: px2rem(13); - left: px2rem(100); - } - - .nhsuk-header__service-name { - padding: px2rem(12) 0 0; - } - - .nhsuk-header__search-toggle { - position: relative; - height: px2rem(40); - order: 2; - padding: px2rem(7) px2rem(10) 0; - margin: 0 - } - - .nhsuk-header__search .nhsuk-search__submit { - padding-top: nhsuk-spacing(1); - } - - .nhsuk-header__menu { - order: 3; - } - - .nhsuk-header__search { - order: 4; - width: 100%; - flex-grow: 1; - margin: px2rem(16) px2rem(-16) 0; - border-bottom: 1px solid $color_nhsuk-grey-4; - } - - #header-mobile-search-control { - display: block; - opacity: 0; - position: absolute; - } - - #header-mobile-search-control:checked ~ .nhsuk-header__search .nhsuk-header__search-wrap { - display: block; - } - - .nhsuk-width-container.nhsuk-header__container.app-width-container { - padding-bottom: 0; - } - .autosuggestion-menu { top: 100%; - } -} \ No newline at end of file + } +} diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/nhsuk-overrides.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/nhsuk-overrides.scss new file mode 100644 index 000000000..22c319751 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/nhsuk-overrides.scss @@ -0,0 +1,672 @@ +@use "../abstracts/all" as *; +@use "nhsuk" as *; + +.nhsuk-u-font-style-italic { + font-style: italic; +} + +.nhsuk-button--red { + background-color: $nhsuk-red !important; + + &:hover { + background-color: $nhsuk-red-hover !important; + border-color: $nhsuk-red-hover !important; + } +} + +.nhsuk-button--beta-login { + background-color: $nhsuk-blue; + box-shadow: 0 4px 0 #002f5c; +} + +.nhsuk-button--beta-login:hover { + background-color: $nhsuk-btn-blue-hover; +} + +// By default, the text on reverse (white) buttons turns white once clicked, rendering them invisible. +.nhsuk-button--reverse:visited { + color: $nhsuk-black +} + +.nhsuk-back-link { + padding: px2rem(20) 0; + margin-bottom: 0; +} + +.nhsuk-radios__divider { + text-align: left; + width: unset; +} + +.nhsuk-radios__item label { + font-family: $font-stack; +} + +/* Conditional radio buttons - Note: The nhsuk-radios__conditional element needs to be a SIBLING of the radio button input element + otherwise the CSS selector won't work. See Views/Bookmark/Move.cshtml for a usage example. + The NHSUK component (nhsuk-radios__conditional) requires JavaScript to work. These tweaks allow it to work without. +*/ +.nhsuk-radios__conditional { + display: none; + margin-left: -22px; + margin-top: 8px; +} + +.nhsuk-radios__input:checked ~ .nhsuk-radios__conditional { + display: block !important; +} + +/* jquery unbobtrusive validation style over */ +.nhsuk-error-summary__list li { + color: $nhsuk-red; +} + +.nhsuk-form-group.input-validation-error { + @extend .nhsuk-form-group--error; +} + +.nhsuk-input.input-validation-error { + @extend .nhsuk-input--error +} + +.nhsuk-input:focus { + border: 2px solid #212b32; + box-shadow: inset 0 0 0 2px; + outline: 4px solid #ffeb3b; /* 1 */ + outline-offset: 0; +} + +.nhsuk-bg-light-blue { + background-color: $nhsuk-light-blue-color; +} + +.nhsuk-bg-pale-blue { + background-color: $nhsuk-pale-blue-color; +} + +.nhsuk-bg-white { + background-color: $color_nhsuk-white; +} + +.nhsuk-width-container.search-width-container { + max-width: px2rem(752); + margin: 0 auto; + padding-left: px2rem(68); + padding-right: px2rem(68); +} + +#maincontent { + button[class^='nhsuk-search__submit'] span.nhsuk-u-visually-hidden { + color: $color_nhsuk-grey-1; + background-color: $color_nhsuk-white; + } +} + +form label.nhsuk-u-visually-hidden { + color: $color_nhsuk-grey-1; + background-color: $color_nhsuk-white; +} + +/* One third column layout that switches to full width at the small desktop breakpoint (990px) instead of mobile. */ +.nhsuk-grid-column-one-third-small-desktop { + @extend .nhsuk-grid-column-one-third; + width: 33.3333333333% !important; + + @media (max-width: px2rem(990)) { + width: 100% !important; + } +} + + +/* Tweaks to styling for single card view. */ +@media(min-width: 768px) and (max-width: 990px) { + .nhsuk-card-group .nhsuk-grid-column-one-third-small-desktop { + max-width: 600px !important; + } + + .nhsuk-card-group--centred { + justify-content: center !important; + } +} + +.nhsuk-card-banner-container { + padding-bottom: 42.86%; + position: relative; +} + +.nhsuk-card-banner { + height: 100%; + object-fit: cover; + object-position: left; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.nhsuk-card-banner-empty { + background-color: $nhsuk-pale-blue-color; + border-bottom: 1px solid #d8dde0; +} + +.nhsuk-error-message.error-message--margin-bottom-1 { + margin-bottom: nhsuk-spacing(1); +} + +.nhsuk-u-margin-bottom-2point5 { + margin-bottom: 12px; + + @media(max-width: 641px) { + margin-bottom: 10px; + } +} + +.nhsuk-button--no-shrink { + white-space: nowrap; +} + +/*Add a background color to the radio button when focused */ +.nhsuk-radios__input:focus + .radioButton { + box-shadow: 0 0 0 3px $nhsuk-yellow; +} +/*Add a background color to the radio button when focused */ +.nhsuk-checkboxes__input:focus + .checkmark { + box-shadow: 0 0 0 4px $nhsuk-yellow; +} + + + +// Below taken from layout.scss +// Overrides largely due to +// - Highly customised header +// - Use of full browser width hero images and full width colour bands +// - Beta banner +// +// Header customisation not needed with current frontend package +// - FGC 19/06/25 + + +.nhsuk-width-container.app-width-container { + max-width: px2rem(1208); + margin: 0 auto; + padding-left: px2rem(32); + padding-right: px2rem(32); +} + +// Header items +.nhsuk-header { + padding: 0 px2rem(32); +} + +.nhsuk-header .nhsuk-width-container.app-width-container { + max-width: px2rem(1144); + margin: 0 auto; +} + +.nhsuk-width-container.app-width-container.beta-banner { + padding: px2rem(8) px2rem(32); + max-width: px2rem(1208); + margin: 0 auto; +} + +.nhsuk-header .nhsuk-header__container::after { + content: none; +} + +.nhsuk-header__navigation.app-width-container { + max-width: px2rem(1144); +} + +.nhsuk-header__container.app-width-container { + display: flex; + justify-content: space-between; + gap: 0 px2rem(24); + padding: px2rem(16) 0; +} + +.nhsuk-header__logo { + flex: 1 0 0; +} + +.nhsuk-header__logo .nhsuk-header__link--service { + display: inline-flex; +} + +.nhsuk-header__service-name { + font-size: px2rem(19); +} + +.nhsuk-header__notification-dot { + position: absolute; + top: px2rem(8); + right: px2rem(-10); + font-size: px2rem(11); + line-height: px2rem(18); + font-weight: 900; + background: $nhsuk-error-color; + color: white; + min-width: px2rem(18); + height: px2rem(18); + text-align: center; + border-radius: px2rem(9); + padding: px2rem(1) px2rem(3) 0; +} + +.nhsuk-header__menu { + display: none; +} + +.nhsuk-header__search .nhsuk-search__input { + width: px2rem(260); +} + +.nhsuk-header__search { + .nhsuk-search__input { + width: px2rem(260); + + &::-moz-placeholder { + opacity: 1; + } + } + + #search > label.nhsuk-u-visually-hidden { + background-color: $nhsuk-white; + } +} + +#header-dropdown-menu-control { + opacity: 0; + position: absolute; +} + + +#header-dropdown-menu-control:checked ~ .nhsuk-header__navigation:not(.js-show) { + display: block +} + +#header-mobile-search-control { + display: none; +} + +.nhsuk-header__break { + display: none; +} + +.nhsuk-header__mobile-only-nav { + display: none; +} + +.nhsuk-header__mobile-break { + display: none; +} + +.nhsuk-header__navigation-item--current { + a { + font-weight: bold; + } +} + +.nhsuk-account__login { + // also a header item + font-size: px2rem(14); + float: right; + position: relative; + z-index: 2; + display: flex; + justify-content: space-between; + align-items: center; + gap: px2rem(24); +} + +.nhsuk-account__login--link, +.nhsuk-account__login--link:visited, +.nhsuk-account__login--link:hover { + // all header items + color: #fff; +} + + +// End of header items + + +.nhsuk-width-container.app-width-container--full { + // used to allow placement of hero (full width) images + margin: 0; + max-width: none +} + +.app-main-wrapper--no-padding { + // allowing hero image to touch headers + padding: 0 +} + +.nhsuk-footer { + padding: px2rem(48) 0; +} + + + +/* large desktop */ +@media (min-width: px2rem(990)) { + // entirely headers + + .nhsuk-header__navigation-item--current a { + border-bottom: 4px solid $nhsuk-grey-lighter; + font-weight: normal; + } + + .nhsuk-header__navigation-link { + position: relative; + } + + .nhsuk-header__navigation-item:last-child { + margin-right: 16px; + } +} + +/* small desktop */ +@media (max-width: px2rem(989)) { + + //entirely headers + + .nhsuk-header__container { + flex-wrap: wrap; + } + + .nhsuk-header { + padding: 0; + } + + .nhsuk-header__container.app-width-container { + flex-wrap: wrap; + gap: 0 0; + padding: px2rem(16) px2rem(32); + } + + .nhsuk-header__link--service { + align-items: center; + -ms-flex-align: center; + margin-bottom: 0; + width: auto; + } + + .nhsuk-header__service-name { + padding-left: px2rem(16); + } + + .nhsuk-header__logo { + order: 0; + } + + .nhsuk-account__login { + // also part of the header + order: 1; + margin-left: auto; + margin-right: 0px; + } + + .nhsuk-header__break { + display: block; + width: 100%; + height: px2rem(24); + order: 2 + } + + .nhsuk-header__search { + order: 3; + flex-grow: 1; + margin-left: 0; + margin-right: px2rem(24); + } + + .nhsuk-header__menu { + display: block; + position: relative; + order: 4; + flex: 0 0 px2rem(74); + } + + .nhsuk-header__navigation-list .nhsuk-header__navigation-item, + .nhsuk-header__navigation .nhsuk-header__navigation-title { + border-top: 1px solid $color_nhsuk-grey-4; + } + + .nhsuk-header__menu-toggle { + text-align: center; + margin: 0; + right: 0; + font-weight: 600; + } + + .nhsuk-header__search-form { + display: flex; + } + + .nhsuk-header__search .nhsuk-search__input { + flex: 1 0 0; + } + + #header-dropdown-menu-control:checked ~ .nhsuk-header__navigation:not(.js-show) { + display: block; + } + + .nhsuk-header__notification-dot { + position: absolute; + top: px2rem(15); + left: px2rem(115); + font-size: px2rem(11); + line-height: px2rem(18); + font-weight: 900; + background: $nhsuk-error-color; + color: $nhsuk-white; + min-width: px2rem(18); + width: fit-content; + height: px2rem(18); + text-align: center; + border-radius: px2rem(9); + } + + .nhsuk-header__navigation-item--current .nhsuk-header__notification-dot { + left: px2rem(125); + } + + .nhsuk-header__menu-notification-dot { + position: absolute; + top: px2rem(-5); + right: px2rem(-6); + background: $nhsuk-error-color; + width: px2rem(12); + height: px2rem(12); + border-radius: px2rem(6); + box-shadow: 0 0 0 2px white; + z-index: 10; + } +} + +@media (max-width: px2rem(768)) { + .nhsuk-width-container.app-width-container, + .nhsuk-width-container.app-width-container.beta-banner { + padding-left: px2rem(16); + padding-right: px2rem(16); + } + + // entirely headers from this point + + .nhsuk-back-link { + padding: 0.5rem 0; + } + + .nhsuk-header__menu .nhsuk-header__not-mobile { + display: none; + } + + .nhsuk-header__not-mobile { + display: none; + } + + .nhsuk-header__mobile-only-nav { + display: flex; + order: 1; + justify-content: space-around; + gap: 0 px2rem(16); + align-items: flex-start; + flex-wrap: wrap; + width: px2rem(166); + } + + .nhsuk-header__mobile-only-nav .nhsuk-header__menu { + margin-right: px2rem(12); + } + + .nhsuk-header__mobile-only-nav .nhsuk-header__search-toggle { + margin-left: px2rem(12); + } + + .nhsuk-header__break { + display: none; + } + + .nhsuk-header__mobile-break { + display: block; + width: 100%; + height: 0; + } + + .nhsuk-header__link--service { + flex-direction: column; + align-items: flex-start; + } + + .nhsuk-header__notification-dot { + top: px2rem(13); + left: px2rem(100); + } + + .nhsuk-header__service-name { + padding: px2rem(12) 0 0; + } + + .nhsuk-header__search-toggle { + position: relative; + height: px2rem(40); + order: 2; + padding: px2rem(7) px2rem(10) 0; + margin: 0 + } + + .nhsuk-header__search .nhsuk-search__submit { + padding-top: nhsuk-spacing(1); + } + + .nhsuk-header__menu { + order: 3; + } + + .nhsuk-header__search { + order: 4; + width: 100%; + flex-grow: 1; + margin: px2rem(16) px2rem(-16) 0; + border-bottom: 1px solid $color_nhsuk-grey-4; + } +} + +@media (max-width: px2rem(640)) { + + //entirely headers + + .nhsuk-header__not-mobile { + display: none; + } + + .nhsuk-header__logo { + max-width: none; + } + + .nhsuk-header__mobile-only-nav { + display: flex; + order: 1; + justify-content: space-around; + gap: 0 px2rem(16); + align-items: flex-start; + flex-wrap: wrap; + width: px2rem(166); + } + + .nhsuk-header__pre-login .nhsuk-header__mobile-only-nav { + align-items: center; + justify-content: flex-end; + padding-bottom: px2rem(16); + } + + .nhsuk-header__mobile-only-nav .nhsuk-header__menu { + margin-right: px2rem(12); + } + + .nhsuk-header__mobile-only-nav .nhsuk-header__search-toggle { + margin-left: px2rem(12); + } + + .nhsuk-header__break { + display: none; + } + + .nhsuk-header__mobile-break { + display: block; + width: 100%; + height: 0; + } + + .nhsuk-header__link--service { + flex-direction: column; + align-items: flex-start; + } + + .nhsuk-header__notification-dot { + top: px2rem(13); + left: px2rem(100); + } + + .nhsuk-header__service-name { + padding: px2rem(12) 0 0; + } + + .nhsuk-header__search-toggle { + position: relative; + height: px2rem(40); + order: 2; + padding: px2rem(7) px2rem(10) 0; + margin: 0 + } + + .nhsuk-header__search .nhsuk-search__submit { + padding-top: nhsuk-spacing(1); + } + + .nhsuk-header__menu { + order: 3; + } + + .nhsuk-header__search { + order: 4; + width: 100%; + flex-grow: 1; + margin: px2rem(16) px2rem(-16) 0; + border-bottom: 1px solid $color_nhsuk-grey-4; + } + + #header-mobile-search-control { + display: block; + opacity: 0; + position: absolute; + } + + #header-mobile-search-control:checked ~ .nhsuk-header__search .nhsuk-header__search-wrap { + display: block; + } + + .nhsuk-width-container.nhsuk-header__container.app-width-container { + padding-bottom: 0; + } +} diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss index bebb4e2f8..46bf7eb76 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/mylearning.scss @@ -3,6 +3,32 @@ .my-learning { + .nhs-progress-container { + display: flex; + height: 10px; + overflow: hidden; + line-height: 0; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.25rem; + } + + .nhs-progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: $nhsuk-green; + transition: width .6s ease; + } + .nhs-item-row { + display: flex; + justify-content: space-between; + align-items: center; + } .downloadbuttonaslink { background: none !important; diff --git a/LearningHub.Nhs.WebUI/Styles/sections/_all.scss b/LearningHub.Nhs.WebUI/Styles/sections/_all.scss index 22c55622d..a127ade9d 100644 --- a/LearningHub.Nhs.WebUI/Styles/sections/_all.scss +++ b/LearningHub.Nhs.WebUI/Styles/sections/_all.scss @@ -140,268 +140,272 @@ div.contribute { } div.contribute { - h2 { - .warningTriangle { - padding-left: 10px; - vertical-align: middle; - } - } - - h3 { - .warningTriangle { - padding-left: 10px; - vertical-align: middle; - } - } - - div.common-content { - div.form-group { - margin-bottom: 0; + h2 { + .warningTriangle { + padding-left: 10px; + vertical-align: middle; + } } - .bg-grey-white { - padding: 17px 25px 30px 25px; - color: $nhsuk-black; + h3 { + .warningTriangle { + padding-left: 10px; + vertical-align: middle; + } } - } - div.white-background { - background-color: $nhsuk-white; - min-height: 420px; - } - - .limit-width { - width: 100%; - max-width: 850px; - margin-left: 40px; - padding-right: 50px; - } + div.common-content { + div.form-group { + margin-bottom: 0; + } - div.contribute-header { - .draftText { - color: $nhsuk-green; - margin-top: 0; + .bg-grey-white { + padding: 17px 25px 30px 25px; + color: $nhsuk-black; + } } - .error-message { - color: $nhsuk-red; - font-weight: bold; + div.white-background { + background-color: $nhsuk-white; + min-height: 420px; } - .warning-banner { - display: flex; - flex-flow: row; - justify-content: space-between; - padding: 1.5rem 1.5rem 0.8rem 1.5rem; - border-bottom: 1px solid $nhsuk-grey-light; - border-top: 1px solid $nhsuk-grey-light; - - div.warningTriangle { - font-size: 40px; - } - - div.info { - padding-left: 12px; - } - - div.dismiss { - text-align: end; - padding-top: .5rem; - padding-right: 2rem; - - button { - font-size: 2.5rem; - color: $nhsuk-grey; - } - } - - .warning-banner-left { - display: flex; - justify-content: space-between; + .limit-width { width: 100%; - max-width: 800px; + max-width: 850px; margin-left: 40px; - } + padding-right: 50px; } - } - .warningTriangle { - color: #ffb81c; - font-size: 2rem; + div.contribute-header { + .draftText { + color: $nhsuk-green; + margin-top: 0; + } - &.large { - font-size: 4rem !important; - } - } + .error-message { + color: $nhsuk-red; + font-weight: bold; + } - button.delete-button { - text-decoration: none; - color: $nhsuk-grey-placeholder; - font-size: 1.6rem; + .warning-banner { + display: flex; + flex-flow: row; + justify-content: space-between; + padding: 1.5rem 1.5rem 0.8rem 1.5rem; + border-bottom: 1px solid $nhsuk-grey-light; + border-top: 1px solid $nhsuk-grey-light; + + div.warningTriangle { + font-size: 40px; + } - i { - margin-right: 4px; - font-size: 1.8rem; - } - } + div.info { + padding-left: 12px; + } - button.publish-button { - color: $nhsuk-white; - background-color: $nhsuk-green; - font-size: 19px; - text-align: center !important; - border: 1px solid $nhsuk-green !important; - min-height: 50px; - min-width: 115px; - padding: 0px 25px 0px 25px; - border-radius: 5px; - } + div.dismiss { + text-align: end; + padding-top: .5rem; + padding-right: 2rem; - button.publish-button:disabled { - background-color: $nhsuk-grey; - border-color: $nhsuk-grey !important; - } + button { + font-size: 2.5rem; + color: $nhsuk-grey; + } + } - button.btn-outline-custom:disabled { - color: $nhsuk-grey !important; - border-color: $nhsuk-grey !important; - } + .warning-banner-left { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 800px; + margin-left: 40px; + } + } + } - button.btn-outline-custom:disabled:hover { - background-color: $nhsuk-white !important; - } + .warningTriangle { + color: #ffb81c; + font-size: 2rem; - div.resource-area-container, div.common-content { - div.resource-area-header { - background-color: $nhsuk-grey; - color: $nhsuk-grey-white; - border-top-left-radius: .5rem; - border-top-right-radius: .5rem; - font-family: $font-stack-bold; - padding: 1.4rem 2rem 1.0rem 2rem; - display: flex; - justify-content: space-between; + &.large { + font-size: 4rem !important; + } + } - a, - button { + button.delete-button { text-decoration: none; - font-size: 19px; - font-family: $font-stack; - color: $nhsuk-grey-white !important; - padding-top: 0; + color: $nhsuk-grey-placeholder; + font-size: 1.6rem; i { - margin-left: 4px; + margin-right: 4px; + font-size: 1.8rem; } - } } - div.resource-area-body { - background-color: $nhsuk-grey-white; - border-bottom-left-radius: .5rem; - border-bottom-right-radius: .5rem; - min-height: 300px; - padding: 20px 10px 20px 10px; + button.publish-button { + color: $nhsuk-white; + background-color: $nhsuk-green; + font-size: 19px; + text-align: center !important; + border: 1px solid $nhsuk-green !important; + min-height: 50px; + min-width: 115px; + padding: 0px 25px 0px 25px; + border-radius: 5px; } - .radio-options { - height: 12px !important; - width: 12px !important; - border: 1px solid $nhsuk-black; - color: $nhsuk-black; + button.publish-button:disabled { + background-color: $nhsuk-grey; + border-color: $nhsuk-grey !important; } - .radio-options-text { - margin-right: 20px; + button.btn-outline-custom:disabled { + color: $nhsuk-grey !important; + border-color: $nhsuk-grey !important; } - .author-details, .file-details, .licence-details { - display: flex; - justify-content: space-between; - background-color: $nhsuk-white; - border-radius: 5px; - padding: 25px; + button.btn-outline-custom:disabled:hover { + background-color: $nhsuk-white !important; + } - button, - a { - font-size: 16px; - color: $nhsuk-red; - text-decoration: none; - display: flex; - justify-content: space-between; + div.resource-area-container, div.common-content { + div.resource-area-header { + background-color: $nhsuk-grey; + color: $nhsuk-grey-white; + border-top-left-radius: .5rem; + border-top-right-radius: .5rem; + font-family: $font-stack-bold; + padding: 1.4rem 2rem 1.0rem 2rem; + display: flex; + justify-content: space-between; - i { - padding-left: 10px; - padding-top: 3px; + a, + button { + text-decoration: none; + font-size: 19px; + font-family: $font-stack; + color: $nhsuk-grey-white !important; + padding-top: 0; + + i { + margin-left: 4px; + } + } } - } - .divider { - color: $nhsuk-grey-light; - } - } + div.resource-area-body { + background-color: $nhsuk-grey-white; + border-bottom-left-radius: .5rem; + border-bottom-right-radius: .5rem; + min-height: 300px; + padding: 20px 10px 20px 10px; + } - .author-details { - border: solid 1px $nhsuk-grey-light; - padding: 22px 15px 10px 15px; + .radio-options { + height: 12px !important; + width: 12px !important; + border: 1px solid $nhsuk-black; + color: $nhsuk-black; + } - i { - color: $nhsuk-grey; - margin-right: 1rem; - } - } + .radio-options-text { + margin-right: 20px; + } - .keyword-container { - display: flex; - flex-wrap: wrap; + .author-details, .file-details, .licence-details { + display: flex; + justify-content: space-between; + background-color: $nhsuk-white; + border-radius: 5px; + padding: 25px; + + button, + a { + font-size: 16px; + color: $nhsuk-red; + text-decoration: none; + display: flex; + justify-content: space-between; + + i { + padding-left: 10px; + padding-top: 3px; + } + } - .keyword-tag { - background-color: $nhsuk-grey-white; - border-radius: 20px; - margin-bottom: 10px; - padding: 8px 10px 3px 15px; + .divider { + color: $nhsuk-grey-light; + } + } - button { - color: $nhsuk-red; - font-size: 2rem; + .author-details { + border: solid 1px $nhsuk-grey-light; + padding: 22px 15px 10px 15px; + + i { + color: $nhsuk-grey; + margin-right: 1rem; + } } - } - } - span.optional { - font-family: $font-stack; - } + .keyword-label { + font-weight: normal; + } + + .keyword-container { + display: flex; + flex-wrap: wrap; + + .keyword-tag { + background-color: $nhsuk-grey-white; + border-radius: 20px; + margin-bottom: 10px; + padding: 8px 10px 3px 15px; + + button { + color: $nhsuk-red; + font-size: 2rem; + } + } + } + + span.optional { + font-family: $font-stack; + } - textarea { - width: 100%; + textarea { + width: 100%; + } } - } - .authored-date label { - font-size: 1.6rem; - } + .authored-date label { + font-size: 1.6rem; + } - div.publish-warning { - display: flex; - flex-direction: row; + div.publish-warning { + display: flex; + flex-direction: row; - .triangle { - margin-right: 10px; - display: flex; - align-items: center; + .triangle { + margin-right: 10px; + display: flex; + align-items: center; - i { - padding-top: 5px; - } + i { + padding-top: 5px; + } + } } - } - div.highlighted-info { - background-color: rgba(255, 184, 28, 0.05); - border: 1px solid #FFB81C; - padding: 15px 24px; - margin: 15px 15px 0 15px; - } + div.highlighted-info { + background-color: rgba(255, 184, 28, 0.05); + border: 1px solid #FFB81C; + padding: 15px 24px; + margin: 15px 15px 0 15px; + } } @media (max-width: 768px) { diff --git a/LearningHub.Nhs.WebUI/Views/Home/_CourseEnrolled.cshtml b/LearningHub.Nhs.WebUI/Views/Home/_CourseEnrolled.cshtml index 68ad72564..a7351226b 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/_CourseEnrolled.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/_CourseEnrolled.cshtml @@ -1,8 +1,9 @@ @using LearningHub.Nhs.Models.Dashboard +@using LearningHub.Nhs.Models.Moodle.API @using LearningHub.Nhs.WebUI.Extensions @using LearningHub.Nhs.WebUI.Helpers -@model MoodleCourseResponseViewModel -@inject Microsoft.Extensions.Configuration.IConfiguration Configuration; +@model MoodleCourseResponseModel +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; @{ bool providerExists = false; @@ -10,11 +11,7 @@ string GetMoodleCourseUrl(int courseId) { - var apiBaseUrl = Configuration["MoodleAPIConfig:BaseUrl"]; - string path = $"course/view.php"; - string returnUrl = $@"{apiBaseUrl}{path}?id={courseId}"; - - return returnUrl; + return moodleApiService.GetCourseUrl(courseId); } }
diff --git a/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml b/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml index 8b8d25f8a..47cf33901 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/_MyAccessedLearningTray.cshtml @@ -32,8 +32,8 @@ TotalCount = Model.MyLearnings.TotalCount }; - var enableMoodle = this.ViewBag.EnableMoodle; - var validMoodleUser = this.ViewBag.ValidMoodleUser; + bool enableMoodle = (bool?)ViewBag.EnableMoodle ?? false; + bool validMoodleUser = (bool?)ViewBag.ValidMoodleUser ?? false; }

My accessed learning

diff --git a/LearningHub.Nhs.WebUI/Views/Home/_ResourceTray.cshtml b/LearningHub.Nhs.WebUI/Views/Home/_ResourceTray.cshtml index cffb07772..756908897 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/_ResourceTray.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/_ResourceTray.cshtml @@ -43,10 +43,6 @@
@@ -62,7 +63,7 @@
    - @if(@Model.Resources.Resources != null) + @if (@Model.Resources.Resources != null) { @foreach (var resource in Model.Resources.Resources) { diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/Index.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/Index.cshtml index 6bbc6c8d6..93df45e8e 100644 --- a/LearningHub.Nhs.WebUI/Views/MyLearning/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/Index.cshtml @@ -1,23 +1,20 @@ @using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers @using LearningHub.Nhs.WebUI.Models -@using LearningHub.Nhs.WebUI.Models.Learning -@model MyLearningViewModel; - +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; +@model MyLearningUserActivitiesViewModel; @{ - - ViewData["Title"] = "My learning"; - var errorHasOccurred = !ViewData.ModelState.IsValid; - var startHintTextLines = new List { $"From" }; - var endHintTextLines = new List { $"To" }; - var routeData = ViewActivityHelper.GetActivityParameters(Model); + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + string GetMoodleCourseUrl(int courseId) + { + return moodleApiService.GetCourseUrl(courseId); + } } - @section styles { } - @section NavBreadcrumbs {
    @@ -38,210 +35,182 @@
    } +
    -
    -
    - @if (errorHasOccurred) - { - - } - @*

    My learning

    *@ -

    - You can use this page to search and filter learning resources you've accessed, download certificates and generate a report - of your activity. -

    - -
    - - - Learn how to manage My learning - - -
    -

    My learning displays an itemised activity table. This table details everything you have accessed in the Learning Hub.

    -

    You can search your learning activity by entering a search term in the search box. This will only search your learning activity and not the entire Learning Hub. Results will be displayed placing your search term as the filter. To return to your full Itemised activity, either select clear all filters or uncheck your search term(s).

    -
    -
    - -

    - Download a report of your learning. -

    - @if (Model.TotalCount != 0) - { -
    - -
    - } - else - { - - } - -
    - -

    Search within My learning

    -
    - - - - -
    -

    -

    @Model.TotalCount activity result@(Model.TotalCount > 1 ? "s" : "")

    -

    -
    +
    + +
    + @await Component.InvokeAsync("SideNav", new { groupTitle = "Activity" })
    - -
    -
    -
    - -
    - -
    -
    - - - - Filter results - - -
    - - -
    -
    - -
    - -
    -
    + +
    +
    +

    Recent learning

    +
    +
    + +
    +
    +
    +
    +
    + @foreach (var activity in Model.Activities) + { + + var isCompleted = ViewActivityHelper.GetActivityStatusDisplayText(activity) == ActivityStatusEnum.Completed.ToString(); + + var typeLabelClass = isCompleted ? "nhsuk-u-primary-text-color" : "nhsuk-u-secondary-text-color"; + var accessedLabelClass = typeLabelClass; + var tagColorClass = isCompleted ? "nhsuk-tag--green" : "nhsuk-tag--blue"; + var statusText = isCompleted ? "Completed" : "In progress"; + var displayText = isCompleted ? "Completed" : "Accessed"; + + var activityDate = activity.ActivityDate.Date; + var today = DateTime.Today; + var dateTimeText = activityDate == today ? "Today" + : activityDate == today.AddDays(-1) ? "Yesterday" + : activityDate.ToString("dd MMM yyyy"); + +
    + + @if (ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) == "Course") + { + @activity.Title + } + else + { + @activity.Title + } + +
    +
    +
    + Type: + + + @ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) + + + + @displayText: + + + + @dateTimeText +
    -
    -
    -
    -
    - -
    - - -
    -
    - -
    -
    -
    - -
    -
    - -
    +
    + + + @statusText + +
    -
    - - -
    - -
    -
    - + @if (ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) == "Course") + { +
    +
    -
    + } + + + @if (ViewActivityHelper.CanCertificateawarded(activity)) + { + +
    + + + + + + + + + + + + + + + Certificate: + awarded [@dateTimeText] -
    -
    -
    -
    -
    -
    - + } + else + { +
    + @if (ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) == "Course") + { +
    @activity.TotalActivities of @activity.CompletedActivities activities completed
    + } + else + { +
    + No certificate available +
    + } + @if (activity.CertificateEnabled == true) + { +
    + Includes a certificate + + + + + + + + + + + + + + + +
    + }
    -
    -
    - + }
    - -
    -
    +
    +
    +
    +
    + }
    - -
    -
    -
    -
    -

    Get help

    -

    - Find further guidance and support on how to manage My learning at the Learning Hub help centre. -

    -
    +
    + @await Html.PartialAsync("_ActivityTablePaging", Model) +
    Not seeing what you are looking for? Go to learning history
    -
    - @await Html.PartialAsync("_ActivityTablePaging", Model) -
    \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/Indexold.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/Indexold.cshtml new file mode 100644 index 000000000..0878d6d06 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/Indexold.cshtml @@ -0,0 +1,253 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@model MyLearningViewModel; + +@{ + + ViewData["Title"] = "My learning"; + var errorHasOccurred = !ViewData.ModelState.IsValid; + var startHintTextLines = new List { $"From" }; + var endHintTextLines = new List { $"To" }; + var routeData = ViewActivityHelper.GetActivityParameters(Model); +} + +@section styles { + + + +} + +@section NavBreadcrumbs { + +
    +
    +
    +
    + +
    + Home + +
    +
    +

    My learning activity

    +
    +
    +
    +
    +
    +} + +
    +
    +
    +
    + @if (errorHasOccurred) + { + + } + @*

    My learning

    *@ +

    + You can use this page to search and filter learning resources you've accessed, download certificates and generate a report + of your activity. +

    + +
    + + + Learn how to manage My learning + + +
    +

    My learning displays an itemised activity table. This table details everything you have accessed in the Learning Hub.

    +

    You can search your learning activity by entering a search term in the search box. This will only search your learning activity and not the entire Learning Hub. Results will be displayed placing your search term as the filter. To return to your full Itemised activity, either select clear all filters or uncheck your search term(s).

    +
    +
    + +

    + Download a report of your learning. +

    + @if (Model.TotalCount != 0) + { +
    + +
    + } + else + { + + } + +
    + +

    Search within My learning

    +
    + + + + +
    +

    +

    @Model.TotalCount activity result@(Model.TotalCount > 1 ? "s" : "")

    +

    +
    +
    + +
    +
    +
    + +
    + +
    +
    + + + + Filter results + + +
    + + +
    +
    + +
    + +
    +
    + + +
    +
    +
    +
    +
    + +
    + + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Get help

    +

    + Find further guidance and support on how to manage My learning at the Learning Hub help centre. +

    +
    +
    +
    +
    + @await Html.PartialAsync("_ActivityTablePaging", Model) +
    +
    +
    +
    \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/LearningHistory.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/LearningHistory.cshtml new file mode 100644 index 000000000..93df45e8e --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/LearningHistory.cshtml @@ -0,0 +1,216 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; +@model MyLearningUserActivitiesViewModel; +@{ + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + string GetMoodleCourseUrl(int courseId) + { + return moodleApiService.GetCourseUrl(courseId); + } +} +@section styles { + + + +} +@section NavBreadcrumbs { + +
    +
    +
    +
    + +
    + Home + +
    +
    +

    My learning activity

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

    Recent learning

    +
    +
    + +
    +
    +
    +
    +
    + @foreach (var activity in Model.Activities) + { + + var isCompleted = ViewActivityHelper.GetActivityStatusDisplayText(activity) == ActivityStatusEnum.Completed.ToString(); + + var typeLabelClass = isCompleted ? "nhsuk-u-primary-text-color" : "nhsuk-u-secondary-text-color"; + var accessedLabelClass = typeLabelClass; + var tagColorClass = isCompleted ? "nhsuk-tag--green" : "nhsuk-tag--blue"; + var statusText = isCompleted ? "Completed" : "In progress"; + var displayText = isCompleted ? "Completed" : "Accessed"; + + var activityDate = activity.ActivityDate.Date; + var today = DateTime.Today; + var dateTimeText = activityDate == today ? "Today" + : activityDate == today.AddDays(-1) ? "Yesterday" + : activityDate.ToString("dd MMM yyyy"); + +
    + + @if (ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) == "Course") + { + @activity.Title + } + else + { + @activity.Title + } + +
    +
    +
    + Type: + + + @ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) + + + + @displayText: + + + + @dateTimeText + +
    +
    + + + @statusText + + +
    +
    + @if (ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) == "Course") + { +
    +
    +
    + } + + + @if (ViewActivityHelper.CanCertificateawarded(activity)) + { + +
    + + + + + + + + + + + + + + + Certificate: + awarded [@dateTimeText] + +
    + + } + else + { +
    + @if (ViewActivityHelper.GetResourceTypeDesc(activity.ResourceType) == "Course") + { +
    @activity.TotalActivities of @activity.CompletedActivities activities completed
    + } + else + { +
    + No certificate available +
    + } + @if (activity.CertificateEnabled == true) + { +
    + Includes a certificate + + + + + + + + + + + + + + + +
    + } +
    + } +
    +
    +
    +
    +
    + } +
    + +
    + @await Html.PartialAsync("_ActivityTablePaging", Model) +
    Not seeing what you are looking for? Go to learning history
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/RecentMyLearningActivitiesPaging.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/RecentMyLearningActivitiesPaging.cshtml new file mode 100644 index 000000000..4abefe945 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/RecentMyLearningActivitiesPaging.cshtml @@ -0,0 +1,30 @@ + diff --git a/LearningHub.Nhs.WebUI/Views/MyLearning/_ActivityTablePaging.cshtml b/LearningHub.Nhs.WebUI/Views/MyLearning/_ActivityTablePaging.cshtml index 2a67789be..b66b04168 100644 --- a/LearningHub.Nhs.WebUI/Views/MyLearning/_ActivityTablePaging.cshtml +++ b/LearningHub.Nhs.WebUI/Views/MyLearning/_ActivityTablePaging.cshtml @@ -1,7 +1,7 @@ @using System.Web; @using LearningHub.Nhs.WebUI.Models.Learning @using LearningHub.Nhs.WebUI.Models.Search; -@model MyLearningViewModel; +@model MyLearningUserActivitiesViewModel; @{ diff --git a/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml index 647583787..50c8df8f4 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml @@ -1,5 +1,5 @@ @model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel -@inject Microsoft.Extensions.Configuration.IConfiguration Configuration; +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; @using System.Linq; @using System.Web; @@ -28,19 +28,30 @@ &query={searchSignalQueryEncoded}&title={payload?.DocumentFields?.Title}"; } - string GetMoodleCourseUrl(string courseId) + string GetMoodleCourseUrl(string courseIdWithPrefix) { - var prefix = "M"; - if (courseId.StartsWith(prefix)) + const string prefix = "M"; + + if (string.IsNullOrWhiteSpace(courseIdWithPrefix)) + { + return string.Empty; + } + + if (!courseIdWithPrefix.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { - courseId = courseId.Replace(prefix, ""); ; + return string.Empty; } - var apiBaseUrl = Configuration["MoodleAPIConfig:BaseUrl"]; - string path = $"course/view.php"; - string returnUrl = $@"{apiBaseUrl}{path}?id={courseId}"; + var courseIdPart = courseIdWithPrefix.Substring(prefix.Length); - return returnUrl; + if (int.TryParse(courseIdPart, out int courseId)) + { + return moodleApiService.GetCourseUrl(courseId); + } + else + { + return string.Empty; + } } bool showCatalogueFieldsInResources = ViewBag.ShowCatalogueFieldsInResources == null || ViewBag.ShowCatalogueFieldsInResources == true; @@ -93,7 +104,7 @@
    Type: - @UtilityHelper.GetPrettifiedResourceTypeNameMoodle(UtilityHelper.ToEnum(item.ResourceType), 0) + @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0)
    @if (item.ResourceType != "moodle") diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml index c10f269d5..0b4698f61 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Tenant/LearningHub/_Layout.cshtml @@ -21,6 +21,7 @@ +
public virtual DbSet NodeContentAdminViewModel { get; set; } + /// + /// Gets or sets the User recent my learning activities. + /// + public virtual DbSet MyLearningActivitiesViewModel { get; set; } + /// /// Gets or sets the node resource.. /// 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 009e83ef8..476ecb503 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,6 +24,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Activity/ResourceActivityRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Activity/ResourceActivityRepository.cs index 12710bfe8..d27b6f13c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Activity/ResourceActivityRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Activity/ResourceActivityRepository.cs @@ -11,6 +11,7 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Repositories.Activity using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.Hierarchy; using LearningHub.Nhs.Models.MyLearning; using LearningHub.Nhs.OpenApi.Repositories.EntityFramework; using LearningHub.Nhs.OpenApi.Repositories.Helpers; @@ -180,7 +181,7 @@ public async Task IsScormActivityFinished(int userId, int scormActivityId) /// requestModel. /// detailedMediaActivityRecordingStartDate. /// ResourceActivity. - public async Task> GetByUserIdFromSP(int userId,Nhs.Models.MyLearning.MyLearningRequestModel requestModel, DateTimeOffset detailedMediaActivityRecordingStartDate) + public async Task> GetByUserIdFromSP(int userId, Nhs.Models.MyLearning.MyLearningRequestModel requestModel, DateTimeOffset detailedMediaActivityRecordingStartDate) { (DateTimeOffset? startDate, DateTimeOffset? endDate) = this.ApplyDatesFilter(requestModel); (string strResourceTypes, bool resourceTypeFlag) = this.ApplyResourceTypesfilters(requestModel); @@ -232,6 +233,46 @@ public async Task> GetByUserIdFromSP(int userId,Nhs return listOfresourceActivities.OrderByDescending(r => r.ActivityStart).AsQueryable(); } + /// + /// Get User Recent My LearningActivities. + /// + /// The user id. + /// requestModel. + /// + public async Task> GetUserRecentMyLearningActivities(int userId, Nhs.Models.MyLearning.MyLearningRequestModel requestModel) + { + try + { + var param0 = new SqlParameter("@userId", SqlDbType.Int) { Value = userId }; + var result = await DbContext.MyLearningActivitiesViewModel.FromSqlRaw("EXEC activity.GetUserRecentLearningActivities @userId", param0).AsNoTracking().ToListAsync(); + return result; + } + catch (Exception ex) + { + return null; + } + } + + /// + /// Get User Recent My LearningActivities. + /// + /// The user id. + /// requestModel. + /// + public async Task> GetUserLearningHistory(int userId, Nhs.Models.MyLearning.MyLearningRequestModel requestModel) + { + try + { + var param0 = new SqlParameter("@userId", SqlDbType.Int) { Value = userId }; + var result = await DbContext.MyLearningActivitiesViewModel.FromSqlRaw("EXEC activity.GetUsersLearningHistory @userId", param0).AsNoTracking().ToListAsync(); + return result; + } + catch (Exception ex) + { + return null; + } + } + /// /// Get Resource Activity by user id. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Hierarchy/CatalogueNodeVersionRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Hierarchy/CatalogueNodeVersionRepository.cs index f533a79be..b8b7e93c7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Hierarchy/CatalogueNodeVersionRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/Hierarchy/CatalogueNodeVersionRepository.cs @@ -82,10 +82,6 @@ public async Task> GetPublishedCatalogues() /// The . public IQueryable GetPublishedCataloguesForUserAsync(int userId) { - var communityCatalogue = DbContext.CatalogueNodeVersion.AsNoTracking() - .Include(cnv => cnv.NodeVersion.Node) - .Where(cnv => cnv.NodeVersion.VersionStatusEnum == VersionStatusEnum.Published - && cnv.NodeVersion.NodeId == 1 /* Community Catalogue */); var cataloguesForUser = from cnv in DbContext.CatalogueNodeVersion.Include(cnv => cnv.NodeVersion.Node).AsNoTracking() join nv in DbContext.NodeVersion.Where(cnv => cnv.VersionStatusEnum == VersionStatusEnum.Published && !cnv.Deleted) // .Include(nv => nv.Node) @@ -100,9 +96,9 @@ join n in DbContext.Node.Where(x => !x.Deleted) on nv.Id equals n.CurrentNodeVersionId select cnv; - var returnedCatalogues = communityCatalogue.Union(cataloguesForUser).Distinct() - .OrderBy(cnv => cnv.NodeVersion.NodeId != 1) - .ThenBy(cnv => cnv.Name); + var returnedCatalogues = cataloguesForUser.Distinct() + .OrderBy(cnv => cnv.NodeVersion.NodeId != 1) + .ThenBy(cnv => cnv.Name); return returnedCatalogues; } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IMoodleHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IMoodleHttpClient.cs similarity index 79% rename from LearningHub.Nhs.WebUI/Interfaces/IMoodleHttpClient.cs rename to OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IMoodleHttpClient.cs index 3348a20a9..4a98b4ebf 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IMoodleHttpClient.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IMoodleHttpClient.cs @@ -1,8 +1,8 @@ -namespace LearningHub.Nhs.Services.Interface -{ - using System.Net.Http; - using System.Threading.Tasks; +using System.Net.Http; +using System.Threading.Tasks; +namespace LearningHub.Nhs.OpenApi.Services.Interface.HttpClients +{ /// /// The Moodle Http Client interface. /// @@ -20,4 +20,4 @@ public interface IMoodleHttpClient /// defaultParameters. string GetDefaultParameters(); } -} \ No newline at end of file +} 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 aa019e18c..5abc71951 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 @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs new file mode 100644 index 000000000..1721140b1 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMoodleApiService.cs @@ -0,0 +1,44 @@ +using LearningHub.Nhs.Models.Moodle.API; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LearningHub.Nhs.OpenApi.Services.Interface.Services +{ + /// + /// IMoodleApiService. + /// + public interface IMoodleApiService + { + /// + /// GetResourcesAsync. + /// + /// The current LH User Id. + /// A representing the result of the asynchronous operation. + Task GetMoodleUserIdByUsernameAsync(int currentUserId); + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// pageNumber. + /// List of MoodleCourseResponseModel. + Task> GetEnrolledCoursesAsync(int currentUserId, int pageNumber); + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// pageNumber. + /// List of MoodleCourseResponseModel. + Task> GetRecentEnrolledCoursesAsync(int userId, int? months = null); + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// Moodle course id. + /// pageNumber. + /// List of MoodleCourseResponseModel. + Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber); + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs index b72447a00..3b53d59cc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IMyLearningService.cs @@ -19,6 +19,22 @@ public interface IMyLearningService /// The . Task GetActivityDetailed(int userId, MyLearningRequestModel requestModel); + /// + /// Gets the user recent my leraning activities. + /// + /// /// The user id. + /// The request model. + /// The . + Task GetUserRecentMyLearningActivitiesAsync(int userId, MyLearningRequestModel requestModel); + + /// + /// Gets history of users my leraning activities. + /// + /// /// The user id. + /// The request model. + /// The . + Task GetUserLearningHistoryAsync(int userId, MyLearningRequestModel requestModel); + /// /// Gets the played segment data for the progress modal in My Learning screen. /// diff --git a/LearningHub.Nhs.WebUI/Services/MoodleHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/MoodleHttpClient.cs similarity index 61% rename from LearningHub.Nhs.WebUI/Services/MoodleHttpClient.cs rename to OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/MoodleHttpClient.cs index 79f44027a..2c9fc410a 100644 --- a/LearningHub.Nhs.WebUI/Services/MoodleHttpClient.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/MoodleHttpClient.cs @@ -1,34 +1,42 @@ -namespace LearningHub.Nhs.Services -{ - using System; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Threading.Tasks; - using LearningHub.Nhs.Services.Interface; - using Microsoft.Extensions.Configuration; +using LearningHub.Nhs.OpenApi.Models.Configuration; +using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +namespace LearningHub.Nhs.OpenApi.Services.HttpClients +{ /// /// The Moodle Http Client. /// public class MoodleHttpClient : IMoodleHttpClient, IDisposable { - private readonly HttpClient httpClient = new (); + private readonly MoodleConfig moodleConfig; + private readonly HttpClient httpClient = new(); + private bool initialised = false; - private string moodleAPIBaseUrl; - private string moodleAPIMoodleWSRestFormat; - private string moodleAPIWSToken; + private string moodleApiUrl; + private string moodleApiMoodleWsRestFormat; + private string moodleApiWsToken; /// /// Initializes a new instance of the class. /// /// httpClient. - /// config. - public MoodleHttpClient(HttpClient httpClient, IConfiguration config) + /// config. + public MoodleHttpClient(HttpClient httpClient, IOptions moodleConfig) { + this.moodleConfig = moodleConfig.Value; this.httpClient = httpClient; - this.moodleAPIBaseUrl = config["MoodleAPIConfig:BaseUrl"] + "webservice/rest/server.php"; - this.moodleAPIMoodleWSRestFormat = config["MoodleAPIConfig:MoodleWSRestFormat"]; - this.moodleAPIWSToken = config["MoodleAPIConfig:WSToken"]; + + this.moodleApiUrl = this.moodleConfig.ApiBaseUrl + this.moodleConfig.ApiPath; + this.moodleApiMoodleWsRestFormat = this.moodleConfig.ApiWsRestFormat; + this.moodleApiWsToken = this.moodleConfig.ApiWsToken; } /// @@ -37,7 +45,7 @@ public MoodleHttpClient(HttpClient httpClient, IConfiguration config) /// The . public async Task GetClient() { - this.Initialise(this.moodleAPIBaseUrl); + this.Initialise(this.moodleApiUrl); return this.httpClient; } @@ -47,8 +55,8 @@ public async Task GetClient() /// defaultParameters. public string GetDefaultParameters() { - string defaultParameters = $"wstoken={this.moodleAPIWSToken}" - + $"&moodlewsrestformat={this.moodleAPIMoodleWSRestFormat}"; + string defaultParameters = $"wstoken={this.moodleApiWsToken}" + + $"&moodlewsrestformat={this.moodleApiMoodleWsRestFormat}"; return defaultParameters; } 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 9002a0237..a8953e951 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -30,7 +30,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs new file mode 100644 index 000000000..80c61d1d8 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MoodleApiService.cs @@ -0,0 +1,261 @@ +namespace LearningHub.Nhs.OpenApi.Services.Services +{ + using LearningHub.Nhs.Models.Moodle.API; + using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; + using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Text; + using System.Text.Json; + using System.Threading.Tasks; + + /// + /// MoodleApiService. + /// + public class MoodleApiService : IMoodleApiService + { + private readonly IMoodleHttpClient moodleHttpClient; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// moodleHttpClient. + /// logger. + public MoodleApiService(IMoodleHttpClient moodleHttpClient, ILogger logger) + { + this.moodleHttpClient = moodleHttpClient; + this.logger = logger; + } + + /// + /// GetMoodleUserIdByUsernameAsync. + /// + /// current User Id. + /// UserId from Moodle. + public async Task GetMoodleUserIdByUsernameAsync(int currentUserId) + { + var parameters = new Dictionary + { + { "criteria[0][key]", "username" }, + { "criteria[0][value]", currentUserId.ToString() } + }; + + var response = await GetCallMoodleApiAsync("core_user_get_users", parameters); + + var user = response?.Users?.FirstOrDefault(u => u.Username == currentUserId.ToString()); + return user?.Id ?? 0; + } + + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + public async Task> GetEnrolledCoursesAsync(int userId, int pageNumber) + { + var parameters = new Dictionary + { + { "userid", userId.ToString() } + }; + + // Fetch enrolled courses + var enrolledCourses = await GetCallMoodleApiAsync>( + "core_enrol_get_users_courses", + parameters + ); + + if (enrolledCourses == null || enrolledCourses.Count == 0) + return new List(); + + // Load course completion info in parallel + var completionTasks = enrolledCourses + .Where(c => c.Id.HasValue) + .Select(async course => + { + try + { + course.CourseCompletionViewModel = await GetCourseCompletionAsync(userId, course.Id.Value, pageNumber); + } + catch (Exception ex) + { + course.CourseCompletionViewModel = new MoodleCourseCompletionModel + { + CompletionStatus = null, + }; + } + + return course; + }); + + var enrichedCourses = await Task.WhenAll(completionTasks); + + return enrichedCourses.ToList(); + + } + + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + public async Task> GetRecentEnrolledCoursesAsync(int userId, int? months = null) + { + try + { + userId = 3; + var parameters = new Dictionary + { + { "userid", userId.ToString() }, + { "months", months.ToString() } + }; + + // Fetch enrolled courses + var recentEnrolledCourses = await GetCallMoodleApiAsync>( + "mylearningservice_get_recent_courses", + parameters + ); + + if (recentEnrolledCourses == null || recentEnrolledCourses.Count == 0) + return new List(); + + return recentEnrolledCourses.ToList(); + } + catch(Exception ex) + { + return null; + } + } + + /// + /// GetUserLearningHistory. + /// + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + public async Task> GetUserLearningHistoryAsync(int userId, int? months = null) + { + try + { + userId = 3; + var parameters = new Dictionary + { + { "userid", userId.ToString() }, + { "months", months.ToString() } + }; + + // Fetch enrolled courses + var recentEnrolledCourses = await GetCallMoodleApiAsync>( + "mylearningservice_get_recent_courses", + parameters + ); + + if (recentEnrolledCourses == null || recentEnrolledCourses.Count == 0) + return new List(); + + return recentEnrolledCourses.ToList(); + } + catch (Exception ex) + { + return null; + } + } + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// Moodle course id. + /// pageNumber. + /// List of MoodleCourseResponseModel. + public async Task GetCourseCompletionAsync(int userId, int courseId, int pageNumber) + { + var parameters = new Dictionary + { + { "userid", userId.ToString() }, + { "courseid", courseId.ToString() } + }; + + // Call Moodle API and parse response + var result = await GetCallMoodleApiAsync("core_completion_get_course_completion_status", parameters); + + // If Moodle did not return an exception, return parsed completion data + if (result.Warnings.Count == 0) + { + // Optionally map/convert if needed + return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(result)); + } + + return new MoodleCourseCompletionModel(); // Return empty model or null as fallback + } + + private async Task GetCallMoodleApiAsync(string wsFunction, Dictionary parameters) + { + var client = await this.moodleHttpClient.GetClient(); + string defaultParameters = this.moodleHttpClient.GetDefaultParameters(); + + // Build URL query string + var queryBuilder = new StringBuilder($"&wsfunction={wsFunction}"); + foreach (var param in parameters) + { + queryBuilder.Append($"&{param.Key}={Uri.EscapeDataString(param.Value)}"); + } + + string fullUrl = "?" + defaultParameters + queryBuilder.ToString(); + + HttpResponseMessage response = await client.GetAsync(fullUrl); + string result = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + // Moodle may still return an error with 200 OK + try + { + using var document = JsonDocument.Parse(result); + var root = document.RootElement; + + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("exception", out var exceptionProp)) + { + string? message = root.TryGetProperty("message", out var messageProp) + ? messageProp.GetString() + : "Unknown error"; + + this.logger.LogError($"Moodle returned an exception: {exceptionProp.GetString()}, Message: {message}"); + throw new Exception($"Moodle API Error: {exceptionProp.GetString()}, Message: {message}"); + } + } + catch (System.Text.Json.JsonException ex) + { + this.logger.LogError(ex, "Failed to parse Moodle API response as JSON."); + throw; + } + + var deserialized = JsonConvert.DeserializeObject(result); + + return deserialized == null + ? throw new Exception($"Failed to deserialize Moodle API response into type {typeof(T).Name}. Raw response: {result}") + : deserialized; + } + else if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) + { + this.logger.LogError($"Moodle API access denied. Status Code: {response.StatusCode}"); + throw new Exception("AccessDenied to MoodleApi"); + } + else + { + this.logger.LogError($"Moodle API error. Status Code: {response.StatusCode}, Response: {result}"); + throw new Exception("Error with MoodleApi"); + } + } + + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs index a6d2f1b06..9f44364a1 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/MyLearningService.cs @@ -2,6 +2,8 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.PerformanceData; using System.Linq; using System.Threading.Tasks; using AutoMapper; @@ -25,6 +27,10 @@ public class MyLearningService : IMyLearningService /// The resourceActivityRepository. /// private readonly IResourceActivityRepository resourceActivityRepository; + + /// + /// The catalogueNodeVersionRepository. + /// private readonly ICatalogueNodeVersionRepository catalogueNodeVersionRepository; /// @@ -52,6 +58,11 @@ public class MyLearningService : IMyLearningService /// private readonly IMediaResourceActivityRepository mediaResourceActivity; + /// + /// The moodleApiService. + /// + private readonly IMoodleApiService moodleApiService; + /// /// The mapper. /// @@ -74,6 +85,7 @@ public class MyLearningService : IMyLearningService /// The settings. /// The scormActivityRepository. /// The mediaResourceActivity. + /// The moodleApiService. public MyLearningService( IResourceActivityRepository resourceActivityRepository, IMediaResourcePlayedSegmentRepository mediaResourcePlayedSegmentRepository, @@ -83,7 +95,8 @@ public MyLearningService( IMapper mapper, IOptions settings, IScormActivityRepository scormActivityRepository, - IMediaResourceActivityRepository mediaResourceActivity) + IMediaResourceActivityRepository mediaResourceActivity, + IMoodleApiService moodleApiService) { this.resourceActivityRepository = resourceActivityRepository; this.mediaResourcePlayedSegmentRepository = mediaResourcePlayedSegmentRepository; @@ -94,6 +107,7 @@ public MyLearningService( this.settings = settings.Value; this.scormActivityRepository = scormActivityRepository; this.mediaResourceActivity = mediaResourceActivity; + this.moodleApiService = moodleApiService; } /// @@ -120,6 +134,178 @@ public async Task GetActivityDetailed(int userId, M return viewModel; } + /// + /// Gets the user recent my leraning activities.. + /// + /// /// The user id. + /// The request model. + /// The . + public async Task GetUserRecentMyLearningActivitiesAsync(int userId, MyLearningRequestModel requestModel) + { + try + { + var result = await resourceActivityRepository.GetUserRecentMyLearningActivities(userId, requestModel); + + var entrolledCourses = await this.moodleApiService.GetRecentEnrolledCoursesAsync(userId, 6); + + var mappedMyLearningActivities = result.Select(Activity => new MyLearningCombinedActivitiesViewModel + { + UserId = userId, + ResourceId = Activity.ResourceId, + ResourceVersionId = Activity.ResourceVersionId, + ResourceReferenceId = Activity.ResourceReferenceId, + IsCurrentResourceVersion = Activity.IsCurrentResourceVersion, + MajorVersion = Activity.MajorVersion, + MinorVersion = Activity.MinorVersion, + ResourceType = Activity.ResourceType, + Title = Activity.Title, + CertificateEnabled = Activity.CertificateEnabled, + ActivityStatus = Activity.ActivityStatus, + ActivityDate = Activity.ActivityDate, + ScorePercentage = Activity.ScorePercentage, + TotalActivities = 0, + CompletedActivities = 0, + }).ToList(); + + var mappedEnrolledCourses = entrolledCourses.Select(course => new MyLearningCombinedActivitiesViewModel + { + UserId = userId, + ResourceId = (int)course.Id, + ResourceVersionId = (int)course.Id, + IsCurrentResourceVersion = true, + ResourceReferenceId = (int)course.Id, + MajorVersion = 1, + MinorVersion = 0, + ResourceType = ResourceTypeEnum.Moodle, + Title = course.DisplayName, + CertificateEnabled = course.CertificateEnabled, + ActivityStatus = (bool)course.Completed ? ActivityStatusEnum.Completed : ActivityStatusEnum.Incomplete, + ActivityDate = DateTimeOffset.FromUnixTimeMilliseconds((long)course.LastAccess), + ScorePercentage = Convert.ToInt32(course.ProgressPercentage.TrimEnd('%')), + TotalActivities = course.TotalActivities, + CompletedActivities = course.CompletedActivities, + }).ToList(); + + // Combine both result sets + var combainedUserActivities = mappedMyLearningActivities.Concat(mappedEnrolledCourses).ToList(); + + if (requestModel.Complete) + { + combainedUserActivities = combainedUserActivities.Where(x => x.ActivityStatus == ActivityStatusEnum.Completed || x.ActivityStatus == ActivityStatusEnum.Passed).ToList(); + } + else if (requestModel.Incomplete) + { + combainedUserActivities = combainedUserActivities.Where(x => x.ActivityStatus == ActivityStatusEnum.Incomplete || x.ActivityStatus == ActivityStatusEnum.Failed).ToList(); + } + + var pagedResults = combainedUserActivities.OrderByDescending(activity => activity.ActivityDate).Skip(requestModel.Skip).Take(requestModel.Take).ToList(); + + // Count total records. + MyLearningActivitiesDetailedViewModel viewModel = new MyLearningActivitiesDetailedViewModel() + { + TotalCount = combainedUserActivities.Count(), + Activities = pagedResults, + }; + + return viewModel; + } + catch (Exception ex) + { + return null; + } + } + + /// + /// Gets the user learning history activities. + /// + /// /// The user id. + /// The request model. + /// The . + public async Task GetUserLearningHistoryAsync(int userId, MyLearningRequestModel requestModel) + { + try + { + var result = await resourceActivityRepository.GetUserLearningHistory(userId, requestModel); + + var entrolledCourses = await this.moodleApiService.GetRecentEnrolledCoursesAsync(userId); + + List mappedMyLearningActivities = new(); + List mappedEnrolledCourses = new(); + List combainedUserActivities = new(); + + if (result != null) + { + mappedMyLearningActivities = result.Select(activity => new MyLearningCombinedActivitiesViewModel + { + UserId = userId, + ResourceId = activity.ResourceId, + ResourceVersionId = activity.ResourceVersionId, + ResourceReferenceId = activity.ResourceReferenceId, + IsCurrentResourceVersion = activity.IsCurrentResourceVersion, + MajorVersion = activity.MajorVersion, + MinorVersion = activity.MinorVersion, + ResourceType = activity.ResourceType, + Title = activity.Title, + CertificateEnabled = activity.CertificateEnabled, + ActivityStatus = activity.ActivityStatus, + ActivityDate = activity.ActivityDate, + ScorePercentage = activity.ScorePercentage, + TotalActivities = 0, + CompletedActivities = 0, + }).ToList(); + } + + if (entrolledCourses != null) + { + mappedEnrolledCourses = entrolledCourses.Select(course => new MyLearningCombinedActivitiesViewModel + { + UserId = userId, + ResourceId = (int)course.Id, + ResourceVersionId = (int)course.Id, + IsCurrentResourceVersion = true, + ResourceReferenceId = (int)course.Id, + MajorVersion = 1, + MinorVersion = 0, + ResourceType = ResourceTypeEnum.Moodle, + Title = course.DisplayName, + CertificateEnabled = course.CertificateEnabled, + ActivityStatus = (bool)course.Completed ? ActivityStatusEnum.Completed : ActivityStatusEnum.Incomplete, + ActivityDate = DateTimeOffset.FromUnixTimeMilliseconds((long)course.LastAccess), + ScorePercentage = int.TryParse(course.ProgressPercentage.TrimEnd('%'), out var score) ? score : 0, + TotalActivities = course.TotalActivities, + CompletedActivities = course.CompletedActivities, + }).ToList(); + } + + // Combine both result sets + combainedUserActivities = mappedMyLearningActivities.Concat(mappedEnrolledCourses).ToList(); + + if (requestModel.Complete) + { + combainedUserActivities = combainedUserActivities.Where(x => x.ActivityStatus == ActivityStatusEnum.Completed || x.ActivityStatus == ActivityStatusEnum.Passed).ToList(); + } + else if (requestModel.Incomplete) + { + combainedUserActivities = combainedUserActivities.Where(x => x.ActivityStatus == ActivityStatusEnum.Incomplete || x.ActivityStatus == ActivityStatusEnum.Failed).ToList(); + } + + var pagedResults = combainedUserActivities.OrderByDescending(activity => activity.ActivityDate).Skip(requestModel.Skip).Take(requestModel.Take).ToList(); + + // Count total records. + MyLearningActivitiesDetailedViewModel viewModel = new MyLearningActivitiesDetailedViewModel() + { + TotalCount = combainedUserActivities.Count(), + Activities = pagedResults, + }; + + return viewModel; + } + catch (Exception ex) + { + return null; + } + } + /// /// Gets the played segment data for the progress modal in My Learning screen. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/UserService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/UserService.cs index 70153fd24..45ae92f7a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/UserService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/UserService.cs @@ -1,5 +1,6 @@ namespace LearningHub.Nhs.OpenApi.Services.Services { + using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -188,8 +189,15 @@ public async Task GetByUsernameAsync(string userName) /// The . public async Task GetByIdAsync(int id) { - var user = await userRepository.GetByIdAsync(id); - return mapper.Map(user); + try + { + var user = await userRepository.GetByIdAsync(id); + return mapper.Map(user); + } + catch(Exception ex) + { + return null; + } } /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs index fb9098bde..f00f13253 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs @@ -23,6 +23,7 @@ public static class Startup public static void AddServices(this IServiceCollection services) { services.AddScoped(); + services.AddHttpClient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -33,6 +34,7 @@ public static void AddServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); 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 c16540ee6..65710bbfd 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs index 63b61f042..7d5a61795 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs @@ -40,6 +40,11 @@ public static class ConfigurationExtensions /// public const string AzureSectionName = "Azure"; + /// + /// The FindwiseSectionName. + /// + public const string MoodleSectionName = "Moodle"; + /// /// Adds config. /// @@ -58,6 +63,8 @@ public static void AddConfig(this IServiceCollection services, IConfiguration co services.AddOptions().Bind(config.GetSection(LearningHubApiSectionName)); services.AddOptions().Bind(config.GetSection(AzureSectionName)); + + services.AddOptions().Bind(config.GetSection(MoodleSectionName)); } private static OptionsBuilder RegisterPostConfigure(this OptionsBuilder builder) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MoodleController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MoodleController.cs new file mode 100644 index 000000000..f2639d005 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MoodleController.cs @@ -0,0 +1,70 @@ +namespace LearningHub.NHS.OpenAPI.Controllers +{ + using LearningHub.Nhs.OpenApi.Models.Configuration; + using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; + using System.Threading.Tasks; + + /// + /// Moodle operations. + /// + [Route("Moodle")] + [ApiController] + [Authorize] + public class MoodleController : Controller + { + private readonly IMoodleApiService moodleService; + + /// + /// Initializes a new instance of the class. + /// + /// The moodle service. + public MoodleController(IMoodleApiService moodleService) + { + this.moodleService = moodleService; + } + + /// + /// The GetMoodleUserId. + /// + /// The LH user id. + /// The . + [HttpGet] + [Route("GetMoodleUserId/{currentUserId?}")] + public async Task GetMoodleUserId(int? currentUserId) + { + if (currentUserId.HasValue) + { + var moodleUser = await this.moodleService.GetMoodleUserIdByUsernameAsync(currentUserId.Value); + return this.Ok(moodleUser); + } + else + { + return this.Ok(0); + } + } + + /// + /// GetEnrolledCoursesAsync. + /// + /// Moodle user id. + /// The page Number. + /// A representing the result of the asynchronous operation. + [HttpGet] + [Route("GetEnrolledCourses/{currentUserId?}")] + public async Task GetEnrolledCoursesAsync(int? currentUserId, int pageNumber = 0) + { + if (currentUserId.HasValue) + { + var entrolledCourses = await this.moodleService.GetEnrolledCoursesAsync(currentUserId.Value, pageNumber); + return this.Ok(entrolledCourses); + } + else + { + return this.Ok(0); + } + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs index 023faa590..e810e95d2 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/MyLearningController.cs @@ -52,6 +52,32 @@ public async Task GetActivityDetailed([FromBody] MyLearningReques return this.Ok(activityModel); } + /// + /// Gets the user recent my leraning activities. + /// + /// The request model. + /// The . + [HttpPost] + [Route("GetUserRecentMyLearningActivities")] + public async Task GetUserRecentMyLearningActivities([FromBody] MyLearningRequestModel requestModel) + { + var activityModel = await this.myLearningService.GetUserRecentMyLearningActivitiesAsync(this.CurrentUserId.GetValueOrDefault(), requestModel); + return this.Ok(activityModel); + } + + /// + /// Gets the user recent my leraning activities. + /// + /// The request model. + /// The . + [HttpPost] + [Route("GetUserLearningHistory")] + public async Task GetUserLearningHistory([FromBody] MyLearningRequestModel requestModel) + { + var activityModel = await this.myLearningService.GetUserLearningHistoryAsync(this.CurrentUserId.GetValueOrDefault(), requestModel); + return this.Ok(activityModel); + } + /// /// Gets the played segment data for the progress modal in My Learning screen. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index e8ddf4415..22aaae154 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -18,6 +18,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index 401183bf6..181c8eaf8 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -119,5 +119,10 @@ "ResponseType": "code id_token", "AuthSecret": "", "AuthTimeout": 20 + }, + "Moodle": { + "ApiBaseUrl": "", + "ApiWsRestFormat": "json", + "ApiWsToken": "" } } diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj index 227e30bf8..1ae229307 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj @@ -16,7 +16,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj index 3af90e39a..59ee42314 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj index c6d03d856..123cee758 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj @@ -19,7 +19,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj index 78f41769e..17b91b264 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj @@ -17,7 +17,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj b/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj index f87bb47d5..85cb95f53 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj @@ -20,7 +20,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj index 3b09a079f..eadeefebb 100644 --- a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj +++ b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj @@ -29,7 +29,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.API/web.config b/WebAPI/LearningHub.Nhs.API/web.config index 798b63b5d..a8d351424 100644 --- a/WebAPI/LearningHub.Nhs.API/web.config +++ b/WebAPI/LearningHub.Nhs.API/web.config @@ -2,6 +2,9 @@ + + + diff --git a/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj b/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj index 1963fd724..a6436881d 100644 --- a/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj +++ b/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj b/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj index e7a054e23..684a874e2 100644 --- a/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj +++ b/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index 59f6120a2..821629fb9 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -539,6 +539,8 @@ + + diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql new file mode 100644 index 000000000..ca8bf962c --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql @@ -0,0 +1,82 @@ +------------------------------------------------------------------------------- +-- Author Swapnamol Abraham +-- Created 29-07-2025 +-- Purpose Get Users recent learning acrtivities +-- +-- Modification History +------------------------------------------------------------------------------- +ALTER PROCEDURE [activity].[GetUserRecentLearningActivities] ( + @userId INT + ) +AS +BEGIN + + WITH CTERecentActivities AS ( + SELECT + ra.Id AS ActivityId, + ara.LaunchResourceActivityId AS LaunchResourceActivityId, + ra.UserId AS UserId, + ra.ResourceId AS ResourceId, + r.CurrentResourceVersionId AS ResourceVersionId, + CASE WHEN r.CurrentResourceVersionId = ra.ResourceVersionId THEN 1 ELSE 0 END AS IsCurrentResourceVersion, + ( + SELECT TOP 1 rr.OriginalResourceReferenceId + FROM [resources].[ResourceReference] rr + WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + ) AS ResourceReferenceID, + ra.MajorVersion AS MajorVersion, + ra.MinorVersion AS MinorVersion, + ra.NodePathId AS NodePathId, + r.ResourceTypeId AS ResourceType, + rv.Title AS Title, + rv.CertificateEnabled AS CertificateEnabled, + rvp.ProviderId AS ProviderId, + ISNULL(ara.ActivityStatusId, ra.ActivityStatusId) AS ActivityStatus, + ra.ActivityStart AS ActivityDate, + ISNULL(ara.DurationSeconds, 0) AS ActivityDurationSeconds, + ara.Score AS ScorePercentage, + arv.AssessmentType AS AssessmentType, + arv.PassMark AS PassMark, + asra.score AS AssesmentScore, + mar.SecondsPlayed AS SecondsPlayed, + mar.PercentComplete AS PercentComplete, + sa.CmiCoreLesson_status AS CmiCoreLessonstatus, + sa.CmiCoreScoreMax AS CmiCoreScoreMax, + sa.CmiCoreSession_time AS CmiCoreSessiontime, + sa.DurationSeconds AS DurationSeconds, + ROW_NUMBER() OVER (PARTITION BY ra.ResourceId ORDER BY ISNULL(ara.ActivityEnd, ra.ActivityStart) DESC) AS rn + FROM activity.ResourceActivity ra + LEFT JOIN activity.ResourceActivity ara ON ara.LaunchResourceActivityId = ra.Id + INNER JOIN [resources].[Resource] r ON ra.ResourceId = r.Id + INNER JOIN [resources].[ResourceVersion] rv ON rv.Id = ra.ResourceVersionId AND rv.Deleted = 0 + LEFT JOIN [resources].[ResourceVersionProvider] rvp on rv.Id = rvp.ResourceVersionId + LEFT JOIN [resources].[AssessmentResourceVersion] arv ON arv.ResourceVersionId = ra.ResourceVersionId + LEFT JOIN [activity].[AssessmentResourceActivity] asra ON asra.ResourceActivityId = ra.Id + LEFT JOIN [activity].[MediaResourceActivity] mar ON mar.ResourceActivityId = ra.Id + LEFT JOIN [activity].[ScormActivity] sa ON sa.ResourceActivityId = ra.Id + WHERE ra.LaunchResourceActivityId IS NULL AND ra.userid = @userId + AND ra.deleted = 0 + AND r.ResourceTypeId IN(2,6,7,10,11) AND ra.ActivityStart >= DATEADD(MONTH, -6, SYSDATETIMEOFFSET()) +) +SELECT ActivityId, + LaunchResourceActivityId, + UserId, + ResourceId, + ResourceVersionId, + CAST(IsCurrentResourceVersion AS BIT) AS IsCurrentResourceVersion, + ResourceReferenceId, + MajorVersion, + MinorVersion, + NodePathId, + ResourceType, + Title, + CertificateEnabled, + ProviderId, + ActivityStatus, + ActivityDate, + ActivityDurationSeconds, + ScorePercentage +FROM CTERecentActivities +WHERE rn = 1 order by ActivityDate desc; + +END \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql new file mode 100644 index 000000000..bf7218e30 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql @@ -0,0 +1,50 @@ +------------------------------------------------------------------------------- +-- Author Swapnamol Abraham +-- Created 08-08-2025 +-- Purpose Get Users learning history +-- +-- Modification History +------------------------------------------------------------------------------- +ALTER PROCEDURE [activity].[GetUsersLearningHistory] ( + @userId INT + ) +AS +BEGIN + + SELECT + ra.Id AS ActivityId, + ara.LaunchResourceActivityId AS LaunchResourceActivityId, + ra.UserId AS UserId, + ra.ResourceId AS ResourceId, + r.CurrentResourceVersionId AS ResourceVersionId, + CAST(CASE WHEN r.CurrentResourceVersionId = ra.ResourceVersionId THEN 1 ELSE 0 END AS BIT) AS IsCurrentResourceVersion, + ( + SELECT TOP 1 rr.OriginalResourceReferenceId + FROM [resources].[ResourceReference] rr + WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + ) AS ResourceReferenceId, + ra.MajorVersion AS MajorVersion, + ra.MinorVersion AS MinorVersion, + ra.NodePathId AS NodePathId, + r.ResourceTypeId AS ResourceType, + rv.Title AS Title, + rv.CertificateEnabled AS CertificateEnabled, + rvp.ProviderId AS ProviderId, + ISNULL(ara.ActivityStatusId, ra.ActivityStatusId) AS ActivityStatus, + ra.ActivityStart AS ActivityDate, + ISNULL(ara.DurationSeconds, 0) AS ActivityDurationSeconds, + ara.Score AS ScorePercentage +FROM activity.ResourceActivity ra +LEFT JOIN activity.ResourceActivity ara + ON ara.LaunchResourceActivityId = ra.Id +INNER JOIN [resources].[Resource] r ON ra.ResourceId = r.Id +INNER JOIN [resources].[ResourceVersion] rv ON rv.Id = ra.ResourceVersionId AND rv.deleted =0 +LEFT JOIN [resources].[ResourceVersionProvider] rvp on rv.Id = rvp.ResourceVersionId +LEFT JOIN [resources].[AssessmentResourceVersion] arv ON arv.ResourceVersionId = ra.ResourceVersionId +LEFT JOIN [activity].[AssessmentResourceActivity] asra ON asra.ResourceActivityId = ra.Id +LEFT JOIN [activity].[MediaResourceActivity] mar ON mar.ResourceActivityId = ra.Id +LEFT JOIN [activity].[ScormActivity] sa ON sa.ResourceActivityId = ra.Id +WHERE ra.LaunchResourceActivityId IS NULL and ra.userid = @userId AND ra.deleted = 0 +ORDER BY ra.Id asc + +END \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj index 7e09ec673..3aff1ade8 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj @@ -10,7 +10,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj index 6da0f9200..af23aff67 100644 --- a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj +++ b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj index 024756991..1a35f760a 100644 --- a/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj @@ -16,7 +16,7 @@ - + all diff --git a/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj b/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj index 6b6743743..a7eb5fce3 100644 --- a/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj +++ b/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj index 44f8119cb..aa727f01b 100644 --- a/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj +++ b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj @@ -13,7 +13,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj index 73e18184e..5accd4eaf 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj @@ -25,7 +25,7 @@ - + all diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj index 7b945c483..2044a7dd0 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj index 189b0348e..d73a8c0d3 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj @@ -10,7 +10,7 @@ - + all diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj index 090dce6fc..9a15c5ddc 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj index a9b7683f7..989c47f2d 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj index 71195c130..6e5a38b22 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive